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:
co-authored by
Claude Opus 5
parent
87030e9718
commit
1cb6a36c92
@@ -0,0 +1,150 @@
|
||||
"""Чистота ASR-сегментов: как часто внутри одного сегмента меняется говорящий.
|
||||
|
||||
uv run --with sherpa-onnx python .scratch/diarization/bench_conflict.py <файл>
|
||||
|
||||
Прогоняет ASR и диаризацию по одному файлу и считает, какая доля ASR-сегментов
|
||||
содержит чужую речь. Это мера того, насколько огрубляет привязка спикера к
|
||||
целому сегменту по мажоритарному перекрытию.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from common import (
|
||||
DEFAULT_THREADS,
|
||||
DISCOVERY_THRESHOLD,
|
||||
HERE,
|
||||
load_audio,
|
||||
make_diarizer,
|
||||
use_project_sources,
|
||||
)
|
||||
|
||||
use_project_sources()
|
||||
|
||||
ASR_MODEL = "gigaam-v3-e2e-rnnt"
|
||||
COMPUTE_TYPE = "int8"
|
||||
|
||||
# чужая речь короче порога — поддакивание, дольше — потерянная реплика
|
||||
INTERJECTION_S = 1.0
|
||||
|
||||
PURITY_LEVELS = (0.95, 0.90, 0.80, 0.70)
|
||||
|
||||
|
||||
def run_asr(audio_path: str):
|
||||
from local_transcriber.backends.onnx_asr import OnnxAsrBackend
|
||||
|
||||
backend = OnnxAsrBackend(compute_type_explicit=False)
|
||||
path = backend.ensure_model_available(ASR_MODEL, COMPUTE_TYPE)
|
||||
model = backend.create_model(path, "onnx", COMPUTE_TYPE)
|
||||
t0 = time.perf_counter()
|
||||
result = backend.transcribe(model, Path(audio_path), "ru")
|
||||
print(f"ASR: {time.perf_counter() - t0:.0f} с, {len(result.segments)} сегм.")
|
||||
return result
|
||||
|
||||
|
||||
def run_diar(samples, threshold: float, threads: int):
|
||||
diarizer = make_diarizer(threshold=threshold, threads=threads)
|
||||
t0 = time.perf_counter()
|
||||
segments = diarizer.process(samples).sort_by_start_time()
|
||||
print(f"диаризация: {time.perf_counter() - t0:.0f} с, {len(segments)} интервалов")
|
||||
return [(s.start, s.end, s.speaker) for s in segments]
|
||||
|
||||
|
||||
def main(audio_path: str, threshold: float, threads: int) -> None:
|
||||
samples = load_audio(audio_path)
|
||||
asr = run_asr(audio_path)
|
||||
diar = run_diar(samples, threshold, threads)
|
||||
print(f"речи по диаризации: {sum(e - s for s, e, _ in diar) / 60:.1f} мин\n")
|
||||
|
||||
rows = []
|
||||
for seg in asr.segments:
|
||||
per_speaker: dict[int, float] = defaultdict(float)
|
||||
for start, end, speaker in diar:
|
||||
overlap = min(seg.end, end) - max(seg.start, start)
|
||||
if overlap > 0:
|
||||
per_speaker[speaker] += overlap
|
||||
total = sum(per_speaker.values())
|
||||
if total <= 0:
|
||||
rows.append((seg, None, 0.0, 0.0, {}))
|
||||
continue
|
||||
major = max(per_speaker, key=lambda k: per_speaker[k])
|
||||
rows.append(
|
||||
(seg, major, per_speaker[major] / total, total - per_speaker[major], dict(per_speaker))
|
||||
)
|
||||
|
||||
n = len(rows)
|
||||
unattributed = [r for r in rows if r[1] is None]
|
||||
attributed = [r for r in rows if r[1] is not None]
|
||||
lost = [r for r in attributed if r[3] >= INTERJECTION_S]
|
||||
interjection = [r for r in attributed if 0 < r[3] < INTERJECTION_S]
|
||||
clean = [r for r in attributed if r[3] == 0]
|
||||
|
||||
def minutes(rs) -> float:
|
||||
return sum(r[0].end - r[0].start for r in rs) / 60
|
||||
|
||||
print("=" * 64)
|
||||
print(f"ASR-сегментов: {n} ({minutes(rows):.1f} мин)\n")
|
||||
for label, group in (
|
||||
("чистых (один говорящий)", clean),
|
||||
(f"с поддакиванием (<{INTERJECTION_S:.0f} с чужой)", interjection),
|
||||
(f"с чужой репликой (>={INTERJECTION_S:.0f} с)", lost),
|
||||
("без говорящего вообще", unattributed),
|
||||
):
|
||||
print(f" {label:<34} {len(group):4d} {len(group) / n * 100:5.1f}% {minutes(group):5.1f} мин")
|
||||
|
||||
print()
|
||||
for level in PURITY_LEVELS:
|
||||
bad = [r for r in attributed if r[2] < level]
|
||||
print(
|
||||
f" чистота мажоритарного < {level:.2f}: {len(bad):4d} сегм. "
|
||||
f"({len(bad) / n * 100:.1f}%), {minutes(bad):.1f} мин"
|
||||
)
|
||||
|
||||
print("\n" + "=" * 64)
|
||||
print("ХУДШИЕ 12 СЕГМЕНТОВ (больше всего чужой речи внутри):")
|
||||
for seg, major, purity, others, per_speaker in sorted(attributed, key=lambda r: -r[3])[:12]:
|
||||
share = ", ".join(
|
||||
f"spk{k}={v:.1f}с" for k, v in sorted(per_speaker.items(), key=lambda x: -x[1])
|
||||
)
|
||||
print(
|
||||
f"\n [{seg.start:7.1f}-{seg.end:7.1f}] ({seg.end - seg.start:4.1f} с) "
|
||||
f"мажор spk{major}, чистота {purity:.2f}, чужой {others:.1f} с"
|
||||
)
|
||||
print(f" {share}")
|
||||
print(f" «{seg.text.strip()[:150]}»")
|
||||
|
||||
out = HERE / f"conflict-{Path(audio_path).stem[:40]}.json"
|
||||
out.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"file": Path(audio_path).name,
|
||||
"threshold": threshold,
|
||||
"asr_segments": n,
|
||||
"clean": len(clean),
|
||||
"interjection": len(interjection),
|
||||
"lost_utterance": len(lost),
|
||||
"unattributed": len(unattributed),
|
||||
"minutes_lost_utterance": round(minutes(lost), 2),
|
||||
"minutes_total": round(minutes(rows), 2),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"\nсводка сохранена: {out.name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
raise SystemExit(__doc__)
|
||||
main(
|
||||
sys.argv[1],
|
||||
float(sys.argv[2]) if len(sys.argv) > 2 else DISCOVERY_THRESHOLD,
|
||||
int(sys.argv[3]) if len(sys.argv) > 3 else DEFAULT_THREADS,
|
||||
)
|
||||
Reference in New Issue
Block a user