Как нейросеть переводит видео. Часть 2: русский длиннее не текстом, а звуком

Я делаю speeek.io, сервис дубляжа видео, один. В первой части был вход конвейера: как звук становится текстом с таймкодами и что Whisper с этим текстом вытворяет. Эта про середину: перевод и укладку перевода в тайминг оригинала.
Середина — самое больное место всего проекта. Распознавание я более-менее победил, синтез покупаю готовый. А вот что делать с русской фразой, которая не лезет в секунды английской, я до сих пор не уверен. В конце будет эксперимент, после которого стало немного понятнее.
Сначала две поправки к первой части
Первая мелкая. Я писал, что длинные сегменты режутся при 300 символах. На проде стоит 250. Не спрашивайте.
Вторая неприятнее. В первой части я довольно гордо рассказывал, как лектора без точек режет LLM: отдаём ей сегмент со словами и таймкодами, она расставляет знаки и делит на предложения. Через два дня после публикации я готовил ролик для другой статьи, 29 секунд, продавец показывает, как пользоваться блендером. Открываю редактор, а там первая фраза разрезана пополам:
«Некоторые из вас спрашивали, как пользоваться блендером,»
«так что вот небольшой урок.»
Полез разбираться. Распознавание вернуло все 29 секунд одним куском без единой точки. Почему именно этот ролик, я так и не понял. Один сегмент длиннее 250 символов, значит, в LLM. А в промпте стояло вот это:
Разбейте на отдельные JSON-сегменты по предложениям,
если невозможно — по смысловым блокам.«По смысловым блокам» модель поняла ровно так, как написано: «так что вот небольшой урок» — вполне себе смысловой блок. Для перевода это не страшно. Для синтеза страшно: каждый сегмент озвучивается отдельно, и хвост «так что вот небольшой урок.» синтез читает как начало новой мысли, с интонацией начала. Звучит как человек, который забыл, о чём говорил.
Промпт поменял в тот же день:
Один сегмент = одно целое предложение, от начала до точки,
вопросительного или восклицательного знака. Никогда не делите
предложение на части по запятой или по смыслу: каждый сегмент
озвучивается отдельно, и обрывок синтез прочитает с неверной интонацией.
Исключение: только если одно предложение длиннее 250 символов,
разделите его по самой естественной паузе (точка с запятой, тире,
союз), так чтобы каждая часть была законченной мыслью.Причину я написал прямо в промпте, это не для красоты. Голый запрет модель соблюдает по букве и ищет, где обойти, а с причиной понимает, чего от неё вообще хотят. Проверил на том же блендере: первая фраза целая, из восьми сегментов стало семь.
LLM в конвейере — не функция, а подрядчик. Делает ровно то, что написано в ТЗ, включая то, что вы написали по ошибке.
Перевод: почему не DeepL
До августа 2025-го переводил DeepL, запасным был Яндекс. Оба хорошие. И оба переводят фразу, а не текст: каждый сегмент уходит отдельно, без соседей. Для дубляжа это смерть по мелочам. «I was tired» — «устал» или «устала»? Переводчик фразы не знает, кто говорит. Термин, который звучит в лекции десятки раз, выходит в нескольких вариантах. Местоимение «it» в начале фразы указывает на что-то из предыдущей, которую переводчик не видел.
Сейчас переводит LLM, и переводит пачками. Задача режется на куски по 50 сегментов, каждый кусок уходит одним JSON-массивом с номером спикера у каждой фразы. В промпте главное:
прочитай весь массив, прежде чем переводить, контекст важен;
input[N] → output[N], объединять и делить фразы запрещено;
естественный разговорный перевод, дословный запрещён;
в ответе только JSON, без пояснений.
Второй пункт важнее, чем кажется. Модель очень хочет «улучшить» текст: склеить две короткие реплики в одну или разбить длинную. Для книги это нормально. Для дубляжа это конец, потому что у каждой фразы свой таймкод, и если фраз на одну меньше, дальше всё съезжает.
Поэтому ответу я не верю, пока не проверю:
private function validateTranslation($response, int $expected, string $dst)
{
$clean = preg_replace('/```json\s*|\s*```/', '', trim($response));
$items = json_decode($clean, true);
if (json_last_error() !== JSON_ERROR_NONE) // не JSON
return false;
if (!is_array($items) || count($items) !== $expected) // потеряла или склеила фразу
return false;
foreach ($items as $item)
if (trim($item[$dst] ?? '') === '') // пустой перевод
return false;
return $items;
}Не прошла проверку — повтор через секунду. Упал сам запрос — повтор через 20 секунд. Всего три попытки. Прогресс сохраняется после каждой пачки в кэш на два часа, так что если на лекции в 600 фраз упала девятая пачка, повтор начнётся с девятой, а не с первой. Если все три попытки провалились, сохраняю то, что успело перевестись, и отдаю остаток Яндексу. Пусть лучше десять фраз в конце будут переведены без контекста, чем пользователь получит ошибку.
У пачек есть очевидная цена: фраза 51 не видит фразу 49. Можно передавать хвост предыдущей пачки как контекст, это лежит в списке дел.
И одна вещь, которой в промпте нет. Ни слова о том, сколько секунд у фразы. Переводчик не знает, что его текст потом озвучат и впихнут в тайминг английского оригинала. К этому я ещё вернусь.
Русский длиннее не текстом
В первой части я написал, что русская озвучка в 1,14 раза длиннее английской. Та цифра была по 300 задачам. Для этой статьи я пересчитал на полутора тысячах последних задач с июня 2026-го, разделив «текст длиннее» и «звук длиннее». Разница оказалась совсем не там, где я её три года искал.
Пара | Фраз | Текст перевода / оригинала | Звук перевода / оригинала | Оригинал, знаков в секунду | Синтез, знаков в секунду |
|---|---|---|---|---|---|
en → ru | 39 082 | 1,016 | 1,172 | 16,5 | 14,3 |
ru → en | 14 566 | 1,008 | 0,831 | 14,3 | 17,3 |
en → de | 628 | 1,134 | 1,198 | 17,0 | 16,1 |
ru → de | 1 818 | 1,142 | 1,051 | 14,6 | 15,9 |
Звук здесь — длина синтезированной фразы до ускорения. Считал по фразам длиннее половины секунды.
Русский перевод длиннее английского текста на 1,6 %. Практически одинаково. Я был уверен, что русский просто многословнее, отсюда и кнопка сокращения в редакторе (о ней ниже). А он не многословнее, по крайней мере в тех роликах, что к нам грузят. Проблема в скорости: англичанин в среднем говорит 16,5 знака в секунду, а синтез читает русский со скоростью 14,3. Тот же объём текста, на 17 % больше времени.
С русского на английский всё наоборот, и это отдельная радость. Русские спикеры говорят медленнее (14,3), английский синтез читает быстрее (17,3), и английская озвучка выходит на 17 % короче оригинала. Экспорт с русского на английский укладывается почти бесплатно: ускорять приходится 19 % фраз против 50 % в обратную сторону.
Почему синтез медленнее живого человека, у меня только версия. Голоса синтеза учат на дикторской, чистой речи, а живой человек на вебинаре глотает слоги, тараторит и не делает пауз между словами. Синтез так не умеет, и слава богу, иначе его невозможно было бы слушать.
Ещё одна деталь из той же выборки. Если считать не сумму, а медиану по фразам, en → ru выходит 1,35, а не 1,17. Раз медиана больше суммарного отношения, короткие фразы растягиваются сильнее длинных. Короткой реплике труднее всего: у неё нет запаса.
Укладка: как фраза втискивается в чужие секунды
После синтеза у каждой фразы есть wav и его длительность. Дальше идём по фразам по порядку и склеиваем одну дорожку. У каждой фразы есть таймкод оригинала, и есть таймкод следующей фразы, раньше которого мы должны закончить.

