jscmp4 commited on
Commit
d72bae3
·
verified ·
1 Parent(s): 290f088
Files changed (1) hide show
  1. index.html +104 -86
index.html CHANGED
@@ -3,68 +3,88 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Web AI - 拖拽上传版</title>
7
  <style>
8
  body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; color: #333; }
9
 
10
  .container { background: #fff; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
11
 
12
- /* --- 核心:拖拽区域样式 --- */
13
- #drop-zone {
14
- border: 2px dashed #ccc;
15
- border-radius: 10px;
16
- padding: 40px 20px;
 
 
 
 
 
 
 
 
 
 
 
17
  text-align: center;
18
- cursor: pointer;
19
- transition: all 0.3s ease;
20
- background: #fafafa;
21
- margin: 20px 0;
 
22
  }
23
 
24
- /* 拖拽文件悬停时的样式 */
25
- #drop-zone.drag-over {
26
- border-color: #007bff;
27
- background-color: #eef6ff;
28
- transform: scale(1.02);
 
29
  }
30
 
31
- #drop-zone p { margin: 0; color: #666; font-size: 1.1em; pointer-events: none; }
32
- #drop-zone span { font-size: 0.8em; color: #999; display: block; margin-top: 5px; pointer-events: none;}
33
-
34
- /* 隐藏原始的文件 input,因为我们点击框体触发它 */
 
 
 
 
 
 
 
35
  #file-upload { display: none; }
36
 
37
  /* 其他控件 */
38
  .controls { display: flex; gap: 10px; align-items: center; margin-bottom: 15px; }
39
  select { padding: 8px; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; }
