Persist web UI translation settings
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}
|
||||||
|
|||||||
101
web_app.py
101
web_app.py
@@ -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_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)
|
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user