Логика такая:
Считаем свободное место: начало следующей фразы оригинала минус текущая позиция на дорожке минус длина перевода.
Если место есть, перевод начинается на 0,3 секунды раньше оригинала. Если он такой длинный, что со старта за 0,3 секунды не успеет закончиться до следующей фразы, стартует ещё раньше, вплотную, насколько позволяет пауза.
Если места нет, ускоряем фразу во столько раз, сколько нужно, но не больше чем в 1,35. И ставим встык за предыдущей.
$next = $nextStart - $timestampAllParts; // начало следующей фразы оригинала
$free = round($next - $pos - $dur, 1) ?: 0.1; // свободное место, секунды
if ($free < 0) {
// не влезает: ускоряем, но не больше чем на 35 %
$available = ($next - $pos) <= 0 ? 1 : $next - $pos;
$boost = min(round($dur / $available, 2), 1.35);
$task->BoostWav($el, $boost); // sox tempo, высота голоса не меняется
$track->append($fragment); // встык за предыдущей
} else {
// влезает: за 0,3 с до оригинала, но так, чтобы закончить до следующей
$start = min($segment['timestamp'] - 0.3, $next - $dur);
$track->appendSilence(max(0, $start - $pos))->append($fragment);
}Откуда 0,3 секунды, я уже не вспомню: это значение стоит с самой старой версии, которая есть в git, декабрь 2024-го. С 1,35 проще: выше голос звучит как аукционист. Ускорение делает sox tempo, он меняет скорость без изменения высоты, так что бурундуков не бывает. Бывает очень быстрый диктор.
А теперь дефект, который в этой схеме заложен и который я долго не замечал. Давайте на цифрах со схемы.
Фраза 2 в оригинале занимает 2,6 секунды, до начала фразы 3 от конца нашей первой — 3,4. Русский перевод синтез прочитал за 5,2. Чтобы влезть, нужно ускорение в 1,53 раза. Даём потолок, 1,35, получаем 3,85 секунды. Не влезла на 0,45. Фраза 3 теперь стартует не за 0,3 секунды до оригинала, а на 0,45 после. Ей тоже осталось меньше места. Если и она длинная, она тоже упрётся в потолок, тоже вылезет, и следующая стартует ещё позже. Отставание копится каскадом, пока не встретится пауза, которая его съест. На схеме повезло, пауза нашлась перед фразой 4.
В лекции, где спикер не делает пауз, не везёт. Каскад тянется долго, и всё это время голос отстаёт от губ и от слайдов. И вот это меня бесит больше всего в укладке: снаружи задача выполнена, ошибок нет, в логах чисто. А зритель смотрит, как лектор уже переключил слайд, а голос всё ещё рассказывает про предыдущий.
В коде это выглядит невинно: $available = ... <= 0 ? 1 : .... Если позиция уже за началом следующей фразы, доступного места «1 секунда», и ускорение сразу упирается в потолок. Честная починка тут одна: не давать фразе быть длинной с самого начала. Об этом эксперимент ниже.
Технические мелочи, из-за каждой из которых что-то однажды падало:
дорожку клеим кусками по 50 фраз, потом куски между собой (почему не одним махом, история из git умалчивает);
уже ускоренную фразу при повторной укладке не ускоряем второй раз, коэффициент хранится в сегменте;
если файл тишины вырос больше 250 МБ, это уже не тишина, а почти наверняка сломанные таймкоды, и задача падает с «ошибкой укладки», а не пишет час пустоты;
фразы, выключенные в редакторе или помеченные как галлюцинация, пропускаются, их место остаётся паузой.
И вот что получается на проде:

