"""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("