anisgtboi commited on
Commit
ea5da31
·
verified ·
1 Parent(s): 84e57d5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +166 -366
app.py CHANGED
@@ -1,386 +1,186 @@
1
  # app.py
2
- # Enhanced Gradio app: Fixed translation with professional UI
3
  import os
4
- import re
5
  import traceback
6
-
7
- import torch
8
  import gradio as gr
9
- from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
10
-
11
- # ---------- Translation (Using NLLB Model) ----------
12
- DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
13
-
14
- # NLLB model configuration
15
- MODEL_NAME = "facebook/nllb-200-distilled-600M"
16
- LANGUAGE_CODES = {
17
- "English": "eng_Latn",
18
- "Bengali": "ben_Beng"
19
- }
20
-
21
- _translation_model = None
22
- _tokenizer = None
23
-
24
- def load_translation_model():
25
- global _translation_model, _tokenizer
26
- if _translation_model is None:
27
- try:
28
- _tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
29
- _translation_model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME).to(DEVICE)
30
- except Exception as e:
31
- print(f"Error loading NLLB model: {e}")
32
- raise Exception(f"Failed to load NLLB model: {e}")
33
-
34
- return _tokenizer, _translation_model
35
-
36
- # Enhanced Benglish to Bengali transliteration mapping
37
- BENGLISH_TO_BENGALI_MAP = {
38
- # Vowels
39
- 'a': 'া', 'aa': 'া', 'i': 'ি', 'ee': 'ী', 'u': 'ু', 'oo': 'ূ', 'ri': 'ৃ', 'e': 'ে', 'oi': 'ৈ', 'o': 'ো', 'ou': 'ৌ',
40
- 'A': 'আ', 'Aa': 'আ', 'I': 'ই', 'Ee': 'ঈ', 'U': 'উ', 'Oo': 'ঊ', 'Ri': 'ঋ', 'E': 'এ', 'Oi': 'ঐ', 'O': 'ও', 'Ou': 'ঔ',
41
- # Consonants
42
- 'k': 'ক', 'kh': 'খ', 'g': 'গ', 'gh': 'ঘ', 'ng': 'ং', 'ch': 'চ', 'chh': 'ছ', 'j': 'জ', 'jh': 'ঝ', 'yn': 'ঞ',
43
- 't': 'ট', 'th': 'ঠ', 'd': 'ড', 'dh': 'ঢ', 'n': 'ণ', 'p': 'প', 'ph': 'ফ', 'f': 'ফ', 'b': 'ব', 'bh': 'ভ', 'v': 'ভ',
44
- 'm': 'ম', 'y': 'য', 'r': 'র', 'l': 'ল', 'w': 'ও', 'sh': 'শ', 'ss': 'श', 's': 'স', 'h': 'হ', 'x': 'ক্ষ', 'z': 'জ',
45
- 'rh': 'ড়', 'rhh': 'ঢ়', 'y': 'য়', 'tt': 'ৎ', 'ng': 'ঁ',
46
- # Numbers
47
- '0': '০', '1': '১', '2': '২', '3': '৩', '4': '৪', '5': '৫', '6': '৬', '7': '৭', '8': '৮', '9': '৯',
48
- }
49
-
50
- def is_benglish(text: str):
51
- """Check if text appears to be Benglish (Bengali in English script)"""
52
- if not text.strip():
53
- return False
54
-
55
- # Check if text contains any Bengali characters
56
- bengali_unicode_range = '\u0980-\u09FF'
57
- if re.search(f'[{bengali_unicode_range}]', text):
58
- return False
59
-
60
- # Check if text contains English letters and common Benglish patterns
61
- if re.search(r'[a-zA-Z]', text) and not re.search(r'[^a-zA-Z0-9\s\.\,\?\!]', text):
62
- return True
63
-
64
- return False
65
-
66
- def transliterate_benglish_to_bengali(text: str):
67
- """Simple transliteration from Benglish to Bengali"""
68
- # Common word mappings
69
- common_words = {
70
- 'ami': 'আমি', 'tumi': 'তুমি', 'se': 'সে', 'amra': 'আমরা', 'tomra': 'তোমরা', 'tara': 'তারা',
71
- 'kothay': 'কোথায়', 'ki': 'কী', 'kemon': 'কেমন', 'kno': 'কেন', 'kobe': 'কবে', 'kor': 'কর',
72
- 'ache': 'আছে', 'nay': 'নয়', 'holo': 'হলো', 'hobe': 'হবে', 'chai': 'চাই', 'ne': 'নে',
73
- 'valo': 'ভালো', 'kharap': 'খারাপ', 'sundor': 'সুন্দর', 'bhalo': 'ভালো', 'odin': 'অদিন',
74
- 'ekhon': 'এখন', 'age': 'আগে', 'pore': 'পরে', 'sobar': 'সবার', 'jonno': 'জন্য',
75
- 'tui': 'তুই', 'tor': 'তোর', 'amar': 'আমার', 'tomar': 'তোমার', 'tar': 'তার',
76
- 'achi': 'আছি', 'achis': 'আছিস', 'achho': 'আচ্ছ', 'achhen': 'আছেন', 'achhe': 'আছে',
77
- 'kothao': 'কোথাও', 'kono': 'কোনো', 'keu': 'কেউ', 'kichu': 'কিছু', 'sob': 'সব',
78
- 'jodi': 'যদি', 'tahole': 'তাহলে', 'kintu': 'কিন্তু', 'je': 'যে', 'na': 'না',
79
- 'ha': 'হা', 're': 'রে', 'o': 'ও', 'aro': 'আরও', 'onek': 'অনেক', 'valo': 'ভালো'
80
- }
81
-
82
- # Replace common words first (case insensitive)
83
- for eng, ben in common_words.items():
84
- text = re.sub(r'\b' + eng + r'\b', ben, text, flags=re.IGNORECASE)
85
-
86
- # Simple character mapping for remaining text
87
- for eng, ben in BENGLISH_TO_BENGALI_MAP.items():
88
- if eng:
89
- text = text.replace(eng, ben)
90
-
91
- return text
92
-
93
- def translate_text(text: str, src_lang: str, tgt_lang: str):
94
- if not text or not text.strip():
95
- return ""
96
-
97
- # If source language is Bengali and text appears to be Benglish, transliterate it
98
- original_text = text
99
- if src_lang == "Bengali" and is_benglish(text):
100
- text = transliterate_benglish_to_bengali(text)
101
- print(f"Transliterated '{original_text}' to '{text}'")
102
-
103
  try:
