"""Второй проход по промежуткам: без него часть фраз пропадает молча.""" import numpy as np from app.pipeline import GAP_MIN_SEC, MIN_RECOVERED_CHARS, Pipeline, gap_ranges def turn(start, end, text="реплика"): return {"speaker": 1, "start": start, "end": end, "text": text, "acoustics": {}} class TestGapRanges: def test_finds_gap_between_turns(self): assert list(gap_ranges([turn(0, 5), turn(20, 25)], 30)) == [(5, 20), (25, 30)] def test_ignores_short_pauses(self): """Пауза в пару секунд - обычное дело в разговоре.""" assert list(gap_ranges([turn(0, 5), turn(6, 25)], 25)) == [] def test_finds_gap_before_first_turn(self): assert (0.0, 10) in list(gap_ranges([turn(10, 20)], 20)) def test_finds_gap_after_last_turn(self): assert (20, 40) in list(gap_ranges([turn(10, 20)], 40)) def test_no_turns_gives_whole_recording(self): assert list(gap_ranges([], 60)) == [(0.0, 60)] def test_overlapping_turns_do_not_create_negative_gap(self): """Реплики могут перекрываться, и конец предыдущей бывает позже начала следующей.""" assert list(gap_ranges([turn(0, 30), turn(10, 20)], 25)) == [] def test_boundary_is_inclusive(self): assert len(list(gap_ranges([turn(0, 5), turn(5 + GAP_MIN_SEC, 20)], 20))) == 1 class FakePipeline(Pipeline): """Подменяет распознавание: модели в этих проверках не нужны.""" def __init__(self, answers): self.answers = answers self._replacements = {} def _recognize_safely(self, audio, depth=0): return self.answers.pop(0) if self.answers else "" class TestRecovery: @staticmethod def samples(seconds=60): return np.zeros(seconds * 16000, dtype=np.float32) def test_recovered_turn_has_unknown_speaker(self): """Диаризация этих участков не видела, поэтому кто говорил - неизвестно.""" pipe = FakePipeline(["Вы приносите справку, я назначаю инструктора."]) out = pipe._recover_gaps(self.samples(), [turn(0, 5), turn(20, 25)], 25) assert out[0]["speaker"] == 0 assert out[0]["recovered"] is True def test_short_result_is_dropped_as_noise(self): """Шелест бумаг звучит почти как речь и даёт короткие обрывки.""" pipe = FakePipeline(["ага"]) assert pipe._recover_gaps(self.samples(), [turn(0, 5), turn(20, 25)], 25) == [] def test_empty_result_is_dropped(self): pipe = FakePipeline([""]) assert pipe._recover_gaps(self.samples(), [turn(0, 5), turn(20, 25)], 25) == [] def test_threshold_is_on_characters(self): text = "x" * MIN_RECOVERED_CHARS pipe = FakePipeline([text]) assert len(pipe._recover_gaps(self.samples(), [turn(0, 5), turn(20, 25)], 25)) == 1 def test_time_span_matches_the_gap(self): pipe = FakePipeline(["Вы приносите справку, я назначаю инструктора."]) out = pipe._recover_gaps(self.samples(), [turn(0, 5), turn(20, 25)], 25) assert (out[0]["start"], out[0]["end"]) == (5, 20) class TestSwitch: """Второй проход подхватывает и радио на фоне - должна быть возможность выключить.""" def test_setting_defaults_to_on(self, tmp_path): from app.config import load_settings config = tmp_path / "config.toml" config.write_text("[processing]\nthreads=1\n", encoding="utf-8") assert load_settings(config).recover_gaps is True def test_setting_can_be_turned_off(self, tmp_path): from app.config import load_settings config = tmp_path / "config.toml" config.write_text("[processing]\nrecover_gaps=false\n", encoding="utf-8") assert load_settings(config).recover_gaps is False class TestDiarizeSwitch: """Диаризация съедает 79 % времени, а её метку мы не используем.""" def test_defaults_to_off(self, tmp_path): from app.config import load_settings config = tmp_path / "config.toml" config.write_text("[processing]\nthreads=1\n", encoding="utf-8") assert load_settings(config).diarize is False def test_can_be_turned_on(self, tmp_path): from app.config import load_settings config = tmp_path / "config.toml" config.write_text("[processing]\ndiarize=true\n", encoding="utf-8") assert load_settings(config).diarize is True def test_missing_vad_model_is_reported_clearly(self, tmp_path): """Отсутствие модели должно называться, а не падать где-то в недрах.""" import numpy as np import pytest from app.pipeline import ModelsMissing, Pipeline pipe = Pipeline.__new__(Pipeline) pipe.models_dir = tmp_path pipe.threads = 1 with pytest.raises(ModelsMissing, match="детектора речи"): pipe._vad_segments(np.zeros(16000, dtype=np.float32))