From 665ea41c65b2c52612d9f903e23cf44a2f1935db Mon Sep 17 00:00:00 2001 From: oimwiodev Date: Fri, 22 May 2026 20:33:00 +0100 Subject: [PATCH] Persist web UI translation settings --- README.md | 2 + tests/test_web_app.py | 37 +++++++++++++++- web_app.py | 101 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 133 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cbaebf9..502c844 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ Gradio provides a local browser UI for starting dub jobs, watching logs, and dow Open `http://127.0.0.1:7860` and submit a YouTube URL. Jobs run through the same `main.py` pipeline, so the CLI options and environment variables still apply. +The OpenAI-compatible translation endpoint, API key, and model can be changed in the UI under **OpenAI-Compatible Settings**. Click **Save Settings** to persist them to `.cache/web_settings.json` for future web jobs. Unsaved values in the fields are still used for the next job you start. + ### Docker Build and run the Gradio UI in a container: diff --git a/tests/test_web_app.py b/tests/test_web_app.py index 4e24578..ff8b29c 100644 --- a/tests/test_web_app.py +++ b/tests/test_web_app.py @@ -4,7 +4,8 @@ from __future__ import annotations import sys -from web_app import build_pipeline_command, create_app +import web_app +from web_app import build_pipeline_command, create_app, load_translation_settings, save_translation_settings def test_build_pipeline_command_uses_cli_parser_defaults(): @@ -42,3 +43,37 @@ def test_create_app_builds_gradio_blocks(): app = create_app() assert app.title == "Gradio YouTube Auto Dub" + + +def test_save_and_load_translation_settings(tmp_path, monkeypatch): + settings_file = tmp_path / "web_settings.json" + monkeypatch.setattr(web_app, "SETTINGS_FILE", settings_file) + + base_url, api_key, model, message = save_translation_settings( + "http://openai-compatible.local:8080/v1", + "secret-key", + "custom-model", + ) + + assert base_url == "http://openai-compatible.local:8080/v1" + assert api_key == "secret-key" + assert model == "custom-model" + assert str(settings_file) in message + assert load_translation_settings() == { + "base_url": "http://openai-compatible.local:8080/v1", + "api_key": "secret-key", + "model": "custom-model", + } + + +def test_load_translation_settings_uses_env_defaults(tmp_path, monkeypatch): + monkeypatch.setattr(web_app, "SETTINGS_FILE", tmp_path / "missing.json") + monkeypatch.setenv("LM_STUDIO_BASE_URL", "http://env-host:1234/v1") + monkeypatch.setenv("LM_STUDIO_API_KEY", "env-key") + monkeypatch.setenv("LM_STUDIO_MODEL", "env-model") + + assert load_translation_settings() == { + "base_url": "http://env-host:1234/v1", + "api_key": "env-key", + "model": "env-model", + } diff --git a/web_app.py b/web_app.py index 1053bed..7f6c847 100644 --- a/web_app.py +++ b/web_app.py @@ -5,6 +5,7 @@ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime, timezone +import json from pathlib import Path import os import subprocess @@ -17,10 +18,16 @@ import gradio as gr from main import build_parser from src.audio_separation import DEFAULT_MIX_MODE from src.engines import OUTPUT_DIR +from src.translation import ( + DEFAULT_LM_STUDIO_API_KEY, + DEFAULT_LM_STUDIO_BASE_URL, + DEFAULT_LM_STUDIO_MODEL, +) BASE_DIR = Path(__file__).resolve().parent LOG_DIR = BASE_DIR / "logs" / "gradio" +SETTINGS_FILE = BASE_DIR / ".cache" / "web_settings.json" @dataclass @@ -30,6 +37,7 @@ class DubJob: id: str command: list[str] log_path: Path + env_overrides: dict[str, str] = field(default_factory=dict) created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) status: str = "queued" returncode: int | None = None @@ -40,6 +48,52 @@ JOBS: dict[str, DubJob] = {} JOBS_LOCK = threading.Lock() +def _default_translation_settings() -> dict[str, str]: + return { + "base_url": os.getenv("LM_STUDIO_BASE_URL") or DEFAULT_LM_STUDIO_BASE_URL, + "api_key": os.getenv("LM_STUDIO_API_KEY") or DEFAULT_LM_STUDIO_API_KEY, + "model": os.getenv("LM_STUDIO_MODEL") or DEFAULT_LM_STUDIO_MODEL, + } + + +def load_translation_settings() -> dict[str, str]: + """Load saved OpenAI-compatible translation settings.""" + settings = _default_translation_settings() + if not SETTINGS_FILE.exists(): + return settings + + try: + payload = json.loads(SETTINGS_FILE.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return settings + + if not isinstance(payload, dict): + return settings + + for key in settings: + value = payload.get(key) + if isinstance(value, str) and value.strip(): + settings[key] = value.strip() + return settings + + +def save_translation_settings(base_url: str, api_key: str, model: str) -> tuple[str, str, str, str]: + """Persist OpenAI-compatible endpoint settings for future web jobs.""" + settings = { + "base_url": (base_url or "").strip() or DEFAULT_LM_STUDIO_BASE_URL, + "api_key": (api_key or "").strip() or DEFAULT_LM_STUDIO_API_KEY, + "model": (model or "").strip() or DEFAULT_LM_STUDIO_MODEL, + } + SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True) + SETTINGS_FILE.write_text(json.dumps(settings, indent=2), encoding="utf-8") + return ( + settings["base_url"], + settings["api_key"], + settings["model"], + f"Saved settings to {SETTINGS_FILE}", + ) + + def _utc_iso(value: datetime | None) -> str | None: if value is None: return None @@ -143,6 +197,7 @@ def _run_job(job: DubJob) -> None: env = os.environ.copy() env["PYTHONUNBUFFERED"] = "1" + env.update(job.env_overrides) with job.log_path.open("w", encoding="utf-8", errors="replace") as log_file: log_file.write("Gradio started a YouTube Auto Dub job.\n") @@ -185,9 +240,14 @@ def _start_job( browser: str, cookies: str, lmstudio_base_url: str, + lmstudio_api_key: str, lmstudio_model: str, gpu: bool, ) -> tuple[str, str, str, gr.Dropdown]: + saved_settings = load_translation_settings() + base_url = (lmstudio_base_url or "").strip() or saved_settings["base_url"] + api_key = (lmstudio_api_key or "").strip() or saved_settings["api_key"] + model = (lmstudio_model or "").strip() or saved_settings["model"] form = { "url": url, "lang": lang, @@ -196,8 +256,8 @@ def _start_job( "browser": browser, "cookies": cookies, "translation_backend": "lmstudio", - "lmstudio_base_url": lmstudio_base_url, - "lmstudio_model": lmstudio_model, + "lmstudio_base_url": base_url, + "lmstudio_model": model, "gpu": gpu, } @@ -213,6 +273,11 @@ def _start_job( id=job_id, command=command, log_path=LOG_DIR / f"{job_id}.log", + env_overrides={ + "LM_STUDIO_BASE_URL": base_url, + "LM_STUDIO_API_KEY": api_key, + "LM_STUDIO_MODEL": model, + }, ) with JOBS_LOCK: @@ -245,6 +310,7 @@ def _select_output(filename: str | None) -> str | None: def create_app() -> gr.Blocks: """Create the Gradio app.""" + saved_settings = load_translation_settings() with gr.Blocks(title="Gradio YouTube Auto Dub") as demo: gr.Markdown( """ @@ -277,12 +343,29 @@ def create_app() -> gr.Blocks: ) cookies = gr.Textbox(label="Cookies File", placeholder=r"C:\path\to\cookies.txt") - with gr.Accordion("Translation Settings", open=False): + with gr.Accordion("OpenAI-Compatible Settings", open=False): lmstudio_base_url = gr.Textbox( - label="LM Studio URL", - placeholder="http://127.0.0.1:1234/v1", + label="Endpoint", + value=saved_settings["base_url"], + placeholder=DEFAULT_LM_STUDIO_BASE_URL, ) - lmstudio_model = gr.Textbox(label="Model", placeholder="gemma-3-4b-it") + lmstudio_api_key = gr.Textbox( + label="API Key", + value=saved_settings["api_key"], + type="password", + ) + lmstudio_model = gr.Textbox( + label="Model", + value=saved_settings["model"], + placeholder=DEFAULT_LM_STUDIO_MODEL, + ) + with gr.Row(): + save_settings = gr.Button("Save Settings") + settings_status = gr.Textbox( + label="Settings Status", + value=f"Loaded from {SETTINGS_FILE if SETTINGS_FILE.exists() else 'environment defaults'}", + interactive=False, + ) gpu = gr.Checkbox(label="Prefer GPU", value=False) start = gr.Button("Start Dub", variant="primary") @@ -309,9 +392,15 @@ def create_app() -> gr.Blocks: browser, cookies, lmstudio_base_url, + lmstudio_api_key, lmstudio_model, gpu, ] + save_settings.click( + save_translation_settings, + inputs=[lmstudio_base_url, lmstudio_api_key, lmstudio_model], + outputs=[lmstudio_base_url, lmstudio_api_key, lmstudio_model, settings_status], + ) start.click( _start_job, inputs=inputs,