PunchUNIUYO to monetise sports with new policy — VCESPN DeportesMax, el más rápido en tercera práctica en Bakú; Checo luce fuerteThe Jerusalem PostGov't warns against bringing Four Species into Israel without authorization ahead of SukkotInquirer2028 polls survey: Sara-Robin 12 points ahead of Leni-Bam tandem — OCTADaily MaverickEDUCATION: R6,600 a month to catch up: Inside SA’s costly private tutoring marketCNN TürkSermaye piyasası soruşturmasında yeni gelişme: 19 tutuklamaUOLRússia tenta criar alternativa à Starlink após perder acesso à rede de Elon MuskVarietyBlock the Merger Coalition Asks Court to Reject Paramount’s Settlement With States Over Warner Bros. DealWirtualna PolskaOdjechał samochodem osobowym. Zaginął 12-latek, policja z apelemThe IndependentDyfed-Powys Police hit by cyber attack as information at riskDaily MailBreakthrough brain cancer test cuts diagnosis from weeks to hours: 'Huge leap forward for patients'RapplerCarlos Yulo falls short of parallel bars medal, concludes stellar Asian Games
The Daily Newsstand · Free, Always
Friday, September 25, 2026

Как нейросеть переводит видео. Часть 1: Whisper врёт, и мы это знаем

Translate

Я делаю speeek.io, сервис дубляжа видео. Один. Код писал сам, с 2022 года, когда OpenAI выложил Whisper и я подумал: речь → текст → перевод → синтез → обратно на видео, делов-то. Делов оказалось на три года. В 2026-м через конвейер прошло 9 203 задачи, и он всё ещё ломается в тех же местах, что и в первый месяц. Просто теперь я знаю, в каких.

Это первая статья из трёх. Здесь про вход конвейера: как звук становится текстом с таймкодами и спикерами и что с этим текстом не так. Вторая будет про перевод и укладку в тайминг (спойлер: русский в 1,14 раза длиннее английского, и это главная боль всего проекта). Третья про синтез, клон голоса и сведение.

Это не туториал по Whisper, их и так сотня. Это про то, что с ним происходит в проде, когда пользователь грузит запись вебинара с телефона, а на телефоне ещё и ребёнок кричит.

Пять этапов, и на каждом своё место, где ломается

Пять этапов, и на каждом своё место, где ломается

Что на входе

Файл mp4, mov, mp3, wav или ссылка на YouTube. С YouTube качаем через yt-dlp в 720p, больше не нужно: видео мы не перекодируем, только подменяем звуковую дорожку. Дальше ffmpeg вытаскивает звук в mp3, а из видео выкидывает все аудиодорожки (-c:v copy -an), и это будущий контейнер для результата.

Первая засада, которую я не ожидал: у части файлов аудиодорожка короче видео. На полсекунды, на секунду. Телефоны так пишут, монтажки так экспортируют. Если это не поправить, укладка в конце съедет, потому что последний фрагмент перевода рассчитывается от длины видео. Лечится тупо: дописываем тишину.

if ($videoDuration - $audioDuration > 0.5) {
    $silence = $videoDuration - $audioDuration;
    exec("ffmpeg -i source.mp3 -f lavfi -t $silence -i anullsrc=r=44100:cl=stereo "
        ."-filter_complex '[0:a][1:a]concat=n=2:v=0:a=1' source_padded.mp3");
}

Мелочь. Таких мелочей в конвейере штук сорок, и почти каждая появилась после чьего-то письма в поддержку. Про большинство я уже не помню, зачем они, и трогать боюсь.

Сначала голос отдельно

До распознавания звук идёт в Demucs, разделение на вокал и всё остальное. Причины две. Whisper на чистом вокале ошибается меньше, чем на вокале с музыкой (особенно на интро вебинаров, где ведущий говорит поверх джингла). И фон нам нужен потом: переведённая речь ложится не на тишину, а на оригинальную подложку с музыкой и шумом зала, иначе результат звучит как звонок из бункера.

На выходе два файла: vocals и no_vocals. Vocals с громкостью 0.25 сохраняем ещё и как «оригинал»: пользователь может подмешать голос спикера под перевод, как в закадровом переводе из девяностых. Кто-то это любит. Я нет, но галочка есть.

Своя видеокарта и три запасных выхода

Распознавание крутится на нашей ноде: одна A4000, whisperX с large-v3-turbo, выравнивание по словам и диаризация pyannote. Дёшево и быстро, пока нода жива. Когда она занята или легла (легла — это обычно я что-то обновил), задача уходит по каскаду: две модели на Replicate, потом Gladia. Каскад описан тремя строчками, и он спас больше ночей, чем любой мониторинг.

// провайдер упал — берём следующего; минуты пользователю возвращаем,
// только когда кончился весь список
public function transcribeModelIdx(int $attempt): ?int
{
    return match ($attempt) { 0 => 0, 1 => 1, 2 => 2, default => null };
}

Куда уходит задача, зависит от языка и от того, жива ли нода:

Ситуация

