Persist web UI translation settings

This commit is contained in:
2026-05-22 20:33:00 +01:00
parent 82d5c3c173
commit 665ea41c65
3 changed files with 133 additions and 7 deletions

View File

@@ -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. 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 ### Docker
Build and run the Gradio UI in a container: Build and run the Gradio UI in a container:

View File

@@ -4,7 +4,8 @@ from __future__ import annotations
import sys 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(): def test_build_pipeline_command_uses_cli_parser_defaults():
@@ -42,3 +43,37 @@ def test_create_app_builds_gradio_blocks():
app = create_app() app = create_app()
assert app.title == "Gradio YouTube Auto Dub" 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",
}

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timezone from datetime import datetime, timezone
import json
from pathlib import Path from pathlib import Path
import os import os
import subprocess import subprocess
@@ -17,10 +18,16 @@ import gradio as gr
from main import build_parser from main import build_parser
from src.audio_separation import DEFAULT_MIX_MODE from src.audio_separation import DEFAULT_MIX_MODE
from src.engines import OUTPUT_DIR 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 BASE_DIR = Path(__file__).resolve().parent
LOG_DIR = BASE_DIR / "logs" / "gradio" LOG_DIR = BASE_DIR / "logs" / "gradio"
SETTINGS_FILE = BASE_DIR / ".cache" / "web_settings.json"
@dataclass @dataclass
@@ -30,6 +37,7 @@ class DubJob:
id: str id: str
command: list[str] command: list[str]
log_path: Path log_path: Path
env_overrides: dict[str, str] = field(default_factory=dict)
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
status: str = "queued" status: str = "queued"
returncode: int | None = None returncode: int | None = None
@@ -40,6 +48,52 @@ JOBS: dict[str, DubJob] = {}
JOBS_LOCK = threading.Lock() 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: def _utc_iso(value: datetime | None) -> str | None:
if value is None: if value is None:
return None return None
@@ -143,6 +197,7 @@ def _run_job(job: DubJob) -> None:
env = os.environ.copy() env = os.environ.copy()
env["PYTHONUNBUFFERED"] = "1" env["PYTHONUNBUFFERED"] = "1"
env.update(job.env_overrides)
with job.log_path.open("w", encoding="utf-8", errors="replace") as log_file: 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") log_file.write("Gradio started a YouTube Auto Dub job.\n")
@@ -185,9 +240,14 @@ def _start_job(
browser: str, browser: str,
cookies: str, cookies: str,
lmstudio_base_url: str, lmstudio_base_url: str,
lmstudio_api_key: str,
lmstudio_model: str, lmstudio_model: str,
gpu: bool, gpu: bool,
) -> tuple[str, str, str, gr.Dropdown]: ) -> 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 = { form = {
"url": url, "url": url,
"lang": lang, "lang": lang,
@@ -196,8 +256,8 @@ def _start_job(
"browser": browser, "browser": browser,
"cookies": cookies, "cookies": cookies,
"translation_backend": "lmstudio", "translation_backend": "lmstudio",
"lmstudio_base_url": lmstudio_base_url, "lmstudio_base_url": base_url,
"lmstudio_model": lmstudio_model, "lmstudio_model": model,
"gpu": gpu, "gpu": gpu,
} }
@@ -213,6 +273,11 @@ def _start_job(
id=job_id, id=job_id,
command=command, command=command,
log_path=LOG_DIR / f"{job_id}.log", 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: with JOBS_LOCK:
@@ -245,6 +310,7 @@ def _select_output(filename: str | None) -> str | None:
def create_app() -> gr.Blocks: def create_app() -> gr.Blocks:
"""Create the Gradio app.""" """Create the Gradio app."""
saved_settings = load_translation_settings()
with gr.Blocks(title="Gradio YouTube Auto Dub") as demo: with gr.Blocks(title="Gradio YouTube Auto Dub") as demo:
gr.Markdown( gr.Markdown(
""" """
@@ -277,12 +343,29 @@ def create_app() -> gr.Blocks:
) )
cookies = gr.Textbox(label="Cookies File", placeholder=r"C:\path\to\cookies.txt") 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( lmstudio_base_url = gr.Textbox(
label="LM Studio URL", label="Endpoint",
placeholder="http://127.0.0.1:1234/v1", 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) gpu = gr.Checkbox(label="Prefer GPU", value=False)
start = gr.Button("Start Dub", variant="primary") start = gr.Button("Start Dub", variant="primary")
@@ -309,9 +392,15 @@ def create_app() -> gr.Blocks:
browser, browser,
cookies, cookies,
lmstudio_base_url, lmstudio_base_url,
lmstudio_api_key,
lmstudio_model, lmstudio_model,
gpu, 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.click(
_start_job, _start_job,
inputs=inputs, inputs=inputs,