Upload 2 files
Browse files
README.md
CHANGED
|
@@ -51,9 +51,10 @@ Nigerian English, Akan, Swahili, Nigerian Pidgin, … add more any time).
|
|
| 51 |
|
| 52 |
| Variable | Default | Purpose |
|
| 53 |
|----------|---------|---------|
|
| 54 |
-
| `MOS_DATA_DIR` | `./data` |
|
|
|
|
| 55 |
| `ADMIN_CODE` | `plotweaver-admin` | Entered on the signup form to create an admin account. **Override this.** |
|
| 56 |
-
| `
|
| 57 |
| `PORT` | `7860` | Port the app binds to. |
|
| 58 |
|
| 59 |
---
|
|
@@ -105,11 +106,14 @@ for the older fixed `/data` storage tier).
|
|
| 105 |
Notes:
|
| 106 |
- The app already calls `launch(allowed_paths=[MOS_DATA_DIR])`, so Gradio is permitted to serve the
|
| 107 |
audio files stored on the bucket. Without this, the audio player can't load clips from `/data`.
|
| 108 |
-
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
### Self-hosted (AlmaLinux / AWS, behind nginx)
|
| 115 |
|
|
|
|
| 51 |
|
| 52 |
| Variable | Default | Purpose |
|
| 53 |
|----------|---------|---------|
|
| 54 |
+
| `MOS_DATA_DIR` | `./data` | **Persistent** location for audio files, exports, and the DB backup. Point at your bucket mount (e.g. `/data`). |
|
| 55 |
+
| `MOS_LOCAL_DIR` | system temp `/…/mos_live` | Fast **local** disk where the live SQLite DB runs. It is backed up to `MOS_DATA_DIR` after every change and restored on startup. Usually leave as default. |
|
| 56 |
| `ADMIN_CODE` | `plotweaver-admin` | Entered on the signup form to create an admin account. **Override this.** |
|
| 57 |
+
| `MOS_SECRET` | derived from `ADMIN_CODE` | Secret used to sign browser session tokens (keeps a refreshed page logged in). Set a stable value in production. |
|
| 58 |
| `PORT` | `7860` | Port the app binds to. |
|
| 59 |
|
| 60 |
---
|
|
|
|
| 106 |
Notes:
|
| 107 |
- The app already calls `launch(allowed_paths=[MOS_DATA_DIR])`, so Gradio is permitted to serve the
|
| 108 |
audio files stored on the bucket. Without this, the audio player can't load clips from `/data`.
|
| 109 |
+
- **Database on buckets:** the live SQLite database does **not** run directly on the bucket, because
|
| 110 |
+
object-store/FUSE mounts don't provide reliable file locking and writes can silently fail to
|
| 111 |
+
appear on later reads (causing "my ratings/files disappeared"). Instead the live DB runs on fast
|
| 112 |
+
local disk (`MOS_LOCAL_DIR`) and is atomically backed up to the bucket (`MOS_DATA_DIR/mos.db`)
|
| 113 |
+
after every change, then restored on startup. Audio files live on the bucket (static, write-once,
|
| 114 |
+
no locking issues). This survives Space restarts as long as the bucket stays mounted.
|
| 115 |
+
- **Staying logged in:** a signed token is stored in the browser (`gr.BrowserState`) so a page
|
| 116 |
+
refresh keeps you signed in. Set a stable `MOS_SECRET` in production.
|
| 117 |
|
| 118 |
### Self-hosted (AlmaLinux / AWS, behind nginx)
|
| 119 |
|
app.py
CHANGED
|
@@ -28,9 +28,11 @@ The effective admin code is printed to the logs on startup.
|
|
| 28 |
|
| 29 |
import os
|
| 30 |
import re
|
|
|
|
| 31 |
import sqlite3
|
| 32 |
import hashlib
|
| 33 |
import secrets
|
|
|
|
| 34 |
import datetime as dt
|
| 35 |
import shutil
|
| 36 |
|
|
@@ -40,15 +42,75 @@ import gradio as gr
|
|
| 40 |
# --------------------------------------------------------------------------- #
|
| 41 |
# Configuration
|
| 42 |
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
| 43 |
DATA_DIR = os.environ.get("MOS_DATA_DIR", os.path.join(os.path.dirname(os.path.abspath(__file__)), "data"))
|
| 44 |
AUDIO_DIR = os.path.join(DATA_DIR, "audio")
|
| 45 |
EXPORT_DIR = os.path.join(DATA_DIR, "exports")
|
| 46 |
-
DB_PATH = os.path.join(DATA_DIR, "mos.db")
|
| 47 |
ADMIN_CODE = os.environ.get("ADMIN_CODE", "plotweaver-admin")
|
| 48 |
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
os.makedirs(d, exist_ok=True)
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
# The 7 MOS criteria, in the order they appear on the evaluation form.
|
| 53 |
# (db_column, display_label, short_definition)
|
| 54 |
CRITERIA = [
|
|
@@ -178,6 +240,7 @@ def create_user(username, email, password, role="reviewer", language_ids=None):
|
|
| 178 |
uid = cur.lastrowid
|
| 179 |
for lid in (language_ids or []):
|
| 180 |
conn.execute("INSERT OR IGNORE INTO user_languages (user_id, language_id) VALUES (?,?)", (uid, lid))
|
|
|
|
| 181 |
return uid
|
| 182 |
|
| 183 |
|
|
@@ -221,6 +284,7 @@ def add_language(code, name):
|
|
| 221 |
conn.execute("INSERT INTO languages (code, name, created_at) VALUES (?,?,?)", (code, name, now_iso()))
|
| 222 |
except sqlite3.IntegrityError:
|
| 223 |
raise ValueError(f"Language code '{code}' already exists.")
|
|
|
|
| 224 |
|
| 225 |
|
| 226 |
def list_languages():
|
|
@@ -233,6 +297,7 @@ def set_user_languages(uid, language_ids):
|
|
| 233 |
conn.execute("DELETE FROM user_languages WHERE user_id = ?", (uid,))
|
| 234 |
for lid in language_ids:
|
| 235 |
conn.execute("INSERT OR IGNORE INTO user_languages (user_id, language_id) VALUES (?,?)", (uid, lid))
|
|
|
|
| 236 |
|
| 237 |
|
| 238 |
# --------------------------------------------------------------------------- #
|
|
@@ -256,6 +321,7 @@ def add_sample(language_id, src_path, sample_name=None, model_name="unspecified"
|
|
| 256 |
(language_id, sample_name, (model_name or "unspecified").strip(), dst,
|
| 257 |
1 if is_reference else 0, (transcript or "").strip(), now_iso()),
|
| 258 |
)
|
|
|
|
| 259 |
|
| 260 |
|
| 261 |
def list_samples(language_id=None):
|
|
@@ -279,12 +345,14 @@ def delete_sample(sample_id):
|
|
| 279 |
except OSError:
|
| 280 |
pass
|
| 281 |
conn.execute("DELETE FROM samples WHERE id = ?", (sample_id,))
|
|
|
|
| 282 |
|
| 283 |
|
| 284 |
def set_sample_transcript(sample_id, transcript):
|
| 285 |
with get_conn() as conn:
|
| 286 |
conn.execute("UPDATE samples SET transcript = ? WHERE id = ?",
|
| 287 |
((transcript or "").strip(), sample_id))
|
|
|
|
| 288 |
|
| 289 |
|
| 290 |
def get_sample_transcript(sample_id):
|
|
@@ -309,6 +377,7 @@ def upsert_rating(user_id, sample_id, scores, comments=""):
|
|
| 309 |
f"comments=excluded.comments, updated_at=excluded.updated_at",
|
| 310 |
[user_id, sample_id, *vals, (comments or "").strip(), now_iso()],
|
| 311 |
)
|
|
|
|
| 312 |
|
| 313 |
|
| 314 |
def get_rating(user_id, sample_id):
|
|
@@ -418,8 +487,9 @@ def export_results(language_id):
|
|
| 418 |
init_db()
|
| 419 |
print("=" * 64)
|
| 420 |
print("Plotweaver AI — TTS MOS Evaluation Platform")
|
| 421 |
-
print(f"
|
| 422 |
-
print(f"
|
|
|
|
| 423 |
print(f"Admin code : {ADMIN_CODE} (use on signup to create an admin)")
|
| 424 |
print("=" * 64)
|
| 425 |
|
|
@@ -459,6 +529,7 @@ with gr.Blocks(title="Plotweaver AI — TTS MOS Evaluation",
|
|
| 459 |
css=CSS) as demo:
|
| 460 |
session = gr.State(None) # logged-in user session dict
|
| 461 |
current_sample = gr.State(None) # sample id currently being rated
|
|
|
|
| 462 |
|
| 463 |
gr.Markdown("# 🎧 Plotweaver AI — TTS MOS Evaluation", elem_id="app-title")
|
| 464 |
gr.Markdown("Rate synthesised speech on 7 quality criteria, by language.", elem_id="app-sub")
|
|
@@ -613,10 +684,18 @@ with gr.Blocks(title="Plotweaver AI — TTS MOS Evaluation",
|
|
| 613 |
# keep typed credentials so the user can correct them
|
| 614 |
return (gr.update(), gr.update(), None, "❌ Invalid username or password.",
|
| 615 |
gr.update(), gr.update(), gr.update(), gr.update(),
|
| 616 |
-
gr.update(), gr.update())
|
|
|
|
| 617 |
sess = user_session(user["id"])
|
| 618 |
is_admin = sess["role"] == "admin"
|
| 619 |
rl = reviewer_lang_choices(sess)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
return (
|
| 621 |
gr.update(visible=False), # auth_col
|
| 622 |
gr.update(visible=True), # app_col
|
|
@@ -628,18 +707,15 @@ with gr.Blocks(title="Plotweaver AI — TTS MOS Evaluation",
|
|
| 628 |
gr.update(choices=rl, value=None), # rate_lang
|
| 629 |
gr.update(value=""), # li_user (clear)
|
| 630 |
gr.update(value=""), # li_pw (clear)
|
|
|
|
|
|
|
| 631 |
)
|
| 632 |
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
)
|
| 638 |
-
li_pw.submit( # allow Enter-to-login
|
| 639 |
-
do_login, [li_user, li_pw],
|
| 640 |
-
[auth_col, app_col, session, li_msg, greeting, reviewer_tabs, admin_panel,
|
| 641 |
-
rate_lang, li_user, li_pw],
|
| 642 |
-
)
|
| 643 |
|
| 644 |
def do_signup(username, email, password, lang_id, code):
|
| 645 |
role = "admin" if (code and code == ADMIN_CODE) else "reviewer"
|
|
@@ -657,8 +733,8 @@ with gr.Blocks(title="Plotweaver AI — TTS MOS Evaluation",
|
|
| 657 |
[su_msg, su_user, su_email, su_pw, su_lang, su_code])
|
| 658 |
|
| 659 |
def do_logout():
|
| 660 |
-
return (gr.update(visible=True), gr.update(visible=False), None, "")
|
| 661 |
-
logout_btn.click(do_logout, None, [auth_col, app_col, session, greeting])
|
| 662 |
|
| 663 |
# ---- Rating flow ----
|
| 664 |
def load_samples_for_lang(sess, language_id):
|
|
@@ -843,6 +919,7 @@ with gr.Blocks(title="Plotweaver AI — TTS MOS Evaluation",
|
|
| 843 |
with get_conn() as conn:
|
| 844 |
conn.execute("UPDATE users SET role=?, is_active=? WHERE id=?",
|
| 845 |
(role, 1 if active == "yes" else 0, int(uid)))
|
|
|
|
| 846 |
return f"✅ Updated user {int(uid)}.", _users_table()
|
| 847 |
update_user_btn.click(admin_update_user, [session, promote_id, role_choice, active_choice],
|
| 848 |
[user_admin_msg, users_tbl])
|
|
@@ -863,12 +940,36 @@ with gr.Blocks(title="Plotweaver AI — TTS MOS Evaluation",
|
|
| 863 |
return export_results(language_id)
|
| 864 |
export_btn.click(admin_export, [session, res_lang], [res_file])
|
| 865 |
|
| 866 |
-
#
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 872 |
|
| 873 |
# Refresh language-dependent choices on every page load, so newly added
|
| 874 |
# languages appear in the signup form (and admin dropdowns) without a restart.
|
|
|
|
| 28 |
|
| 29 |
import os
|
| 30 |
import re
|
| 31 |
+
import hmac
|
| 32 |
import sqlite3
|
| 33 |
import hashlib
|
| 34 |
import secrets
|
| 35 |
+
import tempfile
|
| 36 |
import datetime as dt
|
| 37 |
import shutil
|
| 38 |
|
|
|
|
| 42 |
# --------------------------------------------------------------------------- #
|
| 43 |
# Configuration
|
| 44 |
# --------------------------------------------------------------------------- #
|
| 45 |
+
# DATA_DIR is the *persistent* location (e.g. a Hugging Face Storage Bucket at
|
| 46 |
+
# /data). Audio files and the database BACKUP live here.
|
| 47 |
DATA_DIR = os.environ.get("MOS_DATA_DIR", os.path.join(os.path.dirname(os.path.abspath(__file__)), "data"))
|
| 48 |
AUDIO_DIR = os.path.join(DATA_DIR, "audio")
|
| 49 |
EXPORT_DIR = os.path.join(DATA_DIR, "exports")
|
|
|
|
| 50 |
ADMIN_CODE = os.environ.get("ADMIN_CODE", "plotweaver-admin")
|
| 51 |
|
| 52 |
+
# The LIVE database runs on fast LOCAL disk. SQLite needs real POSIX file
|
| 53 |
+
# locking, which FUSE/object-store mounts (like a bucket) do not provide
|
| 54 |
+
# reliably — running it directly on the bucket causes writes to silently not
|
| 55 |
+
# appear on later reads. So we keep the working DB local and atomically back it
|
| 56 |
+
# up to the bucket after every change (and restore it on startup).
|
| 57 |
+
LOCAL_DIR = os.environ.get("MOS_LOCAL_DIR") or os.path.join(tempfile.gettempdir(), "mos_live")
|
| 58 |
+
DB_PATH = os.path.join(LOCAL_DIR, "mos.db") # live (local disk)
|
| 59 |
+
DB_BACKUP = os.path.join(DATA_DIR, "mos.db") # persistent copy (bucket)
|
| 60 |
+
|
| 61 |
+
for d in (DATA_DIR, AUDIO_DIR, EXPORT_DIR, LOCAL_DIR):
|
| 62 |
os.makedirs(d, exist_ok=True)
|
| 63 |
|
| 64 |
+
# Restore the live DB from the persistent backup on startup (first boot / after
|
| 65 |
+
# the container is recycled), so prior data survives restarts.
|
| 66 |
+
if os.path.abspath(DB_BACKUP) != os.path.abspath(DB_PATH) \
|
| 67 |
+
and os.path.exists(DB_BACKUP) and not os.path.exists(DB_PATH):
|
| 68 |
+
try:
|
| 69 |
+
shutil.copyfile(DB_BACKUP, DB_PATH)
|
| 70 |
+
except OSError:
|
| 71 |
+
pass
|
| 72 |
+
|
| 73 |
+
# Secret used to sign browser session tokens (keeps a refreshed page logged in).
|
| 74 |
+
SESSION_SECRET = os.environ.get("MOS_SECRET") or hashlib.sha256(
|
| 75 |
+
("mos-session::" + ADMIN_CODE).encode()).hexdigest()
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def backup_db():
|
| 79 |
+
"""Atomically copy the live DB to the persistent backup on the bucket."""
|
| 80 |
+
if os.path.abspath(DB_BACKUP) == os.path.abspath(DB_PATH):
|
| 81 |
+
return
|
| 82 |
+
try:
|
| 83 |
+
src = sqlite3.connect(DB_PATH)
|
| 84 |
+
dst = sqlite3.connect(DB_BACKUP)
|
| 85 |
+
with dst:
|
| 86 |
+
src.backup(dst)
|
| 87 |
+
src.close()
|
| 88 |
+
dst.close()
|
| 89 |
+
except Exception: # noqa — best-effort backup; never break a write
|
| 90 |
+
pass
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def make_token(user_id):
|
| 94 |
+
sig = hmac.new(SESSION_SECRET.encode(), str(user_id).encode(), hashlib.sha256).hexdigest()[:32]
|
| 95 |
+
return f"{user_id}.{sig}"
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def verify_token(token):
|
| 99 |
+
"""Return the user_id if the signed token is valid and the user is active."""
|
| 100 |
+
if not token or "." not in str(token):
|
| 101 |
+
return None
|
| 102 |
+
uid_str, sig = str(token).rsplit(".", 1)
|
| 103 |
+
try:
|
| 104 |
+
uid = int(uid_str)
|
| 105 |
+
except ValueError:
|
| 106 |
+
return None
|
| 107 |
+
good = hmac.new(SESSION_SECRET.encode(), str(uid).encode(), hashlib.sha256).hexdigest()[:32]
|
| 108 |
+
if not hmac.compare_digest(sig, good):
|
| 109 |
+
return None
|
| 110 |
+
with get_conn() as conn:
|
| 111 |
+
row = conn.execute("SELECT id, is_active FROM users WHERE id=?", (uid,)).fetchone()
|
| 112 |
+
return uid if (row and row["is_active"]) else None
|
| 113 |
+
|
| 114 |
# The 7 MOS criteria, in the order they appear on the evaluation form.
|
| 115 |
# (db_column, display_label, short_definition)
|
| 116 |
CRITERIA = [
|
|
|
|
| 240 |
uid = cur.lastrowid
|
| 241 |
for lid in (language_ids or []):
|
| 242 |
conn.execute("INSERT OR IGNORE INTO user_languages (user_id, language_id) VALUES (?,?)", (uid, lid))
|
| 243 |
+
backup_db()
|
| 244 |
return uid
|
| 245 |
|
| 246 |
|
|
|
|
| 284 |
conn.execute("INSERT INTO languages (code, name, created_at) VALUES (?,?,?)", (code, name, now_iso()))
|
| 285 |
except sqlite3.IntegrityError:
|
| 286 |
raise ValueError(f"Language code '{code}' already exists.")
|
| 287 |
+
backup_db()
|
| 288 |
|
| 289 |
|
| 290 |
def list_languages():
|
|
|
|
| 297 |
conn.execute("DELETE FROM user_languages WHERE user_id = ?", (uid,))
|
| 298 |
for lid in language_ids:
|
| 299 |
conn.execute("INSERT OR IGNORE INTO user_languages (user_id, language_id) VALUES (?,?)", (uid, lid))
|
| 300 |
+
backup_db()
|
| 301 |
|
| 302 |
|
| 303 |
# --------------------------------------------------------------------------- #
|
|
|
|
| 321 |
(language_id, sample_name, (model_name or "unspecified").strip(), dst,
|
| 322 |
1 if is_reference else 0, (transcript or "").strip(), now_iso()),
|
| 323 |
)
|
| 324 |
+
backup_db()
|
| 325 |
|
| 326 |
|
| 327 |
def list_samples(language_id=None):
|
|
|
|
| 345 |
except OSError:
|
| 346 |
pass
|
| 347 |
conn.execute("DELETE FROM samples WHERE id = ?", (sample_id,))
|
| 348 |
+
backup_db()
|
| 349 |
|
| 350 |
|
| 351 |
def set_sample_transcript(sample_id, transcript):
|
| 352 |
with get_conn() as conn:
|
| 353 |
conn.execute("UPDATE samples SET transcript = ? WHERE id = ?",
|
| 354 |
((transcript or "").strip(), sample_id))
|
| 355 |
+
backup_db()
|
| 356 |
|
| 357 |
|
| 358 |
def get_sample_transcript(sample_id):
|
|
|
|
| 377 |
f"comments=excluded.comments, updated_at=excluded.updated_at",
|
| 378 |
[user_id, sample_id, *vals, (comments or "").strip(), now_iso()],
|
| 379 |
)
|
| 380 |
+
backup_db()
|
| 381 |
|
| 382 |
|
| 383 |
def get_rating(user_id, sample_id):
|
|
|
|
| 487 |
init_db()
|
| 488 |
print("=" * 64)
|
| 489 |
print("Plotweaver AI — TTS MOS Evaluation Platform")
|
| 490 |
+
print(f"Persistent dir : {DATA_DIR}")
|
| 491 |
+
print(f"Live database : {DB_PATH}")
|
| 492 |
+
print(f"DB backup : {DB_BACKUP}")
|
| 493 |
print(f"Admin code : {ADMIN_CODE} (use on signup to create an admin)")
|
| 494 |
print("=" * 64)
|
| 495 |
|
|
|
|
| 529 |
css=CSS) as demo:
|
| 530 |
session = gr.State(None) # logged-in user session dict
|
| 531 |
current_sample = gr.State(None) # sample id currently being rated
|
| 532 |
+
auth_token = gr.BrowserState("") # signed token persisted in the browser (survives refresh)
|
| 533 |
|
| 534 |
gr.Markdown("# 🎧 Plotweaver AI — TTS MOS Evaluation", elem_id="app-title")
|
| 535 |
gr.Markdown("Rate synthesised speech on 7 quality criteria, by language.", elem_id="app-sub")
|
|
|
|
| 684 |
# keep typed credentials so the user can correct them
|
| 685 |
return (gr.update(), gr.update(), None, "❌ Invalid username or password.",
|
| 686 |
gr.update(), gr.update(), gr.update(), gr.update(),
|
| 687 |
+
gr.update(), gr.update(), gr.update(),
|
| 688 |
+
gr.update(), gr.update(), gr.update(), gr.update(), gr.update())
|
| 689 |
sess = user_session(user["id"])
|
| 690 |
is_admin = sess["role"] == "admin"
|
| 691 |
rl = reviewer_lang_choices(sess)
|
| 692 |
+
if is_admin:
|
| 693 |
+
lt, st, ut = _languages_table(), _samples_table(), _users_table()
|
| 694 |
+
dch = _sample_delete_choices()
|
| 695 |
+
du, tu = gr.update(choices=dch), gr.update(choices=dch)
|
| 696 |
+
else:
|
| 697 |
+
lt, st, ut = [], [], []
|
| 698 |
+
du, tu = gr.update(), gr.update()
|
| 699 |
return (
|
| 700 |
gr.update(visible=False), # auth_col
|
| 701 |
gr.update(visible=True), # app_col
|
|
|
|
| 707 |
gr.update(choices=rl, value=None), # rate_lang
|
| 708 |
gr.update(value=""), # li_user (clear)
|
| 709 |
gr.update(value=""), # li_pw (clear)
|
| 710 |
+
make_token(user["id"]), # auth_token (persist login)
|
| 711 |
+
lt, st, ut, du, tu, # admin tables + pickers
|
| 712 |
)
|
| 713 |
|
| 714 |
+
LOGIN_OUTPUTS = [auth_col, app_col, session, li_msg, greeting, reviewer_tabs, admin_panel,
|
| 715 |
+
rate_lang, li_user, li_pw, auth_token,
|
| 716 |
+
langs_tbl, samples_tbl, users_tbl, del_sample, tr_sample]
|
| 717 |
+
li_btn.click(do_login, [li_user, li_pw], LOGIN_OUTPUTS)
|
| 718 |
+
li_pw.submit(do_login, [li_user, li_pw], LOGIN_OUTPUTS) # Enter-to-login
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 719 |
|
| 720 |
def do_signup(username, email, password, lang_id, code):
|
| 721 |
role = "admin" if (code and code == ADMIN_CODE) else "reviewer"
|
|
|
|
| 733 |
[su_msg, su_user, su_email, su_pw, su_lang, su_code])
|
| 734 |
|
| 735 |
def do_logout():
|
| 736 |
+
return (gr.update(visible=True), gr.update(visible=False), None, "", "")
|
| 737 |
+
logout_btn.click(do_logout, None, [auth_col, app_col, session, greeting, auth_token])
|
| 738 |
|
| 739 |
# ---- Rating flow ----
|
| 740 |
def load_samples_for_lang(sess, language_id):
|
|
|
|
| 919 |
with get_conn() as conn:
|
| 920 |
conn.execute("UPDATE users SET role=?, is_active=? WHERE id=?",
|
| 921 |
(role, 1 if active == "yes" else 0, int(uid)))
|
| 922 |
+
backup_db()
|
| 923 |
return f"✅ Updated user {int(uid)}.", _users_table()
|
| 924 |
update_user_btn.click(admin_update_user, [session, promote_id, role_choice, active_choice],
|
| 925 |
[user_admin_msg, users_tbl])
|
|
|
|
| 940 |
return export_results(language_id)
|
| 941 |
export_btn.click(admin_export, [session, res_lang], [res_file])
|
| 942 |
|
| 943 |
+
# Restore the session on page load (so a refresh keeps you signed in).
|
| 944 |
+
def restore_session(token):
|
| 945 |
+
uid = verify_token(token)
|
| 946 |
+
if not uid:
|
| 947 |
+
# stay logged out; leave the UI at its default (auth visible)
|
| 948 |
+
return (gr.update(), gr.update(), None, gr.update(), gr.update(), gr.update(),
|
| 949 |
+
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update())
|
| 950 |
+
sess = user_session(uid)
|
| 951 |
+
is_admin = sess["role"] == "admin"
|
| 952 |
+
rl = reviewer_lang_choices(sess)
|
| 953 |
+
if is_admin:
|
| 954 |
+
lt, st, ut = _languages_table(), _samples_table(), _users_table()
|
| 955 |
+
dch = _sample_delete_choices()
|
| 956 |
+
du, tu = gr.update(choices=dch), gr.update(choices=dch)
|
| 957 |
+
else:
|
| 958 |
+
lt, st, ut = [], [], []
|
| 959 |
+
du, tu = gr.update(), gr.update()
|
| 960 |
+
return (
|
| 961 |
+
gr.update(visible=False), # auth_col
|
| 962 |
+
gr.update(visible=True), # app_col
|
| 963 |
+
sess, # session
|
| 964 |
+
gr.update(value=f"Signed in as **{sess['username']}** · {sess['role']}"), # greeting
|
| 965 |
+
gr.update(visible=not is_admin), # reviewer_tabs
|
| 966 |
+
gr.update(visible=is_admin), # admin_panel
|
| 967 |
+
gr.update(choices=rl, value=None), # rate_lang
|
| 968 |
+
lt, st, ut, du, tu, # admin tables + pickers
|
| 969 |
+
)
|
| 970 |
+
demo.load(restore_session, [auth_token],
|
| 971 |
+
[auth_col, app_col, session, greeting, reviewer_tabs, admin_panel, rate_lang,
|
| 972 |
+
langs_tbl, samples_tbl, users_tbl, del_sample, tr_sample])
|
| 973 |
|
| 974 |
# Refresh language-dependent choices on every page load, so newly added
|
| 975 |
# languages appear in the signup form (and admin dropdowns) without a restart.
|