MichaelChou0806 commited on
Commit
6ddb6b7
·
verified ·
1 Parent(s): 6c57120

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -328
app.py CHANGED
@@ -37,7 +37,9 @@ def _dataurl_to_file(data_url: str, orig_name: str | None = None) -> str:
37
 
38
  def _extract_effective_path(file_obj) -> str:
39
  """從各種格式中提取有效檔案路徑"""
40
- # 字串模式
 
 
41
  if isinstance(file_obj, str):
42
  s = file_obj.strip().strip('"')
43
  if s.startswith("data:"):
@@ -45,8 +47,9 @@ def _extract_effective_path(file_obj) -> str:
45
  if os.path.isfile(s):
46
  return s
47
 
48
- # 字典模式
49
  if isinstance(file_obj, dict):
 
50
  data = file_obj.get("data")
51
  if isinstance(data, str) and data.startswith("data:"):
52
  return _dataurl_to_file(data, file_obj.get("orig_name"))
@@ -54,15 +57,23 @@ def _extract_effective_path(file_obj) -> str:
54
  if p and os.path.isfile(p):
55
  return p
56
 
57
- # 物件模式
58
- for attr in ("name", "path"):
59
- p = getattr(file_obj, attr, None)
60
- if isinstance(p, str):
61
- s = p.strip().strip('"')
62
- if os.path.isfile(s):
63
- return s
 
 
 
 
 
 
 
 
64
 
65
- raise FileNotFoundError("Cannot parse uploaded file")
66
 
67
  def split_audio(path):
68
  """將音訊檔案分割成多個小於 25MB 的片段"""
@@ -139,36 +150,48 @@ def transcribe_core(path, model="whisper-1"):
139
 
140
  # ====== Gradio UI 函式 ======
141
  def transcribe_web(password, audio_file):
142
- """網頁版轉錄處理"""
143
- print(f"\n🌐 [WEB] 收到網頁請求")
 
 
 
 
144
 
145
  # 驗證密碼
146
- if not password or password.strip() != PASSWORD:
147
- return "Incorrect password. Please try again.", "", ""
 
 
 
 
 
148
 
149
  # 檢查檔案
150
  if not audio_file:
151
- return "⚠️ Please upload an audio file first.", "", ""
 
152
 
153
  try:
154
  # 處理檔案
 
155
  path = _extract_effective_path(audio_file)
156
- print(f"[WEB] 檔案路徑: {path}")
157
 
158
  # 轉錄
 
159
  text, summary = transcribe_core(path)
160
 
161
  # 統計資訊
162
  char_count = len(text)
163
- status = f"✅ Transcription completed successfully!\n📝 Total characters: {char_count}"
164
 
165
- print(f"[WEB] ✅ 成功完成")
166
  return status, text, summary
167
 
168
  except Exception as e:
169
  import traceback
170
  error_msg = traceback.format_exc()
171
- print(f"❌ [WEB] 錯誤:\n{error_msg}")
172
  return f"❌ Error: {str(e)}", "", ""
173
 
174
  # ====== FastAPI 應用 ======
@@ -187,11 +210,14 @@ async def api_transcribe(request: Request):
187
  """API 端點 - 用於手機等外部調用"""
188
  try:
189
  body = await request.json()
190
- print(f"\n📱 [API] 收到 API 請求")
 
 
191
 
192
  # 驗證密碼
193
  password = body.get("password", "")
194
  if password.strip() != PASSWORD:
 
