Caoyanyi commited on
Commit
bb38994
·
1 Parent(s): 5823c92

* Add feature.

Browse files
Files changed (5) hide show
  1. .gitignore +68 -0
  2. Dockerfile +6 -0
  3. app.py +91 -4
  4. requirements.txt +8 -0
  5. static/index.html +505 -0
.gitignore ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 虚拟环境
2
+ venv/
3
+ env/
4
+ .env/
5
+ *.venv/
6
+
7
+ # 依赖文件
8
+ __pycache__/
9
+ *.py[cod]
10
+ *$py.class
11
+
12
+ # PyTorch相关
13
+ *.pt
14
+ *.pth
15
+
16
+ # 编辑器配置
17
+ .vscode/
18
+ .idea/
19
+ *.swp
20
+ *.swo
21
+ *~
22
+
23
+ # 日志文件
24
+ logs/
25
+ *.log
26
+
27
+ # 测试输出
28
+ .pytest_cache/
29
+ .coverage
30
+ coverage.xml
31
+
32
+ # Docker相关
33
+ .dockerignore
34
+ docker-compose.override.yml
35
+
36
+ # 环境变量
37
+ .env
38
+ .env.local
39
+ .env.*.local
40
+
41
+ # 构建输出
42
+ build/
43
+ dist/
44
+ *.egg-info/
45
+
46
+ # 操作系统文件
47
+ .DS_Store
48
+ Thumbs.db
49
+
50
+ # 临时文件
51
+ *.tmp
52
+ *.temp
53
+ .tmp/
54
+ .temp/
55
+
56
+ # Jupyter Notebook
57
+ .ipynb_checkpoints/
58
+ *.ipynb
59
+
60
+ # 数据文件
61
+ *.data
62
+ *.csv
63
+ *.jsonl
64
+
65
+ # 模型文件
66
+ models/
67
+ .cache/
68
+ .huggingface/
Dockerfile CHANGED
@@ -3,6 +3,12 @@
3
 
4
  FROM python:3.9
5
 
 
 
 
 
 
 
6
  RUN useradd -m -u 1000 user
7
  USER user
8
  ENV PATH="/home/user/.local/bin:$PATH"
 
3
 
4
  FROM python:3.9
5
 
6
+ # Install system dependencies
7
+ RUN apt-get update && apt-get install -y --no-install-recommends \
8
+ ffmpeg \
9
+ libsndfile1 \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
  RUN useradd -m -u 1000 user
13
  USER user
14
  ENV PATH="/home/user/.local/bin:$PATH"
app.py CHANGED
@@ -1,8 +1,95 @@
1
- from fastapi import FastAPI
 
 
 
 
 
 
 
2
 
3
- app = FastAPI()
 
 
 
 
 
 
 
4
 
5
  @app.get("/")