Куда идёт

Почему

Обычный язык, нода свободна

своя A4000, whisperX large-v3-turbo

дёшево, быстро, слова с таймкодами и спикерами

Нода занята или упала

Replicate: whisper-diarization → whisperX

тот же результат, дороже, зато без очереди

Оба Replicate отказали

Gladia

последний рубеж, предложения отдаёт сама

Казахский

сразу Gladia

whisperX с ним плох

Хинди

только faster-whisper с нативными таймкодами

см. ниже

Про хинди отдельно: у whisperX шаг выравнивания не может выровнять хинди-сегменты и молча их выбрасывает. Из транскрипции пропадали куски по 12–15 секунд речи. Молча. Пользователь получал перевод, в котором нет трети лекции, и писал «у вас перевод неполный». Я, естественно, полез в распознавание. Распознавание было в порядке. Align тоже был в порядке, если верить логам: ни одной ошибки, exit 0, красота. Он просто выкидывал сегменты, которые не смог выровнять, и не считал нужным об этом сообщать.

Логировать ошибки в таком конвейере бесполезно, их нет. Я теперь считаю слова на входе и на выходе каждого шага. Тупо, но работает.

Whisper врёт тремя способами

Про галлюцинации Whisper написано много, но в проде они выглядят очень конкретно. У нас три ловушки. Вернее, две с половиной:

  1. слово длиннее 50 символов — удалить;

  2. одна и та же подстрока пять раз в окне 130 символов — отдать LLM, не починила — не озвучивать;

  3. обрывок короче трёх символов или сегмент без спикера — выкинуть.

Слово-монстр. «Иииииииииииииииииииииииииии», «pre,pre,pre,pre,pre,pre,pre». Появляется на длинных паузах, на музыке, на аплодисментах. Правило: слово длиннее 50 символов удаляем целиком. В живой речи таких слов нет. В немецком, кажется, тоже. Откуда берётся именно «pre,pre,pre», я так и не понял, моя версия: где-то в обучающих субтитрах был служебный тег, и модель его выучила как речь.

Зацикленная фраза. «I love I love I love I love I love». Whisper поймал петлю и повторяет кусок до конца сегмента. Ловим так: берём первые 10 символов окна и считаем, сколько раз они встречаются в окне из 130 символов. Пять и больше — галлюцинация.

$window = 130; $needle = 10; $limit = 5;

foreach (mb_str_split($text, $window) as $chunk) {
    $sub = substr($chunk, 0, $needle);

    if (substr_count(mb_strtolower($chunk), mb_strtolower($sub)) >= $limit) {
        // LLM: «оставь первое осмысленное вхождение, убери повторы»
        $fixed = $llm->hallucineRemove($text);
        $segment[$src] = $fixed ?: $segment[$src];
        $segment['hallucine'] = !$fixed;   // не починили — не озвучиваем
        break;
    }
}

Окно было 120, стало 130 в январе 2025-го, после того как я добавил лог счётчиков и посмотрел на реальных задачах. Типичная петля Whisper это фраза из 2–4 слов, и в 120 символов пять повторов длинной фразы не влезали. Да, десять символов. Да, это подгонка. Работает.

Если LLM не смогла нормализовать, сегмент помечается и не озвучивается вообще, в редакторе он с красным треугольником. Лучше дырка в две секунды, чем «I love I love I love» голосом лектора.

Огрызки, это та самая половина. «A.», «Э.», «Да.». Всё, что короче трёх символов, выкидываем. И сегменты без спикера тоже: диаризация не назначает спикера на [Music] и [Applause], для нас это признак, что речи там нет.

В последних 300 задачах en→ru флаг «галлюцинация, не озвучивать» не сработал ни разу. Звучит как победа, но нет: Whisper врёт по-прежнему, просто LLM чинит петли раньше, чем мы сдаёмся. Ловушка стоит, в неё никто не попадает, убрать рука не поднимается.

Сегменты Whisper выбрасываем. Все

Whisper отдаёт «сегменты», и что это такое, не знает никто. Полтора предложения. Абзац. Два слова. Граница сегмента у него это граница окна декодера, а не мысли. Переводить и озвучивать такое нельзя: у фразы должен быть свой start и своя длительность, иначе укладка в тайминг невозможна.

Поэтому из ответа Whisper мы берём только слова с таймкодами, а сегменты собираем сами. Сегмент в нашем понимании это предложение: с заглавной буквы до точки, вопроса или восклицания. Звучит просто. Код на 50 строк, писался года полтора, и почти каждая строка это чей-то испорченный ролик.

Сначала чистим слова. Знаки препинания иногда приходят отдельными токенами: «help» и следом «.» как самостоятельное слово. Клеим к предыдущему. Токен «run .py» режем до первого пробела, в слове должно быть одно слово. У части слов после align нет таймкода: берём предыдущий плюс 0,35 секунды, для односимвольных плюс 0,1. Откуда 0,35? Средняя длительность слова, подобранная на слух. Точнее не нужно, дальше всё равно округляем до десятых.

