"""Тесты постобработки текста: словарь замен и типографика.""" import pytest from app.text import apply_replacements, load_replacements, normalize_typography, split_sentences class TestReplacements: def test_replaces_known_term(self): rules = {"тэг менеджер": "Tag Manager"} assert apply_replacements("открываем тэг менеджер", rules) == "открываем Tag Manager" def test_replacement_is_case_insensitive_on_match(self): rules = {"тэг менеджер": "Tag Manager"} assert apply_replacements("Открываем Тэг Менеджер тут", rules) == "Открываем Tag Manager тут" def test_replacement_keeps_surrounding_punctuation(self): rules = {"джетур": "G-Tour"} assert apply_replacements("бренд джетур, смотрите", rules) == "бренд G-Tour, смотрите" def test_does_not_replace_inside_word(self): """«лид» не должен ломать «лидер».""" rules = {"лед": "лид"} assert apply_replacements("лидер рынка", rules) == "лидер рынка" def test_multiword_replacement(self): rules = {"гугл так менеджер": "Google Tag Manager"} assert apply_replacements("это гугл так менеджер", rules) == "это Google Tag Manager" def test_empty_rules_returns_original(self): assert apply_replacements("текст без изменений", {}) == "текст без изменений" def test_longest_rule_wins(self): """Более длинное правило применяется раньше короткого.""" rules = {"так менеджер": "Tag Manager", "гугл так менеджер": "Google Tag Manager"} assert apply_replacements("гугл так менеджер", rules) == "Google Tag Manager" class TestLoadReplacements: def test_parses_simple_file(self, tmp_path): f = tmp_path / "r.txt" f.write_text("джетур = G-Tour\nтэг менеджер = Tag Manager\n", encoding="utf-8") rules = load_replacements(f) assert rules == {"джетур": "G-Tour", "тэг менеджер": "Tag Manager"} def test_skips_comments_and_blanks(self, tmp_path): f = tmp_path / "r.txt" f.write_text("# комментарий\n\nджетур = G-Tour\n\n# ещё\n", encoding="utf-8") assert load_replacements(f) == {"джетур": "G-Tour"} def test_missing_file_returns_empty(self, tmp_path): assert load_replacements(tmp_path / "нет.txt") == {} def test_ignores_line_without_separator(self, tmp_path): f = tmp_path / "r.txt" f.write_text("мусор без разделителя\nджетур = G-Tour\n", encoding="utf-8") assert load_replacements(f) == {"джетур": "G-Tour"} class TestTypography: @pytest.mark.parametrize("dash", ["—", "–", "‒", "―", "−"]) def test_replaces_all_dash_variants_with_hyphen(self, dash): assert normalize_typography(f"слово {dash} слово") == "слово - слово" def test_glues_dash_without_spaces(self): """GigaAM пишет «слово—слово» без пробелов.""" assert normalize_typography("это—тестирование") == "это - тестирование" def test_adds_space_after_opening_quote(self): """GigaAM пишет «слово«цитата» без пробела перед кавычкой.""" assert normalize_typography("спрашивает:«По метрике") == "спрашивает: «По метрике" def test_collapses_multiple_spaces(self): assert normalize_typography("много пробелов") == "много пробелов" def test_keeps_hyphen_in_compound_word(self): assert normalize_typography("look-alike и из-за") == "look-alike и из-за" class TestSplitSentences: """Разметку ролей делает LLM по предложениям: реплику целиком разметить нельзя.""" @staticmethod def turn(text, start=0.0, end=10.0, loudness=-20.0): return {"text": text, "start": start, "end": end, "acoustics": {"loudness_db": loudness}} def test_splits_on_sentence_endings(self): out = split_sentences([self.turn("Первое. Второе! Третье? Четвёртое…")]) assert [s["text"] for s in out] == ["Первое.", "Второе!", "Третье?", "Четвёртое…"] def test_numbering_is_continuous_across_turns(self): out = split_sentences([self.turn("А. Б."), self.turn("В. Г.")]) assert [s["n"] for s in out] == [1, 2, 3, 4] def test_turn_index_is_kept(self): """По нему собирают диалог обратно после разметки.""" out = split_sentences([self.turn("А. Б."), self.turn("В.")]) assert [s["turn"] for s in out] == [0, 0, 1] def test_time_is_spread_inside_turn(self): out = split_sentences([self.turn("А. Б. В.", start=0.0, end=30.0)]) assert [s["start"] for s in out] == [0.0, 10.0, 20.0] def test_loudness_comes_from_turn(self): out = split_sentences([self.turn("А. Б.", loudness=-17.5)]) assert all(s["loudness_db"] == -17.5 for s in out) def test_turn_without_acoustics_gives_none(self): """Короткие фрагменты акустику не считают - поле должно быть пустым, не падать.""" out = split_sentences([{"text": "А.", "start": 0.0, "end": 1.0, "acoustics": {}}]) assert out[0]["loudness_db"] is None def test_text_without_punctuation_stays_one_sentence(self): out = split_sentences([self.turn("совсем без знаков препинания")]) assert len(out) == 1 def test_empty_text_is_skipped(self): assert split_sentences([self.turn(" ")]) == [] def test_abbreviation_dots_do_not_split(self): """Точка без пробела после неё концом предложения не считается.""" out = split_sentences([self.turn("Нужен документ п.2.1 из списка.")]) assert len(out) == 1