Голос и TTS

Команды установки Python-зависимостей на этой странице рассчитаны на чекаут исходников, подготовленный через PM. После изменения зависимостей заново активируйте чекаут и перезапустите Hermes.

Hermes Agent умеет и озвучивать текст, и расшифровывать голосовые сообщения — причём на всех платформах обмена сообщениями.

совет
Подписчикам Nous

Есть платная подписка 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
WhatsAppАудиофайл-вложение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 TTS5000
OpenAI4096
xAI15000
MiniMax10000
Mistral4000
Google Gemini32000
ElevenLabsЗависит от модели (см. ниже)
NeuTTS2000
KittenTTS2000
Piper5000

У ElevenLabs потолок берётся из заданного model_id:

model_idПотолок (символов)
eleven_flash_v2_540000
eleven_flash_v230000
eleven_multilingual_v2 (по умолчанию), eleven_multilingual_v1, eleven_english_sts_v2, eleven_english_sts_v110000
eleven_v3, eleven_ttv_v35000
Неизвестная модельОткат к значению провайдера по умолчанию (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}Итоговый множитель скорости (провайдера или глобальный)

Для буквальных фигурных скобок используйте {{ и }}.

Необязательные ключи

КлючПо умолчаниюЗначение
timeout120Секунды простоя; любой вывод в stdout или stderr сбрасывает отсчёт. Дерево процессов убивается после бездействия (Unix killpg, Windows taskkill /T).
output_formatmp3Одно из mp3 / wav / ogg / flac. Если путь выбирает Hermes, определяется автоматически по расширению.
voice_compatiblefalseПри true Hermes конвертирует вывод MP3/WAV в Opus/OGG через ffmpeg, чтобы Telegram показал голосовой пузырёк.
max_text_length5000Максимум символов ввода на один вызов команды; более длинный текст разбивается на упорядоченные фрагменты.
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 недоступен → пробует локальный whisper CLI или 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-фрагмент).

Как считывается расшифровка

После успешного завершения команды:

  1. Если {output_path} существует и непустой → Hermes читает его как UTF-8-текст.
  2. Иначе, если команда писала в stdout → Hermes берёт его.
  3. Иначе → ошибка: “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-командный провайдер: необязательные ключи

КлючПо умолчаниюЗначение
timeout300Секунды; по истечении дерево процессов убивается (Unix start_new_session, Windows taskkill /T).
formattxtОдно из txt / json / srt / vtt. Задаёт расширение {output_path}.
languageenПередаётся в {language}. По умолчанию — stt.language, затем en.
modelпустоПередаётся в {model}. Аргумент model= у transcribe_audio() его переопределяет.
normalizefalseПерекодировать вход в 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> — встроенные работают напрямую

Порядок разрешения

  1. stt.provider — встроенное имя → диспетчеризация встроенного. Всегда побеждает.
  2. stt.provider совпадает с stt.providers.<name>, где задан command: → раннер командного провайдера (см. STT: провайдеры на основе команд). Побеждает одноимённый плагин.
  3. stt.provider совпадает с TranscriptionProvider, зарегистрированным плагином → диспетчеризация плагина:
    • если is_available() плагина возвращает False (нет учётных данных или SDK), вызов выдаёт конверт ошибки о недоступности с указанием плагина — а не общее сообщение “No STT provider available”.
    • иначе вызывается transcribe() плагина с model (из публичного аргумента model=, с откатом к stt.<provider>.model) и language (из stt.<provider>.language).
  4. Совпадений нет → ошибка “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.

перевод и редактура:

обновлено

⌕ ESC