Распределение бимодальное, и это многое объясняет. Половина фраз en → ru влезает без ускорения: где-то пауза после фразы, где-то короткий перевод. Треть упирается в потолок 1,35, то есть и 35 % ускорения им мало. А мягких ускорений на 5–10 %, которых никто бы и не услышал, всего 17 %. Если фраза не влезает, она обычно не влезает сильно.
Другие пары, доля фраз в потолке: ru → en 5 %, en → de 18 %, ru → de 22 %, es → ru 43 % (видимо, испанцы говорят ещё быстрее англичан, но скорость по es я отдельно не мерил).
Эксперимент: сказать переводчику, сколько у него секунд
Раз проблема не в тексте, а в том, что синтез медленнее человека, лечить её сокращениями после синтеза — это лечить симптом. Переводчик мог бы сразу выбирать формулировку покороче, если бы знал, что она должна уложиться в две секунды. Живые переводчики дубляжа так и работают: у них тайминг перед глазами.
Проверил на двух роликах, которые уже прогонял через сервис: лекция по стратегии, 45 фраз, и интервью со стартапером, 102 фразы. Для каждой фразы считаю бюджет: время от её начала до начала следующей плюс 0,3 секунды, умноженное на скорость синтеза в этом ролике (15,1 и 14,5 знака в секунду). Это сколько знаков синтез успеет прочитать без ускорения. Бюджет кладу в поле max_chars и дописываю в промпт абзац:
ДЛИНА (ПЕРЕВОД ОЗВУЧИВАЕТСЯ В ТАЙМИНГ ОРИГИНАЛА):
• Если у элемента есть поле "max_chars" — перевод должен быть не длиннее.
Перефразируй короче: убери вводные слова, повторы, канцелярит,
выбери более короткий синоним.
• Запрещено ради длины выбрасывать факты, имена, цифры, термины и смысл.
• Элементы без max_chars переводи как обычно, не сокращай.Вариантов было два. B: лимит у каждой фразы. C: лимит только у фраз, которые, скорее всего, не влезут. Предсказать это можно до перевода и до синтеза, помогает та самая таблица выше: русский текст почти равен английскому по длине. Если английская фраза длиннее бюджета, русская тоже не влезет.
// до перевода: сколько знаков синтез успеет прочитать за время фразы
$seconds = $next['timestamp'] - $segment['timestamp'] + 0.3;
$maxChars = (int) ($seconds * $charsPerSecond[$dst]); // ru ≈ 14,5
if (mb_strlen($segment[$src]) > $maxChars) // русский ≈ английский по знакам
$item['max_chars'] = $maxChars;Результат:
Лекция, 45 фраз | Интервью, 102 фразы | |
|---|---|---|
A, как сейчас: фраз под ускорение | 67 % | 34 % |
A: не влезают даже с ×1,35 | 9 % | 3 % |
B, лимит всем: фраз под ускорение | 0 %, текст −25 % | 2 %, текст −24 % |
C, лимит только тем, что не влезут: фраз под ускорение | 9 %, текст −20 % | 4 %, текст −15 % |
B и C: не влезают даже с ×1,35 | 0 | 0 |
Сразу оговорка: длительности здесь посчитаны как «знаки поделить на скорость синтеза», а не реальным синтезом. Для прикидки этого хватает, для прода будем мерить честно.
Самый интересный вопрос — не сломался ли смысл. Все 147 пар вычитаны глазами. В первых тридцати я не нашёл вообще ничего и сначала решил, что так и будет дальше. Не так:
лекция: смысловых ошибок ноль; три раза потерялась тонкость термина («лидерство по общим издержкам» стало просто «по издержкам»);
интервью, вариант B: две ошибки. «Picked up on» (подметили) стало «переняли», а ресторан Eleven Madison Park превратился в «ресторан в Мэдисон-парке»: название пропало, получился другой факт;
вариант C Eleven Madison Park сохранил. «Переняли» у него тоже вылезло, хотя эту фразу C вообще не сокращал. То есть это не цена сокращения, а обычный разброс модели;
плюс, которого я не ждал: распознавание услышало автора книги как «Earl Gadara». Обычный перевод честно написал «Эрла Гадары». Перевод с лимитом исправил на настоящего автора, Уилла Гидару. Видимо, когда модель перефразирует, она заодно вспоминает, о ком речь;
чего не поймал никто: распознавание услышало «build great careers» как «build great Koreans». Все три варианта перевели это как «команды». Ошибки распознавания перевод не лечит, это к первой части.
Как это выглядит на конкретных фразах
EN: The main activities you engage in to become a cost leader are simply inconsistent in most cases with the main activities you engage in to be a differentiator. (лимит 140 знаков)
A (193 знака): Основные действия, которые вы выполняете, чтобы стать лидером по издержкам, в большинстве случаев просто несовместимы с основными действиями, которые вы выполняете, чтобы быть дифференциатором.
B (104 знака): Основные действия для лидерства по издержкам, как правило, несовместимы с действиями для дифференциации.
EN: For the purposes of this lecture, a competitive advantage is when a company achieves higher profits than the average competitor in the product market. (лимит 119)
A (158): В рамках этой лекции конкурентное преимущество — это ситуация, когда компания достигает более высокой прибыли, чем средний конкурент на данном товарном рынке.
C (107): В рамках этой лекции конкурентное преимущество — это когда компания получает прибыль выше среднего по рынку.
EN: Huckleberry is essentially a way for people to discover those strengths, discover that level of passion and improve it based on the feedback that they're getting from their teammates. (лимит 162)
A (192): Huckleberry — это, по сути, способ для людей обнаружить эти сильные стороны, обнаружить этот уровень страсти и улучшить его на основе обратной связи, которую они получают от коллег по команде.
C (118): Huckleberry, по сути, помогает людям находить эти сильные стороны, эту страсть и развивать их на основе отзывов коллег.
EN: So he's the guy that turned the restaurant to live in Madison Park into the world's best restaurant.
A: Так вот, это тот парень, который превратил ресторан Eleven Madison Park в лучший ресторан мира.
B: Это тот парень, который превратил ресторан в Мэдисон-парке в лучший ресторан мира.
C: Он тот человек, который превратил ресторан Eleven Madison Park в лучший ресторан мира.
И неожиданное: короткий вариант часто просто лучше. «Основные действия, которые вы выполняете, чтобы…» два раза в одной фразе — это не точный перевод, это канцелярит, который в устной речи никто не произносит. Синтез его честно читает, мы его честно ускоряем, зритель честно устаёт.
Вариант B сжимает всё подряд, в том числе то, что и так влезало, а каждое лишнее сокращение — лишний шанс потерять название ресторана. Поэтому на прод пойдёт C. Сначала за флагом на моём аккаунте на нескольких разных роликах, потом всем. Через неделю после включения сравню ту самую гистограмму: сейчас en → ru ускоряется 50 % фраз и 32 % стоят в потолке. Напишу, что вышло.
Что остаётся руками
Даже после всего этого часть фраз не влезет. Для них в редакторе красный треугольник: он загорается, если фразу ускорили больше чем на 10 % и в ней от 50 знаков. Рядом кнопка с волшебной палочкой: LLM сокращает текст на 10–15 % без потери смысла, и фраза переозвучивается.

