"""Tests for tts/core/offset_map.py — pure Python, no mocking required
except where noted. build_offset_map is generic over any (start, end) span
list; most tests use word_spans() since that's how app.py calls it, but the
function itself doesn't care what produced the spans."""
import pytest
from tts.core.glossary import substitute_with_glossary
from tts.core.offset_map import build_offset_map
from tts.utils.text import word_spans
class TestBuildOffsetMap:
def test_single_word_no_glossary(self):
raw = "Hello"
result = build_offset_map(raw, word_spans(raw), {})
assert result == [
{"raw_start": 0, "raw_end": 5, "sub_start": 0, "sub_end": 5, "text": "Hello"}
]
def test_multiple_words_no_glossary_matches(self):
raw = "Hello world today"
result = build_offset_map(raw, word_spans(raw), {})
assert [e["text"] for e in result] == ["Hello", "world", "today"]
# with no substitution, raw and substituted offsets are identical
for entry in result:
assert entry["raw_start"] == entry["sub_start"]
assert entry["raw_end"] == entry["sub_end"]
def test_maps_offsets_through_a_length_changing_substitution(self):
raw = "Garchen speaks softly"
glossary = {"Garchen": "gar-chen"}
result = build_offset_map(raw, word_spans(raw), glossary)
assert result is not None
substituted_whole = substitute_with_glossary(raw, glossary)
first, second, third = result
assert raw[first["raw_start"]:first["raw_end"]] == "Garchen"
assert substituted_whole[first["sub_start"]:first["sub_end"]] == "gar-chen"
assert substituted_whole[second["sub_start"]:second["sub_end"]] == "speaks"
assert substituted_whole[third["sub_start"]:third["sub_end"]] == "softly"
def test_a_multi_word_span_composes_correctly(self):
# A caller can combine several adjacent word entries (e.g. the words
# touched by a drag-selection) into one (sub_start, sub_end) range —
# verifies that composition is just first.sub_start..last.sub_end.
raw = "Garchen Rinpoche speaks clearly"
glossary = {"Garchen": "gar-chen", "Rinpoche": "rin-po-cheh"}
result = build_offset_map(raw, word_spans(raw), glossary)
substituted_whole = substitute_with_glossary(raw, glossary)
combined_sub_start = result[0]["sub_start"]
combined_sub_end = result[1]["sub_end"]
assert substituted_whole[combined_sub_start:combined_sub_end] == "gar-chen rin-po-cheh"
def test_sub_spans_are_contiguous_and_cover_whole_substituted_text(self):
raw = "Garchen speaks. Rinpoche listens."
glossary = {"Garchen": "gar-chen", "Rinpoche": "rin-po-cheh"}
result = build_offset_map(raw, word_spans(raw), glossary)
substituted_whole = substitute_with_glossary(raw, glossary)
assert result[-1]["sub_end"] == len(substituted_whole)
for a, b in zip(result, result[1:]):
assert a["sub_end"] <= b["sub_start"]
def test_empty_spans_returns_none(self):
assert build_offset_map("", [], {}) is None
def test_tag_spans_left_untouched_in_mapping(self):
raw = 'Hi there.'
glossary = {"time": "tyme"}
spans = word_spans(raw)
result = build_offset_map(raw, spans, glossary)
assert result is not None
substituted_whole = substitute_with_glossary(raw, glossary)
assert 'time="1.5s"' in substituted_whole
tag_entry = next(e for e in result if e["text"].startswith("