104
- tokenizer, model = load_translation_model()
 
 
 
 
105
  except Exception as e:
106
- tb = traceback.format_exc()
107
- return f"Error loading translation model: {e}\n{tb}"
108
-
 
 
 
 
 
 
 
 
109
  try:
110
- # Get language codes
111
- src_code = LANGUAGE_CODES[src_lang]
112
- tgt_code = LANGUAGE_CODES[tgt_lang]
113
-
114
- # Tokenize and translate
115
- tokenizer.src_lang = src_code
116
- inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=512)
117
- inputs = {k: v.to(DEVICE) for k, v in inputs.items()}
118
-
119
- # Generate translation
120
- translated_tokens = model.generate(
121
- **inputs,
122
- forced_bos_token_id=tokenizer.lang_code_to_id[tgt_code],
123
- max_length=512
124
- )
125
-
126
- result = tokenizer.decode(translated_tokens[0], skip_special_tokens=True)
127
- return result
128
  except Exception as e:
129
- return f"[translate error: {e}]"
130
-
131
- # ---------- Gradio UI (Mobile-Friendly Dark Theme) ----------
132
- css = """
133
- /* General App Container */
134
- .app {
135
- background-color: #1c1c1c; /* dark background */
136
- font-family: 'Poppins', sans-serif;
137
- color: #f5f5f5;
138
- padding: 20px;
139
- max-width: 400px; /* mobile size */
140
- margin: auto;
141
- border-radius: 20px;
142
- }
143
-
144
- /* Text Card Areas */
145
- .card {
146
- background-color: #2c2c2c;
147
- border-radius: 15px;
148
- padding: 15px;
149
- margin: 15px 0;
150
- box-shadow: 0 4px 8px rgba(0,0,0,0.3);
151
- min-height: 120px;
152
- display: flex;
153
- flex-direction: column;
154
- justify-content: space-between;
155
- }
156
-
157
- /* Copy Icon Button (inside card) */
158
- .card .copy-btn {
159
- align-self: flex-end;
160
- background: none;
161
- border: none;
162
- color: #ffcc00; /* yellow icon */
163
- font-size: 20px;
164
- cursor: pointer;
165
- }
166
-
167
- /* Language Selector Row */
168
- .lang-row {
169
- display: flex;
170
- align-items: center;
171
- justify-content: center;
172
- gap: 12px;
173
- margin: 10px 0;
174
- }
175
-
176
- .lang-row select {
177
- background: #3a3a3a;
178
- color: #fff;
179
- border: none;
180
- border-radius: 12px;
181
- padding: 8px 16px;
182
- font-size: 14px;
183
- cursor: pointer;
184
- }
185
-
186
- /* Swap Button */
187
- .swap-btn {
188
- background: #ff0057;
189
- color: white;
190
- border: none;
191
- border-radius: 50%;
192
- padding: 10px;
193
- cursor: pointer;
194
- font-size: 16px;
195
- box-shadow: 0 3px 6px rgba(0,0,0,0.4);
196
- }
197
-
198
- /* Translate Button */
199
- .translate-btn {
200
- width: 100%;
201
- background: #ff0057;
202
- color: white;
203
- font-weight: bold;
204
- font-size: 16px;
205
- border: none;
206
- padding: 14px;
207
- border-radius: 12px;
208
- margin-top: 15px;
209
- cursor: pointer;
210
- box-shadow: 0 3px 8px rgba(0,0,0,0.5);
211
- }
212
-
213
- .translate-btn:hover {
214
- background: #e6004d;
215
- }
216
 
217
- /* Text Areas */
218
- .text-input, .text-output {
219
- background-color: #2c2c2c;
220
- color: #f5f5f5;
221
- border: 1px solid #444;
222
- border-radius: 12px;
223
- padding: 12px;
224
- width: 100%;
225
- min-height: 120px;
226
- resize: vertical;
227
- font-family: inherit;
228
- }
229
 
230
- .text-input:focus {
231
- outline: none;
232
- border-color: #ff0057;
233
- }
 
 
234
 
235
- /* Header */
236
- .header {
237
- text-align: center;
238
- margin-bottom: 20px;
239
- }
240
 
241
- .header h1 {
242
- font-size: 24px;
243
- margin-bottom: 5px;
244
- color: #ffcc00;
245
- }
246
 
247
- .header p {
248
- font-size: 14px;
249
- color: #aaa;
250
- }
251
 
252
- /* Quick Phrases */
253
- .quick-phrases {
254
- display: grid;
255
- grid-template-columns: 1fr 1fr;
256
- gap: 10px;
257
- margin: 15px 0;
258
- }
259
 
260
- .quick-btn {
261
- background: #3a3a3a;
262
- color: #fff;
263
- border: none;
264
- border-radius: 8px;
265
- padding: 8px;
266
- font-size: 12px;
267
- cursor: pointer;
268
- transition: background 0.2s;
269
- }
270
 
271
- .quick-btn:hover {
272
- background: #4a4a4a;
273
- }
274
 
275
- /* Footer */
276
- .footer {
277
- text-align: center;
278
- margin-top: 20px;
279
- font-size: 12px;
280
- color: #888;
281
- }
282
- """
283
 
