Pro-Coder commited on
Commit
23c9b7a
·
verified ·
1 Parent(s): f9c247c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -206
app.py CHANGED
@@ -249,135 +249,48 @@ def load_preset(name):
249
  # ==========================================================================
250
 
251
  def eval_intent_section_md():
252
- cls_report = intent_eval.get("classification_report", {})
253
- per_class_rows = []
254
- for cls in intent_eval.get("classes", []):
255
- stats = cls_report.get(cls, {})
256
- per_class_rows.append(
257
- f"| {cls} | {stats.get('precision', 0):.2f} | {stats.get('recall', 0):.2f} | "
258
- f"{stats.get('f1-score', 0):.2f} | {int(stats.get('support', 0))} |"
259
- )
260
- per_class_table = "\n".join(per_class_rows)
261
- return f"""
262
- ## 1. Intent Classifier (TF-IDF + Logistic Regression)
263
-
264
- **What it does:** routes a free-text query (e.g. *"The conveyor belt in Zone C
265
- is making noise"*) into one of 8 operational categories, used by both the
266
- AI Assistant and Inventory & Order Query tabs to decide how to handle a
267
- request.
268
-
269
- **Training data:** {intent_eval.get('n_train', '?') } synthetically generated
270
- example queries (see chart below), built from ~8 hand-written templates per
271
- category with randomised SKU codes, zone names, order IDs, and equipment IDs
272
- slotted in -- e.g. *"How many units of {{sku}} are in {{zone}}?"*. This keeps
273
- the language varied while being fully reproducible (`src/data_generation.py`).
274
-
275
- Evaluated on a **held-out stratified test split** of {intent_eval.get('n_test', '?')}
276
- examples the model never saw during training.
277
-
278
- | Metric | Score |
279
- |---|---|
280
- | **Accuracy** | **{intent_eval.get('accuracy', 0):.2%}** |
281
- | **Macro F1** | **{intent_eval.get('macro_f1', 0):.2%}** |
282
-
283
- **Per-class performance:**
284
-
285
- | Intent | Precision | Recall | F1 | Support |
286
- |---|---|---|---|---|
287
- {per_class_table}
288
- """
289
 
290
 
291
  def eval_anomaly_section_md():
292
- return f"""
293
- ## 2. Predictive Maintenance Anomaly Detector (Isolation Forest)
294
-
295
- **What it does:** flags abnormal conveyor/crane motor sensor readings
296
- (temperature, vibration, current, belt speed) before they cause an
297
- unplanned stoppage -- powers the Predictive Maintenance tab.
298
-
299
- **Training data:** {900 + 100} synthetic sensor readings (900 "normal"
300
- + 100 "anomaly" patterns), each with 4 features. Normal readings are drawn
301
- from realistic operating ranges (e.g. ~55°C motor temp, ~2.2 mm/s vibration);
302
- anomalies simulate bearing wear / misalignment / overload (elevated temp,
303
- vibration, and current with reduced belt speed). See the distribution chart
304
- below for exactly how these two classes differ.
305
-
306
- The model itself is trained **unsupervised** (Isolation Forest never sees
307
- the anomaly label during fitting) -- labels are used only to *evaluate* it
308
- afterward, on a held-out test split of {anomaly_eval.get('n_test', '?')}
309
- readings ({anomaly_eval.get('test_anomaly_rate', 0):.1%} true anomaly rate).
310
-
311
- | Metric | Score |
312
- |---|---|
313
- | **Precision** | **{anomaly_eval.get('precision', 0):.2%}** |
314
- | **Recall** | **{anomaly_eval.get('recall', 0):.2%}** |
315
- | **F1 Score** | **{anomaly_eval.get('f1', 0):.2%}** |
316
- | **ROC-AUC** | **{anomaly_eval.get('roc_auc', 0):.3f}** |
317
- | Accuracy | {anomaly_eval.get('accuracy', 0):.2%} |
318
- """
319
 
320
 
321
  def eval_retrieval_section_md():
