PlotweaverModel commited on
Commit
89cee01
Β·
verified Β·
1 Parent(s): f601a65

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +161 -108
app.py CHANGED
@@ -77,7 +77,14 @@ def get_conn():
77
 
78
  def init_db():
79
  with get_conn() as conn:
80
- conn.execute("PRAGMA journal_mode = WAL;")
 
 
 
 
 
 
 
81
  conn.execute("""
82
  CREATE TABLE IF NOT EXISTS users (
83
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -418,110 +425,141 @@ def reviewer_lang_choices(session):
418
  return [(l["name"], l["id"]) for l in session["languages"]]
419
 
420
 
421
- with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Soft()) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  session = gr.State(None) # logged-in user session dict
423
  current_sample = gr.State(None) # sample id currently being rated
424
 
425
- gr.Markdown("# 🎧 Plotweaver AI β€” TTS MOS Evaluation Platform")
 
426
 
427
  # ----------------------------- AUTH ------------------------------------ #
428
- with gr.Column(visible=True) as auth_col:
429
- gr.Markdown("Sign in or create a reviewer account to begin.")
430
- with gr.Tabs():
431
- with gr.Tab("Sign in"):
432
- li_user = gr.Textbox(label="Username")
433
- li_pw = gr.Textbox(label="Password", type="password")
434
- li_btn = gr.Button("Sign in", variant="primary")
435
- li_msg = gr.Markdown()
436
- with gr.Tab("Create account"):
437
- su_user = gr.Textbox(label="Username", info="3-32 chars: letters, numbers, _ . -")
438
- su_email = gr.Textbox(label="Email (optional)")
439
- su_pw = gr.Textbox(label="Password", type="password", info="At least 6 characters")
440
- su_langs = gr.CheckboxGroup(label="Languages you can evaluate", choices=lang_choices())
441
- su_code = gr.Textbox(label="Admin code (optional)", type="password",
442
- info="Leave blank for a normal reviewer account.")
443
- su_btn = gr.Button("Create account", variant="primary")
444
- su_msg = gr.Markdown()
445
 
446
  # ----------------------------- APP ------------------------------------- #
447
  with gr.Column(visible=False) as app_col:
448
- with gr.Row():
449
  greeting = gr.Markdown()
450
- logout_btn = gr.Button("Log out", scale=0)
451
 
452
- with gr.Tabs() as app_tabs:
453
  # ---------- Rate tab ----------
454
  with gr.Tab("Rate samples"):
 
 
 
 
 
 
 
 
 
455
  gr.Markdown(
456
- "Please listen to **the whole clip** with headphones in a quiet room before rating. "
457
- f"Scale: {SCALE_HINT}"
 
458
  )
459
- with gr.Row():
460
- rate_lang = gr.Dropdown(label="Language", choices=[], interactive=True)
461
- rate_sample = gr.Dropdown(label="Sample (βœ“ = already rated)", choices=[], interactive=True)
462
- next_btn = gr.Button("Next unrated β–Ά", scale=0)
463
- rate_audio = gr.Audio(label="Audio sample", type="filepath", interactive=False)
464
 
465
  criterion_inputs = {}
466
- with gr.Row():
467
- for key, label, definition in CRITERIA[:4]:
468
- criterion_inputs[key] = gr.Radio(
469
- choices=[1, 2, 3, 4, 5], label=label, info=definition, value=None
470
- )
471
- with gr.Row():
472
- for key, label, definition in CRITERIA[4:]:
473
- criterion_inputs[key] = gr.Radio(
474
- choices=[1, 2, 3, 4, 5], label=label, info=definition, value=None
475
- )
476
- rate_comments = gr.Textbox(label="Comments (pronunciation errors, artifacts, etc.)", lines=2)
477
- with gr.Row():
478
- submit_btn = gr.Button("Submit / update rating", variant="primary")
 
 
479
  rate_msg = gr.Markdown()
480
 
481
  # ---------- Progress tab ----------
482
  with gr.Tab("My progress"):
483
- refresh_prog_btn = gr.Button("Refresh")
484
  progress_md = gr.Markdown()
485
- progress_tbl = gr.Dataframe(headers=["Language", "Rated", "Total", "Remaining"], interactive=False)
 
486
 
487
  # ---------- Profile tab ----------
488
  with gr.Tab("My languages"):
489
- gr.Markdown("Update the set of languages you are eligible to evaluate.")
490
  prof_langs = gr.CheckboxGroup(label="Languages", choices=lang_choices())
491
- prof_save = gr.Button("Save", variant="primary")
492
  prof_msg = gr.Markdown()
493
 
494
  # ---------- Admin tab ----------
495
  with gr.Tab("Admin", visible=False) as admin_tab:
496
  gr.Markdown("### Languages")
497
  with gr.Row():
498
- al_code = gr.Textbox(label="Code", info="e.g. yo, ha, ig, pcm, en-NG")
499
- al_name = gr.Textbox(label="Name", info="e.g. Yoruba")
500
- al_btn = gr.Button("Add language", scale=0)
501
  al_msg = gr.Markdown()
502
- langs_tbl = gr.Dataframe(headers=["id", "code", "name"], interactive=False, label="Existing languages")
 
503
 
504
  gr.Markdown("### Upload audio samples")
505
  with gr.Row():
506
- up_lang = gr.Dropdown(label="Language", choices=lang_choices())
 
507
  up_model = gr.Textbox(label="Model / system name", value="",
508
- info="e.g. F5-TTS, XTTS-v2, MMS-TTS, human. Hidden from reviewers.")
 
509
  up_files = gr.File(label="Audio files (wav/mp3/flac/ogg)", file_count="multiple",
510
  file_types=["audio"])
511
  with gr.Row():
512
  up_isref = gr.Checkbox(label="These are reference / human anchor samples", value=False)
513
  up_transcript = gr.Textbox(label="Transcript (optional, applies to all uploaded)", scale=2)
514
- up_btn = gr.Button("Upload", variant="primary")
515
  up_msg = gr.Markdown()
516
  samples_tbl = gr.Dataframe(
517
  headers=["id", "language", "sample_name", "model", "reference"],
518
  interactive=False, label="Samples")
519
  with gr.Row():
520
  del_sample_id = gr.Number(label="Delete sample by id", precision=0)
521
- del_btn = gr.Button("Delete", variant="stop", scale=0)
522
 
523
  gr.Markdown("### Users")
524
- refresh_users_btn = gr.Button("Refresh users")
525
  users_tbl = gr.Dataframe(
526
  headers=["id", "username", "role", "active", "languages", "ratings"],
527
  interactive=False)
@@ -529,14 +567,15 @@ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Sof
529
  promote_id = gr.Number(label="User id", precision=0)
530
  role_choice = gr.Dropdown(label="Set role", choices=["reviewer", "admin"], value="reviewer")
531
  active_choice = gr.Dropdown(label="Set active", choices=["yes", "no"], value="yes")
532
- update_user_btn = gr.Button("Apply", scale=0)
533
  user_admin_msg = gr.Markdown()
534
 
535
  gr.Markdown("### Results")
536
  with gr.Row():
537
- res_lang = gr.Dropdown(label="Language", choices=lang_choices())
538
- res_btn = gr.Button("Compute MOS", variant="primary")
539
- export_btn = gr.Button("Export XLSX")
 
540
  res_summary = gr.Markdown()
541
  res_model_tbl = gr.Dataframe(label="MOS by model", interactive=False)
542
  res_sample_tbl = gr.Dataframe(label="MOS by sample", interactive=False)
@@ -550,26 +589,36 @@ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Sof
550
  def do_login(username, password):
551
  user = authenticate(username, password)
552
  if not user:
 
553
  return (gr.update(), gr.update(), None, "❌ Invalid username or password.",
554
- gr.update(), gr.update(), gr.update(), gr.update())
 
555
  sess = user_session(user["id"])
556
  is_admin = sess["role"] == "admin"
557
  rl = reviewer_lang_choices(sess)
558
  return (
559
  gr.update(visible=False), # auth_col
560
  gr.update(visible=True), # app_col
561
- sess, # session state
562
  "", # li_msg
563
- gr.update(value=f"Signed in as **{sess['username']}** ({sess['role']})"), # greeting
564
  gr.update(visible=is_admin), # admin_tab
565
- gr.update(choices=rl, value=(rl[0][1] if rl else None)), # rate_lang
566
  gr.update(choices=lang_choices(),
567
- value=[l["id"] for l in sess["languages"]]), # prof_langs
 
 
568
  )
569
 
570
  li_btn.click(
571
  do_login, [li_user, li_pw],
572
- [auth_col, app_col, session, li_msg, greeting, admin_tab, rate_lang, prof_langs],
 
 
 
 
 
 
573
  )
574
 
575
  def do_signup(username, email, password, lang_ids, code):
@@ -577,10 +626,14 @@ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Sof
577
  try:
578
  create_user(username, email, password, role=role, language_ids=lang_ids or [])
579
  except ValueError as e:
580
- return f"❌ {e}", gr.update()
581
- note = " (admin)" if role == "admin" else ""
582
- return f"βœ… Account created{note}. Switch to **Sign in** to continue.", gr.update(value="")
583
- su_btn.click(do_signup, [su_user, su_email, su_pw, su_langs, su_code], [su_msg, su_pw])
 
 
 
 
584
 
585
  def do_logout():
586
  return (gr.update(visible=True), gr.update(visible=False), None, "")
@@ -596,7 +649,6 @@ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Sof
596
  rate_lang.change(load_samples_for_lang, [session, rate_lang], [rate_sample])
597
 
598
  def load_sample(sess, sample_id):
599
- """Load audio + any existing rating into the form."""
600
  if not sample_id:
601
  return (None, None, *[None] * len(CRITERIA_KEYS), "")
602
  with get_conn() as conn:
@@ -622,30 +674,28 @@ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Sof
622
  return gr.update(value=nxt)
623
  next_btn.click(go_next_unrated, [session, rate_lang], [rate_sample])
624
 
 
 
 
 
 
625
  def submit_rating(sess, sample_id, comments, *scores):
626
  if not sess:
627
- return "❌ Not signed in.", gr.update()
628
  if not sample_id:
629
- return "❌ No sample selected.", gr.update()
630
  score_map = dict(zip(CRITERIA_KEYS, scores))
631
  missing = [lbl for (k, lbl, _) in CRITERIA if score_map.get(k) in (None, "")]
632
  if missing:
633
- return f"❌ Please rate every criterion. Missing: {', '.join(missing)}", gr.update()
634
  upsert_rating(sess["id"], sample_id, score_map, comments)
635
- # refresh sample dropdown to show the βœ“ and move on
636
- items = samples_for_reviewer(sess["id"], score_lang(sess, sample_id))
637
  rated = sum(1 for s in items if s["rated"])
638
- return (f"βœ… Saved. You have rated {rated}/{len(items)} samples in this language.", gr.update())
639
-
640
- def score_lang(sess, sample_id):
641
- with get_conn() as conn:
642
- row = conn.execute("SELECT language_id FROM samples WHERE id=?", (sample_id,)).fetchone()
643
- return row["language_id"] if row else None
644
 
645
  submit_btn.click(
646
- submit_rating,
647
- [session, current_sample, rate_comments, *rate_outputs],
648
- [rate_msg, rate_sample],
649
  ).then(
650
  load_samples_for_lang, [session, rate_lang], [rate_sample]
651
  )
@@ -667,24 +717,14 @@ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Sof
667
  # ---- Profile ----
668
  def save_profile(sess, lang_ids):
669
  if not sess:
670
- return "❌ Not signed in.", gr.update(), gr.update()
671
  set_user_languages(sess["id"], lang_ids or [])
672
  new_sess = user_session(sess["id"])
673
  rl = reviewer_lang_choices(new_sess)
674
- return ("βœ… Languages updated.", new_sess,
675
- gr.update(choices=rl, value=(rl[0][1] if rl else None)))
676
  prof_save.click(save_profile, [session, prof_langs], [prof_msg, session, rate_lang])
677
 
678
  # ---- Admin: languages ----
679
- def admin_add_language(sess, code, name):
680
- if not sess or sess["role"] != "admin":
681
- return "❌ Admin only.", _languages_table(), *_lang_dropdown_updates()
682
- try:
683
- add_language(code, name)
684
- except ValueError as e:
685
- return f"❌ {e}", _languages_table(), *_lang_dropdown_updates()
686
- return f"βœ… Added {name} ({code}).", _languages_table(), *_lang_dropdown_updates()
687
-
688
  def _languages_table():
689
  return [[l["id"], l["code"], l["name"]] for l in list_languages()]
690
 
@@ -692,12 +732,24 @@ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Sof
692
  ch = lang_choices()
693
  return (gr.update(choices=ch), gr.update(choices=ch), gr.update(choices=ch), gr.update(choices=ch))
694
 
 
 
 
 
 
 
 
 
695
  al_btn.click(
696
  admin_add_language, [session, al_code, al_name],
697
  [al_msg, langs_tbl, up_lang, res_lang, su_langs, prof_langs],
698
  )
699
 
700
  # ---- Admin: samples ----
 
 
 
 
701
  def admin_upload(sess, language_id, files, model, is_ref, transcript):
702
  if not sess or sess["role"] != "admin":
703
  return "❌ Admin only.", _samples_table()
@@ -714,11 +766,6 @@ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Sof
714
  except Exception as e: # noqa
715
  return f"❌ Error on a file: {e}", _samples_table()
716
  return f"βœ… Uploaded {count} sample(s).", _samples_table()
717
-
718
- def _samples_table():
719
- return [[s["id"], f"{s['language_name']} ({s['language_code']})", s["sample_name"],
720
- s["model_name"], "yes" if s["is_reference"] else "no"] for s in list_samples()]
721
-
722
  up_btn.click(admin_upload, [session, up_lang, up_files, up_model, up_isref, up_transcript],
723
  [up_msg, samples_tbl])
724
 
@@ -739,11 +786,13 @@ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Sof
739
  langs = conn.execute(
740
  "SELECT l.name FROM user_languages ul JOIN languages l ON l.id=ul.language_id "
741
  "WHERE ul.user_id=?", (u["id"],)).fetchall()
742
- nratings = conn.execute("SELECT COUNT(*) c FROM ratings WHERE user_id=?", (u["id"],)).fetchone()["c"]
 
743
  rows.append([u["id"], u["username"], u["role"], "yes" if u["is_active"] else "no",
744
  ", ".join(l["name"] for l in langs), nratings])
745
  return rows
746
- refresh_users_btn.click(lambda s: _users_table() if s and s["role"] == "admin" else [], [session], [users_tbl])
 
747
 
748
  def admin_update_user(sess, uid, role, active):
749
  if not sess or sess["role"] != "admin":
@@ -760,9 +809,9 @@ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Sof
760
  # ---- Admin: results ----
761
  def admin_results(sess, language_id):
762
  if not sess or sess["role"] != "admin":
763
- return "❌ Admin only.", pd.DataFrame(), pd.DataFrame()
764
  if not language_id:
765
- return "Choose a language.", pd.DataFrame(), pd.DataFrame()
766
  per_model, per_sample, summary = compute_results(language_id)
767
  return summary, per_model, per_sample
768
  res_btn.click(admin_results, [session, res_lang], [res_summary, res_model_tbl, res_sample_tbl])
@@ -773,11 +822,15 @@ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation", theme=gr.themes.Sof
773
  return export_results(language_id)
774
  export_btn.click(admin_export, [session, res_lang], [res_file])
775
 
776
- # Populate admin tables when the app loads for an admin (via login .then chain)
777
  li_btn.click(lambda s: (_languages_table(), _samples_table(), _users_table())
778
  if s and s["role"] == "admin" else ([], [], []),
779
  [session], [langs_tbl, samples_tbl, users_tbl])
780
 
781
 
782
  if __name__ == "__main__":
783
- demo.queue().launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))
 
 
 
 
 
77
 
78
  def init_db():
79
  with get_conn() as conn:
80
+ # WAL gives better concurrency on a real local disk, but it relies on
81
+ # shared-memory/mmap that does NOT work on FUSE/object-store mounts
82
+ # (e.g. a Hugging Face Storage Bucket). Default to DELETE for safety;
83
+ # set MOS_JOURNAL_MODE=WAL when running on a genuine local disk.
84
+ journal = os.environ.get("MOS_JOURNAL_MODE", "DELETE").upper()
85
+ if journal not in ("DELETE", "WAL", "TRUNCATE", "PERSIST", "MEMORY"):
86
+ journal = "DELETE"
87
+ conn.execute(f"PRAGMA journal_mode = {journal};")
88
  conn.execute("""
89
  CREATE TABLE IF NOT EXISTS users (
90
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
425
  return [(l["name"], l["id"]) for l in session["languages"]]
426
 
427
 
428
+ CSS = """
429
+ .gradio-container { max-width: 1080px !important; margin: auto !important; }
430
+ #app-title { text-align: center; margin-bottom: 0; }
431
+ #app-title h1 { font-size: 1.7rem; margin: 6px 0 0; }
432
+ #app-sub { text-align: center; color: var(--body-text-color-subdued); margin-top: 4px; font-size: 0.95rem; }
433
+ #auth-wrap { max-width: 460px; margin: 18px auto; }
434
+ .soft-card { border: 1px solid var(--block-border-color); border-radius: 16px;
435
+ padding: 18px; background: var(--block-background-fill); }
436
+ #userbar { display: flex; align-items: center; justify-content: space-between;
437
+ padding: 4px 2px 10px; }
438
+ #scale-hint { text-align: center; font-size: 0.9rem; color: var(--body-text-color-subdued);
439
+ margin: 4px 0 12px; }
440
+ #submit-row button { font-size: 1.05rem; padding: 12px; border-radius: 12px; }
441
+ .rating-grid .gr-radio { margin-bottom: 6px; }
442
+ footer { display: none !important; }
443
+ """
444
+
445
+ with gr.Blocks(title="Plotweaver AI β€” TTS MOS Evaluation",
446
+ theme=gr.themes.Soft(primary_hue="indigo", secondary_hue="indigo"),
447
+ css=CSS) as demo:
448
  session = gr.State(None) # logged-in user session dict
449
  current_sample = gr.State(None) # sample id currently being rated
450
 
451
+ gr.Markdown("# 🎧 Plotweaver AI β€” TTS MOS Evaluation", elem_id="app-title")
452
+ gr.Markdown("Rate synthesised speech on 7 quality criteria, by language.", elem_id="app-sub")
453
 
454
  # ----------------------------- AUTH ------------------------------------ #
455
+ with gr.Column(visible=True, elem_id="auth-wrap") as auth_col:
456
+ with gr.Column(elem_classes="soft-card"):
457
+ with gr.Tabs():
458
+ with gr.Tab("Sign in"):
459
+ li_user = gr.Textbox(label="Username", autofocus=True)
460
+ li_pw = gr.Textbox(label="Password", type="password")
461
+ li_btn = gr.Button("Sign in", variant="primary")
462
+ li_msg = gr.Markdown()
463
+ with gr.Tab("Create account"):
464
+ su_user = gr.Textbox(label="Username", info="3–32 chars: letters, numbers, _ . -")
465
+ su_email = gr.Textbox(label="Email (optional)")
466
+ su_pw = gr.Textbox(label="Password", type="password", info="At least 6 characters")
467
+ su_langs = gr.CheckboxGroup(label="Languages you can evaluate", choices=lang_choices())
468
+ su_code = gr.Textbox(label="Admin code (optional)", type="password",
469
+ info="Leave blank for a normal reviewer account.")
470
+ su_btn = gr.Button("Create account", variant="primary")
471
+ su_msg = gr.Markdown()
472
 
473
  # ----------------------------- APP ------------------------------------- #
474
  with gr.Column(visible=False) as app_col:
475
+ with gr.Row(elem_id="userbar"):
476
  greeting = gr.Markdown()
477
+ logout_btn = gr.Button("Log out", scale=0, size="sm")
478
 
479
+ with gr.Tabs():
480
  # ---------- Rate tab ----------
481
  with gr.Tab("Rate samples"):
482
+ with gr.Row():
483
+ rate_lang = gr.Dropdown(label="Language", choices=[], interactive=True,
484
+ allow_custom_value=True, scale=2)
485
+ rate_sample = gr.Dropdown(label="Sample (βœ“ = already rated)", choices=[],
486
+ interactive=True, allow_custom_value=True, scale=3)
487
+ next_btn = gr.Button("Next unrated β–Ά", scale=1, size="sm")
488
+
489
+ rate_audio = gr.Audio(label="β–Ά Listen to the full sample",
490
+ type="filepath", interactive=False)
491
  gr.Markdown(
492
+ "Use headphones in a quiet room.    "
493
+ "**1** Very Poor Β· **2** Poor Β· **3** Fair Β· **4** Good Β· **5** Excellent",
494
+ elem_id="scale-hint",
495
  )
 
 
 
 
 
496
 
497
  criterion_inputs = {}
498
+ with gr.Column(elem_classes="rating-grid"):
499
+ with gr.Row():
500
+ for key, label, definition in CRITERIA[:4]:
501
+ criterion_inputs[key] = gr.Radio(
502
+ choices=[1, 2, 3, 4, 5], label=label, info=definition, value=None)
503
+ with gr.Row():
504
+ for key, label, definition in CRITERIA[4:]:
505
+ criterion_inputs[key] = gr.Radio(
506
+ choices=[1, 2, 3, 4, 5], label=label, info=definition, value=None)
507
+
508
+ rate_comments = gr.Textbox(
509
+ label="Comments (optional)",
510
+ placeholder="Pronunciation errors, unnatural prosody, noise, artifacts…", lines=2)
511
+ with gr.Row(elem_id="submit-row"):
512
+ submit_btn = gr.Button("βœ“ Submit / update rating", variant="primary")
513
  rate_msg = gr.Markdown()
514
 
515
  # ---------- Progress tab ----------
516
  with gr.Tab("My progress"):
517
+ refresh_prog_btn = gr.Button("Refresh", size="sm")
518
  progress_md = gr.Markdown()
519
+ progress_tbl = gr.Dataframe(
520
+ headers=["Language", "Rated", "Total", "Remaining"], interactive=False)
521
 
522
  # ---------- Profile tab ----------
523
  with gr.Tab("My languages"):
524
+ gr.Markdown("Choose which languages you are eligible to evaluate.")
525
  prof_langs = gr.CheckboxGroup(label="Languages", choices=lang_choices())
526
+ prof_save = gr.Button("Save", variant="primary", size="sm")
527
  prof_msg = gr.Markdown()
528
 
529
  # ---------- Admin tab ----------
530
  with gr.Tab("Admin", visible=False) as admin_tab:
531
  gr.Markdown("### Languages")
532
  with gr.Row():
533
+ al_code = gr.Textbox(label="Code", info="e.g. yo, ha, ig, pcm, en-NG", scale=1)
534
+ al_name = gr.Textbox(label="Name", info="e.g. Yoruba", scale=2)
535
+ al_btn = gr.Button("Add language", scale=0, size="sm")
536
  al_msg = gr.Markdown()
537
+ langs_tbl = gr.Dataframe(headers=["id", "code", "name"], interactive=False,
538
+ label="Existing languages")
539
 
540
  gr.Markdown("### Upload audio samples")
541
  with gr.Row():
542
+ up_lang = gr.Dropdown(label="Language", choices=lang_choices(),
543
+ allow_custom_value=True, scale=1)
544
  up_model = gr.Textbox(label="Model / system name", value="",
545
+ info="e.g. F5-TTS, XTTS-v2, MMS-TTS, human. Hidden from reviewers.",
546
+ scale=2)
547
  up_files = gr.File(label="Audio files (wav/mp3/flac/ogg)", file_count="multiple",
548
  file_types=["audio"])
549
  with gr.Row():
550
  up_isref = gr.Checkbox(label="These are reference / human anchor samples", value=False)
551
  up_transcript = gr.Textbox(label="Transcript (optional, applies to all uploaded)", scale=2)
552
+ up_btn = gr.Button("Upload", variant="primary", size="sm")
553
  up_msg = gr.Markdown()
554
  samples_tbl = gr.Dataframe(
555
  headers=["id", "language", "sample_name", "model", "reference"],
556
  interactive=False, label="Samples")
557
  with gr.Row():
558
  del_sample_id = gr.Number(label="Delete sample by id", precision=0)
559
+ del_btn = gr.Button("Delete", variant="stop", scale=0, size="sm")
560
 
561
  gr.Markdown("### Users")
562
+ refresh_users_btn = gr.Button("Refresh users", size="sm")
563
  users_tbl = gr.Dataframe(
564
  headers=["id", "username", "role", "active", "languages", "ratings"],
565
  interactive=False)
 
567
  promote_id = gr.Number(label="User id", precision=0)
568
  role_choice = gr.Dropdown(label="Set role", choices=["reviewer", "admin"], value="reviewer")
569
  active_choice = gr.Dropdown(label="Set active", choices=["yes", "no"], value="yes")
570
+ update_user_btn = gr.Button("Apply", scale=0, size="sm")
571
  user_admin_msg = gr.Markdown()
572
 
573
  gr.Markdown("### Results")
574
  with gr.Row():
575
+ res_lang = gr.Dropdown(label="Language", choices=lang_choices(),
576
+ allow_custom_value=True, scale=2)
577
+ res_btn = gr.Button("Compute MOS", variant="primary", scale=0, size="sm")
578
+ export_btn = gr.Button("Export XLSX", scale=0, size="sm")
579
  res_summary = gr.Markdown()
580
  res_model_tbl = gr.Dataframe(label="MOS by model", interactive=False)
581
  res_sample_tbl = gr.Dataframe(label="MOS by sample", interactive=False)
 
589
  def do_login(username, password):
590
  user = authenticate(username, password)
591
  if not user:
592
+ # keep typed credentials so the user can correct them
593
  return (gr.update(), gr.update(), None, "❌ Invalid username or password.",
594
+ gr.update(), gr.update(), gr.update(), gr.update(),
595
+ gr.update(), gr.update())
596
  sess = user_session(user["id"])
597
  is_admin = sess["role"] == "admin"
598
  rl = reviewer_lang_choices(sess)
599
  return (
600
  gr.update(visible=False), # auth_col
601
  gr.update(visible=True), # app_col
602
+ sess, # session
603
  "", # li_msg
604
+ gr.update(value=f"Signed in as **{sess['username']}** Β· {sess['role']}"), # greeting
605
  gr.update(visible=is_admin), # admin_tab
606
+ gr.update(choices=rl, value=None), # rate_lang
607
  gr.update(choices=lang_choices(),
608
+ value=[l["id"] for l in sess["languages"]]), # prof_langs
609
+ gr.update(value=""), # li_user (clear)
610
+ gr.update(value=""), # li_pw (clear)
611
  )
612
 
613
  li_btn.click(
614
  do_login, [li_user, li_pw],
615
+ [auth_col, app_col, session, li_msg, greeting, admin_tab, rate_lang, prof_langs,
616
+ li_user, li_pw],
617
+ )
618
+ li_pw.submit( # allow Enter-to-login
619
+ do_login, [li_user, li_pw],
620
+ [auth_col, app_col, session, li_msg, greeting, admin_tab, rate_lang, prof_langs,
621
+ li_user, li_pw],
622
  )
623
 
624
  def do_signup(username, email, password, lang_ids, code):
 
626
  try:
627
  create_user(username, email, password, role=role, language_ids=lang_ids or [])
628
  except ValueError as e:
629
+ # keep fields so the user can fix the problem
630
+ return (f"❌ {e}", gr.update(), gr.update(), gr.update(), gr.update(), gr.update())
631
+ note = " (admin account)" if role == "admin" else ""
632
+ return ("βœ… Account created" + note + ". Switch to the **Sign in** tab to continue.",
633
+ gr.update(value=""), gr.update(value=""), gr.update(value=""),
634
+ gr.update(value=[]), gr.update(value=""))
635
+ su_btn.click(do_signup, [su_user, su_email, su_pw, su_langs, su_code],
636
+ [su_msg, su_user, su_email, su_pw, su_langs, su_code])
637
 
638
  def do_logout():
639
  return (gr.update(visible=True), gr.update(visible=False), None, "")
 
649
  rate_lang.change(load_samples_for_lang, [session, rate_lang], [rate_sample])
650
 
651
  def load_sample(sess, sample_id):
 
652
  if not sample_id:
653
  return (None, None, *[None] * len(CRITERIA_KEYS), "")
654
  with get_conn() as conn:
 
674
  return gr.update(value=nxt)
675
  next_btn.click(go_next_unrated, [session, rate_lang], [rate_sample])
676
 
677
+ def score_lang(sess, sample_id):
678
+ with get_conn() as conn:
679
+ row = conn.execute("SELECT language_id FROM samples WHERE id=?", (sample_id,)).fetchone()
680
+ return row["language_id"] if row else None
681
+
682
  def submit_rating(sess, sample_id, comments, *scores):
683
  if not sess:
684
+ return "❌ Not signed in."
685
  if not sample_id:
686
+ return "❌ No sample selected."
687
  score_map = dict(zip(CRITERIA_KEYS, scores))
688
  missing = [lbl for (k, lbl, _) in CRITERIA if score_map.get(k) in (None, "")]
689
  if missing:
690
+ return "❌ Please rate every criterion. Missing: " + ", ".join(missing)
691
  upsert_rating(sess["id"], sample_id, score_map, comments)
692
+ lid = score_lang(sess, sample_id)
693
+ items = samples_for_reviewer(sess["id"], lid)
694
  rated = sum(1 for s in items if s["rated"])
695
+ return f"βœ… Saved β€” {rated}/{len(items)} samples rated in this language. Click **Next unrated β–Ά** to continue."
 
 
 
 
 
696
 
697
  submit_btn.click(
698
+ submit_rating, [session, current_sample, rate_comments, *rate_outputs], [rate_msg],
 
 
699
  ).then(
700
  load_samples_for_lang, [session, rate_lang], [rate_sample]
701
  )
 
717
  # ---- Profile ----
718
  def save_profile(sess, lang_ids):
719
  if not sess:
720
+ return "❌ Not signed in.", sess, gr.update()
721
  set_user_languages(sess["id"], lang_ids or [])
722
  new_sess = user_session(sess["id"])
723
  rl = reviewer_lang_choices(new_sess)
724
+ return "βœ… Languages updated.", new_sess, gr.update(choices=rl, value=None)
 
725
  prof_save.click(save_profile, [session, prof_langs], [prof_msg, session, rate_lang])
726
 
727
  # ---- Admin: languages ----
 
 
 
 
 
 
 
 
 
728
  def _languages_table():
729
  return [[l["id"], l["code"], l["name"]] for l in list_languages()]
730
 
 
732
  ch = lang_choices()
733
  return (gr.update(choices=ch), gr.update(choices=ch), gr.update(choices=ch), gr.update(choices=ch))
734
 
735
+ def admin_add_language(sess, code, name):
736
+ if not sess or sess["role"] != "admin":
737
+ return ("❌ Admin only.", _languages_table(), *_lang_dropdown_updates())
738
+ try:
739
+ add_language(code, name)
740
+ except ValueError as e:
741
+ return (f"❌ {e}", _languages_table(), *_lang_dropdown_updates())
742
+ return (f"βœ… Added {name} ({code}).", _languages_table(), *_lang_dropdown_updates())
743
  al_btn.click(
744
  admin_add_language, [session, al_code, al_name],
745
  [al_msg, langs_tbl, up_lang, res_lang, su_langs, prof_langs],
746
  )
747
 
748
  # ---- Admin: samples ----
749
+ def _samples_table():
750
+ return [[s["id"], f"{s['language_name']} ({s['language_code']})", s["sample_name"],
751
+ s["model_name"], "yes" if s["is_reference"] else "no"] for s in list_samples()]
752
+
753
  def admin_upload(sess, language_id, files, model, is_ref, transcript):
754
  if not sess or sess["role"] != "admin":
755
  return "❌ Admin only.", _samples_table()
 
766
  except Exception as e: # noqa
767
  return f"❌ Error on a file: {e}", _samples_table()
768
  return f"βœ… Uploaded {count} sample(s).", _samples_table()
 
 
 
 
 
769
  up_btn.click(admin_upload, [session, up_lang, up_files, up_model, up_isref, up_transcript],
770
  [up_msg, samples_tbl])
771
 
 
786
  langs = conn.execute(
787
  "SELECT l.name FROM user_languages ul JOIN languages l ON l.id=ul.language_id "
788
  "WHERE ul.user_id=?", (u["id"],)).fetchall()
789
+ nratings = conn.execute("SELECT COUNT(*) c FROM ratings WHERE user_id=?",
790
+ (u["id"],)).fetchone()["c"]
791
  rows.append([u["id"], u["username"], u["role"], "yes" if u["is_active"] else "no",
792
  ", ".join(l["name"] for l in langs), nratings])
793
  return rows
794
+ refresh_users_btn.click(lambda s: _users_table() if s and s["role"] == "admin" else [],
795
+ [session], [users_tbl])
796
 
797
  def admin_update_user(sess, uid, role, active):
798
  if not sess or sess["role"] != "admin":
 
809
  # ---- Admin: results ----
810
  def admin_results(sess, language_id):
811
  if not sess or sess["role"] != "admin":
812
+ return "❌ Admin only.", None, None
813
  if not language_id:
814
+ return "Choose a language.", None, None
815
  per_model, per_sample, summary = compute_results(language_id)
816
  return summary, per_model, per_sample
817
  res_btn.click(admin_results, [session, res_lang], [res_summary, res_model_tbl, res_sample_tbl])
 
822
  return export_results(language_id)
823
  export_btn.click(admin_export, [session, res_lang], [res_file])
824
 
825
+ # Populate admin tables right after an admin logs in.
826
  li_btn.click(lambda s: (_languages_table(), _samples_table(), _users_table())
827
  if s and s["role"] == "admin" else ([], [], []),
828
  [session], [langs_tbl, samples_tbl, users_tbl])
829
 
830
 
831
  if __name__ == "__main__":
832
+ demo.queue().launch(
833
+ server_name="0.0.0.0",
834
+ server_port=int(os.environ.get("PORT", 7860)),
835
+ allowed_paths=[DATA_DIR], # let Gradio serve audio stored under MOS_DATA_DIR (e.g. /data)
836
+ )