195
  return JSONResponse(
196
  status_code=401,
197
  content={"status": "error", "error": "Password incorrect"}
@@ -202,6 +228,7 @@ async def api_transcribe(request: Request):
202
  file_name = body.get("file_name", "recording.m4a")
203
 
204
  if not file_data or not file_data.startswith("data:"):
 
205
  return JSONResponse(
206
  status_code=400,
207
  content={"status": "error", "error": "Invalid file data format"}
@@ -210,7 +237,7 @@ async def api_transcribe(request: Request):
210
  # 處理檔案
211
  file_dict = {"data": file_data, "orig_name": file_name}
212
  path = _extract_effective_path(file_dict)
213
- print(f"[API] 檔案解析成功: {path}")
214
 
215
  # 轉錄
216
  text, summary = transcribe_core(path)
@@ -221,355 +248,109 @@ async def api_transcribe(request: Request):
221
  "summary": summary
222
  }
223
 
224
- print(f"[API] ✅ 成功完成\n")
225
  return JSONResponse(content=result)
226
 
227
  except Exception as e:
228
  import traceback
229
  error_trace = traceback.format_exc()
230
- print(f"❌ [API] 錯誤:\n{error_trace}\n")
231
  return JSONResponse(
232
  status_code=500,
233
  content={"status": "error", "error": str(e)}
234
  )
235
 
236
- # ====== 自定義樣式 ======
237
- custom_css = """
238
- /* 全局設定 */
239
- * {
240
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
241
- }
242
-
243
- .gradio-container {
244
- max-width: 1400px !important;
245
- margin: 0 auto !important;
246
- }
247
-
248
- /* 主容器 */
249
- .main-container {
250
- padding: 2rem;
251
- }
252
-
253
- /* 標題區 */
254
- .hero-section {
255
- text-align: center;
256
- padding: 3rem 2rem;
257
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
258
- border-radius: 16px;
259
- margin-bottom: 3rem;
260
- box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
261
- }
262
-
263
- .hero-section h1 {
264
- color: white;
265
- font-size: 2.5rem;
266
- font-weight: 700;
267
- margin: 0 0 0.5rem 0;
268
- letter-spacing: -0.02em;
269
- }
270
-
271
- .hero-section p {
272
- color: rgba(255, 255, 255, 0.9);
273
- font-size: 1.15rem;
274
- margin: 0;
275
- }
276
-
277
- /* 卡片樣式 */
278
- .card {
279
- background: white;
280
- border-radius: 12px;
281
- padding: 2rem;
282
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
283
- margin-bottom: 1.5rem;
284
- }
285
-
286
- .card h2 {
287
- font-size: 1.5rem;
288
- font-weight: 600;
289
- margin: 0 0 1.5rem 0;
290
- color: #1f2937;
291
- }
292
-
293
- /* 輸入框樣式 */
294
- .input-group {
295
- margin-bottom: 1.5rem;
296
- }
297
-
298
- .input-group label {
299
- display: block;
300
- font-weight: 600;
301
- color: #374151;
302
- margin-bottom: 0.5rem;
303
- font-size: 0.95rem;
304
- }
305
-
306
- input[type="password"],
307
- textarea {
308
- width: 100%;
309
- padding: 0.75rem;
310
- border: 2px solid #e5e7eb;
311
- border-radius: 8px;
312
- font-size: 0.95rem;
313
- transition: all 0.2s;
314
- }
315
-
316
- input[type="password"]:focus,
317
- textarea:focus {
318
- outline: none;
319
- border-color: #667eea;
320
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
321
- }
322
-
323
- /* 按鈕樣式 */
324
- button.primary-btn {
325
- width: 100%;
326
- padding: 1rem 2rem !important;
327
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
328
- border: none !important;
329
- color: white !important;
330
- font-size: 1.1rem !important;
331
- font-weight: 600 !important;
332
- border-radius: 10px !important;
333
- cursor: pointer !important;
334
- transition: all 0.3s !important;
335
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3) !important;
336
- }
337
-
338
- button.primary-btn:hover {
339
- transform: translateY(-2px) !important;
340
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
341
- }
342
-
343
- /* 檔案上傳區 */
344
- .file-upload-area {
345
- border: 2px dashed #d1d5db;
346
- border-radius: 12px;
347
- padding: 2.5rem;
348
- text-align: center;
349
- background: #f9fafb;
350
- transition: all 0.3s;
351
- cursor: pointer;
352
- }
353
-
354
- .file-upload-area:hover {
355
- border-color: #667eea;
356
- background: #f0f4ff;
357
- }
358
-
359
- /* 狀態框 */
360
- .status-box {
361
- padding: 1rem;
362
- border-radius: 8px;
363
- margin-bottom: 1rem;
364
- font-size: 0.95rem;
365
- line-height: 1.5;
366
- }
367
-
368
- .status-success {
369
- background: #d1fae5;
370
- border-left: 4px solid #10b981;
371
- color: #065f46;
372
- }
373
-
374
- .status-error {
375
- background: #fee2e2;
376
- border-left: 4px solid #ef4444;
377
- color: #991b1b;
378
- }
379
-
380
- .status-warning {
381
- background: #fef3c7;
382
- border-left: 4px solid #f59e0b;
383
- color: #92400e;
384
- }
385
-
386
- /* 結果文字框 */
387
- textarea.result-text {
388
- min-height: 200px !important;
389
- font-family: "SF Mono", Monaco, monospace !important;
390
- font-size: 0.9rem !important;
391
- line-height: 1.6 !important;
392
- background: #f9fafb !important;
393
- }
394
-
395
- /* 資訊提示 */
396
- .info-banner {
397
- background: #eff6ff;
398
- border: 1px solid #bfdbfe;
399
- border-radius: 8px;
400
- padding: 1rem;
401
- margin: 1rem 0;
402
- font-size: 0.9rem;
403
- color: #1e40af;
404
- }
405
-
406
- /* 分隔線 */
407
- .divider {
408
- height: 1px;
409
- background: #e5e7eb;
410
- margin: 2rem 0;
411
- }
412
-
413
- /* API 文檔區 */
414
- .api-section {
415
- background: #f9fafb;
416
- border-radius: 12px;
417
- padding: 2rem;
418
- margin-top: 2rem;
419
- }
420
-
421
- .api-section h3 {
422
- font-size: 1.25rem;
423
- font-weight: 600;
424
- color: #1f2937;
425
- margin: 0 0 1rem 0;
426
- }
427
-
428
- .api-endpoint {
429
- background: #1f2937;
430
- color: #f3f4f6;
431
- padding: 1rem;
432
- border-radius: 8px;
433
- font-family: monospace;
434
- font-size: 0.9rem;
435
- margin: 1rem 0;
436
- }
437
-
438
- /* 響應式設計 */
439
- @media (max-width: 768px) {
440
- .hero-section h1 {
441
- font-size: 2rem;
442
- }
443
-
444
- .card {
445
- padding: 1.5rem;
446
- }
447
- }
448
- """
449
-
450
  # ====== Gradio 介面 ======
451
- with gr.Blocks(css=custom_css, theme=gr.themes.Soft(), title="Audio Transcription Service") as demo:
452
 
453
- # 標題
454
- gr.HTML("""
455
- <div class="hero-section">
456
- <h1>🎧 Audio Transcription Service</h1>
457
- <p>AI-Powered Speech Recognition & Summarization</p>
458
- </div>
459
  """)
460
 
461
- # 主要上傳區域
462
- gr.HTML('<div class="card">')
463
- gr.Markdown("## 🎵 Upload & Transcribe")
464
-
465
  with gr.Row():
466
  with gr.Column(scale=1):
 
 
467
  password_input = gr.Textbox(
468
- label="🔐 Password",
469
  type="password",
470
- placeholder="Enter password",
471
- elem_classes="input-group"
472
  )
473
 
474
- audio_input = gr.File(
475
- label="📁 Audio File",
476
- file_types=["audio", ".mp4"],
477
- file_count="single",
478
- elem_classes="file-upload-area"
479
  )
480
 
481
- gr.HTML("""
482
- <div class="info-banner">
483
- <strong>💡 Supported formats:</strong> MP3, M4A, WAV, OGG, WEBM, MP4<br>
484
- <strong>📦 File size:</strong> Automatic chunking for large files
485
- </div>
486
- """)
487
-
488
- submit_button = gr.Button(
489
  "🚀 Start Transcription",
490
  variant="primary",
491
- elem_classes="primary-btn"
492
  )
 
 
 
 
 
 
 
 
493
 
494
  with gr.Column(scale=2):
 
 
495
  status_output = gr.Textbox(
496
- label="📊 Status",
497
  interactive=False,
498
- lines=2,
499
- elem_classes="status-box"
500
  )
501
 
502
  transcription_output = gr.Textbox(
503
- label="📝 Transcription Result",
504
- lines=15,
505
- placeholder="Transcription will appear here...",
506
- show_copy_button=True,
507
- elem_classes="result-text"
508
  )
509
 
510
  summary_output = gr.Textbox(
511
- label="💡 AI Summary",
512
  lines=6,
513
- placeholder="AI-generated summary will appear here...",
514
- show_copy_button=True,
515
- elem_classes="result-text"
516
  )
517
 
518
- gr.HTML('</div>')
519
 
520
- # API 文檔
521
- gr.HTML('<div class="api-section">')
522
- gr.Markdown("## 📱 API Integration")
523
  gr.Markdown("""
524
- ### For Mobile Apps & External Services
525
-
526
- **Endpoint:** `POST /api/transcribe`
527
-
528
- **Request Body (JSON):**
529
- ```json
530
- {
531
- "password": "your_password",
532
- "file_data": "data:audio/m4a;base64,...",
533
- "file_name": "recording.m4a"
534
- }
535
- ```
536
-
537
- **Response:**
538
- ```json
539
- {
540
- "status": "success",
541
- "transcription": "Full text...",
542
- "summary": "Summary..."
543
- }
544
- ```
545
-
546
- **Features:**
547
- - ✅ Fully synchronous - returns complete results
548
- - ✅ Automatic file chunking for large files
549
- - ✅ Traditional Chinese output
550
- - ✅ AI-powered summarization
551
-
552
- **Use Cases:**
553
- - iPhone Shortcuts automation
554
- - Mobile app integration
555
- - Webhook processing
556
- - Batch transcription systems
557
- """)
558
- gr.HTML('</div>')
559
 
560
- # 頁腳
561
- gr.HTML("""
562
- <div style="text-align: center; margin-top: 3rem; padding: 1.5rem; color: #6b7280; font-size: 0.9rem;">
563
- <p><strong>Audio Transcription Service</strong> v2.0</p>
564
- <p>Powered by OpenAI Whisper & GPT-4</p>
565
- </div>
 
 
 
 
 
 
 
 
 
 
 
566
  """)
567
 
568
- # 綁定事件
569
- submit_button.click(
570
  fn=transcribe_web,
571
  inputs=[password_input, audio_input],
572
- outputs=[status_output, transcription_output, summary_output]
 
573
  )
574
 
575
  # ====== 掛載到 FastAPI ======
@@ -578,9 +359,9 @@ app = gr.mount_gradio_app(fastapi_app, demo, path="/")
578
  # ====== 啟動 ======
579
  if __name__ == "__main__":
580
  print("\n" + "="*60)
581
- print("🚀 啟動服務")
582
- print("🌐 網頁介面: http://0.0.0.0:7860")
583
- print("📱 API 端點: http://0.0.0.0:7860/api/transcribe")
584
  print("="*60 + "\n")
585
  import uvicorn
586
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
37
 
38
  def _extract_effective_path(file_obj) -> str:
39
  """從各種格式中提取有效檔案路徑"""
40
+ print(f"[DEBUG] 檔案物件類型: {type(file_obj)}")
41
+
42
+ # 如果是字串路徑
43
  if isinstance(file_obj, str):
44
  s = file_obj.strip().strip('"')
45
  if s.startswith("data:"):
 
47
  if os.path.isfile(s):
48
  return s
49
 
50
+ # 如果是字典
51
  if isinstance(file_obj, dict):
52
+ print(f"[DEBUG] 字典 keys: {list(file_obj.keys())}")
53
  data = file_obj.get("data")
54
  if isinstance(data, str) and data.startswith("data:"):
55
  return _dataurl_to_file(data, file_obj.get("orig_name"))
 
57
  if p and os.path.isfile(p):
58
  return p
59
 
60
+ # 如果是物件,嘗試獲取 path 或 name 屬性
61
+ if hasattr(file_obj, 'name') and file_obj.name:
62
+ if os.path.isfile(file_obj.name):
63
+ return file_obj.name
64
+
65
+ if hasattr(file_obj, 'path') and file_obj.path:
66
+ if os.path.isfile(file_obj.path):
67
+ return file_obj.path
68
+
69
+ # 最後嘗試:直接當作路徑字串
70
+ try:
71
+ if os.path.isfile(str(file_obj)):
72
+ return str(file_obj)
73
+ except:
74
+ pass
75
 
76
+ raise FileNotFoundError(f"Cannot parse uploaded file: {file_obj}")
77
 
78
  def split_audio(path):
79
  """將音訊檔案分割成多個小於 25MB 的片段"""
 
150
 
151
  # ====== Gradio UI 函式 ======
152
  def transcribe_web(password, audio_file):
153
+ """網頁版轉錄處理 - 必須返回三個值"""
154
+ print(f"\n{'='*60}")
155
+ print(f"🌐 [WEB] 收到網頁請求")
156
+ print(f"密碼: {'已提供' if password else '未提供'}")
157
+ print(f"檔案: {audio_file}")
158
+ print(f"{'='*60}")
159
 
160
  # 驗證密碼
161
+ if not password:
162
+ print("[WEB]密碼為空")
163
+ return "❌ Please enter password", "", ""
164
+
165
+ if password.strip() != PASSWORD:
166
+ print(f"[WEB] ❌ 密碼錯誤: '{password}' != '{PASSWORD}'")
167
+ return "❌ Incorrect password", "", ""
168
 
169
  # 檢查檔案
170
  if not audio_file:
171
+ print("[WEB] 未上傳檔案")
172
+ return "⚠️ Please upload an audio file", "", ""
173
 
174
  try:
175
  # 處理檔案
176
+ print(f"[WEB] 開始處理檔案...")
177
  path = _extract_effective_path(audio_file)
178
+ print(f"[WEB] 檔案路徑: {path}")
179
 
180
  # 轉錄
181
+ print(f"[WEB] 開始轉錄...")
182
  text, summary = transcribe_core(path)
183
 
184
  # 統計資訊
185
  char_count = len(text)
186
+ status = f"✅ Completed! ({char_count} characters)"
187
 
188
+ print(f"[WEB] ✅ 轉錄成功\n")
189
  return status, text, summary
190
 
191
  except Exception as e:
192
  import traceback
193
  error_msg = traceback.format_exc()
194
+ print(f"❌ [WEB] 發生錯誤:\n{error_msg}\n")
195
  return f"❌ Error: {str(e)}", "", ""
196
 
197
  # ====== FastAPI 應用 ======
 
210
  """API 端點 - 用於手機等外部調用"""
211
  try:
212
  body = await request.json()
213
+ print(f"\n{'='*60}")
214
+ print(f"📱 [API] 收到 API 請求")
215
+ print(f"{'='*60}")
216
 
217
  # 驗證密碼
218
  password = body.get("password", "")
219
  if password.strip() != PASSWORD:
220
+ print(f"[API] ❌ 密碼錯誤")
221
  return JSONResponse(
222
  status_code=401,
223
  content={"status": "error", "error": "Password incorrect"}
 
228
  file_name = body.get("file_name", "recording.m4a")
229
 
230
  if not file_data or not file_data.startswith("data:"):
231
+ print(f"[API] ❌ 檔案格式錯誤")
232
  return JSONResponse(
233
  status_code=400,
234
  content={"status": "error", "error": "Invalid file data format"}
 
237
  # 處理檔案
238
  file_dict = {"data": file_data, "orig_name": file_name}
239
  path = _extract_effective_path(file_dict)
240
+ print(f"[API] 檔案解析成功: {path}")
241
 
242
  # 轉錄
243
  text, summary = transcribe_core(path)
 
248
  "summary": summary
249
  }
250
 
251
+ print(f"[API] ✅ 轉錄成功\n")
252
  return JSONResponse(content=result)
253
 
254
  except Exception as e:
255
  import traceback
256
  error_trace = traceback.format_exc()
257
+ print(f"❌ [API] 發生錯誤:\n{error_trace}\n")
258
  return JSONResponse(
259
  status_code=500,
260
  content={"status": "error", "error": str(e)}
261
  )
262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  # ====== Gradio 介面 ======
264
+ with gr.Blocks(title="Audio Transcription", theme=gr.themes.Soft()) as demo:
265
 
266
+ gr.Markdown("""
267
+ # 🎧 Audio Transcription Service
268
+ ### AI-Powered Speech-to-Text with Summarization
 
 
 
269
  """)
270
 
 
 
 
 
271
  with gr.Row():
272
  with gr.Column(scale=1):
273
+ gr.Markdown("### 📤 Upload")
274
+
275
  password_input = gr.Textbox(
276
+ label="Password",
277
  type="password",
278
+ placeholder="Enter password"
 
279
  )
280
 
281
+ audio_input = gr.Audio(
282
+ label="Audio File",
283
+ type="filepath",
284
+ sources=["upload"]
 
285
  )
286
 
287
+ submit_btn = gr.Button(
 
 
 
 
 
 
 
288
  "🚀 Start Transcription",
289
  variant="primary",
290
+ size="lg"
291
  )
292
+
293
+ gr.Markdown("""
294
+ **Supported formats:**
295
+ MP3, M4A, WAV, OGG, WEBM, MP4
296
+
297
+ **Processing:**
298
+ Automatic chunking for large files
299
+ """)
300
 
301
  with gr.Column(scale=2):
302
+ gr.Markdown("### 📊 Results")
303
+
304
  status_output = gr.Textbox(
305
+ label="Status",
306
  interactive=False,
307
+ lines=1
 
308
  )
309
 
310
  transcription_output = gr.Textbox(
311
+ label="Transcription",
312
+ lines=12,
313
+ show_copy_button=True
 
 
314
  )
315
 
316
  summary_output = gr.Textbox(
317
+ label="Summary",
318
  lines=6,
319
+ show_copy_button=True
 
 
320
  )
321
 
322
+ gr.Markdown("---")
323
 
 
 
 
324
  gr.Markdown("""
325
+ ## 📱 API Integration
326
+
327
+ **Endpoint:** `POST /api/transcribe`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
+ **Request:**
330
+ ```json
331
+ {
332
+ "password": "your_password",
333
+ "file_data": "data:audio/m4a;base64,...",
334
+ "file_name": "recording.m4a"
335
+ }
336
+ ```
337
+
338
+ **Response:**
339
+ ```json
340
+ {
341
+ "status": "success",
342
+ "transcription": "...",
343
+ "summary": "..."
344
+ }
345
+ ```
346
  """)
347
 
348
+ # 事件綁定 - 這是關鍵!
349
+ submit_btn.click(
350
  fn=transcribe_web,
351
  inputs=[password_input, audio_input],
352
+ outputs=[status_output, transcription_output, summary_output],
353
+ api_name="transcribe"
354
  )
355
 
356
  # ====== 掛載到 FastAPI ======
 
359
  # ====== 啟動 ======
360
  if __name__ == "__main__":
361
  print("\n" + "="*60)
362
+ print("🚀 服務啟動")
363
+ print("🌐 網頁: http://0.0.0.0:7860")
364
+ print("📱 API: http://0.0.0.0:7860/api/transcribe")
365
  print("="*60 + "\n")
366
  import uvicorn
367
  uvicorn.run(app, host="0.0.0.0", port=7860)