322
- retrieval_rows = "\n".join(
323
- f"| {r['query']} | {r['expected']} | {r['retrieved_top1']} | "
324
- f"{'✅' if r['hit@1'] else ('〰️' if r['hit@2'] else '❌')} | {r['top1_score']:.2f} |"
325
- for r in retrieval_eval.get("rows", [])
 
326
  )
327
- return f"""
328
- ## 3. Retrieval (RAG) Evaluation
329
-
330
- **What it does:** before the LLM answers a question, this component finds
331
- the most relevant passages from a 10-article warehouse-operations knowledge
332
- base (AS/RS, AGV/AMR, WMS, sortation, picking strategy, safety, etc. -- see
333
- `src/knowledge_base.py`) using TF-IDF + cosine similarity, so the LLM answers
334
- from real context rather than guessing.
335
-
336
- **Evaluation data:** {retrieval_eval.get('n_queries', '?')} hand-labelled
337
- (query, expected-article) pairs -- a small ground-truth set built by hand to
338
- check the retriever finds the *right* article, not just *an* article.
339
-
340
- | Metric | Score |
341
- |---|---|
342
- | **Hit Rate @ 1** | **{retrieval_eval.get('hit_rate_at_1', 0):.0%}** |
343
- | **Hit Rate @ 2** | **{retrieval_eval.get('hit_rate_at_2', 0):.0%}** |
344
-
345
- | Query | Expected Doc | Retrieved (top-1) | Hit | Score |
346
- |---|---|---|---|---|
347
- {retrieval_rows}
348
- """
349
 
350
 
351
  def eval_latency_section_md():
352
- return f"""
353
- ## 4. Latency Benchmark (per-request, CPU)
354
-
355
- Average of 50 runs each, measured on the same CPU hardware the Space runs on.
356
-
357
- | Component | Avg. latency |
358
- |---|---|
359
- | Intent classification | {latency_eval.get('intent_classifier_ms', '?')} ms |
360
- | Anomaly scoring | {latency_eval.get('anomaly_detector_ms', '?')} ms |
361
- | KB retrieval (TF-IDF) | {latency_eval.get('kb_retrieval_ms', '?')} ms |
362
- | LLM generation | Depends on the hosted Inference API (measured live per-request in the Assistant tab, not benchmarked here) |
363
- """
364
 
365
 
366
- EVAL_METHODOLOGY_MD = """
367
- ### Evaluation methodology notes
368
-
369
- - All datasets are **synthetically generated** (see `src/data_generation.py`) using
370
- templated-but-varied natural language and randomised sensor distributions with a
371
- fixed seed, so results are fully reproducible via `python build_artifacts.py`.
372
- - The intent classifier and anomaly detector are evaluated on a **held-out test
373
- split** they never saw during training (stratified, 25% / 30% respectively).
374
- - The anomaly detector itself is trained **unsupervised** (Isolation Forest never
375
- sees the `label` column during `.fit()`); labels are used only to *evaluate* it,
376
- mirroring how you'd validate an anomaly model against a small set of confirmed
377
- historical incidents in production.
378
- - In a production deployment, all three components would be continuously
379
- re-evaluated against real WMS/WCS/sensor logs rather than synthetic data.
380
- """
381
 
382
 
383
 
@@ -483,8 +396,21 @@ linked repository. Feedback welcome.*
483
  CUSTOM_CSS = """
484
  #title-banner { text-align: center; margin-bottom: 0.5em; }
485
  .gradio-container { max-width: 1150px !important; margin: auto; }
 
 
 
 
 
 
 
 
 
 
486
  """
487
 
 
 
 
488
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=CUSTOM_CSS, title="Smart Warehouse AI Assistant") as demo:
489
  gr.Markdown(
490
  "<h1 id='title-banner'>🏭 Smart Warehouse AI Assistant</h1>"
@@ -500,25 +426,12 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=CUSTOM_CSS, title="
500
  )
501
 
502
  with gr.Tab("💬 AI Assistant"):
