5 Commits
Author SHA1 Message Date
Dmitriy Dementiev 7ba4c6bd27 docs(research): исследован жизненный цикл ORT и OpenVINO
- Зачем:
  - нужна фактическая опора для решений карты о диаризации на Intel-пути.
- Что:
  - исследованы жизненные циклы ONNX Runtime, DirectML и OpenVINO.
  - зафиксированы платформы CPython 3.13 и поддержка таймкодов Whisper.
- Проверка:
  - git diff --cached --check.
2026-08-12 15:55:06 +03:00
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
Dmitriy DementievandClaude Opus 5 87030e9718 chore(git): каталог .scratch больше не игнорируется
- Зачем:
  - рабочие материалы заходов (замеры, черновики, обвязка) должны попадать в
    историю вместе с веткой, а не жить только на машине разработчика.
- Что:
  - строка .scratch/ удалена из .gitignore.
- Проверка:
  - git check-ignore -v .scratch/ не должен ничего возвращать.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:26:55 +03:00
Dmitriy DementievandClaude Opus 5 a65cb74d88 docs(agents): описаны операции карты wayfinder в Gitea
- Зачем:
  - навык wayfinder ожидает раздел про операции карты в документе трекера, а
    в Gitea 1.27 нет подзадач, поэтому конвенции надо было зафиксировать явно.
- Что:
  - описана принадлежность тикета карте через метку и ссылку в теле, поскольку
    родительских связей в API нет.
  - блокировки заведены на нативные зависимости Gitea, добавлены запросы фронтира.
  - зафиксированы три особенности tea api: путь без ведущего слэша, обязательные
    owner и repo в теле зависимости, нулевой код возврата при HTTP 404.
- Проверка:
  - tea issues list --remote origin --labels wayfinder:map
  - tea api --remote origin repos/ddmitry/local-transcriber/issues/14/dependencies

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:23:16 +03:00
Dmitriy DementievandClaude Opus 5 7e348e400f docs(diarization): добавлен разведочный замер и пересмотрен бэклог
- Зачем:
  - схема из бэклога приписывала спикера целому ASR-сегменту, и до замера
    было неизвестно, насколько сильно это огрубляет результат.
- Что:
  - добавлен разведочный замер sherpa-onnx на одной записи: 11,1x RTFx против
    16,4x у ASR, свип порога кластеризации и доля загрязнённых сегментов.
  - пункт бэклога переписан: движок описан как практически закрытый вопрос,
    главной развилкой названа единица привязки спикера к тексту.
  - зафиксировано, что 27% сегментов содержат не менее секунды чужой речи и на
    них приходится больше половины времени транскрипта.
- Проверка:
  - методика и условия замера воспроизводятся по разделам «Оборудование и
    условия» и «Контрольная запись» в docs/benchmarks/2026-08-12-diarization-feasibility.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:22:51 +03:00
12 changed files with 1132 additions and 16 deletions
+1 -2
View File
@@ -5,5 +5,4 @@ __pycache__/
dist/
*.pyc
.codex
.qwen/
.scratch/
.qwen/
+6
View File
@@ -0,0 +1,6 @@
# модели диаризации — 33 МБ, скачиваются по README
models/
# выход замеров
segments-*.tsv
conflict-*.json
+63
View File
@@ -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 и проверяет код возврата.
+49
View File
@@ -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)
+150
View File
@@ -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,
)
+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,
)
+62
View File
@@ -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)
+133
View File
@@ -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
+52
View File
@@ -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
View File
@@ -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?