Tulitula commited on
Commit
04c51ad
·
verified ·
1 Parent(s): afc8385

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -177
app.py CHANGED
@@ -392,20 +392,14 @@ def set_horizon(years: float):
392
  def search_tickers_cb(q: str):
393
  opts = yahoo_search(q)
394
  if not opts:
395
- return gr.update(
396
- choices=["No matches found"],
397
- value=None,
398
- info="No matches."
399
- )
400
- first = opts[0] # preselect the first hit
401
- return gr.update(
402
- choices=opts,
403
- value=first,
404
- info="Select a symbol and click 'Add selected to portfolio'."
405
- )
406
 
407
  def add_symbol(selection: str, table: Optional[pd.DataFrame]):
408
- if (not selection) or ("No matches" in selection):
409
  return table if isinstance(table, pd.DataFrame) else pd.DataFrame(columns=["ticker","amount_usd"]), "Pick a valid match first."
410
  symbol = selection.split("|")[0].strip().upper()
411
 
@@ -478,46 +472,18 @@ def compute(
478
 
479
  symbols = [t for t in df["ticker"].tolist() if t]
480
  if len(symbols) == 0:
481
- out_empty = gr.update(visible=True, value="Add at least one ticker.")
482
- empty_df = gr.update(visible=True, value=empty_positions_df())
483
- empty_sugg = gr.update(visible=True, value=empty_suggestion_df())
484
- none_file = gr.update(visible=True, value=None)
485
  return (
486
- gr.update(visible=True), # show results group after compute
487
- gr.update(visible=True, value=None), # plot
488
- out_empty, # summary
489
- empty_df, # positions
490
- empty_sugg, # sugg_table
491
- none_file, # file
492
- gr.update(visible=True, value=""), # low_txt
493
- gr.update(visible=True, value=""), # med_txt
494
- gr.update(visible=True, value=""), # high_txt
495
- gr.update(visible=True), # md_sugg
496
- gr.update(visible=True), # btn_low
497
- gr.update(visible=True), # btn_med
498
- gr.update(visible=True), # btn_high
499
  )
500
 
501
  symbols = validate_tickers(symbols, years_lookback)
502
  if len(symbols) == 0:
503
- out_empty = gr.update(visible=True, value="Could not validate any tickers.")
504
- empty_df = gr.update(visible=True, value=empty_positions_df())
505
- empty_sugg = gr.update(visible=True, value=empty_suggestion_df())
506
- none_file = gr.update(visible=True, value=None)
507
  return (
508
- gr.update(visible=True),
509
- gr.update(visible=True, value=None),
510
- out_empty,
511
- empty_df,
512
- empty_sugg,
513
- none_file,
514
- gr.update(visible=True, value=""),
515
- gr.update(visible=True, value=""),
516
- gr.update(visible=True, value=""),
517
- gr.update(visible=True),
518
- gr.update(visible=True),
519
- gr.update(visible=True),
520
- gr.update(visible=True),
521
  )
522
 
523
  global UNIVERSE
@@ -527,41 +493,32 @@ def compute(
527
  amounts = {r["ticker"]: float(r["amount_usd"]) for _, r in df.iterrows()}
528
  rf_ann = RF_ANN
529
 
530
- progress(0.20, desc="Downloading prices & computing returns...")
 
 
531
  moms = estimate_all_moments_aligned(symbols, years_lookback, rf_ann)
532
  betas, covA, erp_ann, sigma_mkt = moms["betas"], moms["cov_ann"], moms["erp_ann"], moms["sigma_m_ann"]
533
 
 
534
  gross = sum(abs(v) for v in amounts.values())
535
  if gross <= 1e-12:
536
- out_empty = gr.update(visible=True, value="All amounts are zero.")
537
- empty_df = gr.update(visible=True, value=empty_positions_df())
538
- empty_sugg = gr.update(visible=True, value=empty_suggestion_df())
539
- none_file = gr.update(visible=True, value=None)
540
  return (
541
- gr.update(visible=True),
542
- gr.update(visible=True, value=None),
543
- out_empty,
544
- empty_df,
545
- empty_sugg,
546
- none_file,
547
- gr.update(visible=True, value=""),
548
- gr.update(visible=True, value=""),
549
- gr.update(visible=True, value=""),
550
- gr.update(visible=True),
551
- gr.update(visible=True),
552
- gr.update(visible=True),
553
- gr.update(visible=True),
554
  )
555
  weights = {k: v / gross for k, v in amounts.items()}
556
 
557
- progress(0.35, desc="Computing CAPM stats...")
558
  beta_p, mu_capm, sigma_hist = portfolio_stats(weights, covA, betas, rf_ann, erp_ann)
559
 
560
- progress(0.50, desc="Efficient mixes on CML...")
 
 
561
  a_sigma, b_sigma, mu_eff_same_sigma = efficient_same_sigma(sigma_hist, rf_ann, erp_ann, sigma_mkt)
562
  a_mu, b_mu, sigma_eff_same_mu = efficient_same_return(mu_capm, rf_ann, erp_ann, sigma_mkt)
563
 
564
- progress(0.70, desc="Building 1,000 candidate mixes...")
565
  user_universe = list(symbols)
566
  synth = build_synthetic_dataset(user_universe, covA, betas, rf_ann, erp_ann, sigma_mkt, n_rows=SYNTH_ROWS)
567
  csv_path = os.path.join(DATA_DIR, f"investor_profiles_{int(time.time())}.csv")
@@ -570,7 +527,8 @@ def compute(
570
  except Exception:
571
  csv_path = None
572
 
573
- progress(0.85, desc="Ranking suggestions...")
 
574
  picks = suggest_one_per_band(synth, sigma_mkt, user_universe)
575
 
576
  def _fmt(row: pd.Series) -> str:
@@ -591,6 +549,7 @@ def compute(
591
  else:
592
  chosen_sigma = float(chosen["sigma_hist"])
593
  chosen_mu = float(chosen["mu_capm"])
 
594
  sugg_table = _holdings_table_from_row(chosen, budget=gross)
595
 
596
  pos_table = pd.DataFrame(
@@ -603,7 +562,6 @@ def compute(
603
  columns=["ticker", "amount_usd", "weight_exposure", "beta"]
604
  )
605
 
606
- progress(0.95, desc="Rendering chart...")
607
  img = plot_cml(
608
  rf_ann, erp_ann, sigma_mkt,
609
  sigma_hist, mu_capm,
@@ -632,139 +590,145 @@ def compute(
632
  "If leverage isn’t allowed, scale both weights proportionally toward 1.0 to fit your constraints.",
633
  ])
634
 
635
- progress(1.0, desc="Done.")
 
636
  return (
637
- gr.update(visible=True), # show results group
638
- gr.update(visible=True, value=img), # plot
639
- gr.update(visible=True, value=info), # summary
640
- gr.update(visible=True, value=pos_table), # positions
641
- gr.update(visible=True, value=sugg_table),# sugg_table
642
- gr.update(visible=True, value=csv_path), # file
643
- gr.update(visible=True, value=txt_low), # low_txt
644
- gr.update(visible=True, value=txt_med), # med_txt
645
- gr.update(visible=True, value=txt_high), # high_txt
646
- gr.update(visible=True), # md_sugg
647
- gr.update(visible=True), # btn_low
648
- gr.update(visible=True), # btn_med
649
- gr.update(visible=True), # btn_high
650
  )
651
 
652
  # -------------- UI --------------
653
- with gr.Blocks(title="Efficient Portfolio Advisor", theme=gr.themes.Soft(primary_hue="violet")) as demo:
654
- gr.Markdown(
655
- "## Efficient Portfolio Advisor\n"
656
- "Search symbols, enter **dollar amounts**, set horizon. Returns use Yahoo Finance monthly data; risk-free from FRED."
657
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
- # Single column inputs before compute
660
- with gr.Column():
661
- # --- Vertical flow: Search -> Button -> Matches -> Add ---
662
- q = gr.Textbox(label="Search symbol")
663
- search_btn = gr.Button("Search")
664
- matches = gr.Dropdown(choices=[], label="Matches", allow_custom_value=False)
665
- add_btn = gr.Button("Add selected to portfolio")
666
- # ----------------------------------------------------------
667
-
668
- gr.Markdown("### Portfolio positions")
669
- table = gr.Dataframe(
670
- headers=["ticker", "amount_usd"],
671
- datatype=["str", "number"],
672
- row_count=0,
673
- col_count=(2, "fixed")
674
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
 
676
- horizon = gr.Number(label="Horizon in years (1–100)", value=HORIZON_YEARS, precision=0)
677
- lookback = gr.Slider(1, 15, value=DEFAULT_LOOKBACK_YEARS, step=1, label="Lookback years for betas & covariances")
678
-
679
- # compute button directly under lookback slider
680
- run_btn = gr.Button("Compute")
681
-
682
- # Results section appears only after compute, as two columns
683
- results_group = gr.Group(visible=False)
684
- with results_group:
685
- md_sugg = gr.Markdown("### Suggestions", visible=False)
686
- with gr.Row():
687
- with gr.Column(scale=1):
688
- plot = gr.Image(label="Capital Market Line (CAPM)", type="pil", visible=False)
689
- summary = gr.Markdown(label="Inputs & Results", visible=False)
690
- with gr.Row():
691
- btn_low = gr.Button("Show Low", visible=False)
692
- btn_med = gr.Button("Show Medium", visible=False)
693
- btn_high = gr.Button("Show High", visible=False)
694
- low_txt = gr.Markdown(visible=False)
695
- med_txt = gr.Markdown(visible=False)
696
- high_txt = gr.Markdown(visible=False)
697
- with gr.Column(scale=1):
698
- positions = gr.Dataframe(
699
- label="Computed positions",
700
- headers=["ticker", "amount_usd", "weight_exposure", "beta"],
701
- datatype=["str", "number", "number", "number"],
702
- col_count=(4, "fixed"),
703
- value=empty_positions_df(),
704
- interactive=False,
705
- visible=False
706
- )
707
- sugg_table = gr.Dataframe(
708
- label="Selected suggestion holdings (% / $)",
709
- headers=["ticker", "weight_%", "amount_$"],
710
- datatype=["str", "number", "number"],
711
- col_count=(3, "fixed"),
712
- value=empty_suggestion_df(),
713
- interactive=False,
714
- visible=False
715
- )
716
- dl = gr.File(label="Generated dataset CSV", value=None, visible=False)
717
-
718
- # wire search / add / locking
719
  search_btn.click(fn=search_tickers_cb, inputs=q, outputs=matches)
720
  add_btn.click(fn=add_symbol_table_only, inputs=[matches, table], outputs=table)
721
  table.change(fn=lock_ticker_column, inputs=table, outputs=table)
722
-
723
- # horizon updates globals silently
724
  horizon.change(fn=set_horizon, inputs=horizon, outputs=[])
725
 
726
- # compute + reveal UI group first, default to Medium band
727
- run_btn.click(
728
  fn=compute,
729
  inputs=[lookback, table, gr.State("Medium")],
730
  outputs=[
731
- results_group, # newly added to toggle visibility
732
  plot, summary, positions, sugg_table, dl,
733
  low_txt, med_txt, high_txt,
734
- md_sugg, btn_low, btn_med, btn_high,
735
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
  )
737
 
738
- # band buttons recompute picks quickly while keeping everything visible
 
 
 
 
739
  btn_low.click(
740
  fn=compute,
741
- inputs=[lookback, table, gr.State("Low")],
742
- outputs=[
743
- results_group,
744
- plot, summary, positions, sugg_table, dl,
745
- low_txt, med_txt, high_txt,
746
- md_sugg, btn_low, btn_med, btn_high,
747
- ]
748
  )
749
  btn_med.click(
750
  fn=compute,
751
- inputs=[lookback, table, gr.State("Medium")],
752
- outputs=[
753
- results_group,
754
- plot, summary, positions, sugg_table, dl,
755
- low_txt, med_txt, high_txt,
756
- md_sugg, btn_low, btn_med, btn_high,
757
- ]
758
  )
759
  btn_high.click(
760
  fn=compute,
761
- inputs=[lookback, table, gr.State("High")],
762
- outputs=[
763
- results_group,
764
- plot, summary, positions, sugg_table, dl,
765
- low_txt, med_txt, high_txt,
766
- md_sugg, btn_low, btn_med, btn_high,
767
- ]
768
  )
769
 
770
  # initialize risk-free at launch
@@ -772,4 +736,4 @@ RF_CODE = fred_series_for_horizon(HORIZON_YEARS)
772
  RF_ANN = fetch_fred_yield_annual(RF_CODE)
773
 
774
  if __name__ == "__main__":
775
- demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_api=False)
 
392
  def search_tickers_cb(q: str):
393
  opts = yahoo_search(q)
394
  if not opts:
395
+ opts = ["No matches found"]
396
+ # First match auto-selected, helper info inside dropdown
397
+ first = opts[0] if opts and ("No matches" not in opts[0]) else None
398
+ info = "Select a symbol and click 'Add selected to portfolio'." if first else "No matches."
399
+ return gr.update(choices=opts, value=first, info=info)
 
 
 
 
 
 
400
 
401
  def add_symbol(selection: str, table: Optional[pd.DataFrame]):
402
+ if (not selection) or ("No matches" in selection) or ("Select a symbol" in selection) or ("type above" in selection):
403
  return table if isinstance(table, pd.DataFrame) else pd.DataFrame(columns=["ticker","amount_usd"]), "Pick a valid match first."
404
  symbol = selection.split("|")[0].strip().upper()
405
 
 
472
 
473
  symbols = [t for t in df["ticker"].tolist() if t]
474
  if len(symbols) == 0:
 
 
 
 
475
  return (
476
+ None, "Add at least one ticker.", empty_positions_df(), empty_suggestion_df(), None,
477
+ "", "", "",
478
+ None, None, None, None, None, None, None, None, None
 
 
 
 
 
 
 
 
 
 
479
  )
480
 
481
  symbols = validate_tickers(symbols, years_lookback)
482
  if len(symbols) == 0:
 
 
 
 
483
  return (
484
+ None, "Could not validate any tickers.", empty_positions_df(), empty_suggestion_df(), None,
485
+ "", "", "",
486
+ None, None, None, None, None, None, None, None, None
 
 
 
 
 
 
 
 
 
 
487
  )
488
 
489
  global UNIVERSE
 
493
  amounts = {r["ticker"]: float(r["amount_usd"]) for _, r in df.iterrows()}
494
  rf_ann = RF_ANN
495
 
496
+ progress(0.25, desc="Downloading prices & computing moments...")
497
+
498
+ # Moments
499
  moms = estimate_all_moments_aligned(symbols, years_lookback, rf_ann)
500
  betas, covA, erp_ann, sigma_mkt = moms["betas"], moms["cov_ann"], moms["erp_ann"], moms["sigma_m_ann"]
501
 
502
+ # Weights
503
  gross = sum(abs(v) for v in amounts.values())
504
  if gross <= 1e-12:
 
 
 
 
505
  return (
506
+ None, "All amounts are zero.", empty_positions_df(), empty_suggestion_df(), None,
507
+ "", "", "",
508
+ rf_ann, erp_ann, sigma_mkt, None, None, None, None, None, None
 
 
 
 
 
 
 
 
 
 
509
  )
510
  weights = {k: v / gross for k, v in amounts.items()}
511
 
512
+ # Portfolio CAPM stats
513
  beta_p, mu_capm, sigma_hist = portfolio_stats(weights, covA, betas, rf_ann, erp_ann)
514
 
515
+ progress(0.55, desc="Building synthetic dataset...")
516
+
517
+ # Efficient alternatives on CML
518
  a_sigma, b_sigma, mu_eff_same_sigma = efficient_same_sigma(sigma_hist, rf_ann, erp_ann, sigma_mkt)
519
  a_mu, b_mu, sigma_eff_same_mu = efficient_same_return(mu_capm, rf_ann, erp_ann, sigma_mkt)
520
 
521
+ # Synthetic dataset & suggestions — exactly the user's tickers
522
  user_universe = list(symbols)
523
  synth = build_synthetic_dataset(user_universe, covA, betas, rf_ann, erp_ann, sigma_mkt, n_rows=SYNTH_ROWS)
524
  csv_path = os.path.join(DATA_DIR, f"investor_profiles_{int(time.time())}.csv")
 
527
  except Exception:
528
  csv_path = None
529
 
530
+ progress(0.8, desc="Selecting suggestions...")
531
+
532
  picks = suggest_one_per_band(synth, sigma_mkt, user_universe)
533
 
534
  def _fmt(row: pd.Series) -> str:
 
549
  else:
550
  chosen_sigma = float(chosen["sigma_hist"])
551
  chosen_mu = float(chosen["mu_capm"])
552
+ # holdings table from chosen suggestion
553
  sugg_table = _holdings_table_from_row(chosen, budget=gross)
554
 
555
  pos_table = pd.DataFrame(
 
562
  columns=["ticker", "amount_usd", "weight_exposure", "beta"]
563
  )
564
 
 
565
  img = plot_cml(
566
  rf_ann, erp_ann, sigma_mkt,
567
  sigma_hist, mu_capm,
 
590
  "If leverage isn’t allowed, scale both weights proportionally toward 1.0 to fit your constraints.",
591
  ])
592
 
593
+ progress(1.0, desc="Done")
594
+
595
  return (
596
+ img, info, pos_table, sugg_table, csv_path,
597
+ txt_low, txt_med, txt_high,
598
+ rf_ann, erp_ann, sigma_mkt, sigma_hist, mu_capm, mu_eff_same_sigma, sigma_eff_same_mu,
599
+ chosen_sigma, chosen_mu
 
 
 
 
 
 
 
 
 
600
  )
601
 
602
  # -------------- UI --------------
603
+ APP_CSS = """
604
+ :root {
605
+ --accent: hsl(340 70% 50%);
606
+ --radius: 14px;
607
+ }
608
+ * { font-family: ui-sans-serif, system-ui, -apple-system, "Inter", Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
609
+ .gr-button { border-radius: var(--radius); }
610
+ .gr-textbox, .gr-dropdown, .gr-slider, .gr-number, .gr-dataframe { border-radius: var(--radius); }
611
+ """
612
+
613
+ with gr.Blocks(title="Efficient Portfolio Advisor", css=APP_CSS) as demo:
614
+ gr.Markdown("## Efficient Portfolio Advisor")
615
+
616
+ # States to absorb extra returns
617
+ s1 = gr.State(); s2 = gr.State(); s3 = gr.State(); s4 = gr.State(); s5 = gr.State()
618
+ s6 = gr.State(); s7 = gr.State(); s8 = gr.State(); s9 = gr.State()
619
+
620
+ with gr.Row():
621
+ with gr.Column(scale=1) as left_col:
622
+ # --- Input column (full-width before compute) ---
623
+ q = gr.Textbox(label="Search symbol")
624
+ search_btn = gr.Button("Search")
625
+ matches = gr.Dropdown(choices=[], label="Matches", value=None, info="")
626
+ add_btn = gr.Button("Add selected to portfolio")
627
+
628
+ gr.Markdown("### Portfolio positions")
629
+ table = gr.Dataframe(
630
+ headers=["ticker", "amount_usd"],
631
+ datatype=["str", "number"],
632
+ row_count=0,
633
+ col_count=(2, "fixed")
634
+ )
635
 
636
+ horizon = gr.Number(label="Horizon in years (1–100)", value=HORIZON_YEARS, precision=0)
637
+ lookback = gr.Slider(1, 15, value=DEFAULT_LOOKBACK_YEARS, step=1, label="Lookback years for betas & covariances")
638
+
639
+ # Compute button (shows progress bar while compute runs)
640
+ run_btn = gr.Button("Compute (build dataset & suggest)")
641
+
642
+ # Suggestions (hidden until first compute)
643
+ sugg_hdr = gr.Markdown("### Suggestions", visible=False)
644
+ with gr.Row(visible=False) as sugg_btn_row:
645
+ btn_low = gr.Button("Show Low")
646
+ btn_med = gr.Button("Show Medium")
647
+ btn_high = gr.Button("Show High")
648
+ low_txt = gr.Markdown(visible=False)
649
+ med_txt = gr.Markdown(visible=False)
650
+ high_txt = gr.Markdown(visible=False)
651
+
652
+ # --- Output column (hidden until first compute) ---
653
+ with gr.Column(scale=1, visible=False) as right_col:
654
+ plot = gr.Image(label="Capital Market Line (CAPM)", type="pil")
655
+ summary = gr.Markdown(label="Inputs & Results")
656
+ positions = gr.Dataframe(
657
+ label="Computed positions",
658
+ headers=["ticker", "amount_usd", "weight_exposure", "beta"],
659
+ datatype=["str", "number", "number", "number"],
660
+ col_count=(4, "fixed"),
661
+ value=empty_positions_df(),
662
+ interactive=False
663
+ )
664
+ sugg_table = gr.Dataframe(
665
+ label="Selected suggestion holdings (% / $)",
666
+ headers=["ticker", "weight_%", "amount_$"],
667
+ datatype=["str", "number", "number"],
668
+ col_count=(3, "fixed"),
669
+ value=empty_suggestion_df(),
670
+ interactive=False
671
+ )
672
+ dl = gr.File(label="Generated dataset CSV", value=None, visible=True)
673
 
674
+ # --- Wiring ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
  search_btn.click(fn=search_tickers_cb, inputs=q, outputs=matches)
676
  add_btn.click(fn=add_symbol_table_only, inputs=[matches, table], outputs=table)
677
  table.change(fn=lock_ticker_column, inputs=table, outputs=table)
 
 
678
  horizon.change(fn=set_horizon, inputs=horizon, outputs=[])
679
 
680
+ # Compute (default band = Medium) -> populate outputs
681
+ run_compute = run_btn.click(
682
  fn=compute,
683
  inputs=[lookback, table, gr.State("Medium")],
684
  outputs=[
 
685
  plot, summary, positions, sugg_table, dl,
686
  low_txt, med_txt, high_txt,
687
+ s1, s2, s3, s4, s5, s6, s7, s8, s9
688
+ ],
689
+ show_progress=True,
690
+ scroll_to_output=True,
691
+ )
692
+
693
+ # After compute: reveal right column + suggestions
694
+ def _reveal():
695
+ return (
696
+ gr.update(visible=True), # right_col
697
+ gr.update(visible=True), # sugg_hdr
698
+ gr.update(visible=True), # sugg_btn_row
699
+ gr.update(visible=True), # low_txt
700
+ gr.update(visible=True), # med_txt
701
+ gr.update(visible=True), # high_txt
702
+ )
703
+
704
+ run_compute.then(
705
+ fn=_reveal,
706
+ inputs=[],
707
+ outputs=[right_col, sugg_hdr, sugg_btn_row, low_txt, med_txt, high_txt]
708
  )
709
 
710
+ # Band buttons -> recompute quickly, keeping layout visible
711
+ def _band_low(): return "Low"
712
+ def _band_med(): return "Medium"
713
+ def _band_high(): return "High"
714
+
715
  btn_low.click(
716
  fn=compute,
717
+ inputs=[lookback, table, gr.State(_band_low())],
718
+ outputs=[plot, summary, positions, sugg_table, dl, low_txt, med_txt, high_txt, s1, s2, s3, s4, s5, s6, s7, s8, s9],
719
+ show_progress=True
 
 
 
 
720
  )
721
  btn_med.click(
722
  fn=compute,
723
+ inputs=[lookback, table, gr.State(_band_med())],
724
+ outputs=[plot, summary, positions, sugg_table, dl, low_txt, med_txt, high_txt, s1, s2, s3, s4, s5, s6, s7, s8, s9],
725
+ show_progress=True
 
 
 
 
726
  )
727
  btn_high.click(
728
  fn=compute,
729
+ inputs=[lookback, table, gr.State(_band_high())],
730
+ outputs=[plot, summary, positions, sugg_table, dl, low_txt, med_txt, high_txt, s1, s2, s3, s4, s5, s6, s7, s8, s9],
731
+ show_progress=True
 
 
 
 
732
  )
733
 
734
  # initialize risk-free at launch
 
736
  RF_ANN = fetch_fred_yield_annual(RF_CODE)
737
 
738
  if __name__ == "__main__":
739
+ demo.launch(server_name="0.0.0.0", server_port=7860, show_api=False)