284
- with gr.Blocks(title="NLLB English ↔ Bengali Translator", css=css) as demo:
285
- gr.HTML("""
286
- <div class="header">
287
- <h1>🌐 NLLB Translator</h1>
288
- <p>English ↔ Bengali with Benglish Support</p>
289
- </div>
290
- """)
291
-
292
- with gr.Column(elem_classes="app"):
293
- # Language selection
294
- with gr.Row(elem_classes="lang-row"):
295
- src_lang = gr.Dropdown(
296
- choices=["English", "Bengali"],
297
- value="English",
298
- label="From",
299
- elem_classes="lang-select"
300
- )
301
-
302
- swap_btn = gr.Button("⇄", elem_classes="swap-btn")
303
-
304
- tgt_lang = gr.Dropdown(
305
- choices=["Bengali", "English"],
306
- value="Bengali",
307
- label="To",
308
- elem_classes="lang-select"
309
- )
310
-
311
- # Input and output text areas
312
- input_text = gr.Textbox(
313
- lines=4,
314
- label="Input Text",
315
- placeholder="Type or paste text to translate here...",
316
- elem_classes="text-input card"
317
- )
318
-
319
- output_text = gr.Textbox(
320
- lines=4,
321
- label="Translation",
322
- interactive=False,
323
- elem_classes="text-output card"
324
- )
325
-
326
- # Quick phrases
327
- gr.Markdown("**Quick Phrases:**")
328
- with gr.Row(elem_classes="quick-phrases"):
329
- quick1 = gr.Button("Hello, how are you?", elem_classes="quick-btn")
330
- quick2 = gr.Button("Thank you very much", elem_classes="quick-btn")
331
- quick3 = gr.Button("What is your name?", elem_classes="quick-btn")
332
- quick4 = gr.Button("Kemon achis?", elem_classes="quick-btn")
333
-
334
- # Translate button
335
- translate_btn = gr.Button("Translate", elem_classes="translate-btn")
336
-
337
- gr.HTML("""
338
- <div class="footer">
339
- <p>Powered by Meta NLLB • Professional Translation</p>
340
- </div>
341
- """)
342
-
343
- # Update target language when source changes
344
- def update_target_lang(src_lang):
345
- return "Bengali" if src_lang == "English" else "English"
346
-
347
- src_lang.change(update_target_lang, inputs=src_lang, outputs=tgt_lang)
348
-
349
- # Swap languages function
350
- def swap_languages(src_lang, tgt_lang, input_text, output_text):
351
- new_src = tgt_lang
352
- new_tgt = src_lang
353
- new_input = output_text
354
- new_output = input_text
355
- return new_src, new_tgt, new_input, new_output
356
-
357
- swap_btn.click(
358
- swap_languages,
359
- inputs=[src_lang, tgt_lang, input_text, output_text],
360
- outputs=[src_lang, tgt_lang, input_text, output_text]
361
- )
362
-
363
- # Quick phrase events
364
- quick_phrases = {
365
- quick1: "Hello, how are you?",
366
- quick2: "Thank you very much.",
367
- quick3: "What is your name?",
368
- quick4: "Kemon achis?"
369
- }
370
-
371
- for btn, phrase in quick_phrases.items():
372
- btn.click(lambda p=phrase: p, inputs=None, outputs=input_text)
373
-
374
- # Translate function
375
- translate_btn.click(
376
- fn=translate_text,
377
- inputs=[input_text, src_lang, tgt_lang],
378
- outputs=output_text
379
- )
380
 
 
381
  if __name__ == "__main__":
