Compare commits
5
Commits
3378aac474
...
7ba4c6bd27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ba4c6bd27 | ||
|
|
1cb6a36c92 | ||
|
|
87030e9718 | ||
|
|
a65cb74d88 | ||
|
|
7e348e400f |
@@ -6,4 +6,3 @@ dist/
|
||||
*.pyc
|
||||
.codex
|
||||
.qwen/
|
||||
.scratch/
|
||||
@@ -0,0 +1,6 @@
|
||||
# модели диаризации — 33 МБ, скачиваются по README
|
||||
models/
|
||||
|
||||
# выход замеров
|
||||
segments-*.tsv
|
||||
conflict-*.json
|
||||
@@ -0,0 +1,63 @@
|
||||
# Обвязка замеров диаризации
|
||||
|
||||
Исследовательские скрипты для карты
|
||||
[Карта: диаризация спикеров в транскрипте](https://git.dementev.space/ddmitry/local-transcriber/issues/8) (#8).
|
||||
Не часть пакета: они опираются на `sherpa-onnx`, которого нет в зависимостях
|
||||
проекта, и живут в `.scratch/`, а не в `src/`.
|
||||
|
||||
Результаты первого прогона описаны в
|
||||
[разведочном замере](../../docs/benchmarks/2026-08-12-diarization-feasibility.md).
|
||||
|
||||
## Модели
|
||||
|
||||
Скачиваются один раз в `models/`, в git не попадают (см. `.gitignore` рядом).
|
||||
|
||||
```bash
|
||||
mkdir -p models && cd models
|
||||
curl -sSL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-segmentation-models/sherpa-onnx-pyannote-segmentation-3-0.tar.bz2
|
||||
tar xjf sherpa-onnx-pyannote-segmentation-3-0.tar.bz2
|
||||
curl -sSL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/wespeaker_en_voxceleb_resnet34_LM.onnx
|
||||
```
|
||||
|
||||
Сегментация — 6,9 МБ, эмбеддинги — 26,5 МБ. Опечатка `recongition` в URL
|
||||
относится к самому релизу sherpa-onnx, это не ошибка набора.
|
||||
|
||||
## Скрипты
|
||||
|
||||
| Скрипт | Что делает | Тикеты |
|
||||
|---|---|---|
|
||||
| `bench_asr.py` | ASR тем же путём, что CLI: время, RTF, память | #13 |
|
||||
| `bench_diar.py` | один прогон диаризации, сохраняет разметку в `segments-<порог>.tsv` | #12, #13 |
|
||||
| `bench_sweep.py` | свип порога кластеризации и явного числа говорящих | #10 |
|
||||
| `bench_conflict.py` | доля ASR-сегментов, внутри которых меняется говорящий | #11 |
|
||||
| `common.py` | пути, конфигурация диаризатора, замер памяти | — |
|
||||
|
||||
## Запуск
|
||||
|
||||
Из корня репозитория. `PYTHONIOENCODING=utf-8` нужен, иначе вывод падает на
|
||||
консоли cp1251.
|
||||
|
||||
```bash
|
||||
export PYTHONIOENCODING=utf-8
|
||||
|
||||
uv run python .scratch/diarization/bench_asr.py "<путь к записи>"
|
||||
uv run --with sherpa-onnx python .scratch/diarization/bench_diar.py "<путь>" 8 0.9
|
||||
uv run --with sherpa-onnx python .scratch/diarization/bench_sweep.py "<путь>"
|
||||
uv run --with sherpa-onnx python .scratch/diarization/bench_conflict.py "<путь>"
|
||||
```
|
||||
|
||||
## Что стоит знать до запуска
|
||||
|
||||
- **Порог кластеризации не откалиброван.** По умолчанию стоит 0,9 — значение из
|
||||
разведки, подобранное на одной записи и на ней же проверенное. На пороге 0,5
|
||||
из примеров sherpa-onnx получалось 29 говорящих вместо трёх. Калибровка — это
|
||||
тикет #10, до его закрытия любое значение считается временным.
|
||||
- **Свип дорогой.** Каждая конфигурация — полный прогон сегментации и
|
||||
эмбеддингов, около 2,5 минут на 26-минутную запись, и время от настроек
|
||||
кластеризации практически не зависит. Свип вести на коротком фрагменте.
|
||||
- **Чистота сегментов меряется относительно диаризации.** Если её границы
|
||||
систематически смещены, метрика измеряет не то, что кажется. Проверка границ
|
||||
на слух — тикет #12, и он намеренно идёт до калибровки.
|
||||
- **Замер памяти чинился.** В разведке `psapi.GetProcessMemoryInfo` молча
|
||||
возвращал ноль; `common.peak_rss_mb()` теперь зовёт `K32GetProcessMemoryInfo`
|
||||
из kernel32 и проверяет код возврата.
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Замер ASR тем же путём, что использует CLI — для соотношения с диаризацией.
|
||||
|
||||
uv run python .scratch/diarization/bench_asr.py <файл> [модель]
|
||||
|
||||
sherpa-onnx здесь не нужен: скрипт зовёт бэкенд проекта напрямую.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from common import peak_rss_mb, use_project_sources
|
||||
|
||||
use_project_sources()
|
||||
|
||||
from local_transcriber.backends.onnx_asr import OnnxAsrBackend # noqa: E402
|
||||
|
||||
DEFAULT_MODEL = "gigaam-v3-e2e-rnnt"
|
||||
COMPUTE_TYPE = "int8"
|
||||
|
||||
|
||||
def main(audio_path: str, model_name: str) -> None:
|
||||
backend = OnnxAsrBackend(compute_type_explicit=False)
|
||||
|
||||
t0 = time.perf_counter()
|
||||
model_path = backend.ensure_model_available(model_name, COMPUTE_TYPE)
|
||||
model = backend.create_model(model_path, "onnx", COMPUTE_TYPE)
|
||||
t_load = time.perf_counter() - t0
|
||||
|
||||
t0 = time.perf_counter()
|
||||
result = backend.transcribe(model, Path(audio_path), "ru")
|
||||
t_asr = time.perf_counter() - t0
|
||||
rss = peak_rss_mb()
|
||||
|
||||
print(f"файл: {audio_path}")
|
||||
print(f"модель: {model_name} ({COMPUTE_TYPE})")
|
||||
print(f"длительность: {result.duration / 60:.1f} мин")
|
||||
print(f"загрузка модели: {t_load:.1f} с")
|
||||
print(f"ASR: {t_asr:.1f} с -> {result.duration / t_asr:.1f}x RTF")
|
||||
print(f"пиковая память процесса: {rss:.0f} МБ" if rss else "память: снять не удалось")
|
||||
print(f"сегментов: {len(result.segments)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
raise SystemExit(__doc__)
|
||||
main(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else DEFAULT_MODEL)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Свип порога кластеризации и явного числа говорящих.
|
||||
|
||||
uv run --with sherpa-onnx python .scratch/diarization/bench_sweep.py <файл> [потоки]
|
||||
|
||||
Каждая конфигурация — полный прогон сегментации и эмбеддингов (около 2,5 минут
|
||||
на 26-минутную запись), поэтому свип имеет смысл вести на коротком фрагменте, а
|
||||
полные записи оставить для проверки финального кандидата.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
from common import DEFAULT_THREADS, SAMPLE_RATE, load_audio, make_diarizer
|
||||
|
||||
# подпись, num_clusters, threshold
|
||||
CONFIGS = [
|
||||
("авто, порог 0.5", -1, 0.5),
|
||||
("авто, порог 0.7", -1, 0.7),
|
||||
("авто, порог 0.9", -1, 0.9),
|
||||
("явно k=5", 5, 0.5),
|
||||
]
|
||||
|
||||
# говорящий с речью короче порога считается остаточным кластером, не участником
|
||||
MIN_SPEAKER_S = 30.0
|
||||
|
||||
|
||||
def main(audio_path: str, threads: int) -> None:
|
||||
samples = load_audio(audio_path)
|
||||
duration = len(samples) / SAMPLE_RATE
|
||||
print(f"файл: {audio_path}")
|
||||
print(f"длительность: {duration / 60:.1f} мин, потоков: {threads}\n")
|
||||
|
||||
for label, num_clusters, threshold in CONFIGS:
|
||||
diarizer = make_diarizer(
|
||||
threshold=threshold, num_clusters=num_clusters, threads=threads
|
||||
)
|
||||
t0 = time.perf_counter()
|
||||
segments = diarizer.process(samples).sort_by_start_time()
|
||||
elapsed = time.perf_counter() - t0
|
||||
|
||||
totals: dict[int, float] = {}
|
||||
for seg in segments:
|
||||
totals[seg.speaker] = totals.get(seg.speaker, 0.0) + (seg.end - seg.start)
|
||||
real = [spk for spk, t in totals.items() if t >= MIN_SPEAKER_S]
|
||||
top = sorted(totals.values(), reverse=True)[:8]
|
||||
|
||||
print(f"--- {label}")
|
||||
print(
|
||||
f" {elapsed:.0f} с ({duration / elapsed:.1f}x RTF), "
|
||||
f"говорящих: {len(totals)}, из них >= {MIN_SPEAKER_S:.0f} с речи: {len(real)}, "
|
||||
f"интервалов: {len(segments)}"
|
||||
)
|
||||
print(" топ по времени (мин): " + ", ".join(f"{t / 60:.1f}" for t in top))
|
||||
print()
|
||||
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Общая обвязка для замеров диаризации.
|
||||
|
||||
Скрипты в этом каталоге — исследовательские, не часть пакета. Они опираются на
|
||||
``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
|
||||
@@ -64,3 +64,55 @@ tea labels list --remote origin
|
||||
|
||||
За пределами рабочего дерева явно указывать репозиторий
|
||||
`ddmitry/local-transcriber` и настроенный Gitea login.
|
||||
|
||||
## Wayfinding operations
|
||||
|
||||
Навык `wayfinder` ведёт карту как issue с меткой `wayfinder:map`, а её тикеты —
|
||||
как отдельные issue с метками `wayfinder:research`, `wayfinder:prototype`,
|
||||
`wayfinder:grilling` и `wayfinder:task`.
|
||||
|
||||
### Принадлежность карте
|
||||
|
||||
Gitea 1.27 не имеет подзадач в API: среди эндпоинтов `issues/{index}` есть
|
||||
`dependencies` и `blocks`, но родительских связей нет. Поэтому принадлежность
|
||||
тикета карте выражается двумя способами сразу: меткой `wayfinder:<тип>` и первой
|
||||
строкой тела со ссылкой на карту.
|
||||
|
||||
```markdown
|
||||
Часть карты: [<заголовок карты>](<url>) (#<номер>)
|
||||
```
|
||||
|
||||
### Блокировки
|
||||
|
||||
Блокировки — нативные зависимости Gitea, они отображаются в интерфейсе. Тикет
|
||||
разблокирован, когда закрыты все блокирующие его тикеты.
|
||||
|
||||
```powershell
|
||||
tea api --remote origin -X POST `
|
||||
repos/ddmitry/local-transcriber/issues/<блокируемый>/dependencies `
|
||||
-d '{"index": <блокирующий>, "owner": "ddmitry", "repo": "local-transcriber"}'
|
||||
```
|
||||
|
||||
### Запросы фронтира
|
||||
|
||||
Фронтир — открытые, разблокированные и никому не назначенные тикеты карты.
|
||||
Заявка на тикет — назначение его на себя до начала работы.
|
||||
|
||||
```powershell
|
||||
tea issues list --remote origin --labels wayfinder:map
|
||||
tea issues list --remote origin --labels wayfinder:research,wayfinder:prototype,wayfinder:grilling,wayfinder:task
|
||||
tea api --remote origin repos/ddmitry/local-transcriber/issues/<номер>/dependencies
|
||||
```
|
||||
|
||||
### Особенности `tea api`
|
||||
|
||||
Три вещи, на которых легко потерять время:
|
||||
|
||||
- **Путь без ведущего слэша.** `repos/{owner}/{repo}/...` работает,
|
||||
`/repos/...` возвращает `404 page not found`. Подстановка `{owner}` и `{repo}`
|
||||
из контекста репозитория при этом не срабатывает — писать владельца и имя явно.
|
||||
- **Тело зависимости требует `owner` и `repo`.** Только `{"index": N}` даёт
|
||||
`repository does not exist [id: 0, uid: 0, owner_name: , name: ]`.
|
||||
- **Код возврата не отражает HTTP-статус.** `tea api` завершается с нулевым
|
||||
кодом даже на 404, поэтому скрипты должны запрашивать `-i` и разбирать строку
|
||||
`HTTP/...` из stderr, иначе ошибки пройдут незамеченными.
|
||||
|
||||
+43
-14
@@ -181,27 +181,56 @@ LLM для чистки текста, сопоставление Speaker N с и
|
||||
|
||||
### Диаризация — разделение говорящих
|
||||
|
||||
**Что:** Опциональный пост-процессинг (не четвёртый бэкенд): сегменты
|
||||
уже несут таймкоды; диаризация даёт интервалы «кто когда говорил»;
|
||||
merge по перекрытию интервалов; formatter ломает абзац на смене спикера
|
||||
и подписывает `Speaker 1:`. Ставится как extra:
|
||||
`uv sync --extra diarization`.
|
||||
**Что:** Опциональный пост-процессинг (не четвёртый бэкенд): диаризация
|
||||
даёт интервалы «кто когда говорил», результат сводится с сегментами ASR,
|
||||
formatter ломает абзац на смене спикера и подписывает `Speaker 1:`.
|
||||
Ставится как extra: `uv sync --extra diarization`.
|
||||
|
||||
**Почему:** Без спикеров MoM не собрать — это ключевой разрыв с облаком
|
||||
по внешнему ревью, и никакое качество распознавания его не компенсирует.
|
||||
Заодно естественно решает «разбивку на реплики» (приоритет №4).
|
||||
|
||||
**Варианты реализации (ключевое решение, нужен ADR):**
|
||||
**Промежуточный статус 2026-08-12:** проведена разведка, описанная в
|
||||
[разведочном замере диаризации](benchmarks/2026-08-12-diarization-feasibility.md).
|
||||
Она закрыла вопрос о движке и открыла более важный вопрос о единице привязки.
|
||||
|
||||
- **sherpa-onnx** — диаризация целиком на onnxruntime (сегментация
|
||||
pyannote в ONNX + спикер-эмбеддинги), без torch, в духе нашего
|
||||
onnx-стека и «no cloud, no API keys».
|
||||
- **pyannote.audio** — стандарт качества, но тянет torch и требует
|
||||
HF-токен с принятием лицензии моделей — трение с духом проекта.
|
||||
**Движок — вопрос практически закрыт.** `sherpa-onnx` ставится на Windows с
|
||||
Python 3.13, содержит готовый `OfflineSpeakerDiarization`, не тянет torch и не
|
||||
требует токена Hugging Face; модели сегментации и эмбеддингов весят около 33 МБ.
|
||||
Скорость — 11,1× RTFx, то есть примерно полторы длительности ASR. Вариант
|
||||
`pyannote.audio` остаётся отклонённым по прежней причине: torch и HF-токен с
|
||||
принятием лицензии. Отдельный ADR имеет смысл заводить вместе с решением о
|
||||
единице привязки, а не только про движок.
|
||||
|
||||
**Уточнить перед запуском:** качество обоих вариантов на русской речи
|
||||
и перекрывающихся репликах; скорость на CPU (диаризация — второй проход
|
||||
по всему аудио); лицензии моделей сегментации/эмбеддингов.
|
||||
Замечание для будущих заходов: обе ML-части диаризации уже лежат в
|
||||
`onnx-asr` 0.12 — `PyAnnoteVad` содержит полную локальную сегментацию pyannote
|
||||
(powerset на трёх спикеров, склейка окон), а `WespeakerEmbeddings` даёт
|
||||
эмбеддинги. Публичный API схлопывает сегментацию до речь/не-речь, `load_se` не
|
||||
экспортирован, кластеризации нет. Собирать диаризацию самим на этих деталях —
|
||||
экономия 33 МБ ценой опоры на приватный API; при разведке этот путь не
|
||||
выбирался.
|
||||
|
||||
**Единица привязки — настоящая развилка, решения нет.** Схема «мажоритарный
|
||||
спикер на весь ASR-сегмент», записанная здесь раньше, замером не подтвердилась:
|
||||
27% сегментов содержат не менее секунды чужой речи, и на них приходится больше
|
||||
половины времени транскрипта. Причина — границы сегментов идут по тишине
|
||||
(Silero VAD), а в ВКС собеседники отвечают встык. Варианты:
|
||||
|
||||
- **пословная привязка** — `onnx-asr` отдаёт потокенные таймкоды
|
||||
(`TimestampedResult`), сегмент режется на границе токена при смене
|
||||
говорящего; ASR по-прежнему видит длинное аудио, контекст RNN-T и пунктуация
|
||||
не страдают. Недоступно на OpenVINO GenAI — там потокенных таймкодов нет;
|
||||
- **диаризация первым проходом**, ASR по интервалам говорящего — чистота
|
||||
гарантирована, но короткие куски лишают RNN-T контекста и портят пунктуацию;
|
||||
- **привязка к сегменту с честной пометкой** — оставить огрубление, но считать
|
||||
чистоту и предупреждать в шапке, как уже делается для повторов и потери
|
||||
хвоста.
|
||||
|
||||
**Уточнить перед запуском:** воспроизводится ли доля 27% на других записях,
|
||||
включая разговор на двоих; правильность границ диаризации на слух, а не только
|
||||
совпадение числа говорящих; калибровка порога кластеризации (на пороге из
|
||||
примеров получилось 29 спикеров вместо трёх); эмбеддинги, обученные не только
|
||||
на английском; производительность на целевом Intel Core i5.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
# Разведочный замер диаризации sherpa-onnx
|
||||
|
||||
**Дата:** 2026-08-12
|
||||
|
||||
**Статус:** разведка на одной записи и одной нецелевой машине. Не приёмка.
|
||||
|
||||
## Цель
|
||||
|
||||
Ответить на два вопроса перед проектированием диаризации:
|
||||
|
||||
1. Сколько времени диаризация добавляет к транскрипции.
|
||||
2. Достаточно ли приписывать спикера целому ASR-сегменту по мажоритарному
|
||||
перекрытию — то есть допустима ли схема из
|
||||
[бэклога](../backlog.md#диаризация--разделение-говорящих) без пословной
|
||||
привязки.
|
||||
|
||||
Ни модель по умолчанию, ни код проекта в рамках замера не менялись. Все скрипты
|
||||
выполнялись вне репозитория.
|
||||
|
||||
## Ограничения замера
|
||||
|
||||
Результаты ниже — разведка, а не основание для решения:
|
||||
|
||||
- **одна запись** вместо трёх, принятых в [ADR-006](../adr/006-onnx-asr-backend.md);
|
||||
- **нецелевая машина**: AMD Ryzen 7 8845H, тогда как приёмка производительности
|
||||
по бэклогу требует Intel Core i5 11-го поколения;
|
||||
- **границы диаризации не проверены на слух** — сверялось только число
|
||||
говорящих и косвенный текстовый признак;
|
||||
- **порог кластеризации подобран по этой же записи**, то есть на ней же и
|
||||
проверен.
|
||||
|
||||
## Оборудование и условия
|
||||
|
||||
- ноутбук Lenovo 83D5 (та же машина, что в
|
||||
[сравнении turbo](2026-08-12-openvino-large-v3-turbo-comparison.md#повторный-прогон-на-amd-ryzen-7-8845h));
|
||||
- AMD Ryzen 7 8845H, 8 ядер / 16 логических процессоров;
|
||||
- 29,8 ГиБ LPDDR5X;
|
||||
- Windows 11 Корпоративная, сборка 26200, схема питания «Сбалансированная»;
|
||||
- Python 3.13.13, `onnx-asr` 0.12.0, `onnxruntime` 1.28.0,
|
||||
`faster-whisper` 1.2.1, `sherpa-onnx` 1.13.5;
|
||||
- прогоны последовательные, без конкурирующей нагрузки;
|
||||
- модели предварительно скачаны; время загрузки моделей в замер не входит.
|
||||
|
||||
## Контрольная запись
|
||||
|
||||
| Файл | Длительность | Размер | SHA-256 |
|
||||
|---|---|---|---|
|
||||
| `2026-07-10 Data Test внутренний статус.mp4` | 26:00 | 34 217 769 | `1057616B42E8ADD00E0EB975B02BDEF0EC9F6CDFEC6DBF488E0C60423C9B7B87` |
|
||||
|
||||
Запись выбрана потому, что рядом лежит согласованный MoM, из которого известен
|
||||
состав: **три участника** — Маша, Дима, Роман. Это даёт независимую опорную
|
||||
точку для проверки числа говорящих.
|
||||
|
||||
## Модели диаризации
|
||||
|
||||
| Роль | Модель | Размер | Источник |
|
||||
|---|---|---|---|
|
||||
| Сегментация | `sherpa-onnx-pyannote-segmentation-3-0` | 6,9 МБ | релизы `k2-fsa/sherpa-onnx` |
|
||||
| Эмбеддинги | `wespeaker_en_voxceleb_resnet34_LM.onnx` | 26,5 МБ | релизы `k2-fsa/sherpa-onnx` |
|
||||
|
||||
Обе загружаются в `onnxruntime`, torch и токен Hugging Face не требуются.
|
||||
Эмбеддинги обучены на англоязычном VoxCeleb; их пригодность для русской речи в
|
||||
этом замере не проверялась.
|
||||
|
||||
## Стоимость по времени
|
||||
|
||||
Параметры ASR — модель по умолчанию ONNX-пути, `gigaam-v3-e2e-rnnt` INT8,
|
||||
язык задан явно.
|
||||
|
||||
| Стадия | Время | RTFx |
|
||||
|---|---:|---:|
|
||||
| Декодирование аудио | 1,6 с | ~990× |
|
||||
| ASR `gigaam-v3-e2e-rnnt` INT8 | 95,3 с | 16,4× |
|
||||
| Диаризация, 8 потоков | 140,7 с | 11,1× |
|
||||
| **Последовательно, итого** | **236 с** | **6,6×** |
|
||||
|
||||
Диаризация дороже самой транскрипции и занимает около 60% общего времени. При
|
||||
последовательном исполнении 26-минутная запись обрабатывается 3,9 минуты вместо
|
||||
1,6; часовая — примерно 9 минут вместо 3,7.
|
||||
|
||||
Масштабирование по потокам слабое: 4 потока дают 154 с, 8 потоков — 141 с,
|
||||
выигрыш 9%. Закладываться на увеличение числа потоков не следует.
|
||||
|
||||
Проходы ASR и диаризации независимы по данным, поэтому их можно совместить во
|
||||
времени; тогда общее время стремится к максимуму из двух, а не к сумме. Прямой
|
||||
замер параллельного режима не проводился.
|
||||
|
||||
Пиковую память процесса снять не удалось из-за ошибки в измерительном скрипте.
|
||||
|
||||
## Порог кластеризации
|
||||
|
||||
Число говорящих подбиралось автоматически (`num_clusters=-1`); варьировался
|
||||
порог `FastClusteringConfig.threshold`.
|
||||
|
||||
| Конфигурация | Найдено спикеров | Из них с речью ≥30 с | Речь по спикерам, мин |
|
||||
|---|---:|---:|---|
|
||||
| порог 0,5 | 29 | 11 | 7,4 / 5,5 / 1,6 / 1,2 |
|
||||
| порог 0,7 | 12 | 7 | 7,4 / 7,0 / 2,2 / 2,2 |
|
||||
| **порог 0,9** | **4** | **3** | **9,6 / 8,1 / 5,7 / 0,3** |
|
||||
| явное `k=5`, порог 0,5 | 4 | 3 | 9,6 / 8,1 / 5,7 / 0,3 |
|
||||
|
||||
На пороге 0,9 число содержательных кластеров совпало с составом из MoM: три
|
||||
говорящих с 9,6, 8,1 и 5,7 минуты речи плюс остаточный кластер на 0,3 минуты.
|
||||
На пороге 0,5 получилось 29 говорящих вместо трёх — десятикратное
|
||||
переразбиение.
|
||||
|
||||
Время от порога не зависит (153–157 с во всех конфигурациях): кластеризация
|
||||
стоит доли секунды, платится за сегментацию и эмбеддинги.
|
||||
|
||||
Два следствия. Первое: порог кластеризации — основная ручка качества, и
|
||||
значение по умолчанию из примеров `sherpa-onnx` для этого материала непригодно.
|
||||
Второе: одного удачного совпадения на одной записи недостаточно, чтобы принять
|
||||
0,9 за дефолт, а явное указание числа участников нужно как страховка.
|
||||
|
||||
## Чистота ASR-сегментов
|
||||
|
||||
Основной вопрос замера. Для каждого из 274 ASR-сегментов посчитано перекрытие
|
||||
с интервалами каждого говорящего (диаризация на пороге 0,9, 340 интервалов).
|
||||
Чистота — доля мажоритарного говорящего в суммарном перекрытии сегмента.
|
||||
|
||||
| Категория | Сегментов | Доля | Времени |
|
||||
|---|---:|---:|---:|
|
||||
| Чистых, один говорящий | 164 | 59,9% | 8,3 мин |
|
||||
| С поддакиванием, <1 с чужой речи | 30 | 10,9% | 2,4 мин |
|
||||
| **С чужой репликой, ≥1 с** | **74** | **27,0%** | **11,2 мин** |
|
||||
| Без спикера вообще | 6 | 2,2% | 0,0 мин |
|
||||
|
||||
| Порог чистоты | Сегментов ниже порога | Доля | Времени |
|
||||
|---|---:|---:|---:|
|
||||
| < 0,95 | 100 | 36,5% | 13,0 мин |
|
||||
| < 0,90 | 91 | 33,2% | 11,6 мин |
|
||||
| < 0,80 | 72 | 26,3% | 9,1 мин |
|
||||
| < 0,70 | 56 | 20,4% | 7,1 мин |
|
||||
|
||||
**27% сегментов содержат не менее секунды чужой речи, и на них приходится
|
||||
11,2 минуты из 21,9 — больше половины транскрипта.** У 20% сегментов
|
||||
мажоритарный говорящий занимает менее двух третей сегмента.
|
||||
|
||||
### Подтверждение из текста ASR
|
||||
|
||||
Загрязнённые сегменты содержат диалог, и это видно независимо от диаризации —
|
||||
`gigaam-v3-e2e` обучена с диалоговой пунктуацией и сама ставит тире на смене
|
||||
реплики:
|
||||
|
||||
```
|
||||
[533,8-551,1] 17,3 с, мажоритарный spk3, чистота 0,67
|
||||
«— В нашем, по-моему.— В нашем?— В нашем.— А, отлично.— Ну, у нас просто
|
||||
есть некий дата-тест, который на самом деле...— Мы же у них не
|
||||
разворачиваем.—»
|
||||
|
||||
[432,9-448,6] 15,7 с, мажоритарный spk2, чистота 0,58
|
||||
«Дим, а вот то, что ты из образа вытаскивал, там есть чё-то на что
|
||||
посмотреть?— Бэг, бэг там есть, пи»
|
||||
|
||||
[694,2-706,5] 12,3 с, мажоритарный spk2, чистота 0,37
|
||||
spk2 = 5,4 с, spk1 = 4,9 с, spk3 = 4,3 с — три человека в одном сегменте
|
||||
```
|
||||
|
||||
Акустическая диаризация и пунктуация ASR указывают на одно и то же, поэтому
|
||||
доля 27% вряд ли объясняется только ошибками кластеризации.
|
||||
|
||||
Причина загрязнения — в способе нарезки: границы сегментов на ONNX-пути даёт
|
||||
Silero VAD, то есть они проходят по тишине. В разговоре по ВКС участники
|
||||
отвечают встык, паузы длиной с порог VAD не возникает, и диалог попадает в один
|
||||
сегмент. Предположение о том, что задержка канала связи сама создаёт паузу на
|
||||
смене говорящего, этими данными не подтверждается.
|
||||
|
||||
Приведённые сегменты — сырой выход ASR. `formatter.py` объединяет их в абзацы
|
||||
длиной до 60 секунд, поэтому в готовом транскрипте загрязнение будет выше.
|
||||
|
||||
## Выводы
|
||||
|
||||
- Диаризация через `sherpa-onnx` работает на целевой платформе без torch и без
|
||||
токена Hugging Face; связка сегментация + эмбеддинги весит около 33 МБ.
|
||||
- Стоимость — 11,1× RTFx, примерно 1,5 длительности ASR. Последовательный
|
||||
запуск даёт 2,5-кратное замедление, совмещение проходов может сократить
|
||||
накладные расходы, но отдельно не измерялось.
|
||||
- Автоматическая оценка числа говорящих с порогом из примеров даёт 29 спикеров
|
||||
вместо трёх. Порог требует калибровки, а явное указание числа участников —
|
||||
отдельной ручки.
|
||||
- Привязка спикера к целому ASR-сегменту по мажоритарному перекрытию
|
||||
огрубляет результат существенно: 27% сегментов и больше половины времени
|
||||
транскрипта содержат чужую речь длиннее секунды. Схема из бэклога в этом виде
|
||||
непригодна.
|
||||
- `onnx-asr` отдаёт потокенные таймкоды (`TimestampedResult`), поэтому
|
||||
пословная привязка на ONNX-пути достижима. У OpenVINO GenAI такого выхода
|
||||
нет, что ограничивает диаризацию на `--device openvino-*`.
|
||||
|
||||
## Что нужно проверить дальше
|
||||
|
||||
- Повторить измерение чистоты сегментов ещё на двух-трёх записях, включая
|
||||
разговор на двоих, и убедиться, что 27% — не свойство именно этой встречи.
|
||||
- Проверить границы диаризации на слух или сверкой с внешним транскриптом:
|
||||
совпадение числа говорящих не доказывает правильность интервалов.
|
||||
- Сравнить эмбеддинги, обученные не только на английском, на русской речи.
|
||||
- Измерить производительность на целевом Intel Core i5 11-го поколения.
|
||||
- Измерить параллельный режим ASR и диаризации.
|
||||
@@ -0,0 +1,289 @@
|
||||
# Куда движутся ONNX Runtime и OpenVINO: сравнение жизненного цикла
|
||||
|
||||
**Дата:** 2026-08-12
|
||||
|
||||
**Статус:** исследование для карты диаризации. Не архитектурное решение и не
|
||||
основание для консолидации всех движков распознавания на ONNX Runtime.
|
||||
|
||||
## Вопрос
|
||||
|
||||
Насколько устойчивы ONNX Runtime, его DirectML и OpenVINO Execution Provider,
|
||||
а также нативный стек OpenVINO/OpenVINO GenAI; какой из путей с большей
|
||||
вероятностью сохранит Intel-ускорение и поддержку Whisper/NPU; что из этого
|
||||
практически доступно проекту на CPython 3.13.
|
||||
|
||||
Исследование опирается только на первичные источники: официальную документацию,
|
||||
release notes, репозитории владельцев и метаданные PyPI.
|
||||
|
||||
## Границы в контексте проекта
|
||||
|
||||
Терминология следует [`CONTEXT.md`](../../CONTEXT.md): ONNX Runtime, OpenVINO и
|
||||
OpenVINO GenAI здесь — **движки распознавания**, их обновление само по себе не
|
||||
меняет поддерживаемую модель или модель по умолчанию. Архитектурная точка
|
||||
отсчёта — отдельные pluggable backends из [ADR-003](../adr/003-pluggable-backends.md)
|
||||
и принятый ONNX CPU-путь из [ADR-006](../adr/006-onnx-asr-backend.md).
|
||||
|
||||
Исследование дополняет [срез обновлений движков](2026-08-10-engine-model-updates.md)
|
||||
и отвечает на инфраструктурный вопрос, открытый
|
||||
[разведкой диаризации](../benchmarks/2026-08-12-diarization-feasibility.md) и
|
||||
[бэклогом](../backlog.md#диаризация--разделение-говорящих). Оно не пересматривает
|
||||
качество моделей и не принимает решение о полной консолидации на ORT.
|
||||
|
||||
## Краткий вывод
|
||||
|
||||
1. **ONNX Runtime — активно развиваемый, production-стабильный движок, но не
|
||||
все его EP имеют одинаковый жизненный цикл.** Версии 1.26, 1.27 и 1.28
|
||||
вышли 8 мая, 19 июня и 25 июля 2026 года, то есть три minor-релиза примерно
|
||||
за одиннадцать недель; 1.28 продолжает развивать plugin EP API, ядро,
|
||||
безопасность и аппаратные EP ([1.26.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.26.0),
|
||||
[1.27.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.27.0),
|
||||
[1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)).
|
||||
2. **DirectML EP поддерживается, но переведён в sustained engineering.** Новая
|
||||
функциональность Windows-пути перенесена в WinML; Microsoft рекомендует
|
||||
WinML для новых Windows-развёртываний, а DirectML EP оставляет для legacy и
|
||||
специальных сценариев ([официальная страница DirectML EP](https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html),
|
||||
[Windows-путь ORT](https://onnxruntime.ai/docs/get-started/with-windows.html)).
|
||||
Поэтому `onnxruntime-directml` нельзя считать перспективным
|
||||
кросс-вендорным GPU-дефолтом проекта, хотя пакет не заброшен.
|
||||
3. **OpenVINO и OpenVINO GenAI — основной активно развиваемый Intel-стек.** В
|
||||
2026 году регулярные релизы вышли 23 февраля, 7 апреля, 28 мая и 4 августа;
|
||||
OpenVINO публикует формальную release/LTS policy, где регулярная версия
|
||||
поддерживается до следующей, а последняя версия года становится LTS с двумя
|
||||
годами security updates ([release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html),
|
||||
[release policy](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/release-policy.html)).
|
||||
4. **Для Intel-ускорения более долгоживущая ставка — сам OpenVINO, а не
|
||||
конкретная обвязка ORT OpenVINO EP.** EP остаётся активным мостом из ORT к
|
||||
OpenVINO, но зависит сразу от двух release train и его готовые wheel заметно
|
||||
отстают от обоих ядер. Нативный OpenVINO одновременно является runtime для
|
||||
CPU/GPU/NPU, имеет собственную LTS policy и служит основанием OpenVINO GenAI
|
||||
([OpenVINO EP](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html),
|
||||
[GenAI как расширение runtime](https://docs.openvino.ai/2026/openvino-workflow-generative/inference-with-genai.html)).
|
||||
Это не означает, что проекту нужно переносить ONNX-путь на OpenVINO или
|
||||
консолидироваться на ORT: ONNX-модели сохраняют переносимость, а выбор
|
||||
движка распознавания остаётся отдельным решением по качеству и контракту.
|
||||
5. **CPython 3.13 не блокирует ни один из трёх исследованных PyPI-пакетов на
|
||||
целевой Windows x86-64**, но матрицы платформ радикально различаются:
|
||||
`onnxruntime` кроссплатформенный, DirectML только Windows x86-64,
|
||||
OpenVINO EP только Windows/Linux x86-64
|
||||
([onnxruntime 1.28.0 files](https://pypi.org/project/onnxruntime/1.28.0/#files),
|
||||
[DirectML 1.24.4 files](https://pypi.org/project/onnxruntime-directml/1.24.4/#files),
|
||||
[OpenVINO EP 1.24.1 files](https://pypi.org/project/onnxruntime-openvino/1.24.1/#files)).
|
||||
|
||||
## Что именно является чем
|
||||
|
||||
Слои нельзя сравнивать как взаимозаменяемые пакеты:
|
||||
|
||||
| Слой | Роль | Что фиксирует приложение |
|
||||
|---|---|---|
|
||||
| ONNX | Формат графа, операторов и типов данных; операторы исполняются внешней реализацией | Артефакт модели и его opset ([ONNX About](https://onnx.ai/about)) |
|
||||
| ONNX Runtime | Движок выполнения ONNX-графа, который разбивает его между EP и CPU fallback | API сессии, версия ORT и набор EP ([архитектура ORT](https://onnxruntime.ai/docs/reference/high-level-design.html)) |
|
||||
| OpenVINO | Intel runtime, компилятор и device plugins для CPU/GPU/NPU; умеет принимать в том числе ONNX-графы | API OpenVINO и поддерживаемые устройства/форматы ([поддержанные модели](https://docs.openvino.ai/2026/documentation/compatibility-and-support/supported-models.html)) |
|
||||
| OpenVINO EP | Адаптер внутри ORT: получает поддержанные подграфы, переводит и компилирует их для OpenVINO | Одновременно контракты ORT, EP и совместимой версии OpenVINO ([EP architecture](https://onnxruntime.ai/docs/execution-providers/), [OpenVINO EP](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html)) |
|
||||
| OpenVINO GenAI | Высокоуровневые генеративные pipelines поверх OpenVINO runtime, включая Whisper и общий ASR API | Формат моделей OpenVINO IR, pipeline API и согласованные версии OpenVINO/Tokenizers/GenAI ([GenAI PyPI](https://pypi.org/project/openvino-genai/2026.3.0.0/)) |
|
||||
|
||||
Следствие: **модель в ONNX не означает ONNX Runtime**, а **OpenVINO EP не
|
||||
является форматом модели**. Один ONNX-артефакт можно исполнять CPU EP в ORT,
|
||||
передавать поддержанные подграфы OpenVINO EP либо загружать в OpenVINO
|
||||
напрямую; однако покрытие операторов, квантование, fallback и производительность
|
||||
у этих путей различаются ([ORT EP partitioning](https://onnxruntime.ai/docs/execution-providers/),
|
||||
[OpenVINO EP support coverage](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html),
|
||||
[прямое чтение ONNX в OpenVINO](https://docs.openvino.ai/2026/openvino-workflow/model-preparation/convert-model-onnx.html)).
|
||||
|
||||
## ONNX Runtime и execution providers
|
||||
|
||||
### Ядро ORT
|
||||
|
||||
Официальные страницы расходятся в обещанном cadence: servicing-документ всё
|
||||
ещё говорит о full releases «примерно ежеквартально», тогда как roadmap — о
|
||||
ежемесячных релизах и patch-релизах между ними. Формального LTS/EOL-окна в
|
||||
публичной support policy нет. Поэтому для планирования надёжнее опираться на
|
||||
фактические публикации и backward-compatibility policy, а не превращать
|
||||
текущий почти месячный темп в гарантию
|
||||
([releases and servicing](https://onnxruntime.ai/docs/reference/releases-servicing.html),
|
||||
[roadmap](https://onnxruntime.ai/roadmap),
|
||||
[support policy](https://github.com/microsoft/onnxruntime/blob/main/SUPPORT.md)).
|
||||
|
||||
ORT 1.23 начал переход к независимо подключаемым plugin EP и прямо рекомендует
|
||||
новые EP реализовывать как plugins, а не добавлять внутрь ядра. В 1.24–1.28
|
||||
plugin API последовательно получал prepacking, EP Context, zero-copy I/O,
|
||||
profiling и model packages ([инструкция для нового EP](https://onnxruntime.ai/docs/execution-providers/add-execution-provider.html),
|
||||
[релиз 1.24.1](https://github.com/microsoft/onnxruntime/releases/tag/v1.24.1),
|
||||
[релиз 1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)).
|
||||
Это сильный сигнал продолжения ORT как общего движка, но одновременно сигнал,
|
||||
что жизненный цикл конкретного аппаратного backend всё больше принадлежит его
|
||||
поставщику, а не ядру ORT.
|
||||
|
||||
### DirectML EP
|
||||
|
||||
Официальная формулировка однозначна: DirectML находится в **sustained
|
||||
engineering**, поддержка продолжается, но feature development перешёл в WinML.
|
||||
Документация также фиксирует DirectML 1.15.2 и покрытие только до ONNX opset 20;
|
||||
модели с более высоким требованием официально не поддерживаются
|
||||
([DirectML EP](https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html)).
|
||||
|
||||
Это согласуется с поставкой: последний `onnxruntime-directml` на дату среза —
|
||||
1.24.4 от 17 марта, тогда как ядро ORT уже 1.28.0. При этом ORT 1.28 всё ещё
|
||||
содержит исправление DML readback, то есть sustained engineering означает не
|
||||
«удалён», а «исправления без прежнего темпа новых возможностей»
|
||||
([DirectML на PyPI](https://pypi.org/project/onnxruntime-directml/),
|
||||
[ORT 1.28, DML fix](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)).
|
||||
|
||||
Для проекта DirectML остаётся возможным Windows-only экспериментом на AMD,
|
||||
Intel и NVIDIA GPU, но его стратегический successor — WinML, который требует
|
||||
Windows-специфической интеграции. Это слабее текущего требования ADR-003 о
|
||||
плаггируемых бэкендах и кроссплатформенном ONNX CPU-пути.
|
||||
|
||||
### OpenVINO EP
|
||||
|
||||
Официальная документация не объявляет OpenVINO EP deprecated или maintenance-only.
|
||||
Наоборот, Intel публикует готовые пакеты, принимает issues/PR, заявляет CPU,
|
||||
интегрированные и дискретные GPU и NPU, а ORT 1.26 и 1.28 содержат OpenVINO EP
|
||||
development updates ([страница пакета](https://pypi.org/project/onnxruntime-openvino/),
|
||||
[ORT 1.26](https://github.com/microsoft/onnxruntime/releases/tag/v1.26.0),
|
||||
[ORT 1.28](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)).
|
||||
|
||||
Но готовая поставка имеет свой темп. Последний wheel `onnxruntime-openvino`
|
||||
1.24.1 от 26 февраля 2026 года включает OpenVINO 2025.4.1 на Linux и требует
|
||||
отдельной установки OpenVINO на Windows. Официальная таблица совместимости
|
||||
покрывает только три версии OpenVINO: ORT-EP 1.22/2025.1,
|
||||
1.23/2025.3 и 1.24.1/2025.4.1
|
||||
([PyPI](https://pypi.org/project/onnxruntime-openvino/1.24.1/),
|
||||
[матрица совместимости](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html)).
|
||||
На дату среза нативный OpenVINO уже 2026.3, а ядро ORT — 1.28. Значит, EP
|
||||
активен, но готовый Python-путь не является способом автоматически получить
|
||||
самые новые возможности OpenVINO/NPU.
|
||||
|
||||
Начиная с ORT 1.23 часть старых provider options OpenVINO EP deprecated в
|
||||
пользу `load_config` с нативными OpenVINO properties. Это локальная миграция
|
||||
конфигурации, а не deprecation самого EP
|
||||
([deprecation notice](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html)).
|
||||
|
||||
## OpenVINO, OpenVINO GenAI, Whisper и Intel NPU
|
||||
|
||||
OpenVINO имеет явно описанный цикл: несколько регулярных релизов в год,
|
||||
поддержка каждого до следующего и ежегодный LTS. LTS получает security updates
|
||||
два года либо до двух следующих LTS, а исправления новых bugs — один год;
|
||||
preview-компоненты этой гарантией не покрываются
|
||||
([release policy](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/release-policy.html)).
|
||||
|
||||
OpenVINO GenAI — не конкурирующий runtime, а библиотека pipelines поверх
|
||||
OpenVINO и OpenVINO Tokenizers. Их `major.minor.patch` должны совпадать:
|
||||
разъезд может привести к ABI/import errors; PyPI wheel нельзя смешивать с C++
|
||||
archives другого ABI ([официальные правила совместимости](https://pypi.org/project/openvino-genai/2026.3.0.0/)).
|
||||
|
||||
Whisper — активный, а не legacy use case OpenVINO GenAI:
|
||||
|
||||
- OpenVINO 2026.0 добавил word-level timestamps в WhisperPipeline на CPU, GPU
|
||||
и NPU; в 2026.3 результаты также содержат определённый/заданный язык, а NPU
|
||||
отдаёт word timestamps по умолчанию
|
||||
([2026.0 release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-0-0),
|
||||
[2026.3 release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-3-0));
|
||||
- OpenVINO 2026.3 ввёл общий `ASRPipeline` и поддержку Qwen3-ASR, то есть
|
||||
speech API расширяется за пределы Whisper
|
||||
([2026.3 release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-3-0));
|
||||
- удалён только ранее deprecated **stateless decoder** Whisper; рекомендуемый
|
||||
путь — stateful model, а не отказ от Whisper
|
||||
([deprecation section](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#deprecation-and-support)).
|
||||
|
||||
NPU является первым классом устройств OpenVINO: NPU plugin доступен в
|
||||
дистрибутивах, целевая аппаратная платформа начинается с Intel Core Ultra,
|
||||
Compiler-In-Plugin появился preview в 2026.0 и стал предпочитаемым компилятором
|
||||
в 2026.1. При этом NPU требует отдельный driver, поддерживает только static
|
||||
shapes, а совместимость предкомпилированных blobs между версиями OpenVINO не
|
||||
гарантируется ([NPU device](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/npu-device.html)).
|
||||
WhisperPipeline официально работает на NPU с tiny/base/small/large без
|
||||
специальных ограничений pipeline, но документация рекомендует актуальный NPU
|
||||
driver и даёт workaround для memory failures
|
||||
([Whisper on NPU](https://docs.openvino.ai/2026/openvino-workflow-generative/inference-with-genai/inference-with-genai-on-npu.html#whisper-inference-on-npu)).
|
||||
|
||||
Это не делает NPU актуальным для целевого Intel Core i5 11-го поколения: NPU
|
||||
появился только в Core Ultra. Для текущей целевой машины долгоживущий
|
||||
OpenVINO-путь означает прежде всего CPU/iGPU; NPU — будущий аппаратный профиль,
|
||||
который нужно выбирать явно, тем более что OpenVINO AUTO пока исключает NPU из
|
||||
дефолтного приоритета
|
||||
([NPU hardware](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/npu-device.html),
|
||||
[AUTO priority](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/auto-device-selection.html)).
|
||||
|
||||
## PyPI: версии, платформы и CPython 3.13
|
||||
|
||||
Срез сделан по фактически опубликованным wheel, а не по classifiers страницы.
|
||||
У всех трёх пакетов отсутствует source distribution, поэтому неподдержанная
|
||||
комбинация платформы и Python не сможет штатно собраться через обычный
|
||||
`pip install` без самостоятельной сборки из репозитория.
|
||||
|
||||
| Пакет | Последняя версия на 2026-08-12 | Последняя публикация | wheel для CPython 3.13 | Платформы cp313 |
|
||||
|---|---:|---:|---|---|
|
||||
| `onnxruntime` | 1.28.0 | 2026-07-25 | Да | Windows x86-64 и ARM64; Linux x86-64 и ARM64 (glibc 2.27/2.28+); macOS 14+ ARM64 ([files](https://pypi.org/project/onnxruntime/1.28.0/#files)) |
|
||||
| `onnxruntime-directml` | 1.24.4 | 2026-03-17 | Да | Только Windows x86-64; нет Linux, macOS и Windows ARM64 wheel ([files](https://pypi.org/project/onnxruntime-directml/1.24.4/#files)) |
|
||||
| `onnxruntime-openvino` | 1.24.1 | 2026-02-26 | Да | Windows x86-64 и Linux x86-64 с glibc 2.28+; нет ARM64 и macOS wheel ([files](https://pypi.org/project/onnxruntime-openvino/1.24.1/#files)) |
|
||||
|
||||
Практический вывод для ADR-003 сохраняется: обычный `onnxruntime` остаётся
|
||||
широким zero-config CPU-путём. DirectML и OpenVINO EP нельзя подставить как одну
|
||||
безусловную зависимость на всех платформах; они требуют platform markers и
|
||||
отдельных проверок доступного provider. На Windows OpenVINO EP дополнительно
|
||||
требует совместимый `openvino`, а на Linux wheel уже включает конкретный
|
||||
OpenVINO 2025.4.1
|
||||
([OpenVINO EP installation](https://pypi.org/project/onnxruntime-openvino/1.24.1/)).
|
||||
|
||||
## Что это меняет для карты диаризации
|
||||
|
||||
1. Разведка `sherpa-onnx` на обычном CPU ORT не опирается на затухающий
|
||||
компонент: ядро ORT активно и имеет самый широкий CPython/platform coverage.
|
||||
Это поддерживает текущий вариант диаризации как optional post-processing,
|
||||
но ничего не говорит о качестве DirectML/OpenVINO EP на конкретных двух
|
||||
моделях.
|
||||
2. Формулировка разведочного замера «у OpenVINO GenAI потокенных таймкодов нет»
|
||||
требует уточнения. Upstream с 2026.0 предоставляет word-level timestamps;
|
||||
сейчас их не экспортирует проектный OpenVINO backend. Следовательно,
|
||||
невозможность пословной привязки на `--device openvino-*` — **интеграционный
|
||||
пробел local-transcriber**, а не долгосрочное ограничение движка
|
||||
([OpenVINO 2026.0](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-0-0)).
|
||||
3. Не следует связывать UX диаризации с немедленным выбором EP. Сначала можно
|
||||
определить пользовательский контракт — флаг, `num_speakers`, зависимость,
|
||||
формат и честное поведение при отсутствии word timestamps. Ускорение
|
||||
диаризации через OpenVINO EP или DirectML должно пройти отдельную
|
||||
совместимость и benchmark на обеих моделях `sherpa-onnx`.
|
||||
4. Не следует принимать решение о полной консолидации проекта на ORT. Нативный
|
||||
OpenVINO GenAI развивается как ASR-платформа, в том числе по таймкодам и NPU,
|
||||
а FasterWhisper сохраняет отдельные достоинства CUDA и языкового покрытия,
|
||||
уже зафиксированные ADR-003/006.
|
||||
|
||||
## Рекомендация по жизненному циклу
|
||||
|
||||
Ниже — **интерпретация источников для local-transcriber**, а не опубликованный
|
||||
roadmap Microsoft или Intel. Она исходит из фактов о lifecycle, wheel-матрицах
|
||||
и текущем контракте проекта; реальную пригодность каждого ускорителя должен
|
||||
подтвердить проектный benchmark.
|
||||
|
||||
- Сохранять **ONNX-модели диаризации + обычный CPU ORT** как базовый переносимый
|
||||
путь: это наименее связанный с одним вендором слой и единственная из трёх
|
||||
поставок с wheel на Windows/Linux ARM64 и macOS ARM64.
|
||||
- Рассматривать **OpenVINO EP как опциональное ускорение этих же ONNX-моделей на
|
||||
Intel**, но не обещать его до проверки operator coverage, фактического
|
||||
provider assignment, качества и скорости. Его wheel активен, однако отстаёт
|
||||
от текущих ORT/OpenVINO и требует собственной матрицы версий.
|
||||
- Рассматривать **нативный OpenVINO/OpenVINO GenAI как основной долгосрочный
|
||||
Intel ASR-путь**, особенно для Whisper и будущего NPU. Для пословной
|
||||
диаризации сначала проверить и протянуть уже существующие upstream word
|
||||
timestamps через проектный `Backend` contract.
|
||||
- Не закладывать новый DirectML backend проекта: текущий EP поддерживается, но
|
||||
feature development официально ушёл в WinML. Если кросс-вендорное Windows GPU
|
||||
ускорение станет отдельной целью, исследовать WinML как новый
|
||||
Windows-специфический backend, а не считать `onnxruntime-directml`
|
||||
долгоживущим default.
|
||||
|
||||
## Новые вопросы карты
|
||||
|
||||
- Какой точный контракт word timestamps возвращают `WhisperPipeline` и новый
|
||||
`ASRPipeline` 2026.3, и как без потери совместимости добавить их в проектный
|
||||
`Backend`/`TranscribeResult`?
|
||||
- Дают ли `sherpa-onnx` segmentation и embedding models полный offload в
|
||||
OpenVINO EP, или часть графа уходит в CPU EP; меняются ли границы и
|
||||
эмбеддинги численно?
|
||||
- Есть ли выигрыш OpenVINO EP на целевом Intel Core i5 11-го поколения после
|
||||
учёта второго runtime, загрузки модели и памяти, или CPU ORT уже оптимальнее?
|
||||
- Нужен ли UX явного отказа/огрубления диаризации на backend без word
|
||||
timestamps, либо backend contract должен сначала стать timestamp-aware?
|
||||
- Следует ли разделить extra диаризации на переносимый CPU-вариант и
|
||||
Intel-ускорение с platform marker, чтобы не ухудшить zero-config установку на
|
||||
ARM/macOS?
|
||||
Reference in New Issue
Block a user