Quality-first multi-pass audiobook generation from English manuscripts. Includes: - umbraaudio/ core package (pipeline, config, voices, web review UI) - scripts/ CLI utilities + model download script - tts_server/ remote TTS server - specs/ design docs, tests/, pyproject.toml + uv.lock - Qwen3-TTS git submodule (pinned 022e286) with local demo.py UI patch saved at scripts/qwen3_tts_demo_umbra_patch.patch - .env with runtime config/secrets Excluded (see .gitignore): models/ (downloaded via scripts/download_models.sh), output/, logs/, .venv, .umbra runtime data, and book projects (DealHunterShip_*), which are committed separately.
112 lines
3.5 KiB
Diff
112 lines
3.5 KiB
Diff
diff --git a/qwen_tts/cli/demo.py b/qwen_tts/cli/demo.py
|
|
index e267861..686c3f6 100644
|
|
--- a/qwen_tts/cli/demo.py
|
|
+++ b/qwen_tts/cli/demo.py
|
|
@@ -18,9 +18,11 @@ A gradio demo for Qwen3 TTS models.
|
|
"""
|
|
|
|
import argparse
|
|
+import base64
|
|
import os
|
|
import tempfile
|
|
from dataclasses import asdict
|
|
+from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
import gradio as gr
|
|
@@ -59,6 +61,31 @@ def _maybe(v):
|
|
return v if v is not None else gr.update()
|
|
|
|
|
|
+def _find_icon() -> Optional[Path]:
|
|
+ candidates = [
|
|
+ Path(os.environ.get("UMBRA_AUDIO_ICON", "")) if os.environ.get("UMBRA_AUDIO_ICON") else None,
|
|
+ Path("/Volumes/1TB_APL_Storage/UmbraAudio/UmbraAudio_icon.png"),
|
|
+ Path(__file__).resolve().parents[2] / "assets" / "logo.png",
|
|
+ Path("/Volumes/1TB_APL_Storage/UmbraAudio/UmbraAudio_icon.jpeg"),
|
|
+ ]
|
|
+ for c in candidates:
|
|
+ if c is not None and c.is_file():
|
|
+ return c
|
|
+ return None
|
|
+
|
|
+
|
|
+def _icon_data_uri() -> Optional[str]:
|
|
+ icon = _find_icon()
|
|
+ if icon is None:
|
|
+ return None
|
|
+ try:
|
|
+ data = base64.b64encode(icon.read_bytes()).decode("ascii")
|
|
+ mime = "image/png" if icon.suffix.lower() == ".png" else "image/jpeg"
|
|
+ return f"data:{mime};base64,{data}"
|
|
+ except OSError:
|
|
+ return None
|
|
+
|
|
+
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
prog="qwen-tts-demo",
|
|
@@ -168,6 +195,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|
return parser
|
|
|
|
|
|
+def _find_favicon() -> Optional[Path]:
|
|
+ for name in ("UmbraAudio_icon.png", "logo.png"):
|
|
+ p = Path(__file__).resolve().parents[3] / name
|
|
+ if p.is_file():
|
|
+ return p
|
|
+ alt = Path("/Volumes/1TB_APL_Storage/UmbraAudio") / name
|
|
+ if alt.is_file():
|
|
+ return alt
|
|
+ icon = _find_icon()
|
|
+ return icon if icon is not None and icon.suffix.lower() in (".png", ".gif", ".ico") else None
|
|
+
|
|
+
|
|
def _resolve_checkpoint(args: argparse.Namespace) -> str:
|
|
ckpt = args.checkpoint or args.checkpoint_pos
|
|
if not ckpt:
|
|
@@ -275,13 +314,30 @@ def build_demo(tts: Qwen3TTSModel, ckpt: str, gen_kwargs_default: Dict[str, Any]
|
|
css = ".gradio-container {max-width: none !important;}"
|
|
|
|
with gr.Blocks(theme=theme, css=css) as demo:
|
|
- gr.Markdown(
|
|
- f"""
|
|
+ icon_uri = _icon_data_uri()
|
|
+ if icon_uri:
|
|
+ with gr.Row():
|
|
+ with gr.Column(scale=1, min_width=120):
|
|
+ gr.HTML(
|
|
+ f'<img src="{icon_uri}" alt="UmbraAudio" '
|
|
+ 'style="max-height:96px; display:block; margin:0 auto;">'
|
|
+ )
|
|
+ with gr.Column(scale=4):
|
|
+ gr.Markdown(
|
|
+ f"""
|
|
+# UmbraAudio Voice Studio
|
|
+**Checkpoint:** `{ckpt}`
|
|
+**Model Type:** `{model_kind}`
|
|
+"""
|
|
+ )
|
|
+ else:
|
|
+ gr.Markdown(
|
|
+ f"""
|
|
# Qwen3 TTS Demo
|
|
**Checkpoint:** `{ckpt}`
|
|
**Model Type:** `{model_kind}`
|
|
"""
|
|
- )
|
|
+ )
|
|
|
|
if model_kind == "custom_voice":
|
|
with gr.Row():
|
|
@@ -621,6 +677,9 @@ def main(argv=None) -> int:
|
|
share=args.share,
|
|
ssl_verify=True if args.ssl_verify else False,
|
|
)
|
|
+ favicon = _find_favicon()
|
|
+ if favicon is not None:
|
|
+ launch_kwargs["favicon_path"] = str(favicon)
|
|
if args.ssl_certfile is not None:
|
|
launch_kwargs["ssl_certfile"] = args.ssl_certfile
|
|
if args.ssl_keyfile is not None:
|