Files
local-transcriber/.scratch/diarization/common.py
T
Dmitriy DementievandClaude Opus 5 1cb6a36c92 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>
2026-08-12 15:35:20 +03:00

134 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Общая обвязка для замеров диаризации.
Скрипты в этом каталоге — исследовательские, не часть пакета. Они опираются на
``sherpa-onnx``, которого нет в зависимостях проекта, поэтому запускаются через
``uv run --with sherpa-onnx``.
"""
from __future__ import annotations
import ctypes
import ctypes.wintypes as wt
import sys
from pathlib import Path
from typing import Any
HERE = Path(__file__).resolve().parent
REPO_ROOT = HERE.parents[1]
MODELS = HERE / "models"
SEGMENTATION = MODELS / "sherpa-onnx-pyannote-segmentation-3-0" / "model.onnx"
EMBEDDING = MODELS / "wespeaker_en_voxceleb_resnet34_LM.onnx"
SAMPLE_RATE = 16_000
# Настройки разведки 2026-08-12. Порог 0.9 дал верное число говорящих на
# контрольной записи; на 0.5 из примеров sherpa-onnx получалось 29 вместо трёх.
DISCOVERY_THRESHOLD = 0.9
DEFAULT_THREADS = 8
def use_project_sources() -> None:
"""Делает пакет проекта импортируемым без установки."""
src = str(REPO_ROOT / "src")
if src not in sys.path:
sys.path.insert(0, src)
def require_models() -> None:
"""Останавливает запуск с внятным сообщением, если модели не скачаны."""
missing = [p for p in (SEGMENTATION, EMBEDDING) if not p.exists()]
if missing:
names = "\n ".join(str(p) for p in missing)
raise SystemExit(
f"Не найдены модели диаризации:\n {names}\n\n"
"Скачайте их по инструкции из README.md в этом каталоге."
)
class _ProcessMemoryCounters(ctypes.Structure):
_fields_ = [
("cb", wt.DWORD),
("PageFaultCount", wt.DWORD),
("PeakWorkingSetSize", ctypes.c_size_t),
("WorkingSetSize", ctypes.c_size_t),
("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
("QuotaPagedPoolUsage", ctypes.c_size_t),
("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
("QuotaNonPagedPoolUsage", ctypes.c_size_t),
("PagefileUsage", ctypes.c_size_t),
("PeakPagefileUsage", ctypes.c_size_t),
]
def peak_rss_mb() -> float | None:
"""Пиковая рабочая память процесса в МБ; None, если снять не удалось.
Два подвоха, на которых замер в разведке 2026-08-12 вернул ноль:
экспорт на современных Windows живёт в kernel32 как
``K32GetProcessMemoryInfo``, а без явных ``restype``/``argtypes``
псевдодескриптор процесса уезжает в вызов как 32-битное число и функция
молча не срабатывает.
"""
kernel32 = ctypes.windll.kernel32
kernel32.GetCurrentProcess.restype = ctypes.c_void_p
handle = kernel32.GetCurrentProcess()
pmc = _ProcessMemoryCounters()
pmc.cb = ctypes.sizeof(_ProcessMemoryCounters)
for dll, name in (
(kernel32, "K32GetProcessMemoryInfo"),
(ctypes.windll.psapi, "GetProcessMemoryInfo"),
):
func = getattr(dll, name, None)
if func is None:
continue
func.argtypes = [
ctypes.c_void_p,
ctypes.POINTER(_ProcessMemoryCounters),
wt.DWORD,
]
func.restype = wt.BOOL
if func(handle, ctypes.byref(pmc), pmc.cb):
return pmc.PeakWorkingSetSize / 1024 / 1024
return None
def load_audio(audio_path: str | Path):
"""Декодирует файл в моно 16 кГц — тот же путь, что использует ONNX-бэкенд."""
from faster_whisper import decode_audio
return decode_audio(str(audio_path), sampling_rate=SAMPLE_RATE)
def make_diarizer(
threshold: float = DISCOVERY_THRESHOLD,
num_clusters: int = -1,
threads: int = DEFAULT_THREADS,
) -> Any:
"""Собирает OfflineSpeakerDiarization с параметрами разведки."""
import sherpa_onnx as so
require_models()
config = so.OfflineSpeakerDiarizationConfig(
segmentation=so.OfflineSpeakerSegmentationModelConfig(
pyannote=so.OfflineSpeakerSegmentationPyannoteModelConfig(
model=str(SEGMENTATION)
),
num_threads=threads,
provider="cpu",
),
embedding=so.SpeakerEmbeddingExtractorConfig(
model=str(EMBEDDING), num_threads=threads, provider="cpu"
),
clustering=so.FastClusteringConfig(
num_clusters=num_clusters, threshold=threshold
),
min_duration_on=0.3,
min_duration_off=0.5,
)
diarizer = so.OfflineSpeakerDiarization(config)
assert diarizer.sample_rate == SAMPLE_RATE, diarizer.sample_rate
return diarizer