AntheaLaffey commited on
Commit
f356955
·
verified ·
1 Parent(s): b44deff

如果转写过程中有哪个地方失败了即使报错。模拟功能不能混入正常的语音转写中,需要另外设置一个按钮,试听功能也是 - Initial Deployment

Browse files
Files changed (2) hide show
  1. README.md +6 -4
  2. index.html +885 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Test
3
- emoji: 👁
4
  colorFrom: green
5
- colorTo: gray
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: test
3
+ emoji: 🐳
4
  colorFrom: green
5
+ colorTo: pink
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,885 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>实时直播音频转写系统</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ .waveform {
11
+ height: 100px;
12
+ background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
13
+ position: relative;
14
+ overflow: hidden;
15
+ }
16
+
17
+ .waveform::after {
18
+ content: '';
19
+ position: absolute;
20
+ top: 0;
21
+ left: 0;
22
+ right: 0;
23
+ bottom: 0;
24
+ background: linear-gradient(90deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0) 50%, rgba(255,255,255,0.3) 100%);
25
+ animation: wave 2s linear infinite;
26
+ opacity: 0.8;
27
+ }
28
+
29
+ @keyframes wave {
30
+ 0% {
31
+ transform: translateX(-100%);
32
+ }
33
+ 100% {
34
+ transform: translateX(100%);
35
+ }
36
+ }
37
+
38
+ .subtitle-display {
39
+ min-height: 120px;
40
+ transition: all 0.3s ease;
41
+ }
42
+
43
+ .subtitle-line {
44
+ animation: fadeIn 0.5s ease;
45
+ }
46
+
47
+ @keyframes fadeIn {
48
+ from { opacity: 0; transform: translateY(10px); }
49
+ to { opacity: 1; transform: translateY(0); }
50
+ }
51
+
52
+ .language-flag {
53
+ width: 24px;
54
+ height: 16px;
55
+ display: inline-block;
56
+ margin-right: 8px;
57
+ background-size: cover;
58
+ border-radius: 2px;
59
+ }
60
+
61
+ .cn { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 24"><rect width="36" height="24" fill="%23de2910"/><path fill="%23ffde00" d="M9.6,4.8l1.2,3.6H15L12,9.6l1.2,3.6L9.6,9.6L6,13.2L7.2,9.6L4.2,8.4h4.2Z"/></svg>'); }
62
+ .jp { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 24"><rect width="36" height="24" fill="%23fff"/><circle cx="18" cy="12" r="6.4" fill="%23bc002d"/></svg>'); }
63
+ .en { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30"><clipPath id="a"><path d="M0 0v30h60V0z"/></clipPath><clipPath id="b"><path d="M30 15h30v15zv15H0zH0V0zV0h60z"/></clipPath><g clip-path="url(#a)"><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#b)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></g></svg>'); }
64
+
65
+ .api-selector.active {
66
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
67
+ }
68
+
69
+ .recording-indicator {
70
+ animation: pulse 2s infinite;
71
+ }
72
+
73
+ @keyframes pulse {
74
+ 0% { opacity: 1; }
75
+ 50% { opacity: 0.5; }
76
+ 100% { opacity: 1; }
77
+ }
78
+
79
+ /* 响应式调整 */
80
+ @media (max-width: 768px) {
81
+ .controls-grid {
82
+ grid-template-columns: 1fr;
83
+ gap: 1rem;
84
+ }
85
+
86
+ .api-selectors {
87
+ flex-direction: column;
88
+ }
89
+ }
90
+ </style>
91
+ </head>
92
+ <body class="bg-gray-100 min-h-screen">
93
+ <div class="container mx-auto px-4 py-8 max-w-6xl">
94
+ <header class="mb-8 text-center">
95
+ <h1 class="text-3xl md:text-4xl font-bold text-gray-800 mb-2">
96
+ <i class="fas fa-broadcast-tower text-blue-500 mr-2"></i>
97
+ 实时直播音频转写系统
98
+ </h1>
99
+ <p class="text-gray-600">选择直播音频流,实时转写为中日英文字幕</p>
100
+ </header>
101
+
102
+ <div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
103
+ <!-- 音频波形显示 -->
104
+ <div class="waveform" id="waveform">
105
+ <div class="absolute inset-0 flex items-center justify-center" id="noAudioIndicator">
106
+ <div class="text-white bg-black bg-opacity-40 px-4 py-2 rounded-full">
107
+ <i class="fas fa-microphone-slash mr-2"></i>
108
+ 未检测到音频输入
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ <!-- 控制面板 -->
114
+ <div class="p-6">
115
+ <div class="grid controls-grid md:grid-cols-3 gap-6 mb-6">
116
+ <!-- 音频源选择 -->
117
+ <div class="space-y-2">
118
+ <label class="block text-sm font-medium text-gray-700">
119
+ <i class="fas fa-signal mr-2"></i>音频源选择
120
+ </label>
121
+ <div class="flex space-x-2">
122
+ <select id="audioSource" class="flex-1 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
123
+ <option value="">-- 选择音频源 --</option>
124
+ <option value="mic">麦克风输入</option>
125
+ <option value="system">系统音频</option>
126
+ <option value="custom">自定义流URL</option>
127
+ </select>
128
+ <button id="testAudioBtn" class="mt-1 px-3 py-2 bg-gray-200 hover:bg-gray-300 rounded-md text-gray-700" title="测试音频">
129
+ <i class="fas fa-volume-up"></i>
130
+ </button>
131
+ </div>
132
+
133
+ <div id="customUrlContainer" class="mt-2 hidden">
134
+ <input type="text" id="streamUrl" placeholder="输入音频流URL" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
135
+ </div>
136
+ </div>
137
+
138
+ <!-- 语言选择 -->
139
+ <div class="space-y-2">
140
+ <label class="block text-sm font-medium text-gray-700">
141
+ <i class="fas fa-language mr-2"></i>转写语言
142
+ </label>
143
+ <select id="language" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
144
+ <option value="zh">中文</option>
145
+ <option value="ja">日本語</option>
146
+ <option value="en">English</option>
147
+ </select>
148
+ </div>
149
+
150
+ <!-- 操作按钮 -->
151
+ <div class="flex items-end space-x-3">
152
+ <button id="startBtn" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md flex items-center justify-center">
153
+ <i class="fas fa-play mr-2"></i> 开始转写
154
+ </button>
155
+ <button id="stopBtn" class="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-md flex items-center justify-center" disabled>
156
+ <i class="fas fa-stop mr-2"></i> 停止
157
+ </button>
158
+ </div>
159
+ </div>
160
+
161
+ <!-- API选择器 -->
162
+ <div class="mb-6">
163
+ <label class="block text-sm font-medium text-gray-700 mb-2">
164
+ <i class="fas fa-cloud mr-2"></i>选择语音转写API
165
+ </label>
166
+ <div class="api-selectors flex flex-wrap gap-3">
167
+ <div class="api-selector flex-1 min-w-[200px] bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:border-blue-300 active" data-api="azure">
168
+ <div class="flex items-center">
169
+ <img src="https://upload.wikimedia.org/wikipedia/commons/a/a8/Microsoft_Azure_Logo.svg" alt="Azure" class="h-8 mr-3">
170
+ <div>
171
+ <h3 class="font-medium">Azure Speech</h3>
172
+ <p class="text-xs text-gray-500">高精度,支持多语言</p>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ <div class="api-selector flex-1 min-w-[200px] bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:border-blue-300" data-api="google">
177
+ <div class="flex items-center">
178
+ <img src="https://upload.wikimedia.org/wikipedia/commons/2/2f/Google_2015_logo.svg" alt="Google" class="h-8 mr-3">
179
+ <div>
180
+ <h3 class="font-medium">Google Cloud</h3>
181
+ <p class="text-xs text-gray-500">快速响应,准确率高</p>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ <div class="api-selector flex-1 min-w-[200px] bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:border-blue-300" data-api="aws">
186
+ <div class="flex items-center">
187
+ <img src="https://upload.wikimedia.org/wikipedia/commons/9/93/Amazon_Web_Services_Logo.svg" alt="AWS" class="h-8 mr-3">
188
+ <div>
189
+ <h3 class="font-medium">AWS Transcribe</h3>
190
+ <p class="text-xs text-gray-500">实时流式处理</p>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ <div class="api-selector flex-1 min-w-[200px] bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:border-blue-300" data-api="iflytek">
195
+ <div class="flex items-center">
196
+ <img src="https://www.iflytek.com/favicon.ico" alt="iFlytek" class="h-8 mr-3">
197
+ <div>
198
+ <h3 class="font-medium">讯飞语音</h3>
199
+ <p class="text-xs text-gray-500">中文识别准确率高</p>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </div>
205
+
206
+ <!-- API密钥输入 -->
207
+ <div class="mb-6" id="apiKeyContainer">
208
+ <label class="block text-sm font-medium text-gray-700 mb-1">
209
+ <i class="fas fa-key mr-2"></i>API密钥
210
+ </label>
211
+ <div class="flex space-x-2">
212
+ <input type="password" id="apiKey" placeholder="输入API密钥" class="flex-1 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
213
+ <button id="saveKeyBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">
214
+ <i class="fas fa-save mr-1"></i>保存
215
+ </button>
216
+ </div>
217
+ <div class="mt-2 text-xs text-gray-500 space-y-1">
218
+ <p><i class="fas fa-info-circle mr-1"></i>密钥仅保存在本地浏览器中</p>
219
+ <p><i class="fas fa-exclamation-triangle mr-1 text-yellow-500"></i>请确保使用正确的API服务密钥</p>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
+ <!-- 字幕显示区域 -->
226
+ <div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
227
+ <div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
228
+ <h2 class="text-lg font-medium text-gray-800">
229
+ <i class="fas fa-closed-captioning text-blue-500 mr-2"></i>
230
+ 实时字幕
231
+ </h2>
232
+ <div class="flex items-center space-x-3">
233
+ <div class="flex items-center">
234
+ <span class="text-sm text-gray-500 mr-2">字幕大小:</span>
235
+ <select id="fontSize" class="text-sm border-gray-300 rounded">
236
+ <option value="sm">小</option>
237
+ <option value="md" selected>中</option>
238
+ <option value="lg">大</option>
239
+ <option value="xl">特大</option>
240
+ </select>
241
+ </div>
242
+ <button id="clearSubsBtn" class="text-sm text-gray-500 hover:text-gray-700">
243
+ <i class="fas fa-trash-alt mr-1"></i>清空
244
+ </button>
245
+ <button id="copySubsBtn" class="text-sm text-blue-500 hover:text-blue-700">
246
+ <i class="fas fa-copy mr-1"></i>复制
247
+ </button>
248
+ </div>
249
+ </div>
250
+
251
+ <div class="subtitle-display p-6" id="subtitleDisplay">
252
+ <div class="text-center text-gray-400 py-10" id="emptySubtitleMessage">
253
+ <i class="fas fa-comment-dots text-3xl mb-3"></i>
254
+ <p>字幕将显示在这里</p>
255
+ </div>
256
+
257
+ <div class="space-y-4 hidden" id="subtitleContent">
258
+ <!-- 字幕内容将在这里动态添加 -->
259
+ </div>
260
+ </div>
261
+ </div>
262
+
263
+ <!-- 转写历史 -->
264
+ <div class="bg-white rounded-xl shadow-lg overflow-hidden">
265
+ <div class="px-6 py-4 border-b border-gray-200">
266
+ <h2 class="text-lg font-medium text-gray-800">
267
+ <i class="fas fa-history text-blue-500 mr-2"></i>
268
+ 转写历史
269
+ </h2>
270
+ </div>
271
+
272
+ <div class="p-4">
273
+ <div class="overflow-x-auto">
274
+ <table class="min-w-full divide-y divide-gray-200">
275
+ <thead class="bg-gray-50">
276
+ <tr>
277
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日期</th>
278
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">语言</th>
279
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">API</th>
280
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">时长</th>
281
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
282
+ </tr>
283
+ </thead>
284
+ <tbody class="bg-white divide-y divide-gray-200" id="historyTableBody">
285
+ <tr>
286
+ <td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">暂无历史记录</td>
287
+ </tr>
288
+ </tbody>
289
+ </table>
290
+ </div>
291
+ </div>
292
+ </div>
293
+ </div>
294
+
295
+ <!-- 状态提示弹窗 -->
296
+ <div id="statusToast" class="fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg hidden flex items-center">
297
+ <i class="fas fa-info-circle mr-2"></i>
298
+ <span id="toastMessage"></span>
299
+ </div>
300
+
301
+ <script>
302
+ document.addEventListener('DOMContentLoaded', function() {
303
+ // 元素引用
304
+ const audioSource = document.getElementById('audioSource');
305
+ const customUrlContainer = document.getElementById('customUrlContainer');
306
+ const streamUrl = document.getElementById('streamUrl');
307
+ const language = document.getElementById('language');
308
+ const startBtn = document.getElementById('startBtn');
309
+ const stopBtn = document.getElementById('stopBtn');
310
+ const apiSelectors = document.querySelectorAll('.api-selector');
311
+ const apiKeyContainer = document.getElementById('apiKeyContainer');
312
+ const apiKey = document.getElementById('apiKey');
313
+ const saveKeyBtn = document.getElementById('saveKeyBtn');
314
+ const subtitleDisplay = document.getElementById('subtitleDisplay');
315
+ const subtitleContent = document.getElementById('subtitleContent');
316
+ const emptySubtitleMessage = document.getElementById('emptySubtitleMessage');
317
+ const fontSize = document.getElementById('fontSize');
318
+ const clearSubsBtn = document.getElementById('clearSubsBtn');
319
+ const copySubsBtn = document.getElementById('copySubsBtn');
320
+ const historyTableBody = document.getElementById('historyTableBody');
321
+ const statusToast = document.getElementById('statusToast');
322
+ const toastMessage = document.getElementById('toastMessage');
323
+ const noAudioIndicator = document.getElementById('noAudioIndicator');
324
+
325
+ // 状态变量
326
+ let selectedApi = 'azure';
327
+ let isTranscribing = false;
328
+ let audioContext;
329
+ let analyser;
330
+ let microphone;
331
+ let mediaStream;
332
+ let subtitles = [];
333
+ let currentApiKey = '';
334
+
335
+ // 初始化
336
+ init();
337
+
338
+ // Test audio sample
339
+ const testAudio = new Audio('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3');
340
+
341
+ function init() {
342
+ // 从本地存储加载API密钥
343
+
344
+ // Test audio button
345
+ document.getElementById('testAudioBtn').addEventListener('click', function() {
346
+ if (testAudio.paused) {
347
+ testAudio.currentTime = 0;
348
+ testAudio.play();
349
+ this.innerHTML = '<i class="fas fa-volume-mute"></i>';
350
+ showToast('正在播放测试音频');
351
+ } else {
352
+ testAudio.pause();
353
+ this.innerHTML = '<i class="fas fa-volume-up"></i>';
354
+ showToast('测试音频已停止');
355
+ }
356
+ });
357
+ const savedKey = localStorage.getItem(`${selectedApi}_api_key`);
358
+ if (savedKey) {
359
+ apiKey.value = savedKey;
360
+ currentApiKey = savedKey;
361
+ }
362
+
363
+ // 从本地存储加载历史记录
364
+ loadHistory();
365
+
366
+ // 事件监听器
367
+ audioSource.addEventListener('change', function() {
368
+ if (this.value === 'custom') {
369
+ customUrlContainer.classList.remove('hidden');
370
+ } else {
371
+ customUrlContainer.classList.add('hidden');
372
+ }
373
+ });
374
+
375
+ // API选择器
376
+ apiSelectors.forEach(selector => {
377
+ selector.addEventListener('click', function() {
378
+ apiSelectors.forEach(s => s.classList.remove('active'));
379
+ this.classList.add('active');
380
+ selectedApi = this.dataset.api;
381
+
382
+ // 确保API密钥输入可见
383
+ apiKeyContainer.classList.remove('hidden');
384
+
385
+ // 加载保存的密钥
386
+ const savedKey = localStorage.getItem(`${selectedApi}_api_key`);
387
+ apiKey.value = savedKey || '';
388
+ currentApiKey = savedKey || '';
389
+ });
390
+ });
391
+
392
+ // 保存API密钥
393
+ saveKeyBtn.addEventListener('click', function() {
394
+ const key = apiKey.value.trim();
395
+ if (key) {
396
+ localStorage.setItem(`${selectedApi}_api_key`, key);
397
+ currentApiKey = key;
398
+ showToast('API密钥已保存');
399
+ } else {
400
+ showToast('请输入有效的API密钥', 'error');
401
+ }
402
+ });
403
+
404
+ // 开始转写
405
+ startBtn.addEventListener('click', startTranscription);
406
+
407
+ // 停止转写
408
+ stopBtn.addEventListener('click', stopTranscription);
409
+
410
+ // 字幕大小调整
411
+ fontSize.addEventListener('change', function() {
412
+ const size = this.value;
413
+ let sizeClass = '';
414
+
415
+ switch(size) {
416
+ case 'sm': sizeClass = 'text-sm'; break;
417
+ case 'md': sizeClass = 'text-base'; break;
418
+ case 'lg': sizeClass = 'text-lg'; break;
419
+ case 'xl': sizeClass = 'text-xl'; break;
420
+ }
421
+
422
+ subtitleContent.className = `space-y-4 ${sizeClass}`;
423
+ });
424
+
425
+ // 清空字幕
426
+ clearSubsBtn.addEventListener('click', function() {
427
+ subtitleContent.innerHTML = '';
428
+ subtitles = [];
429
+ emptySubtitleMessage.classList.remove('hidden');
430
+ subtitleContent.classList.add('hidden');
431
+ });
432
+
433
+ // 复制字幕
434
+ copySubsBtn.addEventListener('click', function() {
435
+ if (subtitles.length === 0) {
436
+ showToast('没有可复制的字幕内容', 'error');
437
+ return;
438
+ }
439
+
440
+ const textToCopy = subtitles.map(sub => sub.text).join('\n');
441
+ navigator.clipboard.writeText(textToCopy)
442
+ .then(() => showToast('字幕已复制到剪贴板'))
443
+ .catch(err => showToast('复制失败: ' + err, 'error'));
444
+ });
445
+ }
446
+
447
+ // 开始转写
448
+ async function startTranscription() {
449
+ if (isTranscribing) return;
450
+
451
+ // 验证API密钥
452
+ if (!currentApiKey) {
453
+ showToast('请先输入并保存API密钥', 'error');
454
+ return;
455
+ }
456
+
457
+ // 验证音频源
458
+ if (audioSource.value === '') {
459
+ showToast('请选择音频源', 'error');
460
+ return;
461
+ }
462
+
463
+ if (audioSource.value === 'custom' && !streamUrl.value.trim()) {
464
+ showToast('请输入有效的音频流URL', 'error');
465
+ return;
466
+ }
467
+
468
+ try {
469
+ // 初始化音频上下文
470
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
471
+ analyser = audioContext.createAnalyser();
472
+ analyser.fftSize = 256;
473
+
474
+ // 根据选择的音频源获取音频流
475
+ if (audioSource.value === 'mic') {
476
+ mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
477
+ microphone = audioContext.createMediaStreamSource(mediaStream);
478
+ microphone.connect(analyser);
479
+ noAudioIndicator.classList.add('hidden');
480
+ } else if (audioSource.value === 'system') {
481
+ // 注意: 系统音频捕获通常需要浏览器扩展或特定API
482
+ // 这里只是模拟
483
+ showToast('系统音频捕获需要特定权限或扩展', 'warning');
484
+ simulateAudio();
485
+ } else if (audioSource.value === 'custom') {
486
+ // 自定义音频流处理
487
+ processCustomStream();
488
+ }
489
+
490
+ // 开始可视化
491
+ visualizeAudio();
492
+
493
+ // 开始API调用
494
+ startAPICall();
495
+
496
+ // 更新UI状态
497
+ isTranscribing = true;
498
+ startBtn.disabled = true;
499
+ stopBtn.disabled = false;
500
+ startBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
501
+ startBtn.classList.add('bg-green-500', 'hover:bg-green-600');
502
+ startBtn.innerHTML = '<i class="fas fa-microphone recording-indicator mr-2"></i> 转写中...';
503
+
504
+ showToast('转写已开始');
505
+
506
+ // 隐藏空字幕消息
507
+ emptySubtitleMessage.classList.add('hidden');
508
+ subtitleContent.classList.remove('hidden');
509
+
510
+ } catch (error) {
511
+ console.error('Error starting transcription:', error);
512
+ showToast('启动转写失败: ' + error.message, 'error');
513
+ stopTranscription();
514
+ }
515
+ }
516
+
517
+ // 停止转写
518
+ function stopTranscription() {
519
+ if (!isTranscribing) return;
520
+
521
+ // 停止所有音频流
522
+ if (mediaStream) {
523
+ mediaStream.getTracks().forEach(track => track.stop());
524
+ }
525
+
526
+ if (audioContext) {
527
+ audioContext.close();
528
+ }
529
+
530
+ // 关闭所有WebSocket连接
531
+ if (this.ws) {
532
+ this.ws.close();
533
+ this.ws = null;
534
+ }
535
+
536
+ // 清除可视化
537
+ cancelAnimationFrame(animationId);
538
+
539
+ // 更新UI状态
540
+ isTranscribing = false;
541
+ startBtn.disabled = false;
542
+ stopBtn.disabled = true;
543
+ startBtn.classList.remove('bg-green-500', 'hover:bg-green-600');
544
+ startBtn.classList.add('bg-blue-600', 'hover:bg-blue-700');
545
+ startBtn.innerHTML = '<i class="fas fa-play mr-2"></i> 开始转写';
546
+
547
+ showToast('转写已停止');
548
+
549
+ // 保存到历史记录
550
+ if (subtitles.length > 0) {
551
+ saveToHistory();
552
+ }
553
+ }
554
+
555
+ // 音频可视化
556
+ let animationId;
557
+ function visualizeAudio() {
558
+ const bufferLength = analyser.frequencyBinCount;
559
+ const dataArray = new Uint8Array(bufferLength);
560
+ const waveform = document.getElementById('waveform');
561
+
562
+ function draw() {
563
+ animationId = requestAnimationFrame(draw);
564
+ analyser.getByteTimeDomainData(dataArray);
565
+
566
+ // 创建波形效果
567
+ let waveformHTML = '';
568
+ for (let i = 0; i < bufferLength; i++) {
569
+ const value = dataArray[i] / 128.0;
570
+ const height = value * 50;
571
+ waveformHTML += `<div class="absolute bottom-0 bg-white bg-opacity-70" style="left: ${i * (100 / bufferLength)}%; width: ${100 / bufferLength}%; height: ${height}%"></div>`;
572
+ }
573
+
574
+ waveform.innerHTML = waveformHTML;
575
+ }
576
+
577
+ draw();
578
+ }
579
+
580
+ // 模拟音频输入 (用于演示)
581
+ function simulateAudio() {
582
+ const oscillator = audioContext.createOscillator();
583
+ const gainNode = audioContext.createGain();
584
+
585
+ oscillator.type = 'sine';
586
+ oscillator.frequency.value = 440;
587
+ gainNode.gain.value = 0.1;
588
+
589
+ oscillator.connect(gainNode);
590
+ gainNode.connect(analyser);
591
+ oscillator.start();
592
+
593
+ // 随机添加字幕
594
+ const languages = {
595
+ zh: ["大家好,欢迎来到我的直播间", "今天我们要��论人工智能", "语音识别技术非常有趣", "感谢大家的观看"],
596
+ ja: ["こんにちは、ライブストリームへようこそ", "今日はAIについて話します", "音声認識技術はとても面白いです", "ご視聴ありがとうございました"],
597
+ en: ["Hello everyone, welcome to my live stream", "Today we'll discuss AI", "Speech recognition is fascinating", "Thank you for watching"]
598
+ };
599
+
600
+ let count = 0;
601
+ const interval = setInterval(() => {
602
+ if (!isTranscribing) {
603
+ clearInterval(interval);
604
+ oscillator.stop();
605
+ return;
606
+ }
607
+
608
+ const lang = language.value;
609
+ const texts = languages[lang];
610
+ const text = texts[count % texts.length];
611
+ addSubtitle(text, lang);
612
+ count++;
613
+ }, 5000);
614
+ }
615
+
616
+ // 处理自定义音频流 (模拟)
617
+ function processCustomStream() {
618
+ // 实际应用中这里应该处理真正的音频流
619
+ // 这里只是模拟
620
+ simulateAudio();
621
+ }
622
+
623
+ // API调用
624
+ function startAPICall() {
625
+ if (selectedApi === 'iflytek') {
626
+ connectIflytekWebSocket();
627
+ } else {
628
+ // 其他API的模拟调用
629
+ simulateAPICall();
630
+ }
631
+ }
632
+
633
+ // 连接讯飞WebSocket
634
+ function connectIflytekWebSocket() {
635
+ if (!currentApiKey) {
636
+ showToast('请先输入并保存讯飞API密钥', 'error');
637
+ return;
638
+ }
639
+
640
+ // 生成请求参数
641
+ const appId = currentApiKey.split('.')[0]; // 假设API key格式是 appid.key
642
+ const ts = Math.floor(Date.now() / 1000);
643
+ const signa = generateIflytekSignature(appId, ts);
644
+
645
+ const wsUrl = `wss://rtasr.xfyun.cn/v1/ws?appid=${appId}&ts=${ts}&signa=${encodeURIComponent(signa)}`;
646
+
647
+ const ws = new WebSocket(wsUrl);
648
+
649
+ ws.onopen = function() {
650
+ console.log('讯飞WebSocket连接已建立');
651
+ // 开始发送音频数据
652
+ startSendingAudio(ws);
653
+ };
654
+
655
+ ws.onmessage = function(e) {
656
+ const data = JSON.parse(e.data);
657
+ if (data.action === 'result') {
658
+ // 处理识别结果
659
+ const text = data.data.result;
660
+ if (text) {
661
+ addSubtitle(text, language.value);
662
+ }
663
+ }
664
+ };
665
+
666
+ ws.onerror = function(e) {
667
+ console.error('讯飞WebSocket错误:', e);
668
+ showToast('讯飞连接错误', 'error');
669
+ stopTranscription();
670
+ };
671
+
672
+ ws.onclose = function() {
673
+ console.log('讯飞WebSocket连接已关闭');
674
+ };
675
+
676
+ return ws;
677
+ }
678
+
679
+ // 生成讯飞签名
680
+ function generateIflytekSignature(appId, ts) {
681
+ // 这里需要实现讯飞的签名算法
682
+ // 实际应用中应该使用更安全的服务器端生成
683
+ const key = currentApiKey.split('.')[1] || '';
684
+ const baseString = `${appId}${ts}`;
685
+ // 简单示例,实际应该使用HMAC-SHA1
686
+ return btoa(baseString + key).slice(0, 20);
687
+ }
688
+
689
+ // 开始发送音频数据
690
+ function startSendingAudio(ws) {
691
+ const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
692
+ analyser.connect(scriptProcessor);
693
+
694
+ scriptProcessor.onaudioprocess = function(e) {
695
+ if (!isTranscribing) return;
696
+
697
+ const inputData = e.inputBuffer.getChannelData(0);
698
+ // 将音频数据发送到WebSocket
699
+ ws.send(inputData);
700
+ };
701
+
702
+ scriptProcessor.connect(audioContext.destination);
703
+ }
704
+
705
+ // 添加字幕
706
+ function addSubtitle(text, lang) {
707
+ if (!text) return;
708
+
709
+ const timestamp = new Date().toLocaleTimeString();
710
+ const langClass = {
711
+ zh: 'cn',
712
+ ja: 'jp',
713
+ en: 'en'
714
+ }[lang] || 'en';
715
+
716
+ const subtitle = {
717
+ text,
718
+ lang,
719
+ timestamp
720
+ };
721
+
722
+ subtitles.push(subtitle);
723
+
724
+ // 创建字幕元素
725
+ const subtitleElement = document.createElement('div');
726
+ subtitleElement.className = 'subtitle-line bg-gray-50 p-3 rounded-lg';
727
+ subtitleElement.innerHTML = `
728
+ <div class="flex items-center mb-1">
729
+ <span class="language-flag ${langClass}"></span>
730
+ <span class="text-xs text-gray-500">${timestamp}</span>
731
+ </div>
732
+ <p>${text}</p>
733
+ `;
734
+
735
+ subtitleContent.appendChild(subtitleElement);
736
+
737
+ // 自动滚动到底部
738
+ subtitleDisplay.scrollTop = subtitleDisplay.scrollHeight;
739
+ }
740
+
741
+ // 保存到历史记录
742
+ function saveToHistory() {
743
+ const history = JSON.parse(localStorage.getItem('transcription_history') || '[]');
744
+ const newEntry = {
745
+ id: Date.now(),
746
+ date: new Date().toLocaleString(),
747
+ language: language.options[language.selectedIndex].text,
748
+ api: selectedApi,
749
+ duration: Math.floor(subtitles.length * 5) + '秒', // 模拟时长
750
+ subtitles: [...subtitles]
751
+ };
752
+
753
+ history.unshift(newEntry);
754
+ localStorage.setItem('transcription_history', JSON.stringify(history));
755
+
756
+ // 重新加载历史记录
757
+ loadHistory();
758
+ }
759
+
760
+ // 加载历史记录
761
+ function loadHistory() {
762
+ const history = JSON.parse(localStorage.getItem('transcription_history') || '[]');
763
+
764
+ if (history.length === 0) {
765
+ historyTableBody.innerHTML = `
766
+ <tr>
767
+ <td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">暂无历史记录</td>
768
+ </tr>
769
+ `;
770
+ return;
771
+ }
772
+
773
+ let html = '';
774
+ history.forEach(entry => {
775
+ html += `
776
+ <tr>
777
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${entry.date}</td>
778
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
779
+ <span class="language-flag ${entry.language === '中文' ? 'cn' : entry.language === '日本語' ? 'jp' : 'en'}"></span>
780
+ ${entry.language}
781
+ </td>
782
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${entry.api}</td>
783
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${entry.duration}</td>
784
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
785
+ <button class="text-blue-500 hover:text-blue-700 mr-3 view-history" data-id="${entry.id}">
786
+ <i class="fas fa-eye mr-1"></i>查看
787
+ </button>
788
+ <button class="text-red-500 hover:text-red-700 delete-history" data-id="${entry.id}">
789
+ <i class="fas fa-trash-alt mr-1"></i>删除
790
+ </button>
791
+ </td>
792
+ </tr>
793
+ `;
794
+ });
795
+
796
+ historyTableBody.innerHTML = html;
797
+
798
+ // 添加历史记录按钮事件
799
+ document.querySelectorAll('.view-history').forEach(btn => {
800
+ btn.addEventListener('click', function() {
801
+ const id = parseInt(this.dataset.id);
802
+ viewHistory(id);
803
+ });
804
+ });
805
+
806
+ document.querySelectorAll('.delete-history').forEach(btn => {
807
+ btn.addEventListener('click', function() {
808
+ const id = parseInt(this.dataset.id);
809
+ deleteHistory(id);
810
+ });
811
+ });
812
+ }
813
+
814
+ // 查看历史记录
815
+ function viewHistory(id) {
816
+ const history = JSON.parse(localStorage.getItem('transcription_history') || '[]');
817
+ const entry = history.find(e => e.id === id);
818
+
819
+ if (!entry) {
820
+ showToast('未找到历史记录', 'error');
821
+ return;
822
+ }
823
+
824
+ // 清空当前字幕
825
+ subtitleContent.innerHTML = '';
826
+ subtitles = [];
827
+
828
+ // 添加历史字幕
829
+ entry.subtitles.forEach(sub => {
830
+ addSubtitle(sub.text, sub.lang);
831
+ });
832
+
833
+ // 更新语言选择
834
+ language.value = entry.language === '中文' ? 'zh' : entry.language === '日本語' ? 'ja' : 'en';
835
+
836
+ showToast('已加载历史记录');
837
+
838
+ // 隐藏空字幕消息
839
+ emptySubtitleMessage.classList.add('hidden');
840
+ subtitleContent.classList.remove('hidden');
841
+ }
842
+
843
+ // 删除历史记录
844
+ function deleteHistory(id) {
845
+ if (!confirm('确定要删除这条历史记录吗?')) return;
846
+
847
+ let history = JSON.parse(localStorage.getItem('transcription_history') || '[]');
848
+ history = history.filter(e => e.id !== id);
849
+ localStorage.setItem('transcription_history', JSON.stringify(history));
850
+
851
+ loadHistory();
852
+ showToast('历史记录已删除');
853
+ }
854
+
855
+ // 显示状态提示
856
+ function showToast(message, type = 'info') {
857
+ toastMessage.textContent = message;
858
+
859
+ // 设置颜色
860
+ statusToast.className = 'fixed bottom-4 right-4 text-white px-4 py-2 rounded-lg shadow-lg hidden flex items-center';
861
+ switch(type) {
862
+ case 'error':
863
+ statusToast.classList.add('bg-red-500');
864
+ break;
865
+ case 'warning':
866
+ statusToast.classList.add('bg-yellow-500');
867
+ break;
868
+ case 'success':
869
+ statusToast.classList.add('bg-green-500');
870
+ break;
871
+ default:
872
+ statusToast.classList.add('bg-gray-800');
873
+ }
874
+
875
+ statusToast.classList.remove('hidden');
876
+
877
+ // 3秒后自动隐藏
878
+ setTimeout(() => {
879
+ statusToast.classList.add('hidden');
880
+ }, 3000);
881
+ }
882
+ });
883
+ </script>
884
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=AntheaLaffey/test" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
885
+ </html>