382
- demo.launch(
383
- server_name="0.0.0.0",
384
- server_port=int(os.environ.get("PORT", 7860)),
385
- share=True
386
- )
 
1
  # app.py
 
2
  import os
 
3
  import traceback
 
 
4
  import gradio as gr
5
+ from typing import Tuple
6
+
7
+ # Try to import transformers; if not available, the app will error and tell you to add requirements.
8
+ try:
9
+ from transformers import pipeline, AutoTokenizer, AutoModelForSeq2SeqLM
10
+ except Exception as e:
11
+ pipeline = None
12
+
13
+ # Optional: Hugging Face hosted-inference fallback
14
+ try:
15
+ from huggingface_hub import InferenceApi
16
+ except Exception:
17
+ InferenceApi = None
18
+
19
+ # ---------- CONFIG ----------
20
+ # Lightweight models that work well on CPU / Spaces:
21
+ MODEL_EN_TO_BN = "shhossain/opus-mt-en-to-bn" # small finetuned en -> bn (≈75M params)
22
+ MODEL_BN_TO_EN = "Helsinki-NLP/opus-mt-bn-en" # bn -> en
23
+ # If you prefer other model ids, change the strings above.
24
+
25
+ # Language labels for UI
26
+ DIRECTION_CHOICES = ["English → Bengali", "Bengali → English"]
27
+
28
+ # ---------- GLOBALS ----------
29
+ local_pipeline = None
30
+ local_model_name = None
31
+ use_api_fallback = False
32
+ inference_client = None
33
+
34
+ # ---------- HELPERS ----------
35
+ def try_load_local(model_name: str) -> Tuple[bool, str]:
36
+ """Try to load a local transformers pipeline for translation.
37
+ Returns (success, message)."""
38
+ global local_pipeline, local_model_name, use_api_fallback
39
+ if pipeline is None:
40
+ return False, "transformers not installed (add to requirements.txt)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  try:
42
+ # Use the 'translation' pipeline (Marian / MarianMT based models)
43
+ local_pipeline = pipeline("translation", model=model_name, device=-1, max_length=512)
44
+ local_model_name = model_name
45
+ use_api_fallback = False
46
+ return True, f"Loaded local model: {model_name}"
47
  except Exception as e:
