Files
UmbraAudio/tests/test_character_discovery.py
2026-10-18 19:01:04 -07:00

117 lines
4.0 KiB
Python

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from umbraaudio.analysis.character_discovery import (
ChapterSpeakers,
CharacterDiscoverer,
)
def _make_discoverer() -> CharacterDiscoverer:
cd = CharacterDiscoverer.__new__(CharacterDiscoverer)
cd.TITLE_WORDS = CharacterDiscoverer.TITLE_WORDS
return cd
def _sp(name: str, aliases: list[str], evidence: list[str] | None = None) -> dict:
return {
"primary_name": name,
"aliases": aliases,
"alias_evidence": evidence or [],
"vocal_traits": [],
"demographics": {"type": "Human", "gender": "Female"},
"communication_style": {},
"description": "",
}
def _merge(results: list[ChapterSpeakers]) -> list:
cd = _make_discoverer()
return cd._merge_chapter_results(results, "Test Book", 1000)
def _ch(
index: int,
speaking: list[dict] | None = None,
non_speaking: list[dict] | None = None,
) -> ChapterSpeakers:
return ChapterSpeakers(
chapter_index=index,
chapter_title=f"Ch {index + 1}",
speaking_characters=speaking or [],
non_speaking_characters=non_speaking or [],
validation_notes=[],
)
class TestNonSpeakingNotPromoted:
def test_non_speaking_only_alone_does_not_create_speaker(self):
results = [_ch(0, non_speaking=[_sp("Karel Čapek", [])])]
speakers = _merge(results)
assert not any(s.primary_name == "Karel Čapek" for s in speakers)
def test_non_speaking_appends_chapter_to_existing_speaker(self):
results = [
_ch(0, speaking=[_sp("Kainda", ["Princess Kainda"])]),
_ch(1, non_speaking=[_sp("Kainda", ["Princess Kainda"])]),
]
speakers = _merge(results)
k = next(s for s in speakers if s.primary_name == "Kainda")
assert k.chapters_appeared == [1, 2]
class TestSharedAliasDedup:
def test_same_person_different_primaries_merged_via_shared_alias(self):
results = [
_ch(0, speaking=[_sp("Kainda Walters Foster Murphy", ["Princess Kainda", "Princess"])]),
_ch(1, speaking=[_sp("Kainda Murphy", ["Princess Kainda", "Princess"])]),
]
speakers = _merge(results)
assert len(speakers) == 1
keeper = speakers[0]
assert keeper.primary_name == "Kainda Walters Foster Murphy"
assert keeper.chapters_appeared == [1, 2]
def test_absorb_keeps_richer_entry(self):
results = [
_ch(0, speaking=[_sp("Kainda", ["Princess Kainda"])]),
_ch(1, speaking=[_sp("Kainda Walters Foster Murphy", ["Princess Kainda", "Princess"])]),
_ch(2, speaking=[_sp("Kainda Walters Foster Murphy", ["Princess Kainda", "Princess"])]),
]
speakers = _merge(results)
assert len(speakers) == 1
assert speakers[0].primary_name == "Kainda Walters Foster Murphy"
assert speakers[0].chapters_appeared == [2, 3, 1]
class TestAliasEvidenceKept:
def test_distinct_proper_name_alias_kept_when_evidence_exists(self):
results = [
_ch(
0,
speaking=[
_sp(
"Kainda Walters Foster Murphy",
["Princess Kainda", "Captain Capek"],
evidence=["Are you talking as Princess Murphy or Captain Capek? I'm both."],
)
],
),
_ch(1, speaking=[_sp("Captain Capek", [])]),
]
speakers = _merge(results)
assert len(speakers) == 1
keeper = speakers[0]
assert "Captain Capek" in keeper.aliases
assert keeper.chapters_appeared == [1, 2]
def test_distinct_proper_name_alias_still_filtered_without_evidence(self):
results = [
_ch(0, speaking=[_sp("John Smith", ["Jane Doe"])]),
]
speakers = _merge(results)
john = next(s for s in speakers if s.primary_name == "John Smith")
assert "Jane Doe" not in john.aliases