shayekh commited on
Commit
612024d
·
verified ·
1 Parent(s): 9e87252

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +484 -4
app.py CHANGED
@@ -1,7 +1,487 @@
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
 
 
 
 
 
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
1
+ # Copyright: Shayekh Bin Islam. KAIST, South Korea. 2026.
2
+
3
  import gradio as gr
4
+ import fitz # PyMuPDF
5
+ from PIL import Image
6
+ import io
7
+ import json
8
+ import base64
9
+ import soundfile as sf
10
+ import torch
11
+
12
+ from supertonic import TTS
13
+ from vllm import LLM, SamplingParams
14
+
15
+ llm = None
16
+ sampling_params = None
17
+ tts = None
18
+ voice_style = None
19
+
20
+ def extract_pdf_content(pdf_path, max_pages=2):
21
+ """Extract text and images from up to max_pages of a PDF."""
22
+ doc = fitz.open(pdf_path)
23
+ text = ""
24
+ images = []
25
+ for i in range(min(max_pages, len(doc))):
26
+ page = doc[i]
27
+ text += page.get_text() + "\n"
28
+ pix = page.get_pixmap(dpi=150)
29
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
30
+ images.append(img)
31
+ return text, images
32
+
33
+ import os
34
+
35
+ def get_base64_image(image):
36
+ buffered = io.BytesIO()
37
+ image.save(buffered, format="JPEG")
38
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
39
+ return f"data:image/jpeg;base64,{img_str}"
40
+
41
+ def extract_vocabulary(pdf_text, images, translit_lang, translit_format, target_lang):
42
+ """Use vLLM to extract vocabulary from text and images."""
43
+ global llm, sampling_params
44
+
45
+ os.makedirs("log", exist_ok=True)
46
+
47
+ prompt_text = f"""Extract 3 to 5 key Korean words or phrases from the following text and images.
48
+ Return ONLY a valid JSON list of dictionaries, where each dictionary has four keys:
49
+ - 'korean' (the Korean text)
50
+ - 'transliteration' (the pronunciation transliterated into {translit_lang.upper()} script/characters, formatted as {translit_format}. CRITICAL: You MUST use the native alphabet/script of {translit_lang.upper()}, do NOT use English letters unless requested.)
51
+ - 'translation' (the translation into {target_lang.upper()})
52
+ - 'explanation' (a brief grammar or context note in {target_lang.upper()}).
53
+ No markdown formatting, just raw JSON.
54
+
55
+ Text:
56
+ {pdf_text[:1500]}
57
+ """
58
+
59
+ # DEBUG: Log prompt text
60
+ with open("log/debug_vlm_prompt.txt", "w", encoding="utf-8") as f:
61
+ f.write(prompt_text)
62
+
63
+ content = [{"type": "text", "text": prompt_text}]
64
+
65
+ for i, img in enumerate(images):
66
+ # DEBUG: Log images
67
+ img.save(f"log/debug_image_{i}.png", format="PNG")
68
+
69
+ content.append({
70
+ "type": "image_url",
71
+ "image_url": {"url": get_base64_image(img)}
72
+ })
73
+
74
+ messages = [
75
+ {
76
+ "role": "user",
77
+ "content": content
78
+ }
79
+ ]
80
+
81
+ try:
82
+ outputs = llm.chat(messages=messages, sampling_params=sampling_params)
83
+ output_text = outputs[0].outputs[0].text
84
+
85
+ # DEBUG: Log raw output text
86
+ with open("log/debug_vlm_output.txt", "w", encoding="utf-8") as f:
87
+ f.write(output_text)
88
+
89
+ except Exception as e:
90
+ print(f"Error during vLLM inference: {e}")
91
+ return []
92
+
93
+ try:
94
+ clean_text = output_text.strip()
95
+ if clean_text.startswith("```json"):
96
+ clean_text = clean_text[7:]
97
+ if clean_text.startswith("```"):
98
+ clean_text = clean_text[3:]
99
+ if clean_text.endswith("```"):
100
+ clean_text = clean_text[:-3]
101
+ clean_text = clean_text.strip()
102
+
103
+ data = json.loads(clean_text)
104
+ if not isinstance(data, list):
105
+ data = [data]
106
+ return data
107
+ except Exception as e:
108
+ print(f"Error parsing JSON: {e}\nRaw output: {output_text}")
109
+ return []
110
+
111
+ def numpy_to_base64_audio(wav, sample_rate):
112
+ wav = wav.squeeze()
113
+ buffer = io.BytesIO()
114
+ sf.write(buffer, wav, sample_rate, format='WAV')
115
+ buffer.seek(0)
116
+ audio_base64 = base64.b64encode(buffer.read()).decode('utf-8')
117
+ return f"data:audio/wav;base64,{audio_base64}"
118
+
119
+ def process_pdf(pdf_file, translit_lang, translit_format, target_lang):
120
+ global tts, voice_style
121
+
122
+ # Clean language choices from "Family - Language" to just "Language"
123
+ if " - " in translit_lang:
124
+ translit_lang = translit_lang.split(" - ")[-1]
125
+ if " - " in target_lang:
126
+ target_lang = target_lang.split(" - ")[-1]
127
+
128
+ os.makedirs("log", exist_ok=True)
129
+
130
+ if pdf_file is None:
131
+ return "<p>Please upload a PDF.</p>"
132
+
133
+ try:
134
+ pdf_text, images = extract_pdf_content(pdf_file.name)
135
+ if not pdf_text.strip() and not images:
136
+ return "<p>No content found in PDF.</p>"
137
+ except Exception as e:
138
+ return f"<p>Error reading PDF: {e}</p>"
139
+
140
+ vocab_list = extract_vocabulary(pdf_text, images, translit_lang, translit_format, target_lang)
141
+ if not vocab_list:
142
+ return "<p>Failed to extract vocabulary. The model might not have found Korean text or returned an invalid format.</p>"
143
+
144
+ # Pre-generate TTS audio
145
+ for i, item in enumerate(vocab_list):
146
+ korean = item.get("korean", "")
147
+ # Add dot
148
+ if not korean.endswith("."):
149
+ korean += "."
150
+
151
+ try:
152
+ wav, dur = tts.synthesize(korean, voice_style=voice_style, lang="ko")
153
+
154
+ # DEBUG: Save audio locally
155
+ wav_1d = wav.squeeze()
156
+ sf.write(f"log/debug_audio_{i}.wav", wav_1d, tts.sample_rate, format='WAV')
157
+
158
+ audio_data_uri = numpy_to_base64_audio(wav, tts.sample_rate)
159
+ item['audio_uri'] = audio_data_uri
160
+ except Exception as e:
161
+ print(f"TTS error for '{korean}': {e}")
162
+ item['audio_uri'] = None
163
+
164
+ cards_json = json.dumps(vocab_list).replace("</", "<\\/")
165
+
166
+ iframe_html = f"""
167
+ <!DOCTYPE html>
168
+ <html>
169
+ <head>
170
+ <!-- Flaticon UIcons CDN -->
171
+ <link rel='stylesheet' href='https://cdn-uicons.flaticon.com/uicons-regular-rounded/css/uicons-regular-rounded.css'>
172
+ <style>
173
+ body {{
174
+ margin: 0;
175
+ padding: 0;
176
+ background: transparent;
177
+ }}
178
+ .flashcard-container {{
179
+ perspective: 1000px;
180
+ width: 100%;
181
+ max-width: 500px;
182
+ margin: 0 auto;
183
+ font-family: 'Inter', sans-serif;
184
+ padding-top: 20px;
185
+ }}
186
+ .flashcard {{
187
+ width: 100%;
188
+ height: 350px;
189
+ position: relative;
190
+ transition: transform 0.6s cubic-bezier(0.4, 0.2, 0.2, 1);
191
+ transform-style: preserve-3d;
192
+ cursor: pointer;
193
+ }}
194
+ .flashcard.is-flipped {{
195
+ transform: rotateY(180deg);
196
+ }}
197
+ .card-face {{
198
+ position: absolute;
199
+ width: 100%;
200
+ height: 100%;
201
+ backface-visibility: hidden;
202
+ display: flex;
203
+ flex-direction: column;
204
+ justify-content: center;
205
+ align-items: center;
206
+ border-radius: 20px;
207
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
208
+ padding: 30px;
209
+ box-sizing: border-box;
210
+ background: rgba(255, 255, 255, 0.8);
211
+ backdrop-filter: blur(10px);
212
+ border: 1px solid rgba(255,255,255,0.5);
213
+ text-align: center;
214
+ }}
215
+ .card-front {{
216
+ background: linear-gradient(135deg, #fdfbfb 0%, #ebedee 100%);
217
+ }}
218
+ .card-back {{
219
+ transform: rotateY(180deg);
220
+ background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
221
+ color: #333;
222
+ }}
223
+ .korean-text {{
224
+ font-size: 54px;
225
+ font-weight: 700;
226
+ color: #2c3e50;
227
+ margin-bottom: 20px;
228
+ }}
229
+ .english-text {{
230
+ font-size: 32px;
231
+ font-weight: 600;
232
+ margin-bottom: 5px;
233
+ }}
234
+ .translit-text {{
235
+ font-size: 18px;
236
+ font-style: italic;
237
+ color: #d35400;
238
+ margin-bottom: 15px;
239
+ }}
240
+ .explanation-text {{
241
+ font-size: 16px;
242
+ color: #555;
243
+ line-height: 1.5;
244
+ }}
245
+ .nav-buttons {{
246
+ display: flex;
247
+ justify-content: space-between;
248
+ margin-top: 30px;
249
+ width: 100%;
250
+ max-width: 500px;
251
+ margin-left: auto;
252
+ margin-right: auto;
253
+ }}
254
+ .nav-btn {{
255
+ padding: 12px 24px;
256
+ border: none;
257
+ border-radius: 12px;
258
+ background: #7c3aed;
259
+ color: white;
260
+ font-weight: 600;
261
+ cursor: pointer;
262
+ transition: all 0.2s;
263
+ box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
264
+ flex: 1;
265
+ margin: 0 10px;
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: center;
269
+ gap: 8px;
270
+ }}
271
+ .nav-btn:hover {{
272
+ background: #6d28d9;
273
+ transform: translateY(-2px);
274
+ }}
275
+ .nav-btn:disabled {{
276
+ background: #ccc;
277
+ cursor: not-allowed;
278
+ transform: none;
279
+ box-shadow: none;
280
+ }}
281
+ .audio-btn {{
282
+ margin-top: 20px;
283
+ padding: 12px 24px;
284
+ border-radius: 50px;
285
+ border: none;
286
+ background: #2c3e50;
287
+ color: white;
288
+ cursor: pointer;
289
+ font-size: 16px;
290
+ font-weight: 600;
291
+ transition: all 0.2s;
292
+ display: flex;
293
+ align-items: center;
294
+ justify-content: center;
295
+ gap: 8px;
296
+ }}
297
+ .audio-btn:hover {{
298
+ background: #34495e;
299
+ transform: scale(1.05);
300
+ }}
301
+ .progress {{
302
+ text-align: center;
303
+ margin-top: 15px;
304
+ color: #666;
305
+ font-size: 14px;
306
+ font-weight: 600;
307
+ }}
308
+ </style>
309
+ </head>
310
+ <body>
311
+ <div id="flashcard-app">
312
+ <div class="flashcard-container">
313
+ <div class="flashcard" id="card" onclick="flipCard()">
314
+ <div class="card-face card-front">
315
+ <div class="korean-text" id="front-text"><i class="fi fi-rr-spinner-third fa-spin"></i> Loading...</div>
316
+ <button class="audio-btn" onclick="playAudio(event)" id="audio-btn" style="display:none;"><i class="fi fi-rr-play-circle"></i> Play Audio</button>
317
+ <p style="margin-top:20px; color:#999; font-size:13px; display:flex; align-items:center; gap:5px;"><i class="fi fi-rr-rotate-right"></i> Click card to flip 🎯</p>
318
+ </div>
319
+ <div class="card-face card-back">
320
+ <div class="english-text" id="back-en"></div>
321
+ <div class="translit-text" id="back-translit"></div>
322
+ <div class="explanation-text"><i class="fi fi-rr-lightbulb-on" style="color:#f1c40f;"></i> <span id="back-exp"></span></div>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ <div class="nav-buttons">
327
+ <button class="nav-btn" id="prev-btn" onclick="prevCard()"><i class="fi fi-rr-angle-left"></i> Previous</button>
328
+ <button class="nav-btn" id="next-btn" onclick="nextCard()">Next <i class="fi fi-rr-angle-right"></i></button>
329
+ </div>
330
+ <div class="progress" id="progress-text"></div>
331
+ </div>
332
+
333
+ <script>
334
+ const cards = {cards_json};
335
+ let currentIndex = 0;
336
+ let audioPlayer = new Audio();
337
+
338
+ function updateCard() {{
339
+ if (!cards || cards.length === 0) {{
340
+ document.getElementById('front-text').innerHTML = "No vocabulary found 😥";
341
+ document.getElementById('prev-btn').disabled = true;
342
+ document.getElementById('next-btn').disabled = true;
343
+ return;
344
+ }}
345
+ const card = cards[currentIndex];
346
+ document.getElementById('front-text').innerText = card.korean || "No word";
347
+ document.getElementById('back-en').innerText = card.translation || card.english || "";
348
+ document.getElementById('back-translit').innerText = card.transliteration ? `[${{card.transliteration}}]` : "";
349
+ document.getElementById('back-exp').innerText = card.explanation || "";
350
+
351
+ document.getElementById('prev-btn').disabled = currentIndex === 0;
352
+ document.getElementById('next-btn').disabled = currentIndex === cards.length - 1;
353
+ document.getElementById('progress-text').innerHTML = `📚 Card ${{currentIndex + 1}} of ${{cards.length}}`;
354
+
355
+ const cardEl = document.getElementById('card');
356
+ cardEl.classList.remove('is-flipped');
357
+
358
+ if(card.audio_uri) {{
359
+ audioPlayer.src = card.audio_uri;
360
+ document.getElementById('audio-btn').style.display = 'flex';
361
+ }} else {{
362
+ document.getElementById('audio-btn').style.display = 'none';
363
+ }}
364
+ }}
365
+
366
+ function flipCard() {{
367
+ if (!cards || cards.length === 0) return;
368
+ document.getElementById('card').classList.toggle('is-flipped');
369
+ }}
370
+
371
+ function playAudio(e) {{
372
+ e.stopPropagation();
373
+ audioPlayer.play().catch(err => console.log("Audio play error:", err));
374
+ }}
375
+
376
+ function nextCard() {{
377
+ if (currentIndex < cards.length - 1) {{
378
+ currentIndex++;
379
+ updateCard();
380
+ }}
381
+ }}
382
+
383
+ function prevCard() {{
384
+ if (currentIndex > 0) {{
385
+ currentIndex--;
386
+ updateCard();
387
+ }}
388
+ }}
389
+
390
+ window.onload = function() {{
391
+ updateCard();
392
+ }};
393
+ </script>
394
+ </body>
395
+ </html>
396
+ """
397
+
398
+ import html
399
+ safe_srcdoc = html.escape(iframe_html)
400
+
401
+ # Return the iframe containing the whole SPA
402
+ return f'<iframe srcdoc="{safe_srcdoc}" style="width: 100%; height: 500px; border: none; overflow: hidden;"></iframe>'
403
+
404
+ LANGUAGE_DATA = """Indo-European English, French, Portuguese, German, Romanian, Swedish, Danish, Bulgarian, Russian, Czech, Greek, Ukrainian, Spanish, Dutch, Slovak, Croatian, Polish, Lithuanian, Norwegian Bokmål, Norwegian Nynorsk, Persian, Slovenian, Gujarati, Latvian, Italian, Occitan, Nepali, Marathi, Belarusian, Serbian, Luxembourgish, Venetian, Assamese, Welsh, Silesian, Asturian, Chhattisgarhi, Awadhi, Maithili, Bhojpuri, Sindhi, Irish, Faroese, Hindi, Punjabi, Bengali, Oriya, Tajik, Eastern Yiddish, Lombard, Ligurian, Sicilian, Friulian, Sardinian, Galician, Catalan, Icelandic, Tosk Albanian, Limburgish, Dari, Afrikaans, Macedonian, Sinhala, Urdu, Magahi, Bosnian, Armenian, Latgalian, Scottish Gaelic, Central Kurdish, Northern Kurdish, Southern Pashto, Sanskrit, Dhundari, Marwari, Ahirani, Bagheli, Bagri, Bundeli, Braj, Kumaoni, Kashmiri
405
+ Sino-Tibetan Chinese (Simplified), Chinese (Traditional), Cantonese, Burmese, Standard Tibetan, Meitei
406
+ Afro-Asiatic Arabic (Standard), Arabic (Najdi), Arabic (Levantine), Arabic (Egyptian), Arabic (Moroccan), Arabic (Mesopotamian), Arabic (Ta’izzi-Adeni), Arabic (Tunisian), Arabic (Gulf), Arabic (Algerian), Arabic (Sudanese), Arabic (Libyan), Hebrew, Maltese, Amharic, Tigrinya, Kabyle, Somali, West Central Oromo, Hausa
407
+ Austronesian Indonesian, Malay, Tagalog, Cebuano, Javanese, Sundanese, Minangkabau, Balinese, Banjar, Pangasinan, Iloko, Waray (Philippines), Plateau Malagasy, Malagasy, Buginese, Maori, Samoan, Hawaiian, Fijian
408
+ Dravidian Tamil, Telugu, Kannada, Malayalam
409
+ Turkic Turkish, North Azerbaijani, Northern Uzbek, Kazakh, Bashkir, Tatar, Crimean Tatar, Kyrgyz, Turkmen, Uyghur
410
+ Tai-Kadai Thai, Lao, Shan
411
+ Uralic Finnish, Estonian, Hungarian, Meadow Mari
412
+ Austroasiatic Vietnamese, Khmer
413
+ Niger–Congo Yoruba, Ewe, Kinyarwanda, Lingala, Northern Sotho, Nyanja, Shona, Southern Sotho, Tswana, Xhosa, Zulu, Luganda, Swati, Tsonga, Tumbuka, Venda, Chokwe, Luba-Kasai, Rundi, Umbundu, Kikuyu, Kongo, Nigerian Fulfulde, Wolof, Fon, Kabiyè, Mossi, Akan, Twi, Bambara, Igbo
414
+ Other Japanese, Korean, Georgian, Basque, Haitian, Papiamento, Kabuverdianu, Tok Pisin, Swahili, Central Aymara, Tulu, Nagamese, Nigerian Pidgin, Mauritian Creole, Sango, Ayacucho Quechua, Halh Mongolian, Southwestern Dinka, Nuer, Guarani"""
415
+
416
+ LANGUAGE_CHOICES = []
417
+ for line in LANGUAGE_DATA.strip().split('\n'):
418
+ family, langs = line.split('\t')
419
+ for lang in langs.split(', '):
420
+ LANGUAGE_CHOICES.append(f"{family} - {lang}")
421
+
422
+ def create_demo():
423
+ with gr.Blocks(title="LocalDuo") as demo:
424
+ gr.Markdown("# 🇰🇷✨ LocalDuo - Learn Korean from PDFs")
425
+ gr.Markdown("Upload a Korean book 📖 or document PDF 📄. The app uses **vLLM** 🧠 with Qwen3.5-2B to extract vocabulary from text and images, and Supertonic 🗣️ to generate pronunciation audio.")
426
+
427
+ with gr.Row():
428
+ with gr.Column(scale=1):
429
+ pdf_input = gr.File(label="Upload Book PDF 📚", file_types=[".pdf"])
430
+
431
+ gr.Markdown("### ⚙️ Customization Settings")
432
+ translit_lang = gr.Dropdown(
433
+ label="Word Transliteration Language",
434
+ choices=LANGUAGE_CHOICES,
435
+ value="Indo-European - English"
436
+ )
437
+ translit_format = gr.Dropdown(label="Transliteration Format", choices=["dashed syllable", "regular word with space"], value="dashed syllable")
438
+ target_lang = gr.Dropdown(
439
+ label="Target Language (Full App)",
440
+ choices=LANGUAGE_CHOICES,
441
+ value="Indo-European - English"
442
+ )
443
+
444
+ submit_btn = gr.Button("✨ Generate Flashcards ✨", variant="primary")
445
+
446
+ with gr.Column(scale=2):
447
+ output_html = gr.HTML(label="Flashcards will appear here")
448
+
449
+ submit_btn.click(
450
+ fn=process_pdf,
451
+ inputs=[pdf_input, translit_lang, translit_format, target_lang],
452
+ outputs=output_html
453
+ )
454
+ return demo
455
+
456
+ if __name__ == "__main__":
457
+ print("Loading Qwen3.5-2B model via vLLM...")
458
+ llm = LLM(
459
+ model="Qwen/Qwen3.5-2B",
460
+ # model="Qwen/Qwen3.5-9B",
461
+ max_model_len=65536, # Reduced from 262144 to fit on single GPU
462
+ tensor_parallel_size=1, # Kept at 1 since CUDA_VISIBLE_DEVICES=1
463
+ gpu_memory_utilization=0.5,
464
+ enable_prefix_caching=True,
465
+ trust_remote_code=True,
466
+ limit_mm_per_prompt={"image": 10} # Added image limits
467
+ )
468
 
469
+ sampling_params = SamplingParams(
470
+ temperature=1.0,
471
+ top_p=0.95,
472
+ top_k=20,
473
+ min_p=0.0,
474
+ presence_penalty=0.0,
475
+ repetition_penalty=1.0,
476
+ max_tokens=2048,
477
+ )
478
 
479
+ print("Loading Supertonic TTS...")
480
+ tts = TTS(model="supertonic-3")
481
+ try:
482
+ voice_style = tts.get_voice_style("F1")
483
+ except Exception:
484
+ voice_style = tts.get_voice_style(tts.voice_style_names[0])
485
+
486
+ demo = create_demo()
487
+ demo.launch(server_name="0.0.0.0", server_port=7861)