40
- button { background: #000; color: #fff; border: none; padding: 10px 25px; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 1em;}
41
  button:disabled { background: #ccc; cursor: not-allowed; }
42
 
43
- #status { margin-bottom: 10px; font-weight: 500; }
44
- .error { color: #d32f2f; }
45
- .success { color: #2e7d32; }
46
-
47
- #audio-player { width: 100%; margin: 15px 0; display: none; }
48
- #result-area { width: 100%; height: 200px; padding: 15px; border: 1px solid #ddd; border-radius: 6px; font-family: monospace; resize: vertical; box-sizing: border-box; background: #fdfdfd;}
49
  </style>
50
  </head>
51
  <body>
52
 
53
- <h1>🎙️ AI 语音转文字 (拖拽版)</h1>
54
- <p>支持拖拽上传 .mp3, .m4a, .wav 等音频文件。</p>
55
 
56
  <div class="container">
57
- <div id="status">🔵 正在初始化引擎...</div>
 
 
 
 
 
58
 
59
  <div id="drop-zone">
60
- <p>☁️ 文件拖到这里</p>
61
- <span>或点击此处选择文件 (支持 mp3, m4a, wav)</span>
62
  </div>
63
  <input type="file" id="file-upload" accept="audio/*,video/*,.m4a,.wav,.mp3">
64
 
65
  <div class="controls">
66
  <select id="language-select">
67
- <option value="auto">🌐 自动识别语言</option>
68
  <option value="chinese">🇨🇳 中文</option>
69
  <option value="english">🇺🇸 英文</option>
70
  </select>
@@ -72,8 +92,6 @@
72
  </div>
73
 
74
  <audio id="audio-player" controls></audio>
75
-
76
- <h3>📝 转换结果:</h3>
77
  <textarea id="result-area" placeholder="结果将显示在这里..."></textarea>
78
  </div>
79
 
@@ -84,111 +102,108 @@
84
  env.useBrowserCache = true;
85
 
86
  // DOM 元素
 
 
 
87
  const dropZone = document.getElementById('drop-zone');
88
  const fileInput = document.getElementById('file-upload');
89
- const statusEl = document.getElementById('status');
90
  const runBtn = document.getElementById('run-btn');
91
- const audioPlayer = document.getElementById('audio-player');
92
  const resultArea = document.getElementById('result-area');
 
93
  const langSelect = document.getElementById('language-select');
94
 
95
  let transcriber = null;
96
- let currentFile = null; // 存储当前待处理的文件
97
-
98
- // --- 允许的文件后缀列表 ---
99
  const ALLOWED_EXTENSIONS = ['mp3', 'wav', 'm4a', 'mp4', 'mpeg', 'ogg', 'flac'];
100
 
101
- // 1. 初始化模型
102
  async function initModel() {
103
- statusEl.innerText = "⏳ 正在加载 Whisper 模型...";
 
 
104
  try {
105
- transcriber = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny');
106
- statusEl.innerText = "✅ 模型就绪!请拖入或选择音频。";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  statusEl.className = "success";
 
108
  } catch (err) {
109
  statusEl.innerText = "❌ 模型加载失败: " + err.message;
110
- statusEl.className = "error";
111
  }
112
  }
113
 
114
- // --- 核心逻辑:统一处理文件的函数 ---
115
  function handleFile(file) {
116
- // A. 校验逻辑 (Validation)
117
- const fileName = file.name.toLowerCase();
118
- const ext = fileName.split('.').pop();
119
-
120
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
121
- statusEl.innerText = `❌ 不支持的文件格式 (.${ext})。请上传音频文件。`;
122
- statusEl.className = "error";
123
- runBtn.disabled = true;
124
- audioPlayer.style.display = 'none';
125
  return;
126
  }
127
-
128
- // B. 如果校验通过
129
- currentFile = file; // 保存文件到全局变量
130
-
131
- // 更新 UI
132
  statusEl.innerText = `📂 已加载: ${file.name}`;
133
- statusEl.className = "success";
134
- dropZone.innerHTML = `<p>📄 ${file.name}</p><span>点击更换文件</span>`;
135
 
136
- // 显示播放器
137
  const url = URL.createObjectURL(file);
138
  audioPlayer.src = url;
139
  audioPlayer.style.display = 'block';
140
 
141
- // 激活按钮 (前提是模型加载完了)
142
  if (transcriber) runBtn.disabled = false;
143
- resultArea.value = "";
144
  }
145
 
146
- // --- 2. 拖拽事件监听 ---
147
-
148
- // 拖进来时 (Drag Over) - 阻止默认行为并改变样式
149
  dropZone.addEventListener('dragover', (e) => {
150
- e.preventDefault(); // 必须阻止,否则浏览器会直接打开文件
151
  dropZone.classList.add('drag-over');
152
- dropZone.querySelector('p').innerText = "👇 松手上传";
153
  });
154
-
155
- // 拖出去时 (Drag Leave) - 恢复样式
156
  dropZone.addEventListener('dragleave', (e) => {
157
  e.preventDefault();
158
  dropZone.classList.remove('drag-over');
159
- if(!currentFile) dropZone.querySelector('p').innerText = "☁️ 把文件拖到这里";
160
  });
161
-
162
- // 松手时 (Drop) - 获取文件
163
  dropZone.addEventListener('drop', (e) => {
164
  e.preventDefault();
165
  dropZone.classList.remove('drag-over');
166
-
167
- // 获取拖入的文件
168
- if (e.dataTransfer.files.length > 0) {
169
- handleFile(e.dataTransfer.files[0]);
170
- }
171
  });
172
-
173
- // --- 3. 点击事件 (保留原有的点击选择功能) ---
174
  dropZone.addEventListener('click', () => fileInput.click());
175
-
176
  fileInput.addEventListener('change', (e) => {
177
- if (e.target.files.length > 0) {
178
- handleFile(e.target.files[0]);
179
- }
180
  });
181
 
182
- // --- 4. 开始转换 ---
183
  runBtn.addEventListener('click', async () => {
184
  if (!currentFile) return;
185
 
186
  runBtn.disabled = true;
187
  statusEl.innerText = "🚀 正在转换中... (请勿关闭页面)";
 
 
 
 
 
 
188
  const startTime = performance.now();
189
 
190
  try {
191
  const url = URL.createObjectURL(currentFile);
 
192
  const output = await transcriber(url, {
193
  chunk_length_s: 30,
194
  stride_length_s: 5,
@@ -204,8 +219,11 @@
204
 
205
  } catch (err) {
206
  console.error(err);
207
- statusEl.innerText = "❌ 转换出错: " + err.message;
208
  } finally {
 
 
 
209
  runBtn.disabled = false;
210
  }
211
  });
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Web AI - 终极进度条版</title>
7
  <style>
8
  body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; color: #333; }
9
 
10
  .container { background: #fff; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
11
 
12
+ /* --- 进度条容器 --- */
13
+ .progress-wrapper {
14
+ width: 100%;
15
+ background-color: #e9ecef;
16
+ border-radius: 8px;
17
+ height: 20px;
18
+ margin: 15px 0;
19
+ overflow: hidden;
20
+ display: none; /* 默认隐藏 */
21
+ }
22
+
23
+ /* 进度条本体 */
24
+ .progress-bar {
25
+ height: 100%;
26
+ background-color: #28a745; /* 绿色 */
27
+ width: 0%;
28
  text-align: center;
29
+ line-height: 20px;
30
+ color: white;
31
+ font-size: 12px;
32
+ font-weight: bold;
33
+ transition: width 0.2s ease;
34
  }
35
 
36
+ /* 转换时的条纹动画效果 */
37
+ .progress-bar.processing {
38
+ width: 100% !important; /* 充满 */
39
+ background-image: linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);
40
+ background-size: 1rem 1rem;
41
+ animation: progress-bar-stripes 1s linear infinite;
42
  }
43
 
44
+ @keyframes progress-bar-stripes {
45
+ 0% { background-position: 1rem 0; }
46
+ 100% { background-position: 0 0; }
47
+ }
48
+
49
+ /* 拖拽区域 */
50
+ #drop-zone {
51
+ border: 2px dashed #ccc; border-radius: 10px; padding: 40px 20px;
52
+ text-align: center; cursor: pointer; transition: all 0.3s ease; background: #fafafa; margin-bottom: 20px;
53
+ }
54
+ #drop-zone.drag-over { border-color: #007bff; background-color: #eef6ff; transform: scale(1.02); }
55
  #file-upload { display: none; }
56
 
57
  /* 其他控件 */
58
  .controls { display: flex; gap: 10px; align-items: center; margin-bottom: 15px; }
59
  select { padding: 8px; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; }
60
+ button { background: #000; color: #fff; border: none; padding: 10px 25px; border-radius: 6px; cursor: pointer; font-weight: bold; }
61
  button:disabled { background: #ccc; cursor: not-allowed; }
62
 
63
+ #status { margin-bottom: 10px; font-weight: 500; font-size: 0.95em;}
64
+ #result-area { width: 100%; height: 200px; padding: 15px; border: 1px solid #ddd; border-radius: 6px; font-family: monospace; resize: vertical; box-sizing: border-box; background: #fdfdfd; margin-top: 15px;}
65
+ #audio-player { width: 100%; display: none; }
 
 
 
66
  </style>
67
  </head>
68
  <body>
69
 
70
+ <h1>🎙️ Web AI (带进度条版)</h1>
 
71
 
72
  <div class="container">
73
+
74
+ <div id="status">🔵 等待操作...</div>
75
+
76
+ <div class="progress-wrapper" id="progress-wrapper">
77
+ <div class="progress-bar" id="progress-bar">0%</div>
78
+ </div>
79
 
80
  <div id="drop-zone">
81
+ <p>☁️ 拖入文件 (mp3, m4a, wav)</p>
 
82
  </div>
83
  <input type="file" id="file-upload" accept="audio/*,video/*,.m4a,.wav,.mp3">
84
 
85
  <div class="controls">
86
  <select id="language-select">
87
+ <option value="auto">🌐 自动识别</option>
88
  <option value="chinese">🇨🇳 中文</option>
89
  <option value="english">🇺🇸 英文</option>
90
  </select>
 
92
  </div>
93
 
94
  <audio id="audio-player" controls></audio>
 
 
95
  <textarea id="result-area" placeholder="结果将显示在这里..."></textarea>
96
  </div>
97
 
 
102
  env.useBrowserCache = true;
103
 
104
  // DOM 元素
105
+ const statusEl = document.getElementById('status');
106
+ const progressWrapper = document.getElementById('progress-wrapper');
107
+ const progressBar = document.getElementById('progress-bar');
108
  const dropZone = document.getElementById('drop-zone');
109
  const fileInput = document.getElementById('file-upload');
 
110
  const runBtn = document.getElementById('run-btn');
 
111
  const resultArea = document.getElementById('result-area');
112
+ const audioPlayer = document.getElementById('audio-player');
113
  const langSelect = document.getElementById('language-select');
114
 
115
  let transcriber = null;
116
+ let currentFile = null;
 
 
117
  const ALLOWED_EXTENSIONS = ['mp3', 'wav', 'm4a', 'mp4', 'mpeg', 'ogg', 'flac'];
118
 
119
+ // --- 1. 初始化模型 (带进度回调) ---
120
  async function initModel() {
121
+ statusEl.innerText = "⏳ 正在连接 Hugging Face 下载模型...";
122
+ progressWrapper.style.display = 'block'; // 显示进度条容器
123
+
124
  try {
125
+ transcriber = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny', {
126
+ // 🆕 关键点:这里监听下载进度
127
+ progress_callback: (data) => {
128
+ if (data.status === 'progress') {
129
+ const percent = Math.round((data.loaded / data.total) * 100);
130
+ progressBar.style.width = percent + '%';
131
+ progressBar.innerText = percent + '%';
132
+
133
+ if (percent === 100) {
134
+ statusEl.innerText = `⏳ 下载完成 (${data.file}),正在加载...`;
135
+ }
136
+ }
137
+ }
138
+ });
139
+
140
+ // 加载完成后
141
+ statusEl.innerText = "✅ 模型就绪!请拖入文件。";
142
+ progressBar.style.width = '0%'; // 重置供下次使用
143
+ progressBar.innerText = '';
144
+ progressWrapper.style.display = 'none'; // 暂时隐藏
145
  statusEl.className = "success";
146
+
147
  } catch (err) {
148
  statusEl.innerText = "❌ 模型加载失败: " + err.message;
 
149
  }
150
  }
151
 
152
+ // --- 2. 文件处理 ---
153
  function handleFile(file) {
154
+ const ext = file.name.split('.').pop().toLowerCase();
 
 
 
155
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
156
+ statusEl.innerText = `❌ 不支持格式 .${ext}`;
 
 
 
157
  return;
158
  }
159
+ currentFile = file;
 
 
 
 
160
  statusEl.innerText = `📂 已加载: ${file.name}`;
 
 
161
 
162
+ dropZone.innerHTML = `<p>📄 ${file.name}</p>`;
163
  const url = URL.createObjectURL(file);
164
  audioPlayer.src = url;
165
  audioPlayer.style.display = 'block';
166
 
 
167
  if (transcriber) runBtn.disabled = false;
168
+ resultArea.value = "";
169
  }
170
 
171
+ // --- 3. 拖拽逻辑 ---
 
 
172
  dropZone.addEventListener('dragover', (e) => {
173
+ e.preventDefault();
174
  dropZone.classList.add('drag-over');
 
175
  });
 
 
176
  dropZone.addEventListener('dragleave', (e) => {
177
  e.preventDefault();
178
  dropZone.classList.remove('drag-over');
 
179
  });
 
 
180
  dropZone.addEventListener('drop', (e) => {
181
  e.preventDefault();
182
  dropZone.classList.remove('drag-over');
183
+ if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
 
 
 
 
184
  });
 
 
185
  dropZone.addEventListener('click', () => fileInput.click());
 
186
  fileInput.addEventListener('change', (e) => {
187
+ if (e.target.files.length) handleFile(e.target.files[0]);
 
 
188
  });
189
 
190
+ // --- 4. 转换逻辑 (带动画条) ---
191
  runBtn.addEventListener('click', async () => {
192
  if (!currentFile) return;
193
 
194
  runBtn.disabled = true;
195
  statusEl.innerText = "🚀 正在转换中... (请勿关闭页面)";
196
+
197
+ // 🆕 开启进度条动画 (因为推理很难算具体的百分比,所以用条纹动画)
198
+ progressWrapper.style.display = 'block';
199
+ progressBar.classList.add('processing'); // 加上流动条纹类
200
+ progressBar.innerText = "Processing...";
201
+
202
  const startTime = performance.now();
203
 
204
  try {
205
  const url = URL.createObjectURL(currentFile);
206
+
207
  const output = await transcriber(url, {
208
  chunk_length_s: 30,
209
  stride_length_s: 5,
 
219
 
220
  } catch (err) {
221
  console.error(err);
222
+ statusEl.innerText = "❌ 出错: " + err.message;
223
  } finally {
224
+ // 停止动画,隐藏进度条
225
+ progressBar.classList.remove('processing');
226
+ progressWrapper.style.display = 'none';
227
  runBtn.disabled = false;
228
  }
229
  });