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.
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:

View File

@@ -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",
}

View File

@@ -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_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,
)
lmstudio_model = gr.Textbox(label="Model", placeholder="gemma-3-4b-it")
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,