chore(diarization): добавлена обвязка замеров в .scratch

- Зачем:
  - тикеты карты #10-#13 опираются на измерительную обвязку, которая до сих пор
    жила во временном каталоге сессии и исчезла бы вместе с ним.
- Что:
  - перенесены четыре скрипта разведки: ASR, один прогон диаризации, свип порога
    кластеризации и подсчёт чистоты ASR-сегментов.
  - общая часть вынесена в common.py: пути от корня репозитория вместо
    захардкоженных, конфигурация диаризатора, проверка наличия моделей.
  - починен замер пиковой памяти: нужен экспорт K32GetProcessMemoryInfo из
    kernel32 и явные argtypes, иначе дескриптор процесса уезжает 32-битным.
  - модели и выход замеров исключены из истории локальным .gitignore.
- Проверка:
  - export PYTHONIOENCODING=utf-8
  - uv run --with sherpa-onnx python .scratch/diarization/bench_diar.py "<запись>" 8 0.9

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dmitriy Dementiev
2026-08-12 15:35:20 +03:00
co-authored by Claude Opus 5
parent 87030e9718
commit 1cb6a36c92
7 changed files with 550 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
"""Один прогон диаризации: скорость, память, распределение по говорящим.
uv run --with sherpa-onnx python .scratch/diarization/bench_diar.py <файл> [потоки]
Сохраняет разметку в ``segments-<порог>.tsv`` рядом со скриптом — она нужна
тикету про проверку границ на слух и скрипту bench_conflict.py.
"""
from __future__ import annotations
import sys
import time
import numpy as np
from common import (
DEFAULT_THREADS,
DISCOVERY_THRESHOLD,
HERE,
SAMPLE_RATE,
load_audio,
make_diarizer,
peak_rss_mb,
)
def main(audio_path: str, threads: int, threshold: float) -> None:
print(f"файл: {audio_path}")
print(f"потоков: {threads}, порог кластеризации: {threshold}")
t0 = time.perf_counter()
samples = load_audio(audio_path)
t_decode = time.perf_counter() - t0
duration = len(samples) / SAMPLE_RATE
print(f"длительность: {duration / 60:.1f} мин ({duration:.0f} с)")
print(f"декодирование: {t_decode:.1f} с ({duration / t_decode:.0f}x RTF)")
t0 = time.perf_counter()
diarizer = make_diarizer(threshold=threshold, threads=threads)
print(f"инициализация моделей: {time.perf_counter() - t0:.1f} с")
progress = {"shown": 0.0}
t_start = time.perf_counter()
def on_progress(processed: int, total: int, _arg=None) -> int:
pct = processed / total * 100
if pct - progress["shown"] >= 20:
progress["shown"] = pct
print(f" ... {pct:.0f}% ({time.perf_counter() - t_start:.0f} с)", flush=True)
return 0
segments = diarizer.process(samples, callback=on_progress).sort_by_start_time()
t_diar = time.perf_counter() - t_start
speakers = sorted({s.speaker for s in segments})
speech = sum(s.end - s.start for s in segments)
rss = peak_rss_mb()
print()
print(f"ДИАРИЗАЦИЯ: {t_diar:.1f} с -> {duration / t_diar:.1f}x RTF")
print(f"пиковая память процесса: {rss:.0f} МБ" if rss else "память: снять не удалось")
print(f"спикеров: {len(speakers)}, интервалов: {len(segments)}")
print(f"речи: {speech / 60:.1f} мин ({speech / duration * 100:.0f}% файла)")
print()
print("распределение по говорящим:")
for spk in speakers:
own = [s for s in segments if s.speaker == spk]
total = sum(s.end - s.start for s in own)
median = np.median([s.end - s.start for s in own])
print(f" spk{spk:<3} {total / 60:6.1f} мин {len(own):4d} интерв. медиана {median:.1f} с")
out = HERE / f"segments-{threshold}.tsv"
out.write_text(
"\n".join(f"{s.start:.3f}\t{s.end:.3f}\t{s.speaker}" for s in segments),
encoding="utf-8",
)
print(f"\nразметка сохранена: {out.name}")
if __name__ == "__main__":
if len(sys.argv) < 2:
raise SystemExit(__doc__)
main(
sys.argv[1],
int(sys.argv[2]) if len(sys.argv) > 2 else DEFAULT_THREADS,
float(sys.argv[3]) if len(sys.argv) > 3 else DISCOVERY_THRESHOLD,
)