From f2f266ba1c63093fc3862ebf40c41758489f694b Mon Sep 17 00:00:00 2001 From: Vladimir Bryzgalov Date: Sat, 15 Aug 2026 23:37:17 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A3=D1=81=D1=82=D0=BE=D0=B9=D1=87=D0=B8?= =?UTF-8?q?=D0=B2=D0=BE=D0=B5=20=D1=80=D0=B0=D1=81=D0=BF=D0=BE=D0=B7=D0=BD?= =?UTF-8?q?=D0=B0=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D1=80=D0=B5=D0=BF=D0=BB=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit На Windows реплика в 190 секунд роняет энкодер GigaAM в свёртке, хотя на macOS та же самая проходит: пределы у сборок onnxruntime разные и заранее неизвестны. Вместо подбора константы кусок при ошибке делится пополам и пробуется снова, до пяти уровней. Базовый размер куска снижен со 150 до 60 секунд - с запасом под любую сборку. Co-Authored-By: Claude Opus 5 (1M context) --- app/pipeline.py | 28 ++++++++++++++++++++++++++-- app/version.py | 2 +- tests/test_turns.py | 14 ++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) 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