Files
UmbraAudio/tests/test_voices/test_voice_profile_manager.py
B.D. Murphy c86e323d7a Initial commit: UmbraAudio audiobook generation program
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.
2026-08-30 07:58:24 -07:00

159 lines
6.3 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import Any
from umbraaudio.voices.voice_profile_manager import (
build_profile_from_speaker,
get_profile_for_speaker,
)
def _speaker(**overrides: object) -> dict[str, Any]:
base: dict[str, Any] = {
"primary_name": "Kainda",
"aliases": [],
"demographics": {"type": "Human", "gender": "Female", "age": "Unknown", "pitch": ""},
"vocal_traits": [],
"communication_style": {"primary_register": "Casual", "style_shifts_or_notes": ""},
"description": "",
"is_narrator": False,
"accent": "none",
}
base.update(overrides)
return base
def _demographics(speaker: dict[str, Any]) -> dict[str, Any]:
return speaker["demographics"] # type: ignore[no-any-return]
class TestBuildProfileFromSpeaker:
def test_default_infers_pitch_from_gender(self) -> None:
profile = build_profile_from_speaker(_speaker())
assert profile.pitch_register in ("mid_low", "mid", "mid_high", "low", "high")
def test_explicit_pitch_override_wins(self) -> None:
speaker = _speaker()
_demographics(speaker)["pitch"] = "ultra_high"
profile = build_profile_from_speaker(speaker)
assert profile.pitch_register == "ultra_high"
def test_invalid_explicit_pitch_ignored(self) -> None:
speaker = _speaker()
_demographics(speaker)["pitch"] = "baritone-ish"
profile = build_profile_from_speaker(speaker)
assert profile.pitch_register in (
"very_low",
"low",
"mid_low",
"mid",
"mid_high",
"high",
"very_high",
"ultra_high",
)
def test_age_sets_age_range(self) -> None:
speaker = _speaker()
_demographics(speaker)["age"] = "Elderly"
profile = build_profile_from_speaker(speaker)
assert profile.age_range == "Elderly"
def test_unknown_age_leaves_age_range_none(self) -> None:
profile = build_profile_from_speaker(_speaker())
assert profile.age_range is None
def test_child_age_raises_pitch(self) -> None:
speaker = _speaker()
_demographics(speaker)["age"] = "Child"
profile = build_profile_from_speaker(speaker)
assert profile.pitch_register in ("high", "very_high", "ultra_high")
def test_child_age_appears_in_description(self) -> None:
speaker = _speaker()
_demographics(speaker)["age"] = "Child"
profile = build_profile_from_speaker(speaker)
assert "young child" in (profile.voice_description or "")
def test_elderly_pitch_adjusts_when_mid(self) -> None:
speaker = _speaker()
_demographics(speaker)["age"] = "Elderly"
_demographics(speaker)["gender"] = "Male"
profile = build_profile_from_speaker(speaker)
assert profile.pitch_register == "low"
def test_explicit_pitch_beats_age_adjustment(self) -> None:
speaker = _speaker()
_demographics(speaker)["age"] = "Child"
_demographics(speaker)["pitch"] = "very_low"
profile = build_profile_from_speaker(speaker)
assert profile.pitch_register == "very_low"
def test_accent_appears_in_description(self) -> None:
speaker = _speaker(accent="australian")
profile = build_profile_from_speaker(speaker)
assert profile.accent_marker == "australian"
assert "an australian accent" in (profile.voice_description or "")
def test_consonant_accent_uses_proper_article(self) -> None:
speaker = _speaker(accent="british")
profile = build_profile_from_speaker(speaker)
assert "a british accent" in (profile.voice_description or "")
def test_no_accent_omits_accent_phrase(self) -> None:
profile = build_profile_from_speaker(_speaker())
assert profile.accent_marker == "none"
assert "accent" not in (profile.voice_description or "")
def test_accent_survives_force_rebuild(self, temp_dir: Path) -> None:
speaker = _speaker(accent="australian")
profile = get_profile_for_speaker("Kainda", speaker, temp_dir, force_rebuild=True)
assert profile.accent_marker == "australian"
assert "an australian accent" in (profile.voice_description or "")
def test_narrator_uses_speaker_accent(self) -> None:
speaker = _speaker(is_narrator=True, accent="american")
profile = build_profile_from_speaker(speaker)
assert profile.is_narrator is True
assert profile.accent_marker == "american"
assert "an american accent" in (profile.voice_description or "")
def test_narrator_defaults_to_american_accent(self) -> None:
speaker = _speaker(is_narrator=True, accent="none")
profile = build_profile_from_speaker(speaker)
assert profile.accent_marker == "american"
assert "an american accent" in (profile.voice_description or "")
def test_narrator_with_explicit_non_default_accent(self) -> None:
speaker = _speaker(is_narrator=True, accent="british")
profile = build_profile_from_speaker(speaker)
assert profile.accent_marker == "british"
assert "a british accent" in (profile.voice_description or "")
class TestGetProfileForSpeaker:
def test_cached_profile_returned_without_rebuild(self, temp_dir: Path) -> None:
speaker = _speaker()
_demographics(speaker)["pitch"] = "high"
first = get_profile_for_speaker("Kainda", speaker, temp_dir)
assert first.pitch_register == "high"
# Change the speaker data; cache should win without force_rebuild
_demographics(speaker)["pitch"] = "low"
cached = get_profile_for_speaker("Kainda", speaker, temp_dir)
assert cached.pitch_register == "high"
def test_force_rebuild_reflects_speaker_changes(self, temp_dir: Path) -> None:
speaker = _speaker()
_demographics(speaker)["pitch"] = "high"
get_profile_for_speaker("Kainda", speaker, temp_dir)
_demographics(speaker)["pitch"] = "very_low"
rebuilt = get_profile_for_speaker("Kainda", speaker, temp_dir, force_rebuild=True)
assert rebuilt.pitch_register == "very_low"
# Cache file updated for subsequent reads
again = get_profile_for_speaker("Kainda", speaker, temp_dir)
assert again.pitch_register == "very_low"