Потом режем. Предложение заканчивается на точку, вопрос, восклицание, многоточие, а ещё на «。», «؟» и «।» для китайского, арабского и хинди. Кроме случаев: точка после сокращения (Mr, Dr, тыс, млн, т.е, z.B), после инициала (одна буква с точкой), после числа («15.»). Первую букву предложения поднимаем сами, Whisper этого не гарантирует. Смена спикера режет фразу даже без точки.

Полный список исключений, как есть в коде

Знаки конца: . ! ? … 。 ! ? ؟ । ¿ ¡

Английский: Mr, Mrs, Ms, Dr, Prof, Sr, Jr, Inc, Ltd, Corp, Co, etc, vs, e.g, i.e, a.m, p.m, St, Ave, Blvd, Rd

Русский: г, гг, р, руб, коп, см, км, кг, м, мм, л, мл, тыс, млн, млрд, т.е, т.к, им, ул, пр, д, кв, обл, р-н, гос, мин, зам

Французский: M, Mme, Mlle, Dr.

Немецкий: bzw, inkl, ggf, evtl, usw, z.B.

Испанский: Sr, Sra, Srta, Dr, Dra

Плюс: одна буква с точкой (инициал) и число с точкой не конец предложения.

Список пополняется, когда кто-то присылает ролик, где «млрд.» разрезало фразу пополам.

if ($nextSpeaker !== $segment['speaker']
    || $this->isEndOfSentence($word)
    || $isLastWord) {
    $segment['duration'] = round($word['end'] - $segment['timestamp'], 1);
    $segment['text'] = ucfirst(trim($segment['text']));
    $segments[] = $segment;   // и начинаем новое
}

Про спикеров отдельно. Если пользователь сказал «два спикера», а диаризация нашла одного, второго мы дорисовываем сами с пустым голосом, чтобы в редакторе можно было руками переназначить реплики. Тут был баг, который жил до сентября 2025-го: диаризация нашла спикеров 1 и 3, недостающего добавляли как count + 1 = 3, получался дубль. Теперь max + 1. Почему не max + 1 с самого начала, не спрашивайте.

И ограничение, про которое в лендингах не пишут, а зря. Нахлёста речи у нас не бывает. Никогда. Whisper с диаризацией не умеют «двое говорят одновременно»: слово принадлежит ровно одному спикеру, реплики идут строго одна за другой. Если гость перебил ведущего, кусок перебивания уйдёт кому-то одному, и в переводе они будут говорить по очереди. В тесте на интервью ровно так и вышло: три перепутанные реплики из 102, все три на перебивках. Для лекций и подкастов это незаметно. Для ток-шоу, где трое орут друг на друга, наш конвейер не подходит. Я не знаю ни одного, который подходит, но, может, плохо искал.

Лектор без точек

Осталась проблема лектора, который говорит без точек. Есть такие люди, преподаватели чаще других. Whisper честно отдаёт предложение на 400 символов, потому что человек так и говорил: пятьдесят секунд без единой паузы. Для синтеза это плохо. Одна фраза на 50 секунд, и если перевод вышел длиннее, ускорять придётся весь кусок целиком, а не одну реплику. Порог у нас 300 символов.

До июня 2025-го длинные сегменты резала отдельная модель пунктуации на той же ноде. Она отдаёт знак с уверенностью, мы брали точки от 0.5 и остальные знаки от 0.8. Если ни один знак не прошёл порог, сегмент так и оставался на 400 символов. Сейчас длинный сегмент вместе со словами и таймкодами уходит в LLM: расставь знаки, разбей на предложения, пересчитай start и длительность каждого по первому и последнему слову. Один вызов вместо модели плюс постобработки. Вернула кривой JSON (бывает, и чаще, чем хотелось бы) — запасной ход: сжать текст на 10–15 % без потери смысла, чтобы хоть влез.

Что это значит, если вы переводите вебинар

Распознавание в 2026-м ошибается не в словах. Оно ошибается в именах, брендах и цифрах. В тесте на интервью с двумя спикерами 99 из 102 реплик ушли правильному человеку, зато фамилия гостя превратилась в набор слогов, а название ресторана в абракадабру. Это не лечится порогами, я пробовал. Это лечится глазами: десять минут с текстом перед собой. Редактор для этого и открыт на всех тарифах, включая бесплатный.

Шумный зал и аплодисменты не страшны, разделение вокала их снимает. Два спикера работают, если не говорят одновременно. Речь без пауз на 50 секунд работает, LLM порежет. Не работает два: двое говорят одновременно, и надежда, что результат можно не слушать.

Конвейер, о котором речь, крутится на speeek.io. Первые 5 минут в месяц бесплатные, редактор с красными треугольниками открыт на всех тарифах, так что проверить всё написанное можно на своём ролике.

Во второй части: почему русский перевод длиннее английского оригинала в 1,14 раза, что делать с фразой, которая не влезает в свой тайминг, и почему 31 % фраз у нас упираются в потолок ускорения 35 %.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.