117 lines
4.0 KiB
Python
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
|