CLI для русской озвучки клонированным голосом. Клонирование умеет открытый код; дефицитен слой правки текста перед синтезом: ударения (RUAccent), словарь брендов (Knight Capital → «Найт Кэпитал»), твёрдая «э» в заимствованиях, аббревиатуры по буквам (ТЗ → «тэ-зэ»), числа прописью. И возможность увидеть, что тракт сделает с репликой, ДО того как её услышишь:
explain,lint,doctor, сверка речи после синтеза. Ядро — на голой стандартной библиотеке, ставится в любое чужое окружение.
Быстрый старт — ядру не нужна ни одна зависимость:
pip install rusvoice
rusvoice explain "Компания Knight Capital потеряла 440 миллионов — ТЗ было на MVP."вход: Компания Knight Capital потеряла 440 миллионов — ТЗ было на MVP.
выход: Компания Найт Кэпитал потеряла 440 миллионов — тэ-зэ было на эм-ви-пи.
что сделал каждый слой:
бренды:
Knight → Найт
Capital → Кэпитал
аббревиатуры:
ТЗ → тэ-зэ
транслит:
MVP. → эм-ви-пи.
не применялось:
· числа словами (нужен PRONOUNCE_NUMBERS=1)
⚠️
RUAccent недоступен в /usr/bin/python3 — ударения НЕ проставлены,
текст уйдёт в синтез как есть
Предупреждение внизу и есть смысл всей затеи: без него окружение промолчало бы, озвучка
вышла бы без ударений, а списали бы это на модель. Код возврата при таком выводе — 1:
explain пригоден как проверка в скрипте, а не только для чтения глазами.
Дальше: синтез своим голосом · свой голос один раз · словари · что рядом · установка с экстрами · лицензии
Клонировать голос сегодня умеет открытый код: F5-TTS плюс русский чекпойнт. Дефицитно другое — то, что происходит с текстом ПЕРЕД синтезом (ударения, бренды, твёрдая «э», аббревиатуры, транслит) и возможность понять, почему получилось не так, как хотелось.
Без этого слоя каждый дефект выглядит одинаково: «модель так читает». На деле за одним и тем же симптомом стоят разные причины, и все они молчаливые:
| Слышно | На самом деле |
|---|---|
| «Книгхт Капитал» вместо Knight Capital | слова нет в словаре брендов → побуквенный транслит |
| «модель мямлит» | транскрипт эталона не про это аудио → галлюцинация префикса, +13% WER |
| правка словаря «не сработала» | RUAccent нет в этом интерпретаторе → текст ушёл без ударений |
| «нейрослоп» на одном слове | метка «+» на односложном → F5 отыгрывает её как нажим |
| «тихо» после нормализации | loudnorm вернул −15.3 при rc=0 и это сочли успехом |
Все команды здесь — про то, чтобы каждая строка этой таблицы обнаруживалась ДО прослушивания.
Соседи есть, и притворяться, что их нет, глупо — лучше сразу сказать, когда нужны они, а когда мы.
- RUAccent — ударения. Мы его не заменяем, а зовём внутри: он размечает 98.3% многосложных слов, и переписывать это незачем.
- RUNorm — числа прописью, аббревиатуры, кириллизация, акронимы. Тот же список задач, что у нашего слоя произношения, но нейросетью: три модели на 95M–860M параметров, то есть torch ради нормализации текста. Ударений не ставит, синтеза не делает.
- russian_tts_normalization, ru-tts-norm, saarus72/text_normalization — того же класса: преобразователи текста, которые кладут внутрь своей TTS-системы.
Все они — нормализаторы: на входе текст, на выходе текст. Здесь другое: обвязка вокруг
всей озвучки — эталон, синтез, громкость, — и её отличительная черта не список правил, а
прослеживаемость. Посмотреть, что тракт сделает с репликой, до синтеза (explain);
найти в сценарии реплики, которые прозвучат не так (lint); сверить, что реально
прозвучало (say → проверка речи). Ни у одного соседа этого нет, потому что библиотеке
это и не нужно — а инструменту без этого грош цена: дефект озвучки иначе обнаруживается
только ушами и только после рендера.
Ядро (explain, lint, dict) при этом стоит на голой стандартной библиотеке и ставится
в любое чужое окружение — в том числе туда, где своя версия торча и лишней не будет.
pip install rusvoice # ядро: explain / lint / dict / doctor
pip install "rusvoice[accent,ui]" # + ударения (RUAccent) и экран
pip install "rusvoice[voice,accent]" # + собственно синтез (F5)
rusvoice --helpСвежий main — pip install "git+https://github.com/ilyautov/rusvoice"; из клона —
pip install -e ".[dev]".
subprocess.run(..., text=True, encoding="utf-8"); без неё Python декодирует по кодировке
системы и отдаёт None, что выглядит как «CLI ничего не напечатал».
Ядро — explain, lint, dict — стоит на голой стандартной библиотеке и едет куда угодно.
Это не аскеза: команда, которая объясняет, что тракт сделает с текстом, обязана ставиться
там же, где стоит сам тракт, а он живёт в чужих окружениях с чужими версиями торча.
| Экстра | Что включает |
|---|---|
accent |
RUAccent — ударения. Без него слой не падает, он молча отдаёт текст как есть |
numbers |
num2words для PRONOUNCE_NUMBERS=1 |
ui |
rusvoice ui — живой просмотр в браузере |
ref |
ref grab/ref windows — пословные тайминги и замер окон |
voice |
say — сам синтез: F5-TTS. |
Движок, из которого пакет вырос, ему не нужен ни для чего — включая синтез. Рецепт
клона живёт здесь же, в rusvoice/clone.py. Так было не всегда: он лежал в
pipeline/voiceclone.py, и say — единственная команда, ради которой ставят остальные, —
требовала рядом весь видеотракт. Самостоятельная установка умела всё, кроме озвучки.
Когда движок рядом, три вещи берутся у него: потолок лимитера (константа мирится тестом),
его static-ffmpeg и его whisperx с кэшем. Не потому, что своих нет — своих хватает
(FFMPEG_BIN или PATH; libass пакету не нужен, субтитров он не рисует; ASR из
faster-whisper), — а потому, что иначе поведение внутри репозитория поехало бы.
rusvoice doctor это честно покажет — в этом
половина его смысла.
$ python3 -m rusvoice explain "Claude Code на подписке MAX: лендинг за 5x дешевле."
вход: Claude Code на подписке MAX: лендинг за 5x дешевле.
выход: Клод Код на подп+иске Макс: л+эндинг за пять раз деш+евле.
что сделал каждый слой:
бренды: Claude → Клод · Code → Код · MAX: → Макс:
множители: 5x → пять раз
твёрдая э: лендинг → лэндинг
ударения: подписке → подп+иске · лэндинг → л+эндинг · дешевле → деш+евле
Ценность не в «текст изменился», а в том, кто его изменил: правка словаря, которая «не доехала», отличается от правки, которую перекрыл другой слой.
Флаги: --json, --lang, --no-accent (путь edge-голоса: он «+» не уважает).
Аргументом можно дать текст, путь к файлу или - для stdin.
$ python3 -m rusvoice lint scenario.json
#3 · как это устроено
Knight Capital потеряла 440 миллионов за 45 минут.
✗ [транслит] «Knight» → «Книгхт» — слова нет в словаре брендов
→ rusvoice dict add brands Knight <как читать>
✗ [цифры] «440» уйдёт в синтез цифрами — F5 их путает
Ловит и то, что «нет метки» не поймает: омографы, где метка стоит, но не та.
· [омограф] «потом» → «п+отом»: выбрано редкое чтение — творительный от «пот»
→ если имелось в виду обычное, поставь метку: пот+ом
Замер по корпусу движка (456 сценариев, 6791 реплика): RUAccent размечает 98.3% многосложных слов, и пропущенных меток проблемы нет — а вот выбор он путает. «Потом становится легко» уходило в синтез как «по́том». Словарём это не лечится: одно слово в соседних репликах читается по-разному («за́мок на горе» и «замо́к щёлкнул в двери»), и глобальная замена сломала бы половину. Отвечает автор — меткой прямо в тексте.
Срабатывает на 0.4% реплик: в таблице только пары, где одно чтение подавляюще частотнее. «уже́» встретилось 273 раза, «со́рок» — 133, и оба раза акцентизатор прав; предупреждать там значило бы приучить пролистывать вывод.
Читает сценарий (scenes[].vo), текстовый файл построчно или сам аргумент.
Дефект и риск разведены нарочно: код выхода ненулевой только на дефектах.
Линт, падающий на рисках, отключают в первый же день — а вместе с ним перестают видеть
и «Книгхт».
Каждая проверка стоит на конкретном уже случившемся провале: RUAccent, PYTHONHASHSEED
(дочерний процесс F5 падает, но на одном батче звук всё равно возвращается — поэтому в
логах это выглядит безобидно), ffmpeg, эталон и его транскрипт, размеры словарей, версия
правил. Ничего не чинит и не мутирует: ставит диагноз.
python3 -m rusvoice dict list hard-e --check # что в словаре живого
python3 -m rusvoice dict add brands Netlify Нетлифай
python3 -m rusvoice dict hear brands Netlify # услышать запись, а не прочитать
python3 -m rusvoice dict bump # инвалидировать аудио-кэшadd пишет только после проверки: запись применяется к пробнику на копии таблицы, и если
слой выдал не то — файл не трогается. Отказ по умолчанию на перезаписи и на основе,
задевающей заведомо мягкие слова (сплошного правила «е→э» нет — «лес», «дело», «текст»
читаются мягко, поэтому это словарь, а не правило).
add проверяет строку: что слой на «OpenAI» даёт «Оупен Эй Ай». Что из этой строки
сделает F5, не знает никто — для этого hear: он озвучивает пробник фразой (на одиночном
слове модель ведёт себя иначе, чем в потоке речи). Отдельной командой, потому что синтез на
M1 стоит десятки секунд, а записей за заход бывает тридцать.
bump.
Версия правил живёт рядом с самими правилами (rusvoice/pronounce.py,
rusvoice/accentize.py), и движок её оттуда читает. Копий было три, и правило звучало
«поменял поведение — подними номер руками во всех местах»; копия, которую надо помнить,
однажды не поднимается — так один из потребителей завёл версию ударений в ключе кэша, а
второй нет, и футаж молча выродился в один клип на сцену.
rusvoice voice add запись.mp4 --name ilya # видео, аудио или готовый эталон
rusvoice voice set tempo 1.12 # настройка ГОЛОСА, не окружения
rusvoice say "реплика" --out o.wav # --ref больше не нуженДо этой команды say без --ref брала зашитый путь к эталону внутри репозитория: рядом с
движком работало, в самостоятельной установке — «эталона нет», без единого слова о том, где
его взять. Хотя взять пакет умеет давно: ref grab достаёт годный эталон из любого видео.
Дыра была не в возможностях, а в дефолте.
voice add выбирает путь сам и говорит, какой. Готовый эталон (есть .txt рядом, длина
4–25 с) копируется как есть: гонять ASR по уже разобранному — минута работы ради того, чтобы
заменить выверенный транскрипт свежей ослышкой. Всё остальное проходит ref grab.
Копируем к себе, а не ссылаемся: эталон, который однажды переименуют, — это молчаливо
сломанный голос.
Настройки (tempo, outro) живут у голоса, потому что они и есть его свойства: темп
1.12 у одного человека и 1.0 у другого. Глобальный ключ пришлось бы переставлять при каждой
смене голоса — и однажды не переставить. say берёт их только там, где вы промолчали:
явный --tempo их перебивает, и команда пишет в шагах, откуда взяла значение.
nfe (шагов диффузии F5) сюда не входит: единственный канал до него — env/config, а
подпись провайдера параметра не пропускает. Механизм, который работает не везде и не
говорит об этом, хуже отсутствующего.
voice remove убирает из реестра, файл остаётся. Правка настройки и потеря
материала — разные вещи.
clone_synth (вход рендера в движке) реестр голосов не смотрит — он берёт эталон из
своего конфига. Иначе voice use молча переозвучил бы чужим голосом следующий ролик.
python3 -m rusvoice ref grab видео.mp4 --out ref.wav # одной командой: голос из видео
python3 -m rusvoice ref check --ref ref.wav
python3 -m rusvoice ref windows источник.mp4 # окна-кандидаты по паузам
python3 -m rusvoice ref cut источник.mp4 ref.wav --start 4.4 --end 14.2grab достаёт дорожку, снимает пословные тайминги и измеряет окна-кандидаты, а не
выбирает их на глаз по волне. На глаз не видно двух вещей, которые потом слышны в клоне:
длинной паузы внутри окна (звучит как склейка) и высокого фона между словами (клон
наследует шум эталона вместе с голосом). Фон меряется именно в промежутках между словами —
усреднение по всему окну перевесила бы речь, и грязная запись получила бы хорошую оценку.
речь: 42 слов в источнике
✓ ref.wav — 9.91 с, 27 слов
окно 0.081–9.987 с · пауза 0.1 с · фон -20.3 dBFS · пик -3.0 dBFS
что ещё подошло бы:
2.88– 13.17 (10.29 с) пауза 0.10 · фон -20 · пик -3
cut режет окно по границам слов (границы снимаются с ASR-таймингов) и кладёт рядом
транскрипт, собранный из тех же слов, что попали в окно: дословность по построению, а не
по аккуратности. Полслова в эталоне — это уже не дословный транскрипт, каким бы точным
ни был .txt.
python3 -m rusvoice loud голос.wav # замерить и рассудить
python3 -m rusvoice loud голос.wav --out norm.wav # довести до −14 LUFSУспех объявляется по замеру выхода, а не по коду возврата ffmpeg. Недобор объясняется арифметикой: «нужно было +4.0 дБ, дошло +2.7; вход: среднее −18.0 при пике 0.0 (размах 18.0 дБ) — лимитер упирается раньше громкости».
python3 -m rusvoice say "реплика" --out out.wav --ref голос.wavF5-клон → темп (--tempo) → хвост (--outro) → громкость → сверка речи. Печатает, что
реально сделано: текст по слоям, шаги вместе с пропущенными, замер выхода.
Последний шаг — единственный, который смотрит на результат, а не на замысел. Все прочие проверки стоят до синтеза и ловят то, что мы предусмотрели; модель может уронить слово уже после них. В движке так и было: edge отдал 2.9 с на текст в 53 слова, в треке оказалось два слова, а проверкой был размер файла — обрезок весил 17 КБ и прошёл дальше.
Судим по доле покрытия, не по совпадению слов: ASR ошибается на каждом десятом слове, и
дословная сверка давала бы ложную тревогу на каждой реплике с брендом. Порог не назначен, а
взят с калибровки движка (здоровые сцены 0.955–0.983, обрезанная 0.038). На тексте короче
12 слов не судим вовсе. Отключается --no-verify.
Хвост стоит отдельным шагом, потому что clone_synth кладёт 0.18 с — это паддинг клипа,
рассчитанный на стык со следующей сценой. Для отдельного файла те же 0.18 с слышны как
захлопнутая дверь: это конец всего текста, а не стык. По умолчанию 0.5 с; --outro 0.18
возвращает клиповый хвост.
resemble-enhance) в рецепте нет. Он там стоял, пока клон снимали с
неидеального эталона; на нынешнем выходе F5 он звук портит и на обычной настройке, и на
самой мягкой. Ручка, заведомо делающая хуже, — ровно та молчаливая ловушка, против которой
весь пакет, поэтому её убрали, а не оставили «на всякий случай».
python3 -m rusvoice ui # http://127.0.0.1:8765Живой explain по мере набора, словари с пометкой живая/мёртвая, доктор. Своей логики
нет — те же функции пакета, поэтому разъехаться с CLI нечему.
127.0.0.1 не для красоты: ручки пишут в словари и запускают синтез.
Код пакета — Apache-2.0.
Выбор объясняется одним доводом. Главная ценность здесь не клонирование голоса — F5-TTS и так открыт, — а слой правки текста, и его смысл в том, чтобы им пользовались, в том числе внутри чужих синтезаторов. Копилефт (AGPL) закрыл бы ровно это распространение, а взамен дал бы защиту от закрытого форка CLI — угрозы почти теоретической, потому что инструменты запускают, а не встраивают.
Веса, на которых пакет работает, свои условия несут отдельно:
- F5-TTS — код MIT;
⚠️ русский чекпойнтMisha24-10/F5-TTS_RUSSIAN— CC BY-NC 4.0, некоммерческое использование;- RUAccent — Apache-2.0.
Веса пакет не тянет и не распространяет — скачивает по требованию, и условия остаются на том, кто скачал.
Изменения · Как помочь · Безопасность · English
Самый полезный вклад — слово, которое читается неверно: словарь растёт только от живых ошибок, придумать их за столом нельзя.
Илья Утов, лаборатория AI Frontier. Как эти инструменты устроены внутри, пишу в Telegram и LinkedIn.
Рядом стоят:
- humanizer-ru: убирает следы нейросети из русского текста
- marketplaces-mcp-ru: Wildberries, Ozon, Яндекс Маркет и Авито прямо из агента
- small-business-ru: 34 скилла для малого бизнеса, считают налоги и проверяют контрагента по ИНН
- consilium-principis: совет мыслителей, где каждая цитата сверяется дословно
- hefest: химическая безопасность завода, целиком офлайн
Все проекты одним списком, разобранные по назначению: ilyautov.github.io. Исходники: github.com/ilyautov. Пригодилось, поставьте звезду: по ней это находят другие.
