jscmp4 commited on
Commit
290f088
·
verified ·
1 Parent(s): 94d9fb6

add drag func

Browse files
Files changed (1) hide show
  1. index.html +135 -64
index.html CHANGED
@@ -3,59 +3,78 @@
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
- h1 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
10
 
11
- .container { background: #f9f9f9; padding: 20px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
12
 
13
- /* 控件布局优化 */
14
- .controls { margin: 20px 0; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
15
-
16
- /* 下拉菜单样式 */
17
- select { padding: 10px; border: 1px solid #ddd; border-radius: 6px; background: white; cursor: pointer; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- input[type="file"] { padding: 10px; border: 1px solid #ddd; border-radius: 6px; background: white; }
20
- button { background: #000; color: #fff; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: bold; transition: opacity 0.2s; }
21
- button:disabled { background: #ccc; cursor: not-allowed; }
22
- button:hover:not(:disabled) { opacity: 0.8; }
23
 
24
- #status { color: #666; margin-bottom: 10px; font-size: 0.9em; }
25
- #audio-player { width: 100%; margin: 10px 0; display: none; }
 
 
 
26
 
27
- #result-area {
28
- width: 100%; height: 200px;
29
- padding: 15px; border: 1px solid #ddd; border-radius: 6px;
30
- font-family: monospace; line-height: 1.5; resize: vertical;
31
- background: #fff; box-sizing: border-box;
32
- }
33
  </style>
34
  </head>
35
  <body>
36
 
37
- <h1>🎙️ 本地语音转文字 (多语言版)</h1>
38
- <p>支持、英文自动识别,或手动指定语言。</p>
39
 
40
  <div class="container">
41
  <div id="status">🔵 正在初始化引擎...</div>
42
 
 
 
 
 
 
 
43
  <div class="controls">
44
  <select id="language-select">
45
- <option value="auto">🌐 自动识别 (Auto)</option>
46
- <option value="chinese">🇨🇳 中文 (Chinese)</option>
47
- <option value="english">🇺🇸 英文 (English)</option>
48
- <option value="japanese">🇯🇵 日文 (Japanese)</option>
49
- </select>
50
-
51
- <input type="file" id="file-upload" accept="audio/*,video/*,.m4a,.mp4,.wav">
52
  <button id="run-btn" disabled>开始转换</button>
53
  </div>
54
 
55
  <audio id="audio-player" controls></audio>
56
-
57
- <h3>转换结果:</h3>
58
- <textarea id="result-area" placeholder="识别出的文字将显示在这里..."></textarea>
59
  </div>
60
 
61
  <script type="module">
@@ -64,76 +83,128 @@
64
  env.allowLocalModels = false;
65
  env.useBrowserCache = true;
66
 
67
- const statusEl = document.getElementById('status');
 
68
  const fileInput = document.getElementById('file-upload');
 
69
  const runBtn = document.getElementById('run-btn');
70
  const audioPlayer = document.getElementById('audio-player');
71
  const resultArea = document.getElementById('result-area');
72
- const langSelect = document.getElementById('language-select'); // 获取下拉菜单
73
 
74
  let transcriber = null;
 
 
 
 
75
 
 
76
  async function initModel() {
77
  statusEl.innerText = "⏳ 正在加载 Whisper 模型...";
78
  try {
79
- // 依然使用 tiny 模型,它本身就是 Multilingual 的
80
  transcriber = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny');
81
- statusEl.innerText = "✅ 模型就绪!";
82
- runBtn.disabled = false;
83
  } catch (err) {
84
  statusEl.innerText = "❌ 模型加载失败: " + err.message;
 
85
  }
86
  }
87
 
88
- fileInput.addEventListener('change', (e) => {
89
- const file = e.target.files[0];
90
- if (!file) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  const url = URL.createObjectURL(file);
92
  audioPlayer.src = url;
93
- audioPlayer.style.display = 'block';
 
 
 
94
  resultArea.value = "";
95
- statusEl.innerText = "📂 文件就绪";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  });
97
 
 
 
 
 
 
 
 
 
 
 
98
  runBtn.addEventListener('click', async () => {
99
- const file = fileInput.files[0];
100
- if (!file) { alert("请先选择文件!"); return; }
101
 
102
  runBtn.disabled = true;
103
- statusEl.innerText = "🚀 正在转换中...";
104
  const startTime = performance.now();
105
 
106
  try {
107
- const url = URL.createObjectURL(file);
108
-
109
- // --- 关键修改点 ---
110
- // 获取用户选择的语言
111
- const selectedLang = langSelect.value;
112
-
113
- // 配置推理参数
114
- let options = {
115
  chunk_length_s: 30,
116
  stride_length_s: 5,
117
  task: 'transcribe',
118
- };
119
-
120
- // 只有当用户没有选 "auto" 时,才强制指定语言
121
- if (selectedLang !== 'auto') {
122
- options.language = selectedLang;
123
- }
124
- // ------------------
125
-
126
- const output = await transcriber(url, options);
127
 
128
  const endTime = performance.now();
129
  const timeCost = ((endTime - startTime) / 1000).toFixed(2);
130
 
131
  resultArea.value = output.text;
132
- statusEl.innerText = `✅ 完成!耗时: ${timeCost}秒 (语言模式: ${selectedLang})`;
133
 
134
  } catch (err) {
135
  console.error(err);
136
- statusEl.innerText = "❌ 出错: " + err.message;
137
  } finally {
138
  runBtn.disabled = false;
139
  }
 
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>
 
 
 
71
  <button id="run-btn" disabled>开始转换</button>
72
  </div>
73
 
74
  <audio id="audio-player" controls></audio>
75
+
76
+ <h3>📝 转换结果:</h3>
77
+ <textarea id="result-area" placeholder="结果将显示在这里..."></textarea>
78
  </div>
79
 
80
  <script type="module">
 
83
  env.allowLocalModels = false;
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,
195
  task: 'transcribe',
196
+ language: langSelect.value !== 'auto' ? langSelect.value : undefined
197
+ });
 
 
 
 
 
 
 
198
 
199
  const endTime = performance.now();
200
  const timeCost = ((endTime - startTime) / 1000).toFixed(2);
201
 
202
  resultArea.value = output.text;
203
+ statusEl.innerText = `✅ 完成!耗时: ${timeCost}秒`;
204
 
205
  } catch (err) {
206
  console.error(err);
207
+ statusEl.innerText = "❌ 转换出错: " + err.message;
208
  } finally {
209
  runBtn.disabled = false;
210
  }