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.
54 lines
1.7 KiB
Python
54 lines
1.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from umbraaudio.analysis.voice_analysis import VoiceAnalysisEngine
|
|
from umbraaudio.config import load_config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def run_voice_analysis(project_dir: Path | None = None) -> None:
|
|
"""Compute the objective voice-analysis report for a project."""
|
|
project_dir = project_dir or Path(".")
|
|
cfg = load_config(project_dir)
|
|
|
|
speakers_path = cfg.data_dir / "speakers.json"
|
|
speaker_names: set[str] = set()
|
|
if speakers_path.exists():
|
|
with open(speakers_path) as f:
|
|
speaker_names = {s["primary_name"] for s in json.load(f).get("speakers", [])}
|
|
|
|
engine = VoiceAnalysisEngine(
|
|
refs_dir=cfg.data_dir / "voices",
|
|
output_dir=cfg.data_dir / "analysis",
|
|
target_sample_rate=cfg.tts.sample_rate,
|
|
speaker_names=speaker_names,
|
|
)
|
|
report = engine.analyze()
|
|
summary = report["summary"]
|
|
print(
|
|
f"Analyzed {summary['total']} voices: "
|
|
f"{summary['passed']} passed, {summary['failed']} failed, "
|
|
f"{summary['near_duplicate_count']} near-duplicate pairs"
|
|
)
|
|
if report["near_duplicates"]:
|
|
print("Near-duplicate pairs:")
|
|
for pair in report["near_duplicates"]:
|
|
print(f" {pair['a']} <-> {pair['b']} ({pair['similarity']})")
|
|
if report["errors"]:
|
|
print("Errors:")
|
|
for name, err in report["errors"].items():
|
|
print(f" {name}: {err}")
|
|
print(f"Report written to {engine.report_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
|
project = Path(sys.argv[1]) if len(sys.argv) > 1 else None
|
|
run_voice_analysis(project)
|