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.
80 lines
3.1 KiB
Python
80 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
from umbraaudio.analysis.llm_attributor import LLMAttributor
|
|
|
|
|
|
def _attributor_with_speakers(speakers: list[dict]) -> LLMAttributor:
|
|
attr = LLMAttributor.__new__(LLMAttributor)
|
|
attr._load_speakers = lambda: speakers # type: ignore[method-assign]
|
|
return attr
|
|
|
|
|
|
_SPEAKERS = [
|
|
{
|
|
"primary_name": "Kainda Walters Foster Murphy",
|
|
"aliases": ["Princess Kainda", "Princess", "Captain Capek"],
|
|
},
|
|
{
|
|
"primary_name": "Narrator",
|
|
"aliases": ["narrator", "the narrator"],
|
|
},
|
|
{
|
|
"primary_name": "Nanny",
|
|
"aliases": [],
|
|
},
|
|
]
|
|
|
|
|
|
class TestAliasMap:
|
|
def test_first_word_of_primary_maps_to_full_name(self) -> None:
|
|
attr = _attributor_with_speakers(_SPEAKERS)
|
|
alias_map = attr._build_alias_map()
|
|
assert alias_map["kainda"] == "Kainda Walters Foster Murphy"
|
|
assert alias_map["princess kainda"] == "Kainda Walters Foster Murphy"
|
|
assert alias_map["nanny"] == "Nanny"
|
|
|
|
def test_existing_short_alias_not_overridden(self) -> None:
|
|
speakers = [
|
|
{"primary_name": "Captain Weston", "aliases": ["Weston"]},
|
|
{"primary_name": "Weston", "aliases": []},
|
|
]
|
|
attr = _attributor_with_speakers(speakers)
|
|
alias_map = attr._build_alias_map()
|
|
assert alias_map["weston"] == "Captain Weston"
|
|
|
|
def test_primary_name_always_maps_to_itself(self) -> None:
|
|
attr = _attributor_with_speakers(_SPEAKERS)
|
|
alias_map = attr._build_alias_map()
|
|
assert alias_map["kainda walters foster murphy"] == "Kainda Walters Foster Murphy"
|
|
|
|
|
|
class TestNormalizeSpeaker:
|
|
def test_narrator_stays_lowercase(self) -> None:
|
|
attr = _attributor_with_speakers(_SPEAKERS)
|
|
alias_map = attr._build_alias_map()
|
|
assert attr._normalize_speaker("narrator", alias_map) == "narrator"
|
|
assert attr._normalize_speaker("Narrator", alias_map) == "narrator"
|
|
assert attr._normalize_speaker(" The Narrator ", alias_map) == "narrator"
|
|
|
|
def test_short_name_resolves_to_full_primary(self) -> None:
|
|
attr = _attributor_with_speakers(_SPEAKERS)
|
|
alias_map = attr._build_alias_map()
|
|
assert attr._normalize_speaker("Kainda", alias_map) == "Kainda Walters Foster Murphy"
|
|
assert attr._normalize_speaker("kainda", alias_map) == "Kainda Walters Foster Murphy"
|
|
|
|
def test_unknown_name_unchanged(self) -> None:
|
|
attr = _attributor_with_speakers(_SPEAKERS)
|
|
alias_map = attr._build_alias_map()
|
|
assert attr._normalize_speaker("Unknown Person", alias_map) == "Unknown Person"
|
|
|
|
def test_empty_name_unchanged(self) -> None:
|
|
attr = _attributor_with_speakers(_SPEAKERS)
|
|
alias_map = attr._build_alias_map()
|
|
assert attr._normalize_speaker("", alias_map) == ""
|
|
|
|
def test_pronoun_resolves_to_narrator(self) -> None:
|
|
attr = _attributor_with_speakers(_SPEAKERS)
|
|
alias_map = attr._build_alias_map()
|
|
for pronoun in ("She", "she", "He", "THEY", "her"):
|
|
assert attr._normalize_speaker(pronoun, alias_map) == "narrator"
|