Голос и TTS
Команды установки Python-зависимостей на этой странице рассчитаны на чекаут исходников, подготовленный через PM. После изменения зависимостей заново активируйте чекаут и перезапустите Hermes.
Hermes Agent умеет и озвучивать текст, и расшифровывать голосовые сообщения — причём на всех платформах обмена сообщениями.
Есть платная подписка Nous Portal? Тогда OpenAI TTS доступен через Tool Gateway без отдельного ключа OpenAI API. При свежей установке запустите hermes setup --portal — войдёте в аккаунт и сразу включите все инструменты шлюза. На уже работающей установке выберите Nous Subscription только для TTS через hermes model или hermes tools.
Синтез речи (Text-to-Speech)
Превратить текст в речь можно через одиннадцать провайдеров:
| Провайдер | Качество | Стоимость | API-ключ |
|---|---|---|---|
| Edge TTS (по умолчанию) | Хорошее | Бесплатно | Не нужен |
| ElevenLabs | Отличное | Платно | ELEVENLABS_API_KEY |
| OpenAI TTS | Хорошее | Платно | VOICE_TOOLS_OPENAI_KEY |
| MiniMax TTS | Отличное | Платно | MINIMAX_API_KEY или MINIMAX_CN_API_KEY |
| Mistral (Voxtral TTS) | Отличное | Платно | MISTRAL_API_KEY |
| Google Gemini TTS | Отличное | Бесплатный тариф | GEMINI_API_KEY |
| xAI TTS | Отличное | Платно | XAI_API_KEY |
| DeepInfra TTS | Хорошее | Платно | DEEPINFRA_API_KEY |
| NeuTTS | Хорошее | Бесплатно (локально) | Не нужен |
| KittenTTS | Хорошее | Бесплатно (локально) | Не нужен |
| Piper | Хорошее | Бесплатно (локально) | Не нужен |
Доставка по платформам
| Платформа | Доставка | Формат |
|---|---|---|
| Telegram | Голосовой пузырёк (играет прямо в чате) | Opus .ogg |
| Discord | Голосовой пузырёк (Opus/OGG), при сбое — файл-вложение | Opus/MP3 |
| Аудиофайл-вложение | MP3 | |
| CLI | Сохраняется в ~/.hermes/audio_cache/ | MP3 |
Настройка
# In ~/.hermes/config.yaml
tts:
provider: "edge" # "edge" | "elevenlabs" | "openai" | "minimax" | "mistral" | "gemini" | "xai" | "deepinfra" | "neutts" | "kittentts" | "piper" — либо "nous" для управляемого Tool Gateway (записывается, когда в `hermes tools` выбрана Nous Subscription)
speed: 1.0 # Глобальный множитель скорости (настройки провайдера его переопределяют)
edge:
voice: "en-US-AriaNeural" # 322 голоса, 74 языка
speed: 1.0 # Преобразуется в процент скорости (+/-%)
elevenlabs:
voice_id: "pNInz6obpgDQGcFmaJgB" # Adam
model_id: "eleven_multilingual_v2"
openai:
model: "gpt-4o-mini-tts"
voice: "alloy" # alloy, echo, fable, onyx, nova, shimmer
base_url: "https://api.openai.com/v1" # Подмена для OpenAI-совместимых TTS-эндпоинтов
pcm_sample_rate: 24000 # Частота потокового PCM, которую отдаёт эндпоинт (автоматически переопределяется X-Audio-Sample-Rate)
speed: 1.0 # 0.25 - 4.0
# language: "es" # Уходит как lang_code — только для OpenAI-совместимых эндпоинтов, которые его поддерживают (например, Kokoro)
# consent_attestation: "I have the speaker's consent" # Нужно некоторым OpenAI-совместимым серверам для клонированных голосов
streaming:
min_len: 20 # Потоковый TTS: минимальная длина (в символах) первого предложения, которое озвучивается отдельно; для CJK ставят ~6
minimax:
region: "global" # "global" или "cn"; правила выбора — ниже
model: "speech-02-hd" # speech-02-hd (по умолчанию), speech-02-turbo
voice_id: "English_expressive_narrator" # См. https://platform.minimax.io/faq/system-voice-id
speed: 1 # 0.5 - 2.0
vol: 1 # 0 - 10
pitch: 0 # -12 - 12
# base_url: "https://tts.example/v1/t2a_v2" # Необязательная подмена эндпоинта для выбранного региона
mistral:
model: "voxtral-mini-tts-2603"
voice_id: "c69964a6-ab8b-4f8a-9465-ec0925096ec8" # Paul - Neutral (по умолчанию)
gemini:
model: "gemini-2.5-flash-preview-tts" # или gemini-3.1-flash-tts-preview
voice: "Kore" # 30 готовых голосов: Zephyr, Puck, Kore, Enceladus, Gacrux и т. д.
audio_tags: false # Включить скрытую вставку аудиотегов Gemini 3.1 TTS
persona_prompt_file: "" # Необязательный Markdown/текстовый файл с режиссурой голоса Gemini
xai:
voice_id: "eve" # или собственный voice ID — см. документацию ниже
language: "en" # код BCP-47 (например, "en", "pt-BR") или "auto" для автоопределения
speed: 1.0 # 0.7–1.5, скорость воспроизведения (по умолчанию: 1.0)
auto_speech_tags: false # вставлять выразительные аудиотеги через переписывание LLM
text_normalization: false # нормализовать числа, сокращения и символы в произносимую форму
optimize_streaming_latency: 0 # 0–2, меняет качество на меньшую задержку (по умолчанию: 0)
sample_rate: 24000 # 22050 / 24000 (по умолчанию) / 44100 / 48000
bit_rate: 128000 # битрейт MP3; применяется только при codec=mp3
# base_url: "https://api.x.ai/v1" # Подмена через переменную окружения XAI_BASE_URL
neutts:
ref_audio: ''
ref_text: ''
model: neuphonic/neutts-air-q4-gguf
device: cpu
kittentts:
model: KittenML/kitten-tts-nano-0.8-int8 # 25МБ int8; также: kitten-tts-micro-0.8 (41МБ), kitten-tts-mini-0.8 (80МБ)
voice: Jasper # Jasper, Bella, Luna, Bruno, Rosie, Hugo, Kiki, Leo
speed: 1.0 # 0.5 - 2.0
clean_text: true # Разворачивать числа, валюты, единицы измерения
piper:
voice: en_US-lessac-medium # имя голоса (скачивается автоматически) ИЛИ абсолютный путь к .onnx
# voices_dir: '' # по умолчанию: ~/.hermes/cache/piper-voices/
# use_cuda: false # требует onnxruntime-gpu
# length_scale: 1.0 # 2.0 = вдвое медленнее
# noise_scale: 0.667
# noise_w_scale: 0.8
# volume: 1.0 # 0.5 = вдвое тише
# normalize_audio: true
Управление скоростью. Глобальное значение tts.speed по умолчанию действует для всех провайдеров. Любой провайдер может переопределить его собственной настройкой speed (например, tts.openai.speed: 1.5). Скорость провайдера имеет приоритет над глобальной. По умолчанию — 1.0 (обычный темп).
KittenTTS недоступен на macOS с Intel и на Windows ARM64: его зависимости не публикуют колёса onnxruntime и PyTorch для этих платформ. Если выбрать его там, провайдер будет помечен как недоступный.
MiniMax TTS выбирает регион, эндпоинт и учётные данные вместе:
region: "global"работает черезhttps://api.minimax.io/v1/t2a_v2сMINIMAX_API_KEY.region: "cn"— черезhttps://api.minimaxi.com/v1/t2a_v2сMINIMAX_CN_API_KEY.- Если
regionне указан, ради обратной совместимости приоритет остаётся уMINIMAX_API_KEY. Когда настроен толькоMINIMAX_CN_API_KEY, Hermes выбираетcn. - У явно выбранного региона обязаны быть свои учётные данные — ключ соседнего региона Hermes не заимствует никогда. Подмена
base_urlвыбранные учётные данные не меняет, а подмена, указывающая на официальный эндпоинт другого региона, отвергается.
Промпты-персоны Gemini
Gemini TTS умеет следовать режиссёрским указаниям на естественном языке. В tts.gemini.persona_prompt_file укажите локальный Markdown- или текстовый файл с описанием голосовой персоны. Внутри можно использовать секции в стиле Gemini: AUDIO PROFILE, SCENE, DIRECTOR'S NOTES, SAMPLE CONTEXT, TRANSCRIPT.
Если в файле есть {transcript} или {{ transcript }}, Hermes подставит туда текущий текст для озвучки. Иначе он сам допишет в конец помеченную секцию TRANSCRIPT. Промпт-персона остаётся локальной и в ответе чата не показывается.
tts:
provider: gemini
gemini:
voice: Algieba
persona_prompt_file: ~/.hermes/tts/butler-voice.md
Аудиотеги (Gemini, xAI)
Gemini 3.1 Flash TTS от Google и Grok TTS от xAI поддерживают произвольные аудиотеги в квадратных скобках — [whispers], [excitedly], [very slow], [laughs] и прочие пометки выразительной подачи. Включите tts.gemini.audio_tags или tts.xai.auto_speech_tags, чтобы Hermes перед обращением к TTS прогнал скрытый проход переписывания. Этот проход вставляет инлайновые теги только в скрипт для озвучки; видимый ответ чата остаётся прежним.
tts:
provider: gemini
gemini:
model: gemini-3.1-flash-tts-preview
audio_tags: true
xai:
auto_speech_tags: true
Переписывание идёт через auxiliary.tts_audio_tags и по умолчанию использует вашу основную модель чата. Переопределите эту вспомогательную задачу, если хотите доверить вставку тегов модели подешевле или побыстрее.
Частота дискретизации при стриминге (OpenAI-совместимые эндпоинты): при потоковом воспроизведении приходит сырой PCM без заголовка, поэтому Hermes должен знать его частоту. Официальный API OpenAI отдаёт 24 кГц. Если совместимый сервер сообщает свою частоту — заголовком ответа X-Audio-Sample-Rate или параметром rate= в Content-Type (audio/pcm; rate=44100), — она учитывается автоматически: динамик, проигрыватель временного WAV и аудиопоток шлюза открываются на заявленной частоте, как только приходит ответ. Для серверов, которые ничего не сообщают, задайте tts.openai.pcm_sample_rate равным выходной частоте эндпоинта (например, 22050 для серверов на Piper), иначе речь зазвучит с неправильной скоростью и высотой. Некорректные значения пишут предупреждение в лог и откатываются к 24000.
Язык (OpenAI-совместимые эндпоинты): tts.openai.language уходит в запрос параметром lang_code. Он рассчитан на OpenAI-совместимые TTS-серверы, которые понимают lang_code, — например Kokoro-FastAPI, где language: "es" выбирает испанский фонемизатор вместо английского по умолчанию. С официальным API OpenAI, который этот параметр не принимает, оставляйте поле пустым. Когда оно не задано, ничего лишнего не отправляется.
Согласие на клонированный голос (OpenAI-совместимые эндпоинты): некоторые самостоятельно размещённые OpenAI-совместимые TTS-серверы отклоняют клонированный голос с ошибкой 400 consent_required, если в запросе нет поля consent_attestation. Задайте в tts.openai.consent_attestation текст подтверждения, который ждёт ваш сервер; Hermes дословно передаёт его в теле запроса на всех OpenAI-совместимых путях (синтез целого файла, стриминг и голос в десктопе напрямую с клиента). Для официального API OpenAI оставляйте поле пустым — тогда оно не отправляется.
Ограничения на длину ввода
У каждого провайдера есть задокументированный потолок символов на один запрос. Hermes разбивает длинные ответы на упорядоченные фрагменты по границам предложений ещё до обращения к провайдеру, поэтому весь нормализованный текст сохраняется, а не обрезается молча:
| Провайдер | Потолок по умолчанию (символов) |
|---|---|
| Edge TTS | 5000 |
| OpenAI | 4096 |
| xAI | 15000 |
| MiniMax | 10000 |
| Mistral | 4000 |
| Google Gemini | 32000 |
| ElevenLabs | Зависит от модели (см. ниже) |
| NeuTTS | 2000 |
| KittenTTS | 2000 |
| Piper | 5000 |
У ElevenLabs потолок берётся из заданного model_id:
model_id | Потолок (символов) |
|---|---|
eleven_flash_v2_5 | 40000 |
eleven_flash_v2 | 30000 |
eleven_multilingual_v2 (по умолчанию), eleven_multilingual_v1, eleven_english_sts_v2, eleven_english_sts_v1 | 10000 |
eleven_v3, eleven_ttv_v3 | 5000 |
| Неизвестная модель | Откат к значению провайдера по умолчанию (10000) |
Переопределить потолок для конкретного провайдера можно через max_text_length: в его секции TTS-конфига:
tts:
openai:
max_text_length: 8192 # поднять или опустить потолок провайдера
Учитываются только положительные целые числа. Ноль, отрицательные, нечисловые и булевы значения проваливаются к значению провайдера по умолчанию — так что кривой конфиг не позволит случайно обойти лимит запроса провайдера.
Голосовые пузырьки Telegram и ffmpeg
Голосовым пузырькам Telegram нужен аудиоформат Opus/OGG:
- OpenAI, ElevenLabs и Mistral отдают Opus сразу — ничего настраивать не нужно
- Edge TTS (по умолчанию) выдаёт MP3, и для конвертации нужен ffmpeg:
- MiniMax TTS выдаёт MP3, и для голосовых пузырьков Telegram его нужно конвертировать через ffmpeg
- Google Gemini TTS выдаёт сырой PCM и использует ffmpeg, чтобы сразу кодировать Opus для голосовых пузырьков Telegram
- xAI TTS выдаёт MP3, и для голосовых пузырьков Telegram его нужно конвертировать через ffmpeg
- NeuTTS выдаёт WAV, и для голосовых пузырьков Telegram его тоже нужно конвертировать через ffmpeg
- KittenTTS выдаёт WAV, и для голосовых пузырьков Telegram его тоже нужно конвертировать через ffmpeg
- Piper выдаёт WAV, и для голосовых пузырьков Telegram его тоже нужно конвертировать через ffmpeg
# Ubuntu/Debian
sudo apt install ffmpeg
# macOS
brew install ffmpeg
# Fedora
sudo dnf install ffmpeg
Без ffmpeg аудио от Edge TTS, MiniMax TTS, NeuTTS, KittenTTS и Piper уходит как обычный аудиофайл — слушать можно, но вместо голосового пузырька показывается прямоугольный плеер.
Хотите голосовые пузырьки, но без установки ffmpeg? Переключитесь на провайдера OpenAI, ElevenLabs или Mistral.
Кастомные голоса xAI (клонирование голоса)
xAI умеет клонировать ваш голос и использовать его в TTS. Создайте свой голос в консоли xAI, затем впишите полученный voice_id в конфиг:
tts:
provider: xai
xai:
voice_id: "nlbqfwie" # ваш собственный voice ID
Подробности про запись, поддерживаемые форматы и лимиты — в документации xAI по кастомным голосам.
Piper (локально, 44 языка)
Piper — быстрый локальный нейросетевой движок TTS от Open Home Foundation (это сопровождающие Home Assistant). Работает целиком на CPU, охватывает 44 языка с предобученными голосами и не требует API-ключа.
Установка через hermes tools → Voice & TTS → Piper. Hermes запросит extra piper
через PM. Из подготовленного чекаута исходников явная команда такая:
python -c "import pm; pm.sync_venv(['piper'], explicit=True)". Платформенные маркеры по-прежнему действуют.
Переключение на Piper:
tts:
provider: piper
piper:
voice: en_US-lessac-medium
При первом TTS-вызове для голоса, которого ещё нет в локальном кеше, Hermes выполняет python -m piper.download_voices <name> и скачивает модель (~20–90МБ — зависит от уровня качества) в ~/.hermes/cache/piper-voices/. Дальше уже используется кешированная модель.
Выбор голоса. Полный каталог голосов покрывает английский, испанский, французский, немецкий, итальянский, нидерландский, португальский, русский, польский, турецкий, китайский, арабский, хинди и другие — у каждого есть уровни качества x_low / low / medium / high. Послушать образцы можно на rhasspy.github.io/piper-samples.
Заранее скачанный голос. Укажите в tts.piper.voice абсолютный путь, оканчивающийся на .onnx:
tts:
piper:
voice: /path/to/my-custom-voice.onnx
Тонкие настройки (tts.piper.length_scale / noise_scale / noise_w_scale / volume / normalize_audio, use_cuda) один в один соответствуют SynthesisConfig у Piper. На старых версиях piper-tts они игнорируются.
Прогрев и выгрузка по переключателям речи (локальные движки)
Локальные движки (Piper, KittenTTS) грузят модель лениво, поэтому без посторонней помощи первый озвученный ответ после включения речи оплачивает всю загрузку модели — а на свежей установке ещё и скачивание голоса — тишиной перед первым словом. Переключатели вывода речи Hermes считает сигналом, что TTS вот-вот понадобится:
- Десктоп — Read replies aloud живёт в самом десктопе и не зависит от настройки шлюза
voice.auto_ttsв Settings → Voice. Общее значение переносится один раз, а дальше правки конфигурации шлюза десктопный переключатель уже не перебивают. Если локальное хранилище переполнено или недоступно, выбор всё равно продержится до конца работы окна; сохранение между перезагрузками остаётся «как получится». Включение Read replies aloud или запуск голосового разговора сразу прогревает настроенный движок в фоне. Выключение обоих выгружает модель из памяти (голос Piper весит десятки мегабайт, KittenTTS — до ~80 МБ), чтобы она не занимала ОЗУ впустую. - CLI и TUI — то же делают
/voice tts(и/voice on, когда заданvoice.auto_tts);/voice offосвобождает.
Каждый переключатель держит на движке аренду, и модель выгружается лишь тогда, когда отпущена последняя аренда по всем интерфейсам, — так выключение чтения вслух в одном окне десктопа не выдернет голос из-под разговора в другом. Выгрузка ждёт tts.keep_warm_seconds (по умолчанию 60) после последнего освобождения, и если за это время какой-то переключатель снова включит речь, загруженная модель останется: цикл с wake-word или быстро перезапущенный голосовой разговор не будут каждый раз заново грузить голос. Поставьте 0, чтобы выгружать сразу. У облачных провайдеров держать нечего: переключатель лишь проверяет, что лениво устанавливаемый SDK (edge-tts, ElevenLabs, Mistral) на месте. Прогрев работает по принципу «как получится»: если движок не загрузился, переключатель всё равно срабатывает, а первый ответ, как и раньше, дождётся загрузки по требованию.
Десктоп вызывает POST /api/audio/tts-lease с телом {"lease": "<name>", "active": true|false}; тот же эндпоинт доступен и другим фронтендам.
Та же аренда доходит и до провайдеров, объявленных пользователем, поэтому самостоятельно размещённый TTS-сервер может грузить и выгружать модель по переключателям: провайдер на основе команды выполняет необязательные warm_command / release_command, а провайдер в виде Python-плагина получает вызовы warm() / release().
Провайдеры на основе команд
Нужного вам TTS-движка нет среди встроенных (VoxCPM, MLX-Kokoro, XTTS CLI, скрипт клонирования голоса — да что угодно с интерфейсом командной строки)? Подключите его как провайдер типа command, не написав ни строчки Python. Hermes записывает входной текст во временный UTF-8-файл, запускает вашу shell-команду и читает аудиофайл, который та сгенерировала.
Объявите один или несколько провайдеров под tts.providers.<name> и переключайтесь между ними через tts.provider: <name> — ровно так же, как между встроенными edge и openai.
tts:
provider: voxcpm # любое имя из tts.providers
providers:
voxcpm:
type: command
command: "voxcpm --ref ~/voice.wav --text-file {input_path} --out {output_path}"
output_format: mp3
timeout: 180
voice_compatible: true # попытаться доставить как голосовой пузырёк Telegram
mlx-kokoro:
type: command
command: "python -m mlx_kokoro --in {input_path} --out {output_path} --voice {voice}"
voice: af_sky
output_format: wav
piper-custom: # нативный Piper тоже поддерживает кастомный .onnx через tts.piper.voice
type: command
command: "piper -m /path/to/custom.onnx -f {output_path} < {input_path}"
output_format: wav
Допустимые значения output_format: mp3 (по умолчанию), wav, ogg, flac, m4a, aac, amr, opus. Команда обязана реально выдавать заявленный формат (скажем, через ffmpeg); Hermes лишь проверяет само значение и соответственно называет выходной файл. Неизвестное значение откатывается к mp3. Выбранный формат доступен команде и через подстановку {format}.
Окружение подпроцесса: командные провайдеры (и TTS, и STT) запускаются с вычищенными секретами Hermes — токены ботов шлюза, API-ключи провайдеров LLM и внутренние учётные данные релея из дочернего окружения убираются; PATH, HOME, локаль и прочие обычные переменные остаются. Если вашему шаблону команды нужен собственный API-ключ из окружения (например, однострочнику на curl), перечислите имена переменных в env_passthrough конфигурации провайдера:
tts:
providers:
mycloud:
type: command
command: 'curl -s -H "Authorization: Bearer $MYCLOUD_API_KEY" ... -o {output_path}'
env_passthrough: [MYCLOUD_API_KEY]
Пример: Doubao (китайский seed-tts-2.0)
Для качественного китайского TTS через API двунаправленного стриминга seed-tts-2.0 от ByteDance поставьте PyPI-пакет doubao-speech и подключите его как командный провайдер:
Ставьте этот внешний командный провайдер в отдельное окружение для инструментов, а не в Python-окружение Hermes. Его исполняемый файл должен быть доступен в PATH.
pip install doubao-speech
export VOLCENGINE_APP_ID="your-app-id"
export VOLCENGINE_ACCESS_TOKEN="your-access-token"
tts:
provider: doubao
providers:
doubao:
type: command
command: "doubao-speech say --text-file {input_path} --out {output_path}"
output_format: mp3
max_text_length: 1024
timeout: 30
Учётные данные берутся из окружения shell (VOLCENGINE_APP_ID / VOLCENGINE_ACCESS_TOKEN) либо из ~/.doubao-speech/config.yaml. Голос задаётся добавлением --voice zh-female-warm (или любого другого алиаса из doubao-speech list-voices) к команде. В doubao-speech встроен ещё и стриминговый ASR — про интеграцию с Hermes смотрите раздел STT ниже. Исходники и полная документация: github.com/Hypnus-Yuan/doubao-speech.
Плейсхолдеры
В шаблоне команды можно ссылаться на эти плейсхолдеры. Hermes подставляет их в момент рендера и экранирует каждое значение под окружающий контекст shell (без кавычек / в одинарных / в двойных), так что пути с пробелами и прочими спецсимволами обрабатываются безопасно.
| Плейсхолдер | Значение |
|---|---|
{input_path} | Путь к временному UTF-8-файлу с текстом, записанному Hermes |
{text_path} | Алиас для {input_path} |
{output_path} | Путь, куда команда обязана записать аудио |
{format} | mp3 / wav / ogg / flac |
{voice} | tts.providers.<name>.voice, пусто если не задан |
{model} | tts.providers.<name>.model |
{speed} | Итоговый множитель скорости (провайдера или глобальный) |
Для буквальных фигурных скобок используйте {{ и }}.
Необязательные ключи
| Ключ | По умолчанию | Значение |
|---|---|---|
timeout | 120 | Секунды простоя; любой вывод в stdout или stderr сбрасывает отсчёт. Дерево процессов убивается после бездействия (Unix killpg, Windows taskkill /T). |
output_format | mp3 | Одно из mp3 / wav / ogg / flac. Если путь выбирает Hermes, определяется автоматически по расширению. |
voice_compatible | false | При true Hermes конвертирует вывод MP3/WAV в Opus/OGG через ffmpeg, чтобы Telegram показал голосовой пузырёк. |
max_text_length | 5000 | Максимум символов ввода на один вызов команды; более длинный текст разбивается на упорядоченные фрагменты. |
voice / model | пусто | Передаются в команду только как значения плейсхолдеров. |
warm_command / release_command | не задано | Shell-команды, которые выполняются, когда интерфейс включает вывод речи и когда отпущена последняя аренда по всем интерфейсам, — например, curl -s localhost:5002/load?model={model} для предзагрузки локального TTS-сервера и парная ей unload. Работают «как получится» и не блокируют: идут в фоне с теми же timeout, env_passthrough и плейсхолдерами {voice} / {model} / {speed}, что и command; вывод отбрасывается, а сбои пишутся только в отладочный лог. |
Заметки о поведении
- Встроенные имена всегда побеждают. Запись
tts.providers.openaiникогда не перекрывает нативный провайдер OpenAI, так что никакой пользовательский конфиг не подменит встроенный молча. - По умолчанию доставка — документ. Командные провайдеры на любой платформе отдают вывод как обычное аудиовложение. Чтобы включить доставку голосовым пузырьком для конкретного провайдера, поставьте
voice_compatible: true. - Сбои команды видны агенту. Ненулевой код выхода, пустой вывод или таймаут — всё возвращается как ошибка вместе со stderr/stdout команды, так что отлаживать провайдера можно прямо из разговора.
type: commandподразумевается, когда заданcommand:. Писатьtype: commandявно — хорошая привычка, но не обязательно; запись с непустой строкойcommandуже считается командным провайдером.{input_path}и{text_path}взаимозаменяемы. Берите тот, который читается в вашей команде лучше.
Безопасность
Командные провайдеры запускают любую shell-команду, которую вы настроите, с правами вашего пользователя. Hermes экранирует значения плейсхолдеров и соблюдает заданный таймаут, но сам шаблон команды — доверенный локальный ввод; относитесь к нему так же, как к shell-скрипту в вашем PATH.
Провайдеры в виде Python-плагинов
Для TTS-движков, которые не сводятся к одной shell-команде, — Python-SDK без CLI, стриминговые движки, API со списком голосов, авторизация с обновлением OAuth — зарегистрируйте Python-плагин через ctx.register_tts_provider(). Плагин сосуществует с реестром провайдеров на основе команд (а не заменяет его); выбирайте подход под свой движок.
Что когда выбирать
| У вашего бэкенда есть… | Берите |
|---|---|
| Один CLI, читающий текст из файла/stdin и пишущий аудио в файл/stdout | Командный провайдер (без Python) |
| Две-три CLI, сцепленные через shell-пайпы | Командный провайдер |
| Только Python-SDK, без CLI | Плагин |
| Движок, который отдаёт звук по ходу синтеза, и вы хотите, чтобы озвучка ответа начиналась с первого предложения | Плагин (задайте streams_pcm, переопределите stream()) |
API со списком голосов для hermes setup | Плагин (переопределите list_voices()) |
| Поток обновления OAuth (а не статичный bearer-токен) | Плагин |
Встроенные всегда побеждают, а командные провайдеры — одноимённый плагин, так что плагины можно безопасно регистрировать под любым не-встроенным именем, не переживая за подмену существующего конфига.
Минимальный плагин
Положите это в ~/.hermes/plugins/my-tts/:
plugin.yaml:
name: my-tts
version: 0.1.0
description: "My custom Python TTS backend"
__init__.py:
from agent.tts_provider import TTSProvider
class MyTTSProvider(TTSProvider):
@property
def name(self) -> str:
return "my-tts" # с чем сопоставляется tts.provider
@property
def display_name(self) -> str:
return "My Custom TTS"
def is_available(self) -> bool:
# Возвращайте False, когда нет учётных данных/зависимостей — выбор
# пропустит эту строку, но диспетчер всё равно направит сюда при явном конфиге.
import os
return bool(os.environ.get("MY_TTS_API_KEY"))
def synthesize(self, text, output_path, *, voice=None, model=None,
speed=None, format="mp3", **extra) -> str:
# Запишите байты аудио в output_path, верните путь.
# Бросайте исключение при сбое — диспетчер превратит исключения
# в стандартный конверт ошибки.
import my_tts_sdk
client = my_tts_sdk.Client()
audio_bytes = client.synthesize(text=text, voice=voice or "default")
with open(output_path, "wb") as f:
f.write(audio_bytes)
return output_path
def register(ctx):
ctx.register_tts_provider(MyTTSProvider())
Включите его (hermes plugins enable my-tts), направьте на него tts.provider (tts.provider: my-tts в config.yaml) — и инструмент text_to_speech пойдёт через ваш плагин.
Необязательные хуки
Переопределите их в классе провайдера ради более глубокой интеграции:
list_voices()→ список словарей{id, display, language, gender, preview_url}, показываемых вhermes tools.list_models()→ список словарей{id, display, languages, max_text_length}.get_setup_schema()→ вернуть{name, badge, tag, env_vars: [{key, prompt, url}]}для строки выбора вhermes tools/hermes setup. Без этого плагин всё равно работает, но строка в списке выбора будет минимальной.stream(text, *, voice, model, format, **extra)→ итератор, выдающий байты аудио (по умолчанию бросаетNotImplementedError).streams_pcm = True+stream_sample_rate(Гц) → подключение к потоковому голосовому пути (голосовой режим CLI/TUI, озвучка в десктопном приложении, потоковое аудио шлюза). Тогда Hermes вызываетstream(text, format="pcm", voice=..., model=..., speed=...)с теми жеtts.voice/tts.model/tts.speed, что получаетsynthesize(), и ждёт сырой PCM int16 little-endian, моно, с этой частотой. И оба атрибута, иis_available()читаются при каждом начале ответа, поэтому могут быть свойствами с живым состоянием. В отличие отsynthesize(),stream()могут вызвать для трёх предложений подряд одновременно, пока играет предыдущее аудио, так что он обязан быть потокобезопасным. Еслиstream_sample_rateне положительный илиis_available()возвращаетFalse, Hermes продолжает синтезировать по одному предложению.- свойство
voice_compatible→ поставьтеTrue, если вывод совместим с Opus и шлюз должен доставлять его голосовым пузырьком (по умолчаниюFalse= обычное аудиовложение). warm()/release()→ вызываются, когда интерфейс включает вывод речи и когда отпущена последняя аренда по всем интерфейсам, — но только пока ваш провайдер стоит вtts.provider. Здесь уместно предзагрузить или выгрузить локальный сервер модели. По умолчанию оба ничего не делают; исключения пишутся в отладочный лог и переключатель не ломают.
Полный ABC с docstring-ами смотрите в agent/tts_provider.py.
Расшифровка голосовых сообщений (STT)
Голосовые сообщения, присланные в Telegram, Discord, WhatsApp, Slack или Signal, автоматически расшифровываются и вставляются в разговор как текст. Агент видит расшифровку как обычный текст.
| Провайдер | Качество | Стоимость | API-ключ |
|---|---|---|---|
| Local Whisper (по умолчанию) | Хорошее | Бесплатно | Не нужен |
| Groq Whisper API | Хорошее–Лучшее | Бесплатный тариф | GROQ_API_KEY |
| OpenAI Whisper API | Хорошее–Лучшее | Платно | VOICE_TOOLS_OPENAI_KEY или OPENAI_API_KEY |
Локальная расшифровка работает сразу, если установлен faster-whisper. Когда его нет, Hermes может задействовать локальный whisper CLI из обычных мест установки (вроде /opt/homebrew/bin) либо кастомную команду через HERMES_LOCAL_STT_COMMAND.
Настройка
# In ~/.hermes/config.yaml
stt:
provider: "local" # "local" | "groq" | "openai" | "mistral" | "xai" | "elevenlabs" | "deepinfra"
language: "en" # Глобальная подсказка языка для всех провайдеров, пока её не переопределит настройка конкретного провайдера; "" возвращает автоопределение
local:
model: "base" # tiny, base, small, medium, large-v3, turbo
language: "" # необязательная подсказка ISO-639-1; пусто = взять HERMES_LOCAL_STT_LANGUAGE, если задана, иначе автоопределение
groq:
language: "" # необязательная подсказка ISO-639-1; пусто = взять HERMES_LOCAL_STT_LANGUAGE, если задана, иначе автоопределение
openai:
model: "whisper-1" # whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe, gpt-transcribe
mistral:
model: "voxtral-mini-latest" # voxtral-mini-latest, voxtral-mini-2602
xai:
model: "grok-voice-transcribe-2.0" # или grok-voice-transcribe-1.0
language: "" # необязательная подсказка ISO-639-1; пусто = взять HERMES_LOCAL_STT_LANGUAGE, если задана, иначе "en"
Детали провайдеров
Local (faster-whisper) — запускает Whisper локально через faster-whisper. По умолчанию работает на CPU, при наличии — на GPU. Размеры моделей:
| Модель | Размер | Скорость | Качество |
|---|---|---|---|
tiny | ~75 МБ | Самая быстрая | Базовое |
base | ~150 МБ | Быстро | Хорошее (по умолчанию) |
small | ~500 МБ | Средне | Лучше |
medium | ~1.5 ГБ | Медленнее | Отличное |
large-v3 | ~3 ГБ | Самая медленная | Лучшее |
При первом использовании выбранная модель скачивается с huggingface.co; последующие загрузки предпочитают локальный кэш и не требуют онлайн-проверки ревизии. В сетях, где Hub недоступен, экспортируйте доступное зеркало в shell или сервисе, который запускает Hermes:
HF_ENDPOINT=https://your-hugging-face-mirror.example
HF_HUB_DISABLE_XET=1
HF_HUB_DISABLE_XET=1 оставляет скачивание на обычном HTTP-пути зеркала, вместо обращения к хостам Xet CAS, которые HF_ENDPOINT не учитывают.
Groq API — нужен GROQ_API_KEY. Хороший облачный запасной вариант, когда хочется бесплатного хостинга STT. Задайте stt.groq.language (или глобальную переменную HERMES_LOCAL_STT_LANGUAGE), чтобы пропустить автоопределение Whisper и снизить задержку на аудио с заранее известным языком.
OpenAI API — сначала принимает VOICE_TOOLS_OPENAI_KEY, затем откатывается к OPENAI_API_KEY. Поддерживает whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe и gpt-transcribe.
Mistral API (Voxtral Transcribe) — нужен MISTRAL_API_KEY. Использует модели Voxtral Transcribe от Mistral. Поддерживает 13 языков, диаризацию говорящих и временные метки на уровне слов. Установка: cd ~/.hermes/hermes-agent && python -c "import pm; pm.sync_venv(['mistral'], explicit=True)".
xAI Grok STT — нужен XAI_API_KEY (или OAuth xAI). Отправляет на https://api.x.ai/v1/stt как multipart/form-data и передаёт model (по умолчанию grok-voice-transcribe-2.0; закрепить grok-voice-transcribe-1.0 можно через stt.xai.model или STT_XAI_MODEL). Hermes всегда называет модель явно, так что смена значения по умолчанию на сервере не подменит то, что вы запускаете. При stt.language: "" (автоопределение) флаг format отбрасывается: для форматирования текста xAI требует язык. Хороший выбор, если xAI у вас уже задействован для чата или TTS и хочется один ключ на всё. В порядке автоопределения он идёт после Mistral — чтобы форсировать его, явно задайте stt.provider: xai.
Запасная локальная CLI — задайте HERMES_LOCAL_STT_COMMAND, если хотите, чтобы Hermes вызывал локальную команду расшифровки напрямую. Шаблон команды поддерживает плейсхолдеры {input_path}, {output_dir}, {language} и {model}. Отрендеренный шаблон Hermes разбирает на список аргументов и выполняет без shell, поэтому операторы вроде |, >, && и ; передаются буквальными аргументами. Ваша команда обязана записать расшифровку в .txt где-то под {output_dir}.
Пример: Doubao / Volcengine ASR
Если для Doubao TTS вы используете doubao-speech (см. выше), тот же пакет берёт на себя и распознавание речи через локально-командный интерфейс STT:
Ставьте этот внешний командный провайдер в отдельное окружение для инструментов, а не в Python-окружение Hermes. Его исполняемый файл должен быть доступен в PATH.
pip install doubao-speech
export VOLCENGINE_APP_ID="your-app-id"
export VOLCENGINE_ACCESS_TOKEN="your-access-token"
export HERMES_LOCAL_STT_COMMAND='doubao-speech transcribe {input_path} --out {output_dir}/transcript.txt'
Если доверенному локальному шаблону намеренно нужны конвейеры, перенаправления или другие возможности shell, вызывайте оболочку явно. Динамические пути держите вне программы shell и передавайте позиционными аргументами:
export HERMES_LOCAL_STT_COMMAND='sh -c '\''whisper "$1" --output_format txt --output_dir "$2" | tee "$2/whisper.log"'\'' _ {input_path} {output_dir}'
На Windows используйте явную обёртку cmd /c или PowerShell. Явная обёртка делает интерпретацию shell осознанной частью настроенного argv, а не неявным свойством каждого локального STT-шаблона.
stt:
provider: local_command
Hermes записывает входящее голосовое сообщение в {input_path}, запускает команду и читает .txt-файл, созданный под {output_dir}. Язык определяется автоматически эндпоинтом bigmodel от Volcengine.
Поведение при откате
Явный выбор stt.provider (записанный в config.yaml, например через hermes tools) соблюдается строго: если этот провайдер не может отработать, расшифровка завершится понятной ошибкой (stt is configured to use <provider> (set via hermes tools), but <failure>. Run 'hermes tools' to change it.), а не тихой сменой движка. Учтите, что stt.provider: local, прописанный в конфиге, тоже считается явным выбором.
Когда провайдер не выбирался ни разу, Hermes определяет его автоматически — по тому, что доступно:
- Локальный faster-whisper недоступен → пробует локальный
whisperCLI илиHERMES_LOCAL_STT_COMMANDраньше облачных провайдеров - Ключ Groq не задан → пропускается; берётся следующий доступный провайдер
- Ключ OpenAI не задан → пропускается; берётся следующий доступный провайдер
- Ключ/SDK Mistral не заданы → пропускается в автоопределении; управление уходит к следующему доступному провайдеру
- Ничего недоступно → голосовые сообщения проходят дальше с точной пометкой для пользователя
STT: провайдеры на основе команд
Нужного STT-движка нет среди встроенных (Doubao ASR, NVIDIA Parakeet, сборка whisper.cpp, опенсорсная CLI SenseVoice — что угодно с интерфейсом командной строки)? Подключите его как провайдер типа command, не написав ни строчки Python. Hermes запускает вашу shell-команду на аудиофайле и считывает обратно расшифровку.
Объявите один или несколько провайдеров под stt.providers.<name> и переключайтесь между ними через stt.provider: <name> — та же форма, что и в реестре командных провайдеров для TTS, только под направление вход=аудио → выход=расшифровка.
stt:
provider: parakeet # любое имя из stt.providers
providers:
parakeet:
type: command
command: "parakeet-asr --model nvidia/parakeet-tdt-0.6b-v2 --in {input_path} --out {output_path}"
format: txt
language: en
timeout: 300
whispercpp:
type: command
command: "whisper-cli -m ~/models/ggml-large-v3.bin -f {input_path} -otxt -of {output_dir}/transcript"
format: txt
sensevoice:
type: command
command: "sensevoice-cli {input_path} --json | tee {output_path}"
format: json
Это дополняет старый запасной механизм HERMES_LOCAL_STT_COMMAND через встроенный путь local_command. В отличие от реестра командных провайдеров, работающего через shell, старый шаблон разбирается в argv и выполняется без неявной интерпретации оболочкой. Используйте stt.providers.<name>, когда нужно несколько STT-движков на shell-командах, имя, которое можно выбрать через stt.provider, или что-то, требующее language / model / timeout для каждого провайдера в отдельности.
STT-плейсхолдеры
В шаблоне команды можно ссылаться на эти плейсхолдеры. Hermes подставляет их в момент рендера и экранирует каждое значение под окружающий контекст shell (без кавычек / в одинарных / в двойных), так что пути с пробелами безопасны.
| Плейсхолдер | Значение |
|---|---|
{input_path} | Абсолютный путь к входному аудиофайлу (исходное расположение, только чтение; при normalize: true — m4a 16 кГц, моно) |
{output_path} | Абсолютный путь, куда команда должна записать расшифровку |
{output_dir} | Родительский каталог {output_path} (удобно для инструментов в стиле whisper) |
{format} | Заданный формат вывода: txt / json / srt / vtt |
{language} | Заданный код языка (по умолчанию en) |
{model} | stt.providers.<name>.model, пусто если не задан |
Для буквальных фигурных скобок используйте {{ и }} (удобно, когда в команду встраивается JSON-фрагмент).
Как считывается расшифровка
После успешного завершения команды:
- Если
{output_path}существует и непустой → Hermes читает его как UTF-8-текст. - Иначе, если команда писала в stdout → Hermes берёт его.
- Иначе → ошибка: “Command STT provider wrote no output file and produced no stdout”.
Так реестр годится и для CLI, пишущих в файл (whisper-cli, parakeet-asr), и для однострочников в стиле curl, которые выдают расшифровку в stdout (curl … | jq -r .text).
Для format: json / srt / vtt Hermes возвращает сырое содержимое файла в поле transcript. Извлечение .text из JSON выходит за рамки раннера — либо настройте format: txt, либо обработайте JSON дальше по конвейеру.
STT-командный провайдер: необязательные ключи
| Ключ | По умолчанию | Значение |
|---|---|---|
timeout | 300 | Секунды; по истечении дерево процессов убивается (Unix start_new_session, Windows taskkill /T). |
format | txt | Одно из txt / json / srt / vtt. Задаёт расширение {output_path}. |
language | en | Передаётся в {language}. По умолчанию — stt.language, затем en. |
model | пусто | Передаётся в {model}. Аргумент model= у transcribe_audio() его переопределяет. |
normalize | false | Перекодировать вход в m4a 16 кГц, моно (ffmpeg) до запуска команды; тогда {input_path} указывает на нормализованный файл. |
STT-командный провайдер: заметки о поведении
- Встроенные всегда побеждают. Объявление
stt.providers.openai: type: commandНЕ перекрывает настоящий обработчик OpenAI Whisper. Встроенное имя перехватывается раньше, чем запустится резолвер командных провайдеров. - Очистка дерева процессов. У команды, перешагнувшей
timeout, убивается всё дерево процессов, а не только оболочка. Долгие ASR-конвейеры, форкающие подпроцессы загрузки модели, надёжно подчищаются. - Экранирование shell автоматическое. Плейсхолдеры внутри
'…'получают безопасное для одинарных кавычек экранирование; внутри"…"— экранирование$/`/"; вне кавычек —shlex.quote. Не экранируйте значения плейсхолдеров заранее.
STT-командный провайдер: безопасность
Shell-команда запускается под тем же пользователем, что и Hermes, с полным доступом к файловой системе — та же модель доверия, что у tts.providers.<name>: type: command и HERMES_LOCAL_STT_COMMAND. Объявляйте командные провайдеры только из источников, которым доверяете.
Провайдеры в виде Python-плагинов (STT)
Для STT-движков, которых нет среди встроенных И которые нельзя выразить shell-командой (нужен Python-SDK, авторизация с обновлением OAuth, потоковые куски и т. п.), зарегистрируйте Python-плагин через ctx.register_transcription_provider(). Плагин сосуществует с восемью встроенными провайдерами (local, local_command, groq, openai, mistral, xai, elevenlabs, deepinfra) и реестром stt.providers.<name>: type: command — встроенные сохраняют свои нативные реализации и всегда побеждают при совпадении имён; командные провайдеры побеждают одноимённые плагины (конфиг локальнее, чем установка плагина).
Что когда выбирать (STT)
| У бэкенда есть… | Берите |
|---|---|
| Одна shell-команда, принимающая аудиофайл и выдающая текст | stt.providers.<name>: type: command (без Python) |
| Нужен только старый запасной механизм с одной командой | переменная окружения HERMES_LOCAL_STT_COMMAND (разбирается в argv; без неявного shell) |
| Python-SDK без CLI | плагин register_transcription_provider() |
| Авторизация с обновлением OAuth, потоковые куски, метаданные списка голосов | плагин register_transcription_provider() |
Уже покрыто встроенным (local, groq, openai, …) | задайте stt.provider: <name> — встроенные работают напрямую |
Порядок разрешения
stt.provider— встроенное имя → диспетчеризация встроенного. Всегда побеждает.stt.providerсовпадает сstt.providers.<name>, где заданcommand:→ раннер командного провайдера (см. STT: провайдеры на основе команд). Побеждает одноимённый плагин.stt.providerсовпадает сTranscriptionProvider, зарегистрированным плагином → диспетчеризация плагина:- если
is_available()плагина возвращаетFalse(нет учётных данных или SDK), вызов выдаёт конверт ошибки о недоступности с указанием плагина — а не общее сообщение “No STT provider available”. - иначе вызывается
transcribe()плагина сmodel(из публичного аргументаmodel=, с откатом кstt.<provider>.model) иlanguage(изstt.<provider>.language).
- если
- Совпадений нет → ошибка “No STT provider available”.
Пространство имён для конфига каждого провайдера
Плагины читают свою настройку из stt.<provider> в config.yaml — так же, как встроенные читают stt.openai.model / stt.mistral.model:
stt:
provider: my-stt
my-stt:
model: whisper-large-v3
language: ja # передаётся как language= в transcribe()
# любые другие ключи плагина — сюда; читайте их через собственный
# доступ к config.yaml в __init__/is_available/transcribe
Диспетчер пробрасывает model и language из этой секции; всё остальное плагин читает сам.
Минимальный плагин
Положите это в ~/.hermes/plugins/my-stt/:
plugin.yaml:
name: my-stt
version: 0.1.0
description: "My custom Python STT backend"
__init__.py:
from agent.transcription_provider import TranscriptionProvider
class MySTTProvider(TranscriptionProvider):
@property
def name(self) -> str:
return "my-stt" # с чем сопоставляется stt.provider
@property
def display_name(self) -> str:
return "My Custom STT"
def is_available(self) -> bool:
# Возвращайте False, когда нет учётных данных/зависимостей — выбор
# пропустит эту строку, но диспетчер всё равно направит сюда при явном конфиге.
import os
return bool(os.environ.get("MY_STT_API_KEY"))
def transcribe(self, file_path, *, model=None, language=None, **extra):
# Верните стандартный конверт расшифровки:
# {"success": bool, "transcript": str, "provider": str, "error": str}
# НЕ бросайте исключения — превращайте их в конверт ошибки, чтобы
# вызывающая сторона (шлюз/CLI) видела единообразную форму при сбое.
try:
import my_stt_sdk
client = my_stt_sdk.Client()
text = client.transcribe(open(file_path, "rb"))
return {
"success": True,
"transcript": text,
"provider": "my-stt",
}
except Exception as exc:
return {
"success": False,
"transcript": "",
"error": f"my-stt failed: {exc}",
"provider": "my-stt",
}
def register(ctx):
ctx.register_transcription_provider(MySTTProvider())
Включите его (hermes plugins enable my-stt), задайте stt.provider: my-stt в config.yaml — и расшифровка голосовых сообщений пойдёт через ваш плагин.
Необязательные хуки
Переопределите их в классе провайдера ради более глубокой интеграции:
list_models()→ список словарей{id, display, languages, max_audio_seconds}.default_model()→ строка, возвращаемая, когда пользователь не переопределяет модель.get_setup_schema()→ вернуть{name, badge, tag, env_vars: [{key, prompt, url}]}для строк выбора вhermes tools/hermes setup(категория выбора для STT пока не поставляется — эти метаданные доступны плагинам на будущее, ради совместимости).
Полный ABC с docstring-ами смотрите в agent/transcription_provider.py.