- Зачем: - требовалось проверить долю смешанных ASR-сегментов на трёх записях, включая разговоры на двоих. - Что: - добавлен отчёт с долями сегментов и времени для трёх рабочих созвонов. - обвязка замеров переведена на откалиброванный порог 0,89. - исследовательский скрипт приведён к формату ruff. - Проверка: - выполнены три полных прогона bench_conflict.py с WeSpeaker и порогом 0,89. - uv run ruff check и ruff format --check прошли успешно.
134 lines
4.9 KiB
Python
134 lines
4.9 KiB
Python
"""Общая обвязка для замеров диаризации.
|
||
|
||
Скрипты в этом каталоге — исследовательские, не часть пакета. Они опираются на
|
||
``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-14 на трёх записях.
|
||
# На 0.5 из примеров sherpa-onnx получалось 29 говорящих вместо трёх.
|
||
DISCOVERY_THRESHOLD = 0.89
|
||
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
|