503
- gr.Markdown(
504
- "Ask about equipment status, maintenance, safety, inventory, order status, "
505
- "AGV routing, picking strategy, or general warehouse-automation concepts. "
506
- "Answers are grounded (RAG) in a small warehouse-operations knowledge base."
507
- )
508
- with gr.Accordion("ℹ️ About the data behind this tab", open=False):
509
- gr.Markdown(
510
- "- **Knowledge base (RAG source):** 10 original, hand-written articles "
511
- "covering AS/RS, AGV/AMR, WMS, conveyor/sortation, picking strategy, "
512
- "predictive maintenance, safety protocol, inventory accuracy, KPIs, and "
513
- "energy efficiency (`src/knowledge_base.py`). Every answer's *Retrieved "
514
- "context* line shows exactly which article(s) it drew on.\n"
515
- "- **Intent classifier:** trained on ~480 synthetically generated example "
516
- "queries across 8 categories (see the Model Evaluation tab for accuracy).\n"
517
- "- **LLM:** a hosted instruct model called via the Hugging Face Inference "
518
- "API — not run locally. If no `HF_TOKEN` is configured, or the API call "
519
- "fails, this tab automatically falls back to showing the retrieved "
520
- "knowledge-base passages directly, so it never just breaks."
521
- )
522
  gr.ChatInterface(
523
  fn=chat_fn,
524
  type="messages",
@@ -537,27 +450,12 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=CUSTOM_CSS, title="
537
  test_llm_btn.click(test_llm_fn, inputs=None, outputs=test_llm_output)
538
 
539
  with gr.Tab("📦 Inventory & Order Query"):
540
- gr.Markdown(
541
- "Type a natural-language inventory or order question. The intent classifier "
542
- "decides whether to query the inventory table or the orders table, then "
543
- "extracts SKU / order-id / zone slots to filter the result."
544
- )
545
- with gr.Accordion("ℹ️ About the data behind this tab", open=False):
546
- gr.Markdown(
547
- f"- **Inventory table:** {len(inventory_df)} synthetic SKUs across 5 "
548
- "categories (Electronics, Apparel, Automotive Parts, Food & Beverage, "
549
- "Household) and 4 warehouse zones, with randomised on-hand quantities, "
550
- "reorder points, and unit costs.\n"
551
- f"- **Orders table:** {len(orders_df)} synthetic orders with randomised "
552
- "status (Received / Picking / Packed / Shipped / Delayed), line count, "
553
- "priority, and zone.\n"
554
- "- Both tables are generated by `src/data_generation.py` with a fixed "
555
- "random seed, so they're reproducible but **not real operational data** "
556
- "-- this is a stand-in for a live WMS/WCS query interface.\n"
557
- "- Query parsing is regex-based slot extraction (SKU codes like `SKU-1042`, "
558
- "order IDs like `#10007`, zone names) combined with the same intent "
559
- "classifier used in the AI Assistant tab (`src/inventory_db.py`)."
560
- )
561
  with gr.Row():
562
  inv_input = gr.Textbox(label="Query", placeholder="How many units of SKU-1042 are in Zone B?", scale=4)
563
  inv_btn = gr.Button("Search", variant="primary", scale=1)
@@ -582,26 +480,12 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=CUSTOM_CSS, title="
582
  gr.Dataframe(orders_df, wrap=True)
583
 
584
  with gr.Tab("⚠️ Predictive Maintenance"):
585
- gr.Markdown(
586
- "Enter live (or hypothetical) conveyor/crane motor sensor readings to check "
587
- "for anomalous behaviour using an Isolation Forest model trained on "
588
- "historical sensor patterns."
589
- )
590
- with gr.Accordion("ℹ️ About the data behind this tab", open=False):
591
- gr.Markdown(
592
- "- **Training data:** 1,000 synthetic sensor readings (900 normal + 100 "
593
- "anomalous) across 4 features -- motor temperature, vibration, motor "
594
- "current, and belt speed -- generated by `src/data_generation.py`. "
595
- "Anomalies simulate realistic failure signatures: elevated temperature, "
596
- "vibration, and current combined with reduced/erratic belt speed (the "
597
- "pattern of bearing wear, belt misalignment, or motor overload).\n"
598
- "- **Model:** Isolation Forest (unsupervised) trained on scaled features "
599
- "-- it never sees a 'this is an anomaly' label during training, only "
600
- "learns what 'normal' looks like and flags deviations from it.\n"
601
- "- See the **Model Evaluation** tab for the feature-distribution chart "
602
- "showing exactly how normal vs. anomalous readings differ, plus "
603
- "precision/recall/F1/ROC-AUC on held-out data."
604
- )
605
  preset_dropdown = gr.Dropdown(
606
  choices=list(ANOMALY_PRESETS.keys()), label="Load a preset reading", value="Normal reading"
607
  )
@@ -625,48 +509,47 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=CUSTOM_CSS, title="
625
  )
