diff --git a/app/pipeline.py b/app/pipeline.py index 49e63dc..89ec0bc 100644 --- a/app/pipeline.py +++ b/app/pipeline.py @@ -20,7 +20,12 @@ log = logging.getLogger(__name__) SAMPLE_RATE = 16000 # GigaAM падает на кусках длиннее ~200 с, держим запас. -MAX_CHUNK_SEC = 150.0 +# Предел зависит от сборки onnxruntime: на macOS реплика в 190 с проходит, +# на Windows та же падает в свёртке энкодера. Берём с большим запасом, а на +# случай сюрпризов есть деление куска пополам при ошибке. +MAX_CHUNK_SEC = 60.0 +# Ниже этого делить бессмысленно: явно не длина виновата. +MIN_SPLIT_SEC = 2.0 # Ниже этого значения голоса практически неразличимы и разметка по говорящим # случайна. На записях с одним микрофоном в комнате так бывает часто. RELIABLE_SEPARATION = 0.35 @@ -174,6 +179,25 @@ class Pipeline: return separation_quality(np.array(vectors), np.array([s.speaker for s in usable])) + def _recognize_safely(self, audio: np.ndarray, depth: int = 0) -> str: + """Распознаёт кусок, при ошибке деля его пополам. + + Пределы длины у разных сборок onnxruntime разные и заранее неизвестны, + поэтому вместо подбора константы просто уменьшаем кусок и пробуем снова. + """ + try: + return self._asr.recognize(audio, sample_rate=SAMPLE_RATE) + except Exception: + seconds = len(audio) / SAMPLE_RATE + if seconds <= MIN_SPLIT_SEC or depth >= 5: + log.warning("не удалось распознать кусок в %.1f с, пропускаю", seconds) + return "" + log.info("кусок в %.1f с не прошёл, делю пополам", seconds) + middle = len(audio) // 2 + left = self._recognize_safely(audio[:middle], depth + 1) + right = self._recognize_safely(audio[middle:], depth + 1) + return " ".join(part for part in (left, right) if part) + def transcribe(self, wav_path: Path, num_speakers: int = 2) -> dict: """Полный проход: диаризация, распознавание реплик, постобработка. @@ -201,7 +225,7 @@ class Pipeline: audio = samples[int(start * SAMPLE_RATE):int(stop * SAMPLE_RATE)] if len(audio) < SAMPLE_RATE * 0.2: continue - pieces.append(self._asr.recognize(audio, sample_rate=SAMPLE_RATE)) + pieces.append(self._recognize_safely(audio)) text = " ".join(p.strip() for p in pieces if p and p.strip()) if not text: continue diff --git a/app/version.py b/app/version.py index 906d362..43c4ab0 100644 --- a/app/version.py +++ b/app/version.py @@ -1 +1 @@ -__version__ = "0.6.0" +__version__ = "0.6.1" diff --git a/tests/test_turns.py b/tests/test_turns.py index 0dd3a2c..7146c03 100644 --- a/tests/test_turns.py +++ b/tests/test_turns.py @@ -71,3 +71,17 @@ class TestSpeechStats: stats = speech_stats([]) assert stats["speakers"] == 0 assert stats["speech_sec"] == 0 + + +class TestChunkLimit: + def test_default_chunk_is_conservative(self): + """Пределы у разных сборок onnxruntime разные, берём с запасом.""" + from app.pipeline import MAX_CHUNK_SEC + + assert MAX_CHUNK_SEC <= 60 + + def test_long_turn_split_below_limit(self): + from app.pipeline import MAX_CHUNK_SEC + + for a, b in chunk_ranges(0.0, 600.0, max_len=MAX_CHUNK_SEC): + assert b - a <= MAX_CHUNK_SEC