Режим на детекторе речи вместо диаризации
Диаризация занимала 79 % времени, а её метку мы не используем: на записи с одним микрофоном голоса не расходятся, роли расставляет LLM по смыслу. Silero VAD решает единственную нужную задачу и делает это вчетверо быстрее. Замер на 205 минутах: x16 -> x60, слов на 1,8 % меньше. Семь записей из восьми в пределах +-4 %, на одной теряется 19 %. Отдельно найдено: max_speech_duration у Silero по умолчанию 20 с, и на таких кусках распознавание теряет текст. Снижение до 6 с даёт 6-8 п.п. полноты бесплатно. Диаризация сохранена настройкой diarize. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b2bad9d8e6
commit
5d576dd3c4
+74
-9
@@ -43,6 +43,17 @@ ASR_MODEL_NAME = "gigaam-v3-e2e-rnnt"
|
||||
ASR_MODEL_REL = Path("gigaam")
|
||||
SEG_MODEL_REL = Path("sherpa-onnx-pyannote-segmentation-3-0") / "model.onnx"
|
||||
EMB_MODEL_REL = Path("nemo_en_titanet_small.onnx")
|
||||
VAD_MODEL_REL = Path("silero_vad.onnx")
|
||||
# Порог детектора речи. Ниже - подхватывает фон, выше - теряет тихие реплики.
|
||||
VAD_THRESHOLD = 0.5
|
||||
# Паузу короче этой внутри речи не считаем разрывом: иначе фраза рвётся
|
||||
# на каждом вдохе.
|
||||
VAD_MIN_SILENCE_SEC = 0.5
|
||||
VAD_MIN_SPEECH_SEC = 0.25
|
||||
# Предельная длина отрезка речи. Решающая настройка: распознавание теряет
|
||||
# текст на длинных кусках, и умолчание Silero в 20 с даёт заметно меньше
|
||||
# слов, чем более мелкие отрезки диаризации.
|
||||
VAD_MAX_SPEECH_SEC = 6.0
|
||||
|
||||
|
||||
class ModelsMissing(RuntimeError):
|
||||
@@ -236,6 +247,54 @@ class Pipeline:
|
||||
right = self._recognize_safely(audio[middle:], depth + 1)
|
||||
return " ".join(part for part in (left, right) if part)
|
||||
|
||||
def _vad_segments(self, samples: np.ndarray) -> list[Segment]:
|
||||
"""Границы речи без разделения по говорящим.
|
||||
|
||||
Диаризация занимает около 79 % времени обработки, а её метку мы всё
|
||||
равно не используем: на записи с одним микрофоном голоса не расходятся,
|
||||
и роли расставляет LLM по смыслу. Детектор речи решает единственную
|
||||
нужную здесь задачу - где речь есть - и делает это на порядок быстрее.
|
||||
|
||||
Говорящий у всех отрезков один и тот же (0): это честнее, чем выдавать
|
||||
номер, за которым ничего не стоит.
|
||||
"""
|
||||
import sherpa_onnx
|
||||
|
||||
model = self.models_dir / VAD_MODEL_REL
|
||||
if not model.is_file():
|
||||
raise ModelsMissing(f"нет модели детектора речи: {model}")
|
||||
|
||||
config = sherpa_onnx.VadModelConfig()
|
||||
config.silero_vad.model = str(model)
|
||||
config.silero_vad.threshold = VAD_THRESHOLD
|
||||
config.silero_vad.min_silence_duration = VAD_MIN_SILENCE_SEC
|
||||
config.silero_vad.min_speech_duration = VAD_MIN_SPEECH_SEC
|
||||
config.silero_vad.max_speech_duration = VAD_MAX_SPEECH_SEC
|
||||
config.sample_rate = SAMPLE_RATE
|
||||
config.num_threads = self.threads
|
||||
|
||||
window = 512
|
||||
detector = sherpa_onnx.VoiceActivityDetector(config, buffer_size_in_seconds=60)
|
||||
out: list[Segment] = []
|
||||
for offset in range(0, len(samples), window):
|
||||
detector.accept_waveform(samples[offset:offset + window])
|
||||
while not detector.empty():
|
||||
segment = detector.front
|
||||
start = segment.start / SAMPLE_RATE
|
||||
out.append(Segment(start=start,
|
||||
end=start + len(segment.samples) / SAMPLE_RATE,
|
||||
speaker=0))
|
||||
detector.pop()
|
||||
detector.flush()
|
||||
while not detector.empty():
|
||||
segment = detector.front
|
||||
start = segment.start / SAMPLE_RATE
|
||||
out.append(Segment(start=start,
|
||||
end=start + len(segment.samples) / SAMPLE_RATE,
|
||||
speaker=0))
|
||||
detector.pop()
|
||||
return out
|
||||
|
||||
def _recover_gaps(self, samples: np.ndarray, turns: list[dict],
|
||||
duration: float) -> list[dict]:
|
||||
"""Распознаёт то, что детектор речи не отметил как речь.
|
||||
@@ -275,7 +334,7 @@ class Pipeline:
|
||||
return out
|
||||
|
||||
def transcribe(self, wav_path: Path, num_speakers: int = 2,
|
||||
recover_gaps: bool = True) -> dict:
|
||||
recover_gaps: bool = True, diarize: bool = False) -> dict:
|
||||
"""Полный проход: диаризация, распознавание реплик, постобработка.
|
||||
|
||||
Модели грузятся при первой задаче, а не при создании: воркеров несколько,
|
||||
@@ -289,13 +348,18 @@ class Pipeline:
|
||||
duration = len(samples) / SAMPLE_RATE
|
||||
|
||||
t0 = time.time()
|
||||
raw = self._diarizer(num_speakers).process(samples).sort_by_start_time()
|
||||
segments = [Segment(start=s.start, end=s.end, speaker=s.speaker) for s in raw]
|
||||
try:
|
||||
quality = self._separation_quality(samples, segments)
|
||||
except Exception as exc: # noqa: BLE001 - оценка вспомогательная
|
||||
# Метрика не должна ронять задачу: без неё расшифровка всё равно нужна.
|
||||
log.warning("не удалось оценить разделение говорящих: %s", exc)
|
||||
if diarize:
|
||||
raw = self._diarizer(num_speakers).process(samples).sort_by_start_time()
|
||||
segments = [Segment(start=s.start, end=s.end, speaker=s.speaker) for s in raw]
|
||||
try:
|
||||
quality = self._separation_quality(samples, segments)
|
||||
except Exception as exc: # noqa: BLE001 - оценка вспомогательная
|
||||
# Метрика не должна ронять задачу: расшифровка нужна и без неё.
|
||||
log.warning("не удалось оценить разделение говорящих: %s", exc)
|
||||
quality = 0.0
|
||||
else:
|
||||
segments = self._vad_segments(samples)
|
||||
# Разделения не было, и притворяться, что оно удалось, нельзя.
|
||||
quality = 0.0
|
||||
t_diar = time.time() - t0
|
||||
|
||||
@@ -341,8 +405,9 @@ class Pipeline:
|
||||
"sentences": split_sentences(turns_out),
|
||||
"stats": {
|
||||
**speech_stats(segments),
|
||||
"diarized": diarize,
|
||||
"separation_quality": quality,
|
||||
"speakers_reliable": quality >= RELIABLE_SEPARATION,
|
||||
"speakers_reliable": diarize and quality >= RELIABLE_SEPARATION,
|
||||
"silence_sec": round(max(0.0, duration - sum(s.end - s.start for s in segments)), 1),
|
||||
"turns_count": len(turns_out),
|
||||
"recovered_turns": len(recovered),
|
||||
|
||||
Reference in New Issue
Block a user