С этой палочкой была история. Переводили ролик с русского на английский, жмём «сократить» на английской фразе и получаем сокращённый текст по-русски. Промпт сокращения был написан по-русски, а язык текста в него не передавался. Модель видела русскую инструкцию и отвечала по-русски. Для en → ru, самой частой пары, всё работало, поэтому никто и не замечал. Вот за такие баги я не люблю LLM в проде: они не падают, они тихо делают не то. Как align с хинди из первой части. Теперь язык передаётся явно, а ответ проверяется: если исходный текст был не кириллицей, а вернулась кириллица, запрос повторяется, две попытки.
Если эксперимент выше взлетит на проде, треугольников станет в разы меньше. Не ноль, но в разы.
Что это значит, если вы переводите своё видео
С английского на русский: половина фраз ляжет сама, треть будет ускорена на все 35 %. Всё решает скорость спикера: кто говорит медленнее 14 знаков в секунду, почти не ускоряется, быстрый рассказчик — это каскады.
С русского на английский: почти бесплатно, английский синтез быстрее русской речи.
Слушайте ускоренные фразы. Не весь ролик, только треугольники. Сократить длинную фразу руками — полминуты, и это та самая разница между «нейросеть перевела» и «звучит нормально».
Конвейер крутится на speeek.io. Первые 5 минут в месяц бесплатно, редактор с треугольниками открыт на всех тарифах, так что посмотреть на свой ролик можно без оплаты.
В третьей, последней части: синтез и сведение. Как клон голоса делается прямо из видео по минуте речи, почему синтез ставит ударения лучше меня (буквально), и как свести речь с оригинальным фоном так, чтобы она не звучала как звонок из бункера.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.