{stem}.words.json (mlx/wordrole, текст+диаризация) + аудио клипа. Не нужно, если режете на глаз/по маркерам. Требует изолированный venv .venv_whisperx с torch·torchaudio·transformers (пакета whisperx инструмент не использует) и локальную модель wav2vec2-xlsr-russian в --model-dir — в рантайме ничего не качается (см. грабли).mlx-whisper large-v3) не слышит границы слов напрямую — он восстанавливает их из кросс-аттеншена декодера методом DTW. Это даёт приблизительный тайминг: сетка ~20 мс, но реальная ошибка границы слова ±50–100 мс, а на паузах/дыхании/числах слово «прилипает» к соседнему. Для чтения транскрипта норм, для покадрового реза — нет: рез по такому таймкоду садится в середину предыдущего слова или в паузу.
wav2vec2) выдаёт для каждого 20-мс кадра распределение по фонемам/буквам; алгоритм forced-align прокладывает единственный путь через эти кадры, совпадающий с текстом → каждая буква и слово садятся на реальный звук. Именно так работает WhisperX (whisper для текста + wav2vec2 для таймингов). Точность границы падает до ±10–20 мс — суб-кадрово.
ЧЧ:ММ:СС,ммм): она мельче любого fps.
| fps | 1 кадр | вывод |
|---|---|---|
| 23.976 / 24 | 41.7 / 41.67 мс | ms-точность с большим запасом |
| 25 (PAL) | 40 мс | — |
| 29.97 (наш) | 33.37 мс | forced-align ±20 мс = суб-кадр |
| 50 / 59.94 / 60 | 20 / 16.68 / 16.67 мс | тут whisper ±50–100 мс уже даёт промах на 1–3 кадра — forced-align обязателен |
torchaudio.functional.forced_align (тот же приём, что у WhisperX, но без пакета whisperx):
# 1. EMISSION — акустическая модель → лог-вероятности по кадрам emission = wav2vec2(audio_16k) # [T кадров × V символов], кадр = 20 мс # 2. TRELLIS + BACKTRACK — единственный путь через кадры, дающий ИМЕННО наш текст tokens = tokenize("мне было двенадцать лет") # текст из whisper path = forced_align(emission, tokens) # CTC: буква↔кадр # 3. FRAME→SEC — индекс кадра × (длит.аудио / число кадров) word_spans = group_tokens_to_words(path) # {word, s, e, score}Ключевое отличие от whisper: тайминг берётся из акустики напрямую (где реально звучит буква), а не из внимания декодера. Модель для русского —
wav2vec2-large-xlsr-53-russian (jonatasgrosman/… на HuggingFace), лежит локально в ~/YTAI/environment/models/wav2vec2-xlsr-russian и грузится через transformers (Wav2Vec2Processor + Wav2Vec2ForCTC). Язык задаётся моделью, а не флагом. Мы переиспользуем готовый текст и диаризацию из words.json — гоняется только выравнивание, не повторная транскрибация.
Из конвейера транскрибации — {stem}.words.json: segments[] с текстом + speaker (диаризация pyannote). Это источник ТЕКСТА и РОЛЕЙ; тайминги перепишем.
ffmpeg -ac 1 -ar 16000 во временный WAV → numpy float32. wav2vec2 работает на 16 кГц.
Wav2Vec2Processor/Wav2Vec2ForCTC.from_pretrained(--model-dir) → wav2vec2-CTC для русского. Словарь символов модели (vocab) определяет, какие буквы выравниваются; blank = pad_token_id.
По сегментам: emission считается по аудио-окну сегмента, текст разбивается на буквы из словаря модели, forced_align кладёт их на кадры; буквы собираются обратно в слова → {w, s, e, score}. score = средняя уверенность букв слова (0–1). Сегмент, где выравнивание упало или нет ни одной буквы из словаря, целиком уходит на whisper-таймкоды.
Каждому выровненному слову — speaker по перекрытию его середины с исходным диаризационным турном. Слова без выравнивания (числа/пунктуация) — fallback на whisper-таймкод.
{stem}.aligned.words.json (мс) + .aligned.words.srt + .aligned.words.csv. В JSON — блок alignment: медианный/средний/p90 сдвиг границ vs whisper (доказательство, насколько подвинулось).
~/YTAI/scripts/999_extra/forced_align/forced_align_words.py — принимает медиа + готовый транскрипт, пишет высокоточную версию рядом с медиа (или в --out-dir).
PY=~/YTAI/environment/.venv_whisperx/bin/python $PY ~/YTAI/scripts/999_extra/forced_align/forced_align_words.py \ --media clip.mp4 \ --transcript clip.words.json \ --model-dir ~/YTAI/environment/models/wav2vec2-xlsr-russian \ --out-dir DIR --device cpuКлючи:
--media и --transcript обязательны; --model-dir (по умолчанию — путь выше), --out-dir, --base (имя выхода, по умолчанию stem медиа), --device (cpu). Ключа --language нет — язык задаёт модель; с ним команда падает на разборе аргументов. Выход перезаписывается по {base}. В консоль — отчёт точности: median / mean / p90 сдвига границ относительно whisper.
--model-dir должна содержать веса и словарь (config.json, vocab.json, веса) — сам инструмент ничего не скачивает, пустая папка = падение на загрузке. venv .venv_whisperx — отдельный от .venv_transcribe (там работает конвейер транскрибации): forced-align не должен трогать его зависимости.{
"words": [ {"w":"мне", "s":5.560, "e":5.740, "score":0.98, "speaker":"Speaker 1"}, … ],
"segments": [ … ], "diarization": { … },
"alignment": { "aligned_words": …, "engine":"torchaudio.forced_align + wav2vec2-xlsr-ru",
"median_shift_vs_whisper_ms": …, "mean_shift_vs_whisper_ms": …, "p90_shift_vs_whisper_ms": … },
"align_engine": "wav2vec2 CTC forced alignment (torchaudio)"
}
s/e — секунды, 3 знака (мс). Импорт в Premiere: .aligned.words.srt → как транскрипт/caption-трек (Premiere ест SRT с мс), либо words.json читает наш UXP/скрипт сборки и ставит маркеры/резы прямо по w.s/w.e. Пословный .csv (word,start,end,score,speaker) — для проверки и таблиц.
.venv_whisperx. pip install в .venv_transcribe апнет numpy/tokenizers и сломает mlx/pyannote работающего конвейера.pip install (PyPI/files.pythonhosted.org) виснет по той же причине, что rclone/yt-dlp — режется IPv6/DPI. При этом модели с HuggingFace качаются по IPv4 (curl --ipv4 = HTTP 200). Установку пакетов проводит Winston через VPN/IPv4 (см. Метод); модель один раз скачивается в --model-dir заранее.uroman.score у них 0.words.json; forced-align только двигает границы. Ошибку РАСПОЗНАВАНИЯ он не чинит — только тайминг.