48
+ use_api_fallback = True
49
+ return False, f"Local load failed: {str(e)}"
50
+
51
+ def try_init_inference_api(token_env="HF_API_TOKEN", model_name_fallback=None):
52
+ """Initialize huggingface_hub Inference API client if token present."""
53
+ global inference_client, use_api_fallback
54
+ token = os.environ.get(token_env)
55
+ if not token:
56
+ return False, "No HF_API_TOKEN found in env (set Space secret HF_API_TOKEN)"
57
+ if InferenceApi is None:
58
+ return False, "huggingface_hub not installed (add to requirements.txt)"
59
  try:
60
+ inference_client = InferenceApi(repo_id=model_name_fallback or "facebook/nllb-200-distilled-600M", token=token)
61
+ use_api_fallback = True
62
+ return True, "Inference API client ready"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  except Exception as e:
64
+ return False, f"Inference API init failed: {str(e)}"
65
+
66
+ def translate_with_local(text: str):
67
+ global local_pipeline
68
+ if local_pipeline is None:
69
+ raise RuntimeError("Local pipeline not loaded")
70
+ out = local_pipeline(text, max_length=512)
71
+ if isinstance(out, list) and len(out) > 0:
72
+ # many Marian models use 'translation_text' or 'generated_text'
73
+ res = out[0].get("translation_text") if isinstance(out[0], dict) else None
74
+ if not res:
75
+ # fallback to first value in dict
76
+ if isinstance(out[0], dict):
77
+ res = list(out[0].values())[0]
78
+ return res or str(out)
79
+ return str(out)
80
+
81
+ def translate_with_api(text: str, model_name: str):
82
+ global inference_client
83
+ if inference_client is None:
84
+ raise RuntimeError("Inference client not ready")
85
+ # Note: the Inference API will run the model hosted on HF; for Marian models, you just pass the text.
86
+ res = inference_client(inputs=text, parameters={})
87
+ # API returns either list or dict; try to extract text
88
+ if isinstance(res, list) and len(res) > 0:
89
+ first = res[0]
90
+ if isinstance(first, dict):
91
+ return first.get("translation_text") or first.get("generated_text") or str(first)
92
+ return str(first)
93
+ if isinstance(res, dict):
94
+ return res.get("translation_text") or res.get("generated_text") or str(res)
95
+ return str(res)
96
+
97
+ # ---------- ON START: try local load (best-effort) ----------
98
+ # We'll pre-load both directions lazily on first use; try EN->BN by default
99
+ _success, _msg = try_load_local(MODEL_EN_TO_BN)
100
+ print("Model load attempt:", _success, _msg)
101
+
102
+ # If local load failed, but user supplied HF_API_TOKEN in Secrets, init inference client as fallback
103
+ if use_api_fallback:
104
+ ok, msg = try_init_inference_api(model_name_fallback=MODEL_EN_TO_BN)
105
+ print("Inference API init:", ok, msg)
106
+
107
+ # ---------- TRANSLATION FUNCTION FOR UI ----------
108
+ def translate_text(text: str, direction: str):
109
+ """Main translate function: returns (translation, status, analysis)"""
110
+ if not text or not text.strip():
111
+ return "", "Please type text to translate", ""
112
+ try:
113
+ model_name = MODEL_EN_TO_BN if direction == DIRECTION_CHOICES[0] else MODEL_BN_TO_EN
114
+
115
+ # If local model not loaded or different than needed, try loading it
116
+ global local_model_name
117
+ if local_pipeline is None or local_model_name != model_name:
118
+ ok, msg = try_load_local(model_name)
119
+ print("Reload attempt:", ok, msg)
120
+ # if local load failed, try to init API if token present
121
+ if not ok and inference_client is None:
122
+ ok2, msg2 = try_init_inference_api(model_name_fallback=model_name)
123
+ print("Fallback init:", ok2, msg2)
124
+
125
+ # If local available, use it
126
+ if local_pipeline is not None and local_model_name == model_name:
127
+ translated = translate_with_local(text)
128
+ status = f"Local model used: {local_model_name}"
129
+ else:
130
+ # fallback to hosted inference
131
+ if inference_client is None:
132
+ return "", "No model available locally and no HF_API_TOKEN set for API fallback. Set HF_API_TOKEN in Space secrets.", ""
133
+ translated = translate_with_api(text, model_name)
134
+ status = f"Hosted Inference API used: {model_name}"
135
+
136
+ # small "analysis" block: length, word count, suggestions
137
+ words = len(text.split())
138
+ analysis = f"Input words: {words}. Output length: {len(translated.split())} words."
139
+ return translated, status, analysis
 
 
 
 
 
 
 
 
 
 
 