6
- def greet_json():
7
- return {"Hello": "World!"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
 
1
+ from fastapi import FastAPI, UploadFile, File
2
+ from fastapi.responses import JSONResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from faster_whisper import WhisperModel
5
+ from transformers import pipeline
6
+ import os
7
+ import tempfile
8
+ from pydub import AudioSegment
9
 
10
+ app = FastAPI(title="智能音频转录与摘要服务")
11
+
12
+ # 挂载静态文件目录,前端通过/front访问
13
+ app.mount("/front", StaticFiles(directory="static", html=True), name="static")
14
+
15
+ # 加载模型
16
+ whisper_model = WhisperModel("medium", device="cpu", compute_type="int8")
17
+ summarizer = pipeline("summarization", model="facebook/bart-large-cnn", device=-1)
18
 
19
  @app.get("/")
20
+ def root():
21
+ return {"message": "智能音频转录与摘要服务已启动"}
22
+
23
+ @app.post("/transcribe")
24
+ async def transcribe_audio(file: UploadFile = File(...)):
25
+ """上传音频文件,返回转录文本"""
26
+ try:
27
+ # 保存临时文件
28
+ with tempfile.NamedTemporaryFile(suffix=".tmp", delete=False) as temp_file:
29
+ temp_file.write(await file.read())
30
+ temp_file_path = temp_file.name
31
+
32
+ # 转换为wav格式(如果不是)
33
+ audio = AudioSegment.from_file(temp_file_path)
34
+ wav_path = temp_file_path + ".wav"
35
+ audio.export(wav_path, format="wav")
36
+
37
+ # 使用faster-whisper转录
38
+ segments, info = whisper_model.transcribe(wav_path, beam_size=5, language="zh")
39
+ transcription = "".join([segment.text for segment in segments])
40
+
41
+ # 清理临时文件
42
+ os.unlink(temp_file_path)
43
+ os.unlink(wav_path)
44
+
45
+ return JSONResponse(content={"transcription": transcription})
46
+ except Exception as e:
47
+ return JSONResponse(content={"error": str(e)}, status_code=500)
48
+
49
+ @app.post("/summarize")
50
+ async def summarize_text(text: dict):
51
+ """对文本进行摘要"""
52
+ try:
53
+ transcription = text.get("text", "")
54
+ if not transcription:
55
+ return JSONResponse(content={"error": "没有提供文本"}, status_code=400)
56
+
57
+ # 使用bart-large-cnn进行摘要
58
+ summary = summarizer(transcription, max_length=150, min_length=30, do_sample=False)
59
+
60
+ return JSONResponse(content={"summary": summary[0]["summary_text"]})
61
+ except Exception as e:
62
+ return JSONResponse(content={"error": str(e)}, status_code=500)
63
+
64
+ @app.post("/process")
65
+ async def process_audio(file: UploadFile = File(...)):
66
+ """上传音频文件,返回转录文本和摘要"""
67
+ try:
68
+ # 保存临时文件
69
+ with tempfile.NamedTemporaryFile(suffix=".tmp", delete=False) as temp_file:
70
+ temp_file.write(await file.read())
71
+ temp_file_path = temp_file.name
72
+
73
+ # 转换为wav格式
74
+ audio = AudioSegment.from_file(temp_file_path)
75
+ wav_path = temp_file_path + ".wav"
76
+ audio.export(wav_path, format="wav")
77
+
78
+ # 转录
79
+ segments, info = whisper_model.transcribe(wav_path, beam_size=5, language="zh")
80
+ transcription = "".join([segment.text for segment in segments])
81
+
82
+ # 摘要
83
+ summary = summarizer(transcription, max_length=150, min_length=30, do_sample=False)
84
+
85
+ # 清理临时文件
86
+ os.unlink(temp_file_path)
87
+ os.unlink(wav_path)
88
+
89
+ return JSONResponse(content={
90
+ "transcription": transcription,
91
+ "summary": summary[0]["summary_text"]
92
+ })
93
+ except Exception as e:
94
+ return JSONResponse(content={"error": str(e)}, status_code=500)
95
 
requirements.txt CHANGED
@@ -1,2 +1,10 @@
1
  fastapi
2
  uvicorn[standard]
 
 
 
 
 
 
 
 
 
1
  fastapi
2
  uvicorn[standard]
3
+ faster-whisper
4
+ transformers
5
+ torch
6
+ pydub
7
+ python-multipart
8
+ accelerate
9
+ optimum
10
+
static/index.html ADDED
@@ -0,0 +1,505 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 800px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 12px;
26
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
27
+ overflow: hidden;
28
+ }
29
+
30
+ .header {
31
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
32
+ color: white;
33
+ padding: 30px;
34
+ text-align: center;
35
+ }
36
+
37
+ .header h1 {
38
+ font-size: 28px;
39
+ margin-bottom: 10px;
40
+ }
41
+
42
+ .header p {
43
+ font-size: 16px;
44
+ opacity: 0.9;
45
+ }
46
+
47
+ .content {
48
+ padding: 30px;
49
+ }
50
+
51
+ .upload-section {
52
+ margin-bottom: 30px;
53
+ }
54
+
55
+ .upload-area {
56
+ border: 2px dashed #667eea;
57
+ border-radius: 8px;
58
+ padding: 40px;
59
+ text-align: center;
60
+ cursor: pointer;
61
+ transition: all 0.3s ease;
62
+ margin-bottom: 20px;
63
+ }
64
+
65
+ .upload-area:hover {
66
+ border-color: #764ba2;
67
+ background-color: #f8f9ff;
68
+ }
69
+
70
+ .upload-area.dragover {
71
+ border-color: #764ba2;
72
+ background-color: #f0f4ff;
73
+ }
74
+
75
+ .upload-area input[type="file"] {
76
+ display: none;
77
+ }
78
+
79
+ .upload-area label {
80
+ cursor: pointer;
81
+ color: #667eea;
82
+ font-weight: 600;
83
+ display: block;
84
+ margin-bottom: 10px;
85
+ }
86
+
87
+ .upload-area .file-info {
88
+ font-size: 14px;
89
+ color: #666;
90
+ }
91
+
92
+ .btn-group {
93
+ display: flex;
94
+ gap: 10px;
95
+ margin-bottom: 30px;
96
+ flex-wrap: wrap;
97
+ }
98
+
99
+ .btn {
100
+ padding: 12px 24px;
101
+ border: none;
102
+ border-radius: 6px;
103
+ font-size: 16px;
104
+ font-weight: 600;
105
+ cursor: pointer;
106
+ transition: all 0.3s ease;
107
+ flex: 1;
108
+ min-width: 150px;
109
+ }
110
+
111
+ .btn-primary {
112
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
113
+ color: white;
114
+ }
115
+
116
+ .btn-primary:hover {
117
+ transform: translateY(-2px);
118
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
119
+ }
120
+
121
+ .btn-secondary {
122
+ background-color: #f0f2f5;
123
+ color: #666;
124
+ }
125
+
126
+ .btn-secondary:hover {
127
+ background-color: #e4e6eb;
128
+ }
129
+
130
+ .btn:disabled {
131
+ opacity: 0.6;
132
+ cursor: not-allowed;
133
+ transform: none;
134
+ }
135
+
136
+ .progress-section {
137
+ margin-bottom: 30px;
138
+ display: none;
139
+ }
140
+
141
+ .progress-bar {
142
+ width: 100%;
143
+ height: 8px;
144
+ background-color: #e0e0e0;
145
+ border-radius: 4px;
146
+ overflow: hidden;
147
+ margin-bottom: 10px;
148
+ }
149
+
150
+ .progress-fill {
151
+ height: 100%;
152
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
153
+ border-radius: 4px;
154
+ width: 0%;
155
+ transition: width 0.3s ease;
156
+ }
157
+
158
+ .progress-text {
159
+ font-size: 14px;
160
+ color: #666;
161
+ text-align: center;
162
+ }
163
+
164
+ .result-section {
165
+ margin-bottom: 30px;
166
+ }
167
+
168
+ .result-card {
169
+ border: 1px solid #e0e0e0;
170
+ border-radius: 8px;
171
+ padding: 20px;
172
+ margin-bottom: 20px;
173
+ background-color: #fafafa;
174
+ }
175
+
176
+ .result-card h3 {
177
+ color: #333;
178
+ margin-bottom: 15px;
179
+ font-size: 18px;
180
+ display: flex;
181
+ align-items: center;
182
+ gap: 10px;
183
+ }
184
+
185
+ .result-card h3::before {
186
+ content: '';
187
+ width: 4px;
188
+ height: 20px;
189
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
190
+ border-radius: 2px;
191
+ }
192
+
193
+ .result-content {
194
+ background-color: white;
195
+ border: 1px solid #e0e0e0;
196
+ border-radius: 6px;
197
+ padding: 15px;
198
+ min-height: 100px;
199
+ font-size: 16px;
200
+ line-height: 1.6;
201
+ color: #333;
202
+ white-space: pre-wrap;
203
+ }
204
+
205
+ .copy-btn {
206
+ background-color: #667eea;
207
+ color: white;
208
+ border: none;
209
+ padding: 8px 16px;
210
+ border-radius: 4px;
211
+ font-size: 14px;
212
+ cursor: pointer;
213
+ margin-top: 10px;
214
+ transition: all 0.3s ease;
215
+ }
216
+
217
+ .copy-btn:hover {
218
+ background-color: #5a6fd8;
219
+ }
220
+
221
+ .status-message {
222
+ padding: 12px;
223
+ border-radius: 6px;
224
+ margin-bottom: 20px;
225
+ font-size: 14px;
226
+ display: none;
227
+ }
228
+
229
+ .status-message.success {
230
+ background-color: #d4edda;
231
+ color: #155724;
232
+ border: 1px solid #c3e6cb;
233
+ }
234
+
235
+ .status-message.error {
236
+ background-color: #f8d7da;
237
+ color: #721c24;
238
+ border: 1px solid #f5c6cb;
239
+ }
240
+
241
+ @media (max-width: 768px) {
242
+ .container {
243
+ margin: 10px;
244
+ border-radius: 8px;
245
+ }
246
+
247
+ .header {
248
+ padding: 20px;
249
+ }
250
+
251
+ .header h1 {
252
+ font-size: 24px;
253
+ }
254
+
255
+ .content {
256
+ padding: 20px;
257
+ }
258
+
259
+ .upload-area {
260
+ padding: 30px 20px;
261
+ }
262
+
263
+ .btn-group {
264
+ flex-direction: column;
265
+ }
266
+
267
+ .btn {
268
+ min-width: auto;
269
+ }
270
+ }
271
+ </style>
272
+ </head>
273
+ <body>
274
+ <div class="container">
275
+ <div class="header">
276
+ <h1>🎙️ 智能音频转录与摘要服务</h1>
277
+ <p>上传音频文件,自动转成文字并生成摘要</p>
278
+ </div>
279
+
280
+ <div class="content">
281
+ <div class="status-message" id="statusMessage"></div>
282
+
283
+ <div class="upload-section">
284
+ <div class="upload-area" id="uploadArea">
285
+ <input type="file" id="audioFile" accept="audio/*">
286
+ <label for="audioFile">📁 点击选择音频文件或拖拽文件到此处</label>
287
+ <div class="file-info" id="fileInfo">支持 MP3、WAV、M4A 等格式</div>
288
+ </div>
289
+ </div>
290
+
291
+ <div class="btn-group">
292
+ <button class="btn btn-primary" id="transcribeBtn" disabled>🔊 仅转录</button>
293
+ <button class="btn btn-primary" id="processBtn" disabled>✨ 转录+摘要</button>
294
+ </div>
295
+
296
+ <div class="progress-section" id="progressSection">
297
+ <div class="progress-bar">
298
+ <div class="progress-fill" id="progressFill"></div>
299
+ </div>
300
+ <div class="progress-text" id="progressText">准备中...</div>
301
+ </div>
302
+
303
+ <div class="result-section">
304
+ <div class="result-card">
305
+ <h3>📝 转录结果</h3>
306
+ <div class="result-content" id="transcriptionResult">请上传音频文件并点击转录按钮</div>
307
+ <button class="copy-btn" id="copyTranscriptionBtn">📋 复制转录内容</button>
308
+ </div>
309
+
310
+ <div class="result-card">
311
+ <h3>📋 摘要结果</h3>
312
+ <div class="result-content" id="summaryResult">请先获取转录结果</div>
313
+ <button class="copy-btn" id="copySummaryBtn">📋 复制摘要内容</button>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ </div>
318
+
319
+ <script>
320
+ const apiBaseUrl = 'https://caoyanyi-ai.hf.space';
321
+ let selectedFile = null;
322
+
323
+ // DOM 元素
324
+ const uploadArea = document.getElementById('uploadArea');
325
+ const audioFileInput = document.getElementById('audioFile');
326
+ const fileInfo = document.getElementById('fileInfo');
327
+ const transcribeBtn = document.getElementById('transcribeBtn');
328
+ const processBtn = document.getElementById('processBtn');
329
+ const progressSection = document.getElementById('progressSection');
330
+ const progressFill = document.getElementById('progressFill');
331
+ const progressText = document.getElementById('progressText');
332
+ const transcriptionResult = document.getElementById('transcriptionResult');
333
+ const summaryResult = document.getElementById('summaryResult');
334
+ const copyTranscriptionBtn = document.getElementById('copyTranscriptionBtn');
335
+ const copySummaryBtn = document.getElementById('copySummaryBtn');
336
+ const statusMessage = document.getElementById('statusMessage');
337
+
338
+ // 文件选择处理
339
+ audioFileInput.addEventListener('change', (e) => {
340
+ handleFileSelection(e.target.files[0]);
341
+ });
342
+
343
+ // 拖拽处理
344
+ uploadArea.addEventListener('dragover', (e) => {
345
+ e.preventDefault();
346
+ uploadArea.classList.add('dragover');
347
+ });
348
+
349
+ uploadArea.addEventListener('dragleave', () => {
350
+ uploadArea.classList.remove('dragover');
351
+ });
352
+
353
+ uploadArea.addEventListener('drop', (e) => {
354
+ e.preventDefault();
355
+ uploadArea.classList.remove('dragover');
356
+ handleFileSelection(e.dataTransfer.files[0]);
357
+ });
358
+
359
+ function handleFileSelection(file) {
360
+ if (file && file.type.startsWith('audio/')) {
361
+ selectedFile = file;
362
+ fileInfo.textContent = `已选择: ${file.name} (${formatFileSize(file.size)})`;
363
+ transcribeBtn.disabled = false;
364
+ processBtn.disabled = false;
365
+ showStatus('文件已选择,点击按钮开始处理', 'success');
366
+ } else {
367
+ showStatus('请选择有效的音频文件', 'error');
368
+ }
369
+ }
370
+
371
+ // 按钮事件
372
+ transcribeBtn.addEventListener('click', () => {
373
+ if (selectedFile) {
374
+ transcribeAudio();
375
+ }
376
+ });
377
+
378
+ processBtn.addEventListener('click', () => {
379
+ if (selectedFile) {
380
+ processAudio();
381
+ }
382
+ });
383
+
384
+ // 复制按钮
385
+ copyTranscriptionBtn.addEventListener('click', () => {
386
+ copyToClipboard(transcriptionResult.textContent, '转录内容已复制到剪贴板');
387
+ });
388
+
389
+ copySummaryBtn.addEventListener('click', () => {
390
+ copyToClipboard(summaryResult.textContent, '摘要内容已复制到剪贴板');
391
+ });
392
+
393
+ // API调用:仅转录
394
+ async function transcribeAudio() {
395
+ setLoading(true, '正在转录音频...');
396
+ try {
397
+ const formData = new FormData();
398
+ formData.append('file', selectedFile);
399
+
400
+ const response = await fetch(`${apiBaseUrl}/transcribe`, {
401
+ method: 'POST',
402
+ body: formData
403
+ });
404
+
405
+ if (!response.ok) {
406
+ throw new Error('转录失败');
407
+ }
408
+
409
+ const data = await response.json();
410
+ transcriptionResult.textContent = data.transcription;
411
+ showStatus('转录完成', 'success');
412
+ } catch (error) {
413
+ showStatus(`转录失败: ${error.message}`, 'error');
414
+ console.error('转录错误:', error);
415
+ } finally {
416
+ setLoading(false);
417
+ }
418
+ }
419
+
420
+ // API调用:转录+摘要
421
+ async function processAudio() {
422
+ setLoading(true, '正在处理音频...');
423
+ try {
424
+ const formData = new FormData();
425
+ formData.append('file', selectedFile);
426
+
427
+ const response = await fetch(`${apiBaseUrl}/process`, {
428
+ method: 'POST',
429
+ body: formData
430
+ });
431
+
432
+ if (!response.ok) {
433
+ throw new Error('处理失败');
434
+ }
435
+
436
+ const data = await response.json();
437
+ transcriptionResult.textContent = data.transcription;
438
+ summaryResult.textContent = data.summary;
439
+ showStatus('音频处理完成', 'success');
440
+ } catch (error) {
441
+ showStatus(`处理失败: ${error.message}`, 'error');
442
+ console.error('处理错误:', error);
443
+ } finally {
444
+ setLoading(false);
445
+ }
446
+ }
447
+
448
+ // 辅助函数
449
+ function setLoading(isLoading, message = '') {
450
+ if (isLoading) {
451
+ progressSection.style.display = 'block';
452
+ progressFill.style.width = '0%';
453
+ progressText.textContent = message;
454
+ transcribeBtn.disabled = true;
455
+ processBtn.disabled = true;
456
+
457
+ // 模拟进度
458
+ let progress = 0;
459
+ const interval = setInterval(() => {
460
+ progress += Math.random() * 10;
461
+ if (progress > 90) {
462
+ clearInterval(interval);
463
+ }
464
+ progressFill.style.width = `${progress}%`;
465
+ }, 500);
466
+ window.loadingInterval = interval;
467
+ } else {
468
+ progressSection.style.display = 'none';
469
+ transcribeBtn.disabled = !selectedFile;
470
+ processBtn.disabled = !selectedFile;
471
+ if (window.loadingInterval) {
472
+ clearInterval(window.loadingInterval);
473
+ }
474
+ }
475
+ }
476
+
477
+ function showStatus(message, type) {
478
+ statusMessage.textContent = message;
479
+ statusMessage.className = `status-message ${type}`;
480
+ statusMessage.style.display = 'block';
481
+
482
+ setTimeout(() => {
483
+ statusMessage.style.display = 'none';
484
+ }, 5000);
485
+ }
486
+
487
+ function copyToClipboard(text, successMessage) {
488
+ navigator.clipboard.writeText(text).then(() => {
489
+ showStatus(successMessage, 'success');
490
+ }).catch(err => {
491
+ showStatus('复制失败', 'error');
492
+ console.error('复制错误:', err);
493
+ });
494
+ }
495
+
496
+ function formatFileSize(bytes) {
497
+ if (bytes === 0) return '0 Bytes';
498
+ const k = 1024;
499
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
500
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
501
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
502
+ }
503
+ </script>
504
+ </body>
505
+ </html>