Nior18867 commited on
Commit
023fd98
·
verified ·
1 Parent(s): 5a8f984

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. Dockerfile +17 -0
  2. README.md +6 -6
  3. app.py +507 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install ffmpeg for audio processing
6
+ RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
7
+
8
+ RUN pip install --no-cache-dir gradio==3.50.2 requests>=2.28.0
9
+
10
+ COPY app.py .
11
+
12
+
13
+ EXPOSE 7860
14
+ ENV GRADIO_SERVER_NAME="0.0.0.0"
15
+ ENV GRADIO_SERVER_PORT="7860"
16
+
17
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,10 @@
1
  ---
2
- title: Infinitetalk
3
- emoji: 🐨
4
- colorFrom: indigo
5
- colorTo: pink
6
  sdk: docker
7
  pinned: false
 
 
8
  ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: InfiniteTalk
3
+ emoji: 🎬
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
+ short_description: Generate talking videos from image and audio.
10
  ---
 
 
app.py ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ InfiniteTalk - Powered by WaveSpeed AI
3
+ Auto-generated by Space Generator
4
+ """
5
+
6
+ import gradio as gr
7
+ import requests
8
+ import time
9
+ from typing import Dict, Any
10
+
11
+ # ============ API Configuration ============
12
+ WAVESPEED_API_BASE = "https://api.wavespeed.ai/api/v3"
13
+ UPLOAD_ENDPOINT = "https://api.wavespeed.ai/api/v3/media/upload/binary"
14
+ MODEL_ENDPOINT = "wavespeed-ai/infinitetalk"
15
+ POLL_INTERVAL = 1.5
16
+ POLL_MAX_SECONDS = 300
17
+
18
+
19
+
20
+ # ============ CSS ============
21
+ CUSTOM_CSS = """
22
+ /* ===== Base Styles ===== */
23
+ html, body, .gradio-container {
24
+ background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 50%, #e0e7ff 100%) !important;
25
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
26
+ }
27
+
28
+ .gradio-container {
29
+ max-width: 960px !important;
30
+ margin: 0 auto !important;
31
+ padding: 24px !important;
32
+ }
33
+
34
+ /* ===== Hero Section ===== */
35
+ .hero-container {
36
+ text-align: center;
37
+ padding: 48px 20px 36px;
38
+ }
39
+
40
+ .hero-badge {
41
+ display: inline-block;
42
+ background: linear-gradient(135deg, #8b5cf6, #7c3aed);
43
+ padding: 10px 24px;
44
+ border-radius: 50px;
45
+ font-size: 0.7rem;
46
+ color: #fff;
47
+ font-weight: 700;
48
+ letter-spacing: 1.5px;
49
+ margin-bottom: 20px;
50
+ box-shadow: 0 4px 20px rgba(139, 92, 246, 0.35);
51
+ }
52
+
53
+ .hero-title {
54
+ font-size: 3rem;
55
+ font-weight: 800;
56
+ margin: 0 0 16px 0;
57
+ background: linear-gradient(135deg, #6d28d9 0%, #8b5cf6 50%, #a78bfa 100%);
58
+ -webkit-background-clip: text;
59
+ -webkit-text-fill-color: transparent;
60
+ background-clip: text;
61
+ letter-spacing: -0.5px;
62
+ }
63
+
64
+ .hero-desc {
65
+ font-size: 1.05rem;
66
+ color: #64748b;
67
+ max-width: 100%;
68
+ margin: 0 auto 20px;
69
+ line-height: 1.6;
70
+ }
71
+
72
+ .hero-badges {
73
+ display: flex;
74
+ gap: 28px;
75
+ justify-content: center;
76
+ flex-wrap: wrap;
77
+ }
78
+
79
+ .hero-badges span {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 8px;
83
+ color: #475569;
84
+ font-size: 0.9rem;
85
+ font-weight: 600;
86
+ }
87
+
88
+ /* ===== Hero Image ===== */
89
+ .hero-image {
90
+ max-width: 100%;
91
+ max-height: 300px;
92
+ border-radius: 16px;
93
+ margin: 24px auto;
94
+ box-shadow: 0 8px 32px rgba(139, 92, 246, 0.15);
95
+ }
96
+
97
+ /* ===== Main Card ===== */
98
+ .main-card {
99
+ background: #ffffff;
100
+ border: 1px solid rgba(139, 92, 246, 0.1);
101
+ border-radius: 20px;
102
+ padding: 28px;
103
+ margin-bottom: 20px;
104
+ box-shadow: 0 4px 24px rgba(139, 92, 246, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);
105
+ transition: box-shadow 0.3s ease;
106
+ }
107
+
108
+ .main-card:hover {
109
+ box-shadow: 0 8px 32px rgba(139, 92, 246, 0.12), 0 2px 6px rgba(0, 0, 0, 0.04);
110
+ }
111
+
112
+ /* ===== API Key Section ===== */
113
+ .api-key-row {
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: space-between;
117
+ margin-bottom: 12px;
118
+ }
119
+
120
+ .api-key-label {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 10px;
124
+ color: #1e293b;
125
+ font-weight: 700;
126
+ font-size: 1rem;
127
+ }
128
+
129
+ .get-key-btn {
130
+ padding: 10px 20px;
131
+ background: linear-gradient(135deg, #8b5cf6, #7c3aed);
132
+ border: none;
133
+ border-radius: 10px;
134
+ color: #fff !important;
135
+ text-decoration: none;
136
+ font-weight: 600;
137
+ font-size: 0.85rem;
138
+ transition: all 0.25s ease;
139
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
140
+ }
141
+
142
+ .get-key-btn:hover {
143
+ transform: translateY(-2px);
144
+ box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
145
+ color: #fff;
146
+ }
147
+
148
+ /* ===== Section Title ===== */
149
+ .section-title {
150
+ color: #1e293b;
151
+ font-weight: 700;
152
+ font-size: 1rem;
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 10px;
156
+ margin-bottom: 16px;
157
+ }
158
+
159
+ /* ===== Upload Area ===== */
160
+ .upload-area {
161
+ border: 2px dashed rgba(139, 92, 246, 0.3) !important;
162
+ border-radius: 16px !important;
163
+ background: linear-gradient(145deg, #faf5ff 0%, #f5f3ff 100%) !important;
164
+ transition: all 0.3s ease !important;
165
+ min-height: 220px !important;
166
+ }
167
+
168
+ .upload-area:hover {
169
+ border-color: rgba(139, 92, 246, 0.5) !important;
170
+ background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 100%) !important;
171
+ }
172
+
173
+ /* ===== Result Area ===== */
174
+ .result-area {
175
+ border: 2px solid rgba(139, 92, 246, 0.15) !important;
176
+ border-radius: 16px !important;
177
+ background: #fafafa !important;
178
+ min-height: 220px !important;
179
+ }
180
+
181
+ /* ===== Button Styling ===== */
182
+ .primary-btn {
183
+ width: 100%;
184
+ margin-top: 20px !important;
185
+ background: linear-gradient(135deg, #8b5cf6, #7c3aed) !important;
186
+ border: none !important;
187
+ color: #fff !important;
188
+ font-weight: 700 !important;
189
+ font-size: 1rem !important;
190
+ padding: 14px 28px !important;
191
+ border-radius: 12px !important;
192
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.35) !important;
193
+ transition: all 0.25s ease !important;
194
+ cursor: pointer !important;
195
+ }
196
+
197
+ .primary-btn:hover {
198
+ transform: translateY(-2px) !important;
199
+ box-shadow: 0 8px 24px rgba(139, 92, 246, 0.45) !important;
200
+ }
201
+
202
+ /* ===== CTA Section ===== */
203
+ .cta-container {
204
+ text-align: center;
205
+ padding: 44px 32px;
206
+ background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 50%, #6d28d9 100%);
207
+ border-radius: 20px;
208
+ margin-top: 8px;
209
+ box-shadow: 0 8px 32px rgba(139, 92, 246, 0.35);
210
+ position: relative;
211
+ overflow: hidden;
212
+ }
213
+
214
+ .cta-container::before {
215
+ content: '';
216
+ position: absolute;
217
+ top: -50%;
218
+ right: -50%;
219
+ width: 100%;
220
+ height: 100%;
221
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
222
+ pointer-events: none;
223
+ }
224
+
225
+ .cta-title {
226
+ color: #fff;
227
+ font-size: 1.5rem;
228
+ font-weight: 800;
229
+ margin: 0 0 8px 0;
230
+ position: relative;
231
+ }
232
+
233
+ .cta-desc {
234
+ color: rgba(255, 255, 255, 0.9);
235
+ font-size: 1rem;
236
+ margin: 0 0 24px 0;
237
+ position: relative;
238
+ }
239
+
240
+ .cta-btn {
241
+ display: inline-block;
242
+ padding: 14px 36px;
243
+ background: #fff;
244
+ border-radius: 12px;
245
+ color: #7c3aed !important;
246
+ text-decoration: none;
247
+ font-weight: 700;
248
+ font-size: 1rem;
249
+ transition: all 0.25s ease;
250
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
251
+ position: relative;
252
+ }
253
+
254
+ .cta-btn:hover {
255
+ transform: translateY(-3px);
256
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
257
+ color: #6d28d9 !important;
258
+ }
259
+
260
+ /* ===== Hide Elements ===== */
261
+ footer { display: none !important; }
262
+
263
+ /* ===== Input Styling ===== */
264
+ .gradio-container input[type="password"],
265
+ .gradio-container input[type="text"] {
266
+ border: 2px solid #e2e8f0 !important;
267
+ border-radius: 12px !important;
268
+ padding: 14px 16px !important;
269
+ font-size: 0.95rem !important;
270
+ transition: all 0.2s ease !important;
271
+ }
272
+
273
+ .gradio-container input[type="password"]:focus,
274
+ .gradio-container input[type="text"]:focus {
275
+ border-color: #8b5cf6 !important;
276
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15) !important;
277
+ outline: none !important;
278
+ }
279
+ """
280
+
281
+
282
+ # ============ API Functions ============
283
+ def upload_image(api_key: str, file_path: str) -> str:
284
+ headers = {"Authorization": f"Bearer {api_key.strip()}"}
285
+ with open(file_path, "rb") as f:
286
+ resp = requests.post(UPLOAD_ENDPOINT, headers=headers, files={"file": f}, timeout=60)
287
+ if resp.status_code == 401:
288
+ raise Exception("Invalid API Key")
289
+ elif resp.status_code >= 400:
290
+ raise Exception(f"Upload failed: {resp.status_code}")
291
+ data = resp.json()
292
+ if data.get("code") != 200:
293
+ raise Exception(data.get("message", "Upload failed"))
294
+ return data.get("data", {}).get("download_url")
295
+
296
+
297
+ def call_api(api_key: str, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]:
298
+ headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
299
+ resp = requests.post(f"{WAVESPEED_API_BASE}/{endpoint}", json=payload, headers=headers, timeout=30)
300
+ if resp.status_code == 401:
301
+ raise Exception("Invalid API Key")
302
+ elif resp.status_code == 429:
303
+ raise Exception("Quota exceeded")
304
+ elif resp.status_code >= 400:
305
+ raise Exception(f"API error: {resp.status_code}")
306
+ data = resp.json()
307
+ if data.get("code") != 200:
308
+ raise Exception(data.get("message", "Unknown error"))
309
+ return data.get("data", {})
310
+
311
+
312
+ def poll_result(api_key: str, request_id: str) -> Dict[str, Any]:
313
+ headers = {"Authorization": f"Bearer {api_key.strip()}"}
314
+ url = f"{WAVESPEED_API_BASE}/predictions/{request_id}/result"
315
+ start_time = time.time()
316
+ while time.time() - start_time < POLL_MAX_SECONDS:
317
+ resp = requests.get(url, headers=headers, timeout=30)
318
+ if resp.status_code >= 400:
319
+ raise Exception("Failed to get result")
320
+ result = resp.json().get("data", {})
321
+ status = result.get("status", "")
322
+ if status == "completed":
323
+ return result
324
+ elif status == "failed":
325
+ raise Exception("Generation failed")
326
+ time.sleep(POLL_INTERVAL)
327
+ raise Exception("Timeout")
328
+
329
+
330
+ def download_video(url: str) -> str:
331
+ """下载视频到临时文件,返回文件路径"""
332
+ import tempfile
333
+ import os
334
+ try:
335
+ resp = requests.get(url, timeout=120, stream=True)
336
+ if resp.status_code != 200:
337
+ return None
338
+ # 从 URL 或 Content-Type 确定扩展名
339
+ ext = ".mp4"
340
+ if "webm" in url.lower() or "webm" in resp.headers.get("content-type", ""):
341
+ ext = ".webm"
342
+ # 创建临时文件
343
+ fd, path = tempfile.mkstemp(suffix=ext)
344
+ with os.fdopen(fd, 'wb') as f:
345
+ for chunk in resp.iter_content(chunk_size=8192):
346
+ f.write(chunk)
347
+ return path
348
+ except Exception as e:
349
+ print(f"Download error: {e}")
350
+ return None
351
+
352
+
353
+ def process(api_key: str, image_path: str, audio_path: str):
354
+ if not api_key or not api_key.strip():
355
+ gr.Warning("Please enter your API Key")
356
+ return None
357
+ if not image_path:
358
+ gr.Warning("Please upload an image")
359
+ return None
360
+ if not audio_path:
361
+ gr.Warning("Please upload an audio file")
362
+ return None
363
+
364
+ try:
365
+ gr.Info("Uploading image...")
366
+ image_url = upload_image(api_key, image_path)
367
+ gr.Info("Uploading audio...")
368
+ audio_url = upload_image(api_key, audio_path) # 同样用 upload 接口
369
+
370
+ gr.Info("Generating video...")
371
+ payload = {
372
+ "image": image_url,
373
+ "audio": audio_url,
374
+ "resolution": "480p",
375
+ "seed": -1,
376
+ }
377
+ result = call_api(api_key, MODEL_ENDPOINT, payload)
378
+ request_id = result.get("id")
379
+ if not request_id:
380
+ gr.Warning("Failed to start")
381
+ return None
382
+
383
+ final_result = poll_result(api_key, request_id)
384
+ outputs = final_result.get("outputs", [])
385
+ if outputs:
386
+ gr.Info("Downloading video...")
387
+ video_path = download_video(outputs[0])
388
+ if video_path:
389
+ gr.Info("Done!")
390
+ return video_path
391
+ return None
392
+
393
+ except Exception as e:
394
+ error_msg = str(e).lower()
395
+ if "invalid" in error_msg or "401" in error_msg:
396
+ gr.Warning("Invalid API Key")
397
+ elif "quota" in error_msg or "429" in error_msg:
398
+ gr.Warning("Quota exceeded")
399
+ elif "timeout" in error_msg:
400
+ gr.Warning("Timeout - please try again")
401
+ else:
402
+ gr.Warning(str(e))
403
+ return None
404
+
405
+
406
+ # ============ Gradio UI ============
407
+ with gr.Blocks(css=CUSTOM_CSS, title="InfiniteTalk - WaveSpeed") as demo:
408
+
409
+ # Hero Section
410
+ gr.HTML("""
411
+ <div class="hero-container">
412
+ <div class="hero-badge">WAVESPEED AI</div>
413
+ <h1 class="hero-title">InfiniteTalk</h1>
414
+ <p class="hero-desc">Generate talking videos from image and audio.</p>
415
+ <div class="hero-badges">
416
+ <span>
417
+ <svg width="18" height="18" fill="#10b981" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/></svg>
418
+ Fast Processing
419
+ </span>
420
+ <span>
421
+ <svg width="18" height="18" fill="#10b981" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/></svg>
422
+ High Quality
423
+ </span>
424
+ <span>
425
+ <svg width="18" height="18" fill="#10b981" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/></svg>
426
+ Easy to Use
427
+ </span>
428
+ </div>
429
+ </div>
430
+ """)
431
+
432
+
433
+
434
+ # API Key Card
435
+ with gr.Group(elem_classes="main-card"):
436
+ gr.HTML("""
437
+ <div class="api-key-row">
438
+ <span class="api-key-label">
439
+ <svg width="20" height="20" fill="#8b5cf6" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd"/></svg>
440
+ API Key
441
+ </span>
442
+ <a href="https://wavespeed.ai/accesskey" target="_blank" class="get-key-btn">Get API Key</a>
443
+ </div>
444
+ """)
445
+ api_key_input = gr.Textbox(
446
+ placeholder="Enter your WaveSpeed API key",
447
+ type="password",
448
+ show_label=False
449
+ )
450
+
451
+ # Main Content Card
452
+ with gr.Group(elem_classes="main-card"):
453
+ with gr.Row():
454
+ # Left Column - Input
455
+ with gr.Column(scale=1):
456
+ gr.HTML("""
457
+ <div class="section-title">
458
+ <svg width="20" height="20" fill="#8b5cf6" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/></svg>
459
+ Enter Prompt
460
+ </div>
461
+ """)
462
+ image_input = gr.Image(
463
+ label="Upload Image",
464
+ type="filepath",
465
+ source="upload",
466
+ elem_classes=["upload-area"]
467
+ )
468
+ audio_input = gr.Audio(
469
+ label="Upload Audio",
470
+ type="filepath"
471
+ )
472
+ submit_btn = gr.Button("Process", variant="primary", elem_classes="primary-btn")
473
+
474
+ # Right Column - Output
475
+ with gr.Column(scale=1):
476
+ gr.HTML("""
477
+ <div class="section-title">
478
+ <svg width="20" height="20" fill="#8b5cf6" viewBox="0 0 20 20"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/></svg>
479
+ Result
480
+ </div>
481
+ """)
482
+ output_video = gr.Video(
483
+ label="",
484
+ interactive=False
485
+ )
486
+
487
+ # CTA Section
488
+ gr.HTML("""
489
+ <div class="cta-container">
490
+ <h3 class="cta-title">Want More Features?</h3>
491
+ <p class="cta-desc">Higher resolutions, batch processing, and 700+ AI models</p>
492
+ <a href="https://wavespeed.ai/models" target="_blank" class="cta-btn">
493
+ Explore WaveSpeed.ai
494
+ </a>
495
+ </div>
496
+ """)
497
+
498
+ # Event binding
499
+ submit_btn.click(
500
+ fn=process,
501
+ inputs=[api_key_input, image_input, audio_input],
502
+ outputs=output_video,
503
+ )
504
+
505
+
506
+ if __name__ == "__main__":
507
+ demo.launch(server_name="0.0.0.0", server_port=7860)