140
 
141
+ except Exception as e:
142
+ tb = traceback.format_exc()
143
+ return "", f"Error: {str(e)}", tb
 
 
 
 
 
 
 
 
 
144
 
145
+ # ---------- GRADIO APP UI ----------
146
+ with gr.Blocks(title="English ↔ Bengali — Fast Translator") as demo:
147
+ gr.Markdown("# English ↔ Bengali — Fast Translator")
148
+ gr.Markdown(
149
+ "Small, fast models (OPUS-MT) used for speed. If local loading fails the app will use the Hugging Face Inference API (requires HF_API_TOKEN set in Space secrets)."
150
+ )
151
 
152
+ with gr.Row():
153
+ direction = gr.Radio(label="Direction", choices=DIRECTION_CHOICES, value=DIRECTION_CHOICES[0])
154
+ swap = gr.Button("Swap")
 
 
155
 
156
+ input_text = gr.Textbox(label="Input text", lines=4, placeholder="Type in English or Bengali...")
157
+ translate_btn = gr.Button("Translate", variant="primary")
 
 
 
158
 
159
+ with gr.Row():
160
+ out_translation = gr.Textbox(label="Translation", lines=4)
161
+ out_status = gr.Textbox(label="Status / Tips", lines=2)
162
+ out_analysis = gr.Textbox(label="Analysis / Notes", lines=3)
163
 
164
+ # examples
165
+ with gr.Row():
166
+ ex1 = gr.Button("Hello, how are you?")
167
+ ex2 = gr.Button("Ami bhalo achi")
168
+ ex3 = gr.Button("Where is the market?")
 
 
169
 
170
+ # wiring
171
+ def do_swap(cur):
172
+ return DIRECTION_CHOICES[1] if cur == DIRECTION_CHOICES[0] else DIRECTION_CHOICES[0]
173
+ swap.click(do_swap, inputs=direction, outputs=direction)
 
 
 
 
 
 
174
 
175
+ translate_btn.click(translate_text, inputs=[input_text, direction], outputs=[out_translation, out_status, out_analysis])
 
 
176
 
177
+ ex1.click(lambda: "Hello, how are you?", outputs=input_text)
178
+ ex2.click(lambda: "Ami bhalo achi", outputs=input_text)
179
+ ex3.click(lambda: "Where is the market?", outputs=input_text)
 
 
 
 
 
180
 
181
+ gr.Markdown("---")
182
+ gr.Markdown("If the app shows `No model available` error: go to Space Settings → Secrets and add `HF_API_TOKEN` (your Hugging Face token).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
+ # Launch if run directly
185
  if __name__ == "__main__":
186
+ demo.launch(debug=True)