626
 
627
  with gr.Tab("📊 Model Evaluation"):
628
- gr.Markdown(
629
- "Every metric on this tab is computed on **held-out test data** by "
630
- "`build_artifacts.py` (not cherry-picked from a live demo run) -- "
631
- "re-run that script any time to reproduce these numbers from scratch."
632
- )
633
-
634
- gr.Markdown(eval_intent_section_md())
635
- gr.Markdown("**Dataset composition** (how many training examples per intent):")
636
- gr.Image(os.path.join(ASSETS_DIR, "intent_dataset_composition.png"), show_label=False, container=False)
637
- gr.Markdown("**Per-class precision / recall / F1:**")
638
- gr.Image(os.path.join(ASSETS_DIR, "intent_per_class_bar.png"), show_label=False, container=False)
639
- gr.Markdown("**Confusion matrix:**")
640
  gr.Image(os.path.join(ASSETS_DIR, "intent_confusion_matrix.png"), show_label=False, container=False)
641
 
642
- gr.Markdown("---")
643
- gr.Markdown(eval_anomaly_section_md())
644
- gr.Markdown("**Sensor feature distributions (normal vs. anomaly):**")
645
  gr.Image(os.path.join(ASSETS_DIR, "sensor_distributions.png"), show_label=False, container=False)
646
  with gr.Row():
647
- with gr.Column():
648
- gr.Markdown("**Evaluation metrics:**")
649
- gr.Image(os.path.join(ASSETS_DIR, "anomaly_metrics_bar.png"), show_label=False, container=False)
650
- with gr.Column():
651
- gr.Markdown("**Confusion matrix:**")
652
- gr.Image(os.path.join(ASSETS_DIR, "anomaly_confusion_matrix.png"), show_label=False, container=False)
653
- gr.Markdown("**ROC curve:**")
654
  gr.Image(os.path.join(ASSETS_DIR, "anomaly_roc_curve.png"), show_label=False, container=False)
655
 
656
- gr.Markdown("---")
657
- gr.Markdown(eval_retrieval_section_md())
 
658
  gr.Image(os.path.join(ASSETS_DIR, "retrieval_hitrate_bar.png"), show_label=False, container=False)
659
 
660
- gr.Markdown("---")
661
- gr.Markdown(eval_latency_section_md())
662
  gr.Image(os.path.join(ASSETS_DIR, "latency_bar.png"), show_label=False, container=False)
663
 
664
- gr.Markdown("---")
665
- gr.Markdown(EVAL_METHODOLOGY_MD)
 
 
 
 
666
 
667
  with gr.Tab("ℹ️ About"):
668
  gr.Markdown(ABOUT_MD)
669
 
670
 
 
671
  if __name__ == "__main__":
672
- demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))
 
249
  # ==========================================================================
250
 
251
  def eval_intent_section_md():
252
+ return (
253
+ f"### 1. Intent Classifier &nbsp; "
254
+ f"🎯 **{intent_eval.get('accuracy', 0):.0%} accuracy** &nbsp;·&nbsp; "
255
+ f"**{intent_eval.get('macro_f1', 0):.0%} macro F1** &nbsp;·&nbsp; "
256
+ f"held-out test set, {intent_eval.get('n_test', '?')} examples, "
257
+ f"{intent_eval.get('n_classes', '?')} classes"
258
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
 
261
  def eval_anomaly_section_md():
262
+ return (
263
+ f"### 2. Predictive Maintenance (Anomaly Detector) &nbsp; "
264
+ f"🎯 **{anomaly_eval.get('f1', 0):.0%} F1** &nbsp;·&nbsp; "
265
+ f"**{anomaly_eval.get('roc_auc', 0):.2f} ROC-AUC** &nbsp;·&nbsp; "
266
+ f"**{anomaly_eval.get('precision', 0):.0%} precision** &nbsp;·&nbsp; "
267
+ f"**{anomaly_eval.get('recall', 0):.0%} recall**"
268
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
 
271
  def eval_retrieval_section_md():
272
+ return (
273
+ f"### 3. RAG Retrieval &nbsp; "
274
+ f"🎯 **{retrieval_eval.get('hit_rate_at_1', 0):.0%} hit-rate@1** &nbsp;·&nbsp; "
275
+ f"**{retrieval_eval.get('hit_rate_at_2', 0):.0%} hit-rate@2** &nbsp;·&nbsp; "
276
+ f"{retrieval_eval.get('n_queries', '?')} labelled test queries"
277
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
 
280
  def eval_latency_section_md():
281
+ return (
282
+ f"### 4. Latency (CPU, avg of 50 runs) &nbsp; "
283
+ f"🎯 **{latency_eval.get('intent_classifier_ms', '?')} ms** intent &nbsp;·&nbsp; "
284
+ f"**{latency_eval.get('anomaly_detector_ms', '?')} ms** anomaly &nbsp;·&nbsp; "
285
+ f"**{latency_eval.get('kb_retrieval_ms', '?')} ms** retrieval"
286
+ )
 
 
 
 
 
 
287
 
288
 
289
+ EVAL_METHODOLOGY_MD = (
290
+ "*All metrics above are computed on held-out synthetic test data by "
291
+ "`build_artifacts.py` (fully reproducible) — see the **About** tab for "
292
+ "dataset details and how each model works.*"
293
+ )
 
 
 
 
 
 
 
 
 
 
294
 
295
 
296
 
 
396
  CUSTOM_CSS = """
397
  #title-banner { text-align: center; margin-bottom: 0.5em; }
398
  .gradio-container { max-width: 1150px !important; margin: auto; }
399
+ .data-badge {
400
+ background: #eff6ff;
401
+ border-left: 4px solid #3b82f6;
402
+ border-radius: 6px;
403
+ padding: 10px 14px;
404
+ margin-bottom: 10px;
405
+ font-size: 0.92em;
406
+ line-height: 1.5;
407
+ }
408
+ .data-badge b { color: #1e3a8a; }
409
  """
410
 
411
+ def data_badge(html: str) -> str:
412
+ return f'<div class="data-badge">{html}</div>'
413
+
414
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=CUSTOM_CSS, title="Smart Warehouse AI Assistant") as demo:
415
  gr.Markdown(
416
  "<h1 id='title-banner'>🏭 Smart Warehouse AI Assistant</h1>"
 
426
  )
427
 
428
  with gr.Tab("💬 AI Assistant"):
429
+ gr.HTML(data_badge(
430
+ "🧪 <b>You are testing:</b> an LLM chatbot grounded in a 10-article synthetic "
431
+ "warehouse-ops knowledge base (RAG), routed by an intent classifier trained on "
432
+ "~480 synthetic queries. <b>Data type:</b> hand-written knowledge articles + "
433
+ "template-generated questions — not real Daifuku data."
434
+ ))
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  gr.ChatInterface(
436
  fn=chat_fn,
437
  type="messages",
 
450
  test_llm_btn.click(test_llm_fn, inputs=None, outputs=test_llm_output)
451
 
452
  with gr.Tab("📦 Inventory & Order Query"):
453
+ gr.HTML(data_badge(
454
+ f"🧪 <b>You are testing:</b> NL→structured-query search over "
455
+ f"{len(inventory_df)} synthetic SKUs and {len(orders_df)} synthetic orders. "
456
+ "<b>Data type:</b> randomly generated inventory/order records (fixed seed, "
457
+ "reproducible) — a stand-in for a live WMS, not real warehouse data."
458
+ ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  with gr.Row():
460
  inv_input = gr.Textbox(label="Query", placeholder="How many units of SKU-1042 are in Zone B?", scale=4)
461
  inv_btn = gr.Button("Search", variant="primary", scale=1)
 
480
  gr.Dataframe(orders_df, wrap=True)
481
 
482
  with gr.Tab("⚠️ Predictive Maintenance"):
483
+ gr.HTML(data_badge(
484
+ "🧪 <b>You are testing:</b> an Isolation Forest anomaly detector for conveyor/"
485
+ "crane motor sensors. <b>Data type:</b> 1,000 synthetic sensor readings (900 "
486
+ "normal + 100 simulated-fault) across temperature, vibration, current, and belt "
487
+ "speed — trained unsupervised, not on real Daifuku equipment telemetry."
488
+ ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  preset_dropdown = gr.Dropdown(
490
  choices=list(ANOMALY_PRESETS.keys()), label="Load a preset reading", value="Normal reading"
491
  )
 
509
  )
510
 
511
  with gr.Tab("📊 Model Evaluation"):
512
+ gr.HTML(data_badge(
513
+ "🧪 All charts below are computed on <b>held-out test data</b> by "
514
+ "<code>build_artifacts.py</code> fully reproducible, not cherry-picked."
515
+ ))
516
+
517
+ gr.Markdown("### 1. Intent Classifier — accuracy: **{:.1%}** · macro F1: **{:.1%}**".format(
518
+ intent_eval.get("accuracy", 0), intent_eval.get("macro_f1", 0)
519
+ ))
520
+ with gr.Row():
521
+ gr.Image(os.path.join(ASSETS_DIR, "intent_dataset_composition.png"), show_label=False, container=False)
522
+ gr.Image(os.path.join(ASSETS_DIR, "intent_per_class_bar.png"), show_label=False, container=False)
 
523
  gr.Image(os.path.join(ASSETS_DIR, "intent_confusion_matrix.png"), show_label=False, container=False)
524
 
525
+ gr.Markdown("### 2. Predictive Maintenance — F1: **{:.1%}** · ROC-AUC: **{:.2f}**".format(
526
+ anomaly_eval.get("f1", 0), anomaly_eval.get("roc_auc", 0)
527
+ ))
528
  gr.Image(os.path.join(ASSETS_DIR, "sensor_distributions.png"), show_label=False, container=False)
529
  with gr.Row():
530
+ gr.Image(os.path.join(ASSETS_DIR, "anomaly_metrics_bar.png"), show_label=False, container=False)
531
+ gr.Image(os.path.join(ASSETS_DIR, "anomaly_confusion_matrix.png"), show_label=False, container=False)
 
 
 
 
 
532
  gr.Image(os.path.join(ASSETS_DIR, "anomaly_roc_curve.png"), show_label=False, container=False)
533
 
534
+ gr.Markdown("### 3. RAG Retrieval — Hit-rate@1: **{:.0%}** · Hit-rate@2: **{:.0%}**".format(
535
+ retrieval_eval.get("hit_rate_at_1", 0), retrieval_eval.get("hit_rate_at_2", 0)
536
+ ))
537
  gr.Image(os.path.join(ASSETS_DIR, "retrieval_hitrate_bar.png"), show_label=False, container=False)
538
 
539
+ gr.Markdown("### 4. Latency Benchmark (CPU, local components)")
 
540
  gr.Image(os.path.join(ASSETS_DIR, "latency_bar.png"), show_label=False, container=False)
541
 
542
+ with gr.Accordion("📋 Full metrics tables & methodology notes", open=False):
543
+ gr.Markdown(eval_intent_section_md())
544
+ gr.Markdown(eval_anomaly_section_md())
545
+ gr.Markdown(eval_retrieval_section_md())
546
+ gr.Markdown(eval_latency_section_md())
547
+ gr.Markdown(EVAL_METHODOLOGY_MD)
548
 
549
  with gr.Tab("ℹ️ About"):
550
  gr.Markdown(ABOUT_MD)
551
 
552
 
553
+
554
  if __name__ == "__main__":
555
+ demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))