jscmp4 commited on
Commit
f22a52c
·
verified ·
1 Parent(s): 5af1ef3
Files changed (1) hide show
  1. index.html +101 -96
index.html CHANGED
@@ -3,18 +3,16 @@
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, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; color: #333; }
9
  .container { background: #fff; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
10
 
11
- /* 进度条 */
12
  .progress-wrapper { width: 100%; background-color: #e9ecef; border-radius: 8px; height: 20px; margin: 15px 0; overflow: hidden; display: none; }
13
  .progress-bar { height: 100%; background-color: #28a745; width: 0%; text-align: center; line-height: 20px; color: white; font-size: 12px; font-weight: bold; transition: width 0.2s ease; }
14
  .progress-bar.processing { 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); background-size: 1rem 1rem; animation: stripes 1s linear infinite; }
15
  @keyframes stripes { from { background-position: 1rem 0; } to { background-position: 0 0; } }
16
 
17
- /* 拖拽区 */
18
  #drop-zone { border: 2px dashed #ccc; border-radius: 10px; padding: 40px 20px; text-align: center; cursor: pointer; background: #fafafa; margin-bottom: 20px; transition: 0.2s;}
19
  #drop-zone.drag-over { border-color: #007bff; background-color: #eef6ff; }
20
  #file-upload { display: none; }
@@ -25,23 +23,22 @@
25
  button:disabled { background: #ccc; cursor: not-allowed; }
26
 
27
  #result-area { width: 100%; height: 300px; padding: 15px; border: 1px solid #ddd; border-radius: 6px; font-family: monospace; resize: vertical; background: #fdfdfd; margin-top: 15px;}
28
- .tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; margin-right: 5px; background: #eee;}
29
  </style>
30
  </head>
31
  <body>
32
 
33
- <h1>🚀 21分钟长音频专用版 (Web Worker)</h1>
34
- <p>AI 后台独立线程运行,彻底告别“页面无响应”。</p>
35
 
36
  <div class="container">
37
- <div id="status">🔵 正在启动多线程引擎...</div>
38
 
39
  <div class="progress-wrapper" id="progress-wrapper">
40
  <div class="progress-bar" id="progress-bar">0%</div>
41
  </div>
42
 
43
  <div id="drop-zone">
44
- <p>☁️ 拖入长录音 (mp3, m4a, wav)</p>
45
  </div>
46
  <input type="file" id="file-upload" accept="audio/*,video/*,.m4a,.wav,.mp3">
47
 
@@ -59,80 +56,64 @@
59
  </div>
60
 
61
  <script id="worker-code" type="javascript/worker">
62
- // 这里的代码将会在后台线程独立运行,不卡主界面
63
-
64
  import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2';
65
 
66
- // 允许缓存
67
- env.allowLocalModels = false;
68
- env.useBrowserCache = true;
69
 
70
- let transcriber = null;
71
 
72
- self.onmessage = async (e) => {
73
- const msg = e.data;
74
 
75
- if (msg.type === 'load') {
76
- try {
77
- self.postMessage({ type: 'status', text: '⏳ 正在加载 Whisper-Base (更聪明版)...' });
78
-
79
- // 🔴 改动 1: 从 tiny 换成 base,更不容易发疯
80
- transcriber = await pipeline('automatic-speech-recognition', 'Xenova/whisper-base', {
81
- progress_callback: (data) => {
82
- if (data.status === 'progress') {
83
- self.postMessage({
84
- type: 'download_progress',
85
- percent: Math.round((data.loaded / data.total) * 100),
86
- file: data.file
87
- });
88
  }
89
- }
90
- });
91
-
92
- self.postMessage({ type: 'ready' });
93
- } catch (err) {
94
- self.postMessage({ type: 'error', error: err.message });
95
  }
96
- }
97
 
98
- if (msg.type === 'run') {
99
- if (!transcriber) return;
100
-
101
- try {
102
- self.postMessage({ type: 'status', text: '🚀 正在分析长音频 (已开启防幻觉)...' });
103
-
104
- // 开始推理
105
- const output = await transcriber(msg.audio, {
106
- // 基础设置
107
- chunk_length_s: 30,
108
- stride_length_s: 5,
109
- task: 'transcribe',
110
- language: msg.language !== 'auto' ? msg.language : undefined,
111
 
112
- // 🔴 改动 2: 关键参优化
113
- return_timestamps: true, // 强制让模型对齐时间轴,能极大减少乱说
114
- no_repeat_ngram_size: 2, // 强行禁止连续重复 2 个词以上的短语
115
-
116
- // 🔴 改动 3: 温度设置 (Temperature)
117
- // 0 代表最冷静、最严谨。默认可能是随机的,导致它乱猜。
118
- temperature: 0,
119
- });
120
-
121
- self.postMessage({ type: 'result', text: output.text });
122
- } catch (err) {
123
- self.postMessage({ type: 'error', error: err.message });
 
 
 
124
  }
125
- }
126
- };
127
  </script>
128
 
129
  <script type="module">
130
- // 1. 创建 Worker
131
  const workerBlob = new Blob([document.getElementById('worker-code').textContent], { type: "text/javascript" });
132
  const workerUrl = URL.createObjectURL(workerBlob);
133
  const worker = new Worker(workerUrl, { type: "module" });
134
 
135
- // DOM 元素
136
  const statusEl = document.getElementById('status');
137
  const progressBar = document.getElementById('progress-bar');
138
  const progressWrapper = document.getElementById('progress-wrapper');
@@ -143,31 +124,45 @@
143
  const fileInput = document.getElementById('file-upload');
144
  const langSelect = document.getElementById('language-select');
145
 
146
- let currentAudioUrl = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- // 2. 监听 Worker 发回来的消息
149
  worker.onmessage = (e) => {
150
  const msg = e.data;
151
-
152
  if (msg.type === 'download_progress') {
153
  progressWrapper.style.display = 'block';
154
  progressBar.style.width = msg.percent + '%';
155
  progressBar.innerText = msg.percent + '%';
156
- if(msg.percent === 100) statusEl.innerText = "⏳ 模型下载完成,正在编译...";
157
  }
158
-
159
  if (msg.type === 'ready') {
160
- statusEl.innerText = "✅ 引擎就绪 (后台线程)";
161
  statusEl.style.color = "green";
162
  runBtn.disabled = false;
163
  progressBar.style.width = '0%';
164
  progressWrapper.style.display = 'none';
165
  }
166
-
167
- if (msg.type === 'status') {
168
- statusEl.innerText = msg.text;
169
- }
170
-
171
  if (msg.type === 'result') {
172
  resultArea.value = msg.text;
173
  statusEl.innerText = "✅ 转换完成!";
@@ -175,23 +170,20 @@
175
  runBtn.disabled = false;
176
  stopBtn.style.display = 'none';
177
  }
178
-
179
  if (msg.type === 'error') {
180
- statusEl.innerText = "❌ 错误: " + msg.error;
181
  statusEl.style.color = "red";
182
  progressBar.classList.remove('processing');
183
  runBtn.disabled = false;
184
  }
185
  };
186
 
187
- // 启动加载模型
188
  worker.postMessage({ type: 'load' });
189
 
190
- // 3. 文件处理逻辑
191
  function handleFile(file) {
 
192
  statusEl.innerText = `📂 已加载: ${file.name}`;
193
  dropZone.innerHTML = `<p>📄 ${file.name}</p>`;
194
- currentAudioUrl = URL.createObjectURL(file); // 生成 blob URL 传给 worker
195
  }
196
 
197
  dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
@@ -200,32 +192,45 @@
200
  dropZone.addEventListener('click', () => fileInput.click());
201
  fileInput.addEventListener('change', (e) => { if(e.target.files.length) handleFile(e.target.files[0]); });
202
 
203
- // 4. 点击开始
204
- runBtn.addEventListener('click', () => {
205
- if(!currentAudioUrl) return alert("请先上传文件");
206
 
207
  runBtn.disabled = true;
208
  stopBtn.style.display = 'inline-block';
209
  progressWrapper.style.display = 'block';
210
  progressBar.classList.add('processing');
211
- progressBar.innerText = "计算中...";
212
-
213
- // 把任务发给后台
214
- worker.postMessage({
215
- type: 'run',
216
- audio: currentAudioUrl,
217
- language: langSelect.value
218
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  });
220
 
221
- // 停止按钮 (重载 Worker)
222
  stopBtn.addEventListener('click', () => {
223
  if(confirm("确定要终止吗?")) {
224
- worker.terminate(); // 杀掉后台线程
225
- location.reload(); // 简单粗暴刷新重来
226
  }
227
  });
228
-
229
  </script>
230
  </body>
231
  </html>
 
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, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; color: #333; }
9
  .container { background: #fff; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
10
 
 
11
  .progress-wrapper { width: 100%; background-color: #e9ecef; border-radius: 8px; height: 20px; margin: 15px 0; overflow: hidden; display: none; }
12
  .progress-bar { height: 100%; background-color: #28a745; width: 0%; text-align: center; line-height: 20px; color: white; font-size: 12px; font-weight: bold; transition: width 0.2s ease; }
13
  .progress-bar.processing { 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); background-size: 1rem 1rem; animation: stripes 1s linear infinite; }
14
  @keyframes stripes { from { background-position: 1rem 0; } to { background-position: 0 0; } }
15
 
 
16
  #drop-zone { border: 2px dashed #ccc; border-radius: 10px; padding: 40px 20px; text-align: center; cursor: pointer; background: #fafafa; margin-bottom: 20px; transition: 0.2s;}
17
  #drop-zone.drag-over { border-color: #007bff; background-color: #eef6ff; }
18
  #file-upload { display: none; }
 
23
  button:disabled { background: #ccc; cursor: not-allowed; }
24
 
25
  #result-area { width: 100%; height: 300px; padding: 15px; border: 1px solid #ddd; border-radius: 6px; font-family: monospace; resize: vertical; background: #fdfdfd; margin-top: 15px;}
 
26
  </style>
27
  </head>
28
  <body>
29
 
30
+ <h1>🚀 21分钟长音频修复版 (解码分离)</h1>
31
+ <p>解决了 "AudioContext is not available" 错误。主线程解码,后台线程计算。</p>
32
 
33
  <div class="container">
34
+ <div id="status">🔵 正在启动引擎...</div>
35
 
36
  <div class="progress-wrapper" id="progress-wrapper">
37
  <div class="progress-bar" id="progress-bar">0%</div>
38
  </div>
39
 
40
  <div id="drop-zone">
41
+ <p>☁️ 拖入音频文件 (mp3, m4a, wav)</p>
42
  </div>
43
  <input type="file" id="file-upload" accept="audio/*,video/*,.m4a,.wav,.mp3">
44
 
 
56
  </div>
57
 
58
  <script id="worker-code" type="javascript/worker">
 
 
59
  import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2';
60
 
61
+ env.allowLocalModels = false;
62
+ env.useBrowserCache = true;
 
63
 
64
+ let transcriber = null;
65
 
66
+ self.onmessage = async (e) => {
67
+ const msg = e.data;
68
 
69
+ if (msg.type === 'load') {
70
+ try {
71
+ self.postMessage({ type: 'status', text: '⏳ 后台加载模型 (Whisper-Base)...' });
72
+ // 使用 base 模型防幻觉
73
+ transcriber = await pipeline('automatic-speech-recognition', 'Xenova/whisper-base', {
74
+ progress_callback: (data) => {
75
+ if (data.status === 'progress') {
76
+ self.postMessage({
77
+ type: 'download_progress',
78
+ percent: Math.round((data.loaded / data.total) * 100)
79
+ });
80
+ }
 
81
  }
82
+ });
83
+ self.postMessage({ type: 'ready' });
84
+ } catch (err) {
85
+ self.postMessage({ type: 'error', error: err.message });
86
+ }
 
87
  }
 
88
 
89
+ if (msg.type === 'run') {
90
+ try {
91
+ self.postMessage({ type: 'status', text: '🚀 模型正在推理中...' });
 
 
 
 
 
 
 
 
 
 
92
 
93
+ // 这里的 msg.audio 已经是 Float32Array 纯字了,不需要 AudioContext 解码
94
+ const output = await transcriber(msg.audio, {
95
+ chunk_length_s: 30,
96
+ stride_length_s: 5,
97
+ task: 'transcribe',
98
+ language: msg.language !== 'auto' ? msg.language : undefined,
99
+ return_timestamps: true, // 防复读关键
100
+ no_repeat_ngram_size: 2, // 防复读关键
101
+ temperature: 0, // 降低随机性
102
+ });
103
+
104
+ self.postMessage({ type: 'result', text: output.text });
105
+ } catch (err) {
106
+ self.postMessage({ type: 'error', error: err.message });
107
+ }
108
  }
109
+ };
 
110
  </script>
111
 
112
  <script type="module">
 
113
  const workerBlob = new Blob([document.getElementById('worker-code').textContent], { type: "text/javascript" });
114
  const workerUrl = URL.createObjectURL(workerBlob);
115
  const worker = new Worker(workerUrl, { type: "module" });
116
 
 
117
  const statusEl = document.getElementById('status');
118
  const progressBar = document.getElementById('progress-bar');
119
  const progressWrapper = document.getElementById('progress-wrapper');
 
124
  const fileInput = document.getElementById('file-upload');
125
  const langSelect = document.getElementById('language-select');
126
 
127
+ let currentFile = null;
128
+
129
+ // --- 核心修复函数:在主线程解码音频 ---
130
+ async function decodeAudio(file) {
131
+ // 1. 创建 AudioContext,强制采样率为 16000 (Whisper 需要 16k)
132
+ const audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
133
+
134
+ // 2. 读取文件为 ArrayBuffer
135
+ const arrayBuffer = await file.arrayBuffer();
136
+
137
+ // 3. 解码 (这一步必须在主线程做)
138
+ const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
139
+
140
+ // 4. 获取单声道数据 (Float32Array)
141
+ let audioData = audioBuffer.getChannelData(0);
142
+
143
+ // 5. 关闭上下文释放资源
144
+ await audioCtx.close();
145
+
146
+ return audioData;
147
+ }
148
+ // ------------------------------------
149
 
 
150
  worker.onmessage = (e) => {
151
  const msg = e.data;
 
152
  if (msg.type === 'download_progress') {
153
  progressWrapper.style.display = 'block';
154
  progressBar.style.width = msg.percent + '%';
155
  progressBar.innerText = msg.percent + '%';
156
+ if(msg.percent === 100) statusEl.innerText = "⏳ 下载完成,正在编译...";
157
  }
 
158
  if (msg.type === 'ready') {
159
+ statusEl.innerText = "✅ 引擎就绪";
160
  statusEl.style.color = "green";
161
  runBtn.disabled = false;
162
  progressBar.style.width = '0%';
163
  progressWrapper.style.display = 'none';
164
  }
165
+ if (msg.type === 'status') statusEl.innerText = msg.text;
 
 
 
 
166
  if (msg.type === 'result') {
167
  resultArea.value = msg.text;
168
  statusEl.innerText = "✅ 转换完成!";
 
170
  runBtn.disabled = false;
171
  stopBtn.style.display = 'none';
172
  }
 
173
  if (msg.type === 'error') {
174
+ statusEl.innerText = "❌ " + msg.error;
175
  statusEl.style.color = "red";
176
  progressBar.classList.remove('processing');
177
  runBtn.disabled = false;
178
  }
179
  };
180
 
 
181
  worker.postMessage({ type: 'load' });
182
 
 
183
  function handleFile(file) {
184
+ currentFile = file;
185
  statusEl.innerText = `📂 已加载: ${file.name}`;
186
  dropZone.innerHTML = `<p>📄 ${file.name}</p>`;
 
187
  }
188
 
189
  dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
 
192
  dropZone.addEventListener('click', () => fileInput.click());
193
  fileInput.addEventListener('change', (e) => { if(e.target.files.length) handleFile(e.target.files[0]); });
194
 
195
+ runBtn.addEventListener('click', async () => {
196
+ if(!currentFile) return alert("请先上传文件");
 
197
 
198
  runBtn.disabled = true;
199
  stopBtn.style.display = 'inline-block';
200
  progressWrapper.style.display = 'block';
201
  progressBar.classList.add('processing');
202
+ progressBar.innerText = "解码中...";
203
+ statusEl.innerText = "⏳ 正在预处理音频 (解码)...";
204
+
205
+ try {
206
+ // 1. 先在主线程解码
207
+ const audioData = await decodeAudio(currentFile);
208
+
209
+ progressBar.innerText = "计算中...";
210
+ statusEl.innerText = "🚀 音频数据已发送给后台,正在推理...";
211
+
212
+ // 2. 把解码好的纯数据 (audioData) 发给 Worker
213
+ // 注意:这里我们使用 Transferable Object 传输,效率极高
214
+ worker.postMessage({
215
+ type: 'run',
216
+ audio: audioData,
217
+ language: langSelect.value
218
+ }, [audioData.buffer]); // 这里的 buffer 转移所有权,零拷贝
219
+
220
+ } catch (err) {
221
+ console.error(err);
222
+ statusEl.innerText = "❌ 解码失败: " + err.message;
223
+ runBtn.disabled = false;
224
+ progressBar.classList.remove('processing');
225
+ }
226
  });
227
 
 
228
  stopBtn.addEventListener('click', () => {
229
  if(confirm("确定要终止吗?")) {
230
+ worker.terminate();
231
+ location.reload();
232
  }
233
  });
 
234
  </script>
235
  </body>
236
  </html>