Pular para o conteúdo principal

Documentação oficial desta página · v0.20.6

Voz e síntese de fala

O Hermes Agent faz tanto a leitura em voz alta quanto a transcrição de mensagens de áudio, em todas as plataformas de mensagem.

Assinantes da Nous

Com uma assinatura paga do Nous Portal, a síntese da OpenAI vem pelo Tool Gateway, sem chave separada. Instalações novas podem rodar hermes setup --portal para autenticar e ligar todas as ferramentas do gateway de uma vez; instalações existentes escolhem Nous Subscription só para a síntese, em hermes model ou hermes tools.

Síntese de fala​

Converta texto em voz com onze provedores:

ProvedorQualidadeCustoChave
Edge TTS (padrão)BoaGrátisNenhuma
ElevenLabsExcelentePagoELEVENLABS_API_KEY
OpenAI TTSBoaPagoVOICE_TOOLS_OPENAI_KEY
MiniMax TTSExcelentePagoMINIMAX_API_KEY ou MINIMAX_CN_API_KEY
Mistral (Voxtral TTS)ExcelentePagoMISTRAL_API_KEY
Google Gemini TTSExcelenteCamada gratuitaGEMINI_API_KEY
xAI TTSExcelentePagoXAI_API_KEY
DeepInfra TTSBoaPagoDEEPINFRA_API_KEY
NeuTTSBoaGrátis (local)Nenhuma
KittenTTSBoaGrátis (local)Nenhuma
PiperBoaGrátis (local)Nenhuma
Vozes em português

O Edge TTS, que é o padrão e não pede chave, traz vozes brasileiras nativas — pt-BR-FranciscaNeural, pt-BR-AntonioNeural e pt-BR-ThalitaNeural, entre outras. O Piper também cobre português nas suas 44 línguas. Para começar em português sem gastar nada, é o caminho.

Entrega por plataforma​

PlataformaEntregaFormato
TelegramBolha de voz (toca na conversa)Opus .ogg
DiscordBolha de voz (Opus/OGG), com recuo para anexoOpus/MP3
WhatsAppArquivo de áudio anexadoMP3
CLISalvo em ~/.hermes/audio_cache/MP3

Configuração​

# em ~/.hermes/config.yaml
tts:
provider: "edge" # "edge" | "elevenlabs" | "openai" | "minimax" | "mistral" | "gemini" | "xai" | "deepinfra" | "neutts" | "kittentts" | "piper" — ou "nous", para o Tool Gateway gerenciado
speed: 1.0 # multiplicador global (as opções por provedor sobrescrevem)
edge:
voice: "pt-BR-FranciscaNeural" # 322 vozes, 74 idiomas
speed: 1.0 # convertido em porcentagem de ritmo (+/-%)
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" # para endpoints compatíveis
speed: 1.0 # 0,25 a 4,0
# language: "pt" # enviado como lang_code — só em endpoints compatíveis que aceitam (Kokoro, por exemplo)
minimax:
region: "global" # "global" ou "cn"; veja as regras abaixo
model: "speech-02-hd" # speech-02-hd (padrão), speech-02-turbo
voice_id: "English_expressive_narrator"
speed: 1 # 0,5 a 2,0
vol: 1 # 0 a 10
pitch: 0 # -12 a 12
# base_url: "https://tts.exemplo/v1/t2a_v2" # troca o endpoint da região escolhida
mistral:
model: "voxtral-mini-tts-2603"
voice_id: "c69964a6-ab8b-4f8a-9465-ec0925096ec8" # Paul - Neutral (padrão)
gemini:
model: "gemini-2.5-flash-preview-tts" # ou gemini-3.1-flash-tts-preview
voice: "Kore" # 30 vozes prontas: Zephyr, Puck, Kore, Enceladus, Gacrux etc.
audio_tags: false # insere marcações de áudio ocultas do Gemini 3.1
persona_prompt_file: "" # arquivo opcional com direção de voz
xai:
voice_id: "eve" # ou um identificador de voz próprio
language: "pt-BR" # código BCP-47, ou "auto" para detectar
speed: 1.0 # 0,7 a 1,5 (padrão: 1,0)
auto_speech_tags: false # insere marcações expressivas por reescrita do modelo
text_normalization: false # normaliza números, abreviações e símbolos para a fala
optimize_streaming_latency: 0 # 0 a 2, troca qualidade por menos espera
sample_rate: 24000 # 22050 / 24000 (padrão) / 44100 / 48000
bit_rate: 128000 # taxa do MP3; só vale com codec mp3
# base_url: "https://api.x.ai/v1" # ou pela variável 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 MB int8; também kitten-tts-micro-0.8 (41 MB) e kitten-tts-mini-0.8 (80 MB)
voice: Jasper # Jasper, Bella, Luna, Bruno, Rosie, Hugo, Kiki, Leo
speed: 1.0 # 0,5 a 2,0
clean_text: true # expande números, moedas e unidades
piper:
voice: pt_BR-faber-medium # nome da voz (baixada sozinha) OU caminho absoluto de um .onnx
# voices_dir: '' # padrão: ~/.hermes/cache/piper-voices/
# use_cuda: false # exige onnxruntime-gpu
# length_scale: 1.0 # 2.0 = duas vezes mais lento
# noise_scale: 0.667
# noise_w_scale: 0.8
# volume: 1.0 # 0.5 = metade do volume
# normalize_audio: true

O MiniMax escolhe região, endpoint e credencial juntos:

  • region: "global" usa https://api.minimax.io/v1/t2a_v2, com a MINIMAX_API_KEY.
  • region: "cn" usa https://api.minimaxi.com/v1/t2a_v2, com a MINIMAX_CN_API_KEY.
  • Sem region, a MINIMAX_API_KEY tem precedência, por compatibilidade. Se só a MINIMAX_CN_API_KEY estiver configurada, o Hermes escolhe cn.
  • Uma região escolhida explicitamente precisa ter a credencial correspondente. O Hermes nunca pega emprestada a chave da outra região. Uma sobrescrita de base_url não muda a credencial, e uma sobrescrita apontando para o endpoint oficial da outra região é recusada.

Controle de velocidade: o tts.speed global vale para todos os provedores por padrão. Cada provedor pode sobrescrever com o próprio speed (por exemplo, tts.openai.speed: 1.5). O valor do provedor vence o global. O padrão é 1.0.

Prompts de persona no Gemini​

O Gemini TTS segue direção de atuação em linguagem natural. Aponte tts.gemini.persona_prompt_file para um arquivo local em Markdown ou texto, descrevendo a persona. O arquivo pode ter seções no estilo do Gemini, como AUDIO PROFILE, SCENE, DIRECTOR'S NOTES, SAMPLE CONTEXT e TRANSCRIPT.

Se o arquivo tiver {transcript} ou {{ transcript }}, o Hermes substitui pelo texto que vai ser falado. Senão, ele acrescenta uma seção TRANSCRIPT sozinho. O prompt de persona fica local e não aparece na resposta da conversa.

tts:
provider: gemini
gemini:
voice: Algieba
persona_prompt_file: ~/.hermes/tts/voz-mordomo.md

Marcações de áudio (Gemini e xAI)​

O Gemini 3.1 Flash TTS e o Grok TTS da xAI aceitam marcações livres entre colchetes, como [whispers], [excitedly], [very slow] e [laughs], entre outras notas de interpretação. Ligue tts.gemini.audio_tags ou tts.xai.auto_speech_tags para o Hermes fazer uma reescrita oculta antes da síntese. A reescrita insere as marcações só no roteiro de voz; a resposta visível na conversa não muda.

tts:
provider: gemini
gemini:
model: gemini-3.1-flash-tts-preview
audio_tags: true
xai:
auto_speech_tags: true

A reescrita usa o auxiliary.tts_audio_tags, que por padrão é o seu modelo principal. Troque essa tarefa auxiliar se quiser um modelo mais barato ou mais rápido cuidando das marcações.

Idioma em endpoints compatíveis com a OpenAI: o tts.openai.language é repassado como parâmetro lang_code. Serve para servidores compatíveis que aceitam esse campo — por exemplo, o Kokoro-FastAPI, em que language: "pt" escolhe o fonemizador de português em vez do inglês. Deixe sem valor na API oficial da OpenAI, que não aceita esse parâmetro. Sem valor, nada extra é enviado.

Limites de tamanho de entrada​

Cada provedor tem um teto documentado de caracteres por requisição. O Hermes divide as respostas mais longas em pedaços ordenados, respeitando as frases, antes de chamar o provedor — então o texto inteiro é preservado, em vez de cortado em silêncio:

ProvedorTeto padrão (caracteres)
Edge TTS5000
OpenAI4096
xAI15000
MiniMax10000
Mistral4000
Google Gemini32000
ElevenLabsDepende do modelo (abaixo)
NeuTTS2000
KittenTTS2000
Piper5000

A ElevenLabs escolhe o teto pelo model_id:

model_idTeto
eleven_flash_v2_540000
eleven_flash_v230000
eleven_multilingual_v2 (padrão), eleven_multilingual_v1, eleven_english_sts_v2, eleven_english_sts_v110000
eleven_v3, eleven_ttv_v35000
Modelo desconhecidoCai no padrão do provedor (10000)

Para sobrescrever por provedor, use max_text_length: na seção dele:

tts:
openai:
max_text_length: 8192 # aumenta ou diminui o teto

Só inteiros positivos são aceitos. Zero, negativo, texto ou booleano caem no padrão do provedor, então uma configuração quebrada não fura o limite da API por acidente.

Bolhas de voz no Telegram, e o ffmpeg​

Bolhas de voz no Telegram exigem áudio em Opus/OGG:

  • OpenAI, ElevenLabs e Mistral produzem Opus nativamente — nada a fazer
  • Edge TTS (padrão) produz MP3 e precisa do ffmpeg para converter
  • MiniMax TTS produz MP3 e precisa do ffmpeg
  • Google Gemini TTS produz PCM cru e usa o ffmpeg para codificar Opus direto
  • xAI TTS produz MP3 e precisa do ffmpeg
  • NeuTTS, KittenTTS e Piper produzem WAV e também precisam do ffmpeg
# Ubuntu e Debian
sudo apt install ffmpeg

# macOS
brew install ffmpeg

# Fedora
sudo dnf install ffmpeg

Sem o ffmpeg, o áudio do Edge, do MiniMax, do NeuTTS, do KittenTTS e do Piper é enviado como arquivo comum (toca, mas aparece como um tocador retangular, e não como bolha de voz).

dica

Se você quer bolha de voz sem instalar o ffmpeg, use a OpenAI, a ElevenLabs ou a Mistral.

Vozes próprias na xAI (clonagem)​

A xAI permite clonar a sua voz e usá-la na síntese. Crie uma voz no console da xAI e coloque o voice_id na configuração:

tts:
provider: xai
xai:
voice_id: "nlbqfwie" # o identificador da sua voz

Veja a documentação de vozes da xAI para gravação, formatos suportados e limites.

Piper (local, 44 idiomas)​

O Piper é um motor de voz neural local e rápido, da Open Home Foundation (quem mantém o Home Assistant). Roda inteiramente no processador, suporta 44 idiomas com vozes pré-treinadas (português incluído) e não pede chave de API.

Instale por hermes tools → Voice & TTS → Piper — o Hermes roda o pip install piper-tts para você. Ou na mão: pip install piper-tts.

Mudar para o Piper:

tts:
provider: piper
piper:
voice: pt_BR-faber-medium

Na primeira chamada com uma voz que ainda não está em cache, o Hermes roda python -m piper.download_voices <nome> e baixa o modelo (de 20 a 90 MB, conforme a qualidade) em ~/.hermes/cache/piper-voices/. As chamadas seguintes reaproveitam.

Escolher uma voz. O catálogo completo cobre português, inglês, espanhol, francês, alemão, italiano, holandês, russo, polonês, turco, chinês, árabe, hindi e mais — cada um com as qualidades x_low, low, medium e high. Ouça amostras em rhasspy.github.io/piper-samples.

Usar uma voz já baixada. Coloque em tts.piper.voice um caminho absoluto terminando em .onnx:

tts:
piper:
voice: /caminho/para/minha-voz.onnx

Os ajustes avançados (tts.piper.length_scale, noise_scale, noise_w_scale, volume, normalize_audio, use_cuda) correspondem um a um ao SynthesisConfig do Piper. Versões antigas do piper-tts os ignoram.

Provedores por comando​

Se o motor de voz que você quer não é suportado nativamente (VoxCPM, MLX-Kokoro, XTTS por linha de comando, um script de clonagem, qualquer coisa com CLI), dá para ligá-lo como provedor do tipo comando, sem escrever Python. O Hermes grava o texto num arquivo temporário em UTF-8, roda o seu comando e lê o arquivo de áudio que ele produziu.

Declare um ou mais provedores em tts.providers.<nome> e troque entre eles com tts.provider: <nome> — do mesmo jeito que se troca entre os embutidos.

tts:
provider: voxcpm # qualquer nome sob tts.providers
providers:
voxcpm:
type: command
command: "voxcpm --ref ~/voz.wav --text-file {input_path} --out {output_path}"
output_format: mp3
timeout: 180
voice_compatible: true # tenta entregar como bolha de voz no 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: # o Piper nativo também aceita .onnx próprio em tts.piper.voice
type: command
command: "piper -m /caminho/custom.onnx -f {output_path} < {input_path}"
output_format: wav

Valores aceitos em output_format: mp3 (padrão), wav, ogg, flac, m4a, aac, amr, opus. O seu comando precisa produzir mesmo aquele formato (por exemplo, via ffmpeg); o Hermes só valida o valor declarado e nomeia o arquivo de acordo. Um valor desconhecido cai em mp3. O formato escolhido também é exposto ao comando pelo espaço reservado {format}.

Ambiente do subprocesso: provedores por comando (de síntese e de transcrição) rodam com os segredos do Hermes removidos do ambiente filho — tokens de bot, chaves de provedor de modelo e credenciais internas saem; PATH, HOME, localidade e outras variáveis normais ficam. Se o seu comando precisa de uma chave própria do ambiente (numa chamada de curl, por exemplo), liste os nomes em env_passthrough:

tts:
providers:
mycloud:
type: command
command: 'curl -s -H "Authorization: Bearer $MYCLOUD_API_KEY" ... -o {output_path}'
env_passthrough: [MYCLOUD_API_KEY]

Exemplo: Doubao (seed-tts-2.0, chinês)​

Para síntese de alta qualidade em chinês, pela API de transmissão bidirecional seed-tts-2.0 da ByteDance, instale o pacote doubao-speech e ligue como provedor por comando:

pip install doubao-speech
export VOLCENGINE_APP_ID="seu-app-id"
export VOLCENGINE_ACCESS_TOKEN="seu-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

As credenciais vêm do ambiente do seu shell (VOLCENGINE_APP_ID e VOLCENGINE_ACCESS_TOKEN) ou do ~/.doubao-speech/config.yaml. Escolha uma voz acrescentando --voice zh-female-warm (ou outro apelido de doubao-speech list-voices) ao comando. O doubao-speech também traz transcrição em transmissão contínua — veja a seção de transcrição. Código e documentação completa: github.com/Hypnus-Yuan/doubao-speech.

Espaços reservados​

O seu modelo de comando pode usar estes espaços. O Hermes substitui na hora e coloca aspas conforme o contexto (sem aspas, com aspas simples, com aspas duplas), então caminhos com espaço e outros caracteres sensíveis ao shell ficam seguros.

Espaço reservadoSignificado
{input_path}Caminho do arquivo temporário de texto em UTF-8
{text_path}Apelido de {input_path}
{output_path}Caminho em que o comando precisa gravar o áudio
{format}mp3 / wav / ogg / flac
{voice}tts.providers.<nome>.voice, vazio quando não definido
{model}tts.providers.<nome>.model
{speed}Multiplicador de velocidade resolvido (do provedor ou global)

Use {{ e }} para chaves literais.

Chaves opcionais​

ChavePadrãoSignificado
timeout120Segundos de inatividade; saída padrão ou de erro reinicia a contagem. A árvore de processos é encerrada depois disso (killpg no Unix, taskkill /T no Windows).
output_formatmp3Um de mp3, wav, ogg ou flac. Deduzido da extensão quando o Hermes escolhe o caminho.
voice_compatiblefalseCom true, o Hermes converte a saída MP3 ou WAV para Opus/OGG com o ffmpeg, para o Telegram mostrar bolha de voz.
max_text_length5000Máximo de caracteres por invocação; texto maior é dividido em pedaços ordenados.
voice / modelvazioPassados ao comando só como valores dos espaços reservados.

Detalhes de comportamento​

  • Nomes embutidos sempre vencem. Uma entrada tts.providers.openai nunca cobre o provedor nativo da OpenAI, então nenhuma configuração de usuário substitui um embutido em silêncio.
  • A entrega padrão é como documento. Provedores por comando entregam como anexo de áudio comum em todas as plataformas. Para bolha de voz, use voice_compatible: true naquele provedor.
  • Falhas do comando chegam ao agente. Saída diferente de zero, saída vazia ou tempo esgotado devolvem um erro com a saída de erro e a saída padrão do comando, para você depurar pela conversa.
  • type: command é o padrão quando há command:. Escrever type: command explicitamente é boa prática, mas não é obrigatório.
  • {input_path} e {text_path} são intercambiáveis. Use o que ler melhor.

Segurança​

Provedores por comando rodam qualquer coisa que você configurar, com as permissões do seu usuário. O Hermes coloca aspas nos valores e respeita o tempo limite, mas o modelo de comando em si é entrada local confiável — trate do mesmo jeito que trataria um script de shell no seu PATH.

Provedores por plugin em Python​

Para motores que não cabem num comando só — SDKs de Python sem CLI, motores de transmissão contínua, APIs de listagem de voz, autenticação com renovação de OAuth — registre um plugin com ctx.register_tts_provider(). O plugin convive com o registro de provedores por comando (não o substitui); escolha a superfície que combina com o seu motor.

Qual escolher​

Seu backend tem…Use
Uma CLI que lê texto de arquivo ou entrada padrão e grava áudio num arquivoProvedor por comando (sem Python)
Duas ou três CLIs encadeadas com pipesProvedor por comando
Só um SDK de Python, sem CLIPlugin
Bytes em transmissão contínua, para entrega em pedaçosPlugin (sobrescrevendo stream())
Uma API de listagem de vozes, usada pelo hermes setupPlugin (sobrescrevendo list_voices())
Fluxo de renovação OAuth (não um token estático)Plugin

Embutidos sempre vencem, e provedores por comando vencem um plugin de mesmo nome — então plugins são seguros de registrar com qualquer nome que não seja embutido.

Plugin mínimo​

Coloque isto em ~/.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" # com o que o tts.provider casa

@property
def display_name(self) -> str:
return "My Custom TTS"

def is_available(self) -> bool:
# Devolva False quando faltar credencial ou dependência — o seletor
# pula essa linha, mas o despachante ainda roteia aqui se configurado.
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:
# Grave os bytes de áudio em output_path e devolva o caminho.
# Levante exceção em caso de falha — o despachante converte em erro padrão.
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())

Habilite (hermes plugins enable my-tts), aponte o tts.provider para ele (tts.provider: my-tts, no config.yaml) e a ferramenta text_to_speech passa pelo seu plugin.

Ganchos opcionais​

Sobrescreva na sua classe, para uma integração mais rica:

  • list_voices() → lista de dicionários {id, display, language, gender, preview_url}, mostrados em hermes tools.
  • list_models() → lista de {id, display, languages, max_text_length}.
  • get_setup_schema() → devolve {name, badge, tag, env_vars: [{key, prompt, url}]}, para montar a linha do seletor em hermes tools e hermes setup. Sem isso, o plugin funciona, mas a linha fica mínima.
  • stream(text, *, voice, model, format, **extra) → iterador de bytes de áudio, para entrega em transmissão contínua (por padrão, levanta NotImplementedError).
  • Propriedade voice_compatible → True se a sua saída é compatível com Opus e o gateway deve entregar como bolha de voz (padrão False, que entrega como anexo comum).

Veja o agent/tts_provider.py para a classe base completa.

Transcrição de mensagens de áudio​

Mensagens de voz enviadas no Telegram, Discord, WhatsApp, Slack ou Signal são transcritas automaticamente e injetadas como texto na conversa. O agente vê a transcrição como texto normal.

ProvedorQualidadeCustoChave
Whisper local (padrão)BoaGrátisNenhuma
Whisper da GroqBoa a ótimaCamada gratuitaGROQ_API_KEY
Whisper da OpenAIBoa a ótimaPagoVOICE_TOOLS_OPENAI_KEY ou OPENAI_API_KEY
Sem configuração

A transcrição local funciona de imediato com o faster-whisper instalado. Se ele não estiver disponível, o Hermes também usa uma CLI whisper local, de locais comuns de instalação (como /opt/homebrew/bin), ou um comando próprio, definido em HERMES_LOCAL_STT_COMMAND.

Configuração​

# em ~/.hermes/config.yaml
stt:
provider: "local" # "local" | "groq" | "openai" | "mistral" | "xai" | "elevenlabs" | "deepinfra"
language: "pt" # dica global de idioma, aplicada a todo provedor sem language próprio; "" volta à detecção automática
local:
model: "base" # tiny, base, small, medium, large-v3
language: "" # dica ISO-639-1 opcional; vazio usa HERMES_LOCAL_STT_LANGUAGE, senão detecta
groq:
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-stt"
language: ""

Detalhes dos provedores​

Local (faster-whisper) — roda o Whisper localmente, pelo faster-whisper. Usa o processador por padrão, e a placa de vídeo quando há. Tamanhos:

ModeloTamanhoVelocidadeQualidade
tiny~75 MBO mais rápidoBásica
base~150 MBRápidoBoa (padrão)
small~500 MBMédioMelhor
medium~1,5 GBMais lentoÓtima
large-v3~3 GBO mais lentoA melhor

Groq — exige GROQ_API_KEY. Boa reserva na nuvem, quando você quer uma opção hospedada gratuita. Defina stt.groq.language (ou a variável global HERMES_LOCAL_STT_LANGUAGE) para pular a detecção automática e reduzir a espera em áudio de idioma conhecido.

OpenAI — aceita primeiro a VOICE_TOOLS_OPENAI_KEY e cai na OPENAI_API_KEY. Suporta whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe e gpt-transcribe.

Mistral (Voxtral Transcribe) — exige MISTRAL_API_KEY. Usa os modelos Voxtral Transcribe. Suporta 13 idiomas, separação de falantes e marcação de tempo por palavra. Instale com cd ~/.hermes/hermes-agent && uv pip install -e ".[mistral]".

xAI Grok STT — exige XAI_API_KEY. Envia para https://api.x.ai/v1/stt, como multipart/form-data. Boa escolha se você já usa a xAI para conversa ou voz e quer uma chave só. Na detecção automática, ele vem depois da Groq — defina stt.provider: xai para forçar.

Comando local próprio — defina HERMES_LOCAL_STT_COMMAND se quiser que o Hermes chame um comando de transcrição direto. O modelo aceita {input_path}, {output_dir}, {language} e {model}. O Hermes divide o comando renderizado numa lista de argumentos e executa sem shell, então operadores como |, >, && e ; chegam como argumentos literais. O seu comando precisa gravar um .txt com a transcrição em algum lugar dentro de {output_dir}.

Exemplo: Doubao e Volcengine ASR​

Se você usa o doubao-speech para a síntese Doubao, o mesmo pacote faz a transcrição, pelo caminho de comando local:

pip install doubao-speech
export VOLCENGINE_APP_ID="seu-app-id"
export VOLCENGINE_ACCESS_TOKEN="seu-token"
export HERMES_LOCAL_STT_COMMAND='doubao-speech transcribe {input_path} --out {output_dir}/transcript.txt'

Se um modelo local confiável precisa mesmo de pipes ou redirecionamentos, invoque o shell explicitamente. Mantenha os caminhos dinâmicos fora do programa do shell, passando como argumentos posicionais:

export HERMES_LOCAL_STT_COMMAND='sh -c '\''whisper "$1" --output_format txt --output_dir "$2" | tee "$2/whisper.log"'\'' _ {input_path} {output_dir}'

No Windows, use um invólucro explícito de cmd /c ou PowerShell. Um invólucro explícito torna a interpretação pelo shell uma escolha consciente, e não uma propriedade implícita de todo comando local de transcrição.

stt:
provider: local_command

O Hermes grava a mensagem de voz recebida em {input_path}, roda o comando e lê o .txt produzido em {output_dir}. O idioma é detectado sozinho pelo endpoint da Volcengine.

Comportamento de reserva​

Uma escolha explícita em stt.provider (gravada no config.yaml, por hermes tools, por exemplo) é respeitada estritamente — se aquele provedor não roda, a transcrição falha com um erro claro (stt is configured to use <provedor> (set via hermes tools), but <falha>. Run 'hermes tools' to change it.) em vez de trocar de motor em silêncio. Vale notar que stt.provider: local gravado na sua configuração conta como escolha explícita.

Quando nenhum provedor foi escolhido, o Hermes detecta pelo que houver:

  • faster-whisper local indisponível → tenta uma CLI whisper local ou o HERMES_LOCAL_STT_COMMAND, antes dos provedores de nuvem
  • chave da Groq não definida → pulado; vai para o próximo
  • chave da OpenAI não definida → pulado; vai para o próximo
  • chave ou SDK da Mistral não definidos → pulado na detecção; vai para o próximo
  • nada disponível → as mensagens de voz passam adiante, com uma nota precisa para a pessoa

Provedores de transcrição por comando​

Se o motor que você quer não é suportado nativamente (Doubao ASR, NVIDIA Parakeet, uma compilação do whisper.cpp, uma CLI do SenseVoice, qualquer coisa com comando de shell), ligue como provedor do tipo comando, sem escrever Python. O Hermes roda o seu comando contra o arquivo de áudio e lê a transcrição de volta.

Declare um ou mais em stt.providers.<nome> e troque com stt.provider: <nome> — mesmo formato do registro de comandos da síntese, adaptado para o sentido de áudio na entrada e transcrição na saída.

stt:
provider: parakeet # qualquer nome sob 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: pt
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

Isso complementa o escape antigo do HERMES_LOCAL_STT_COMMAND, pelo caminho embutido local_command. Diferente do registro por comando, o modelo antigo é dividido em argumentos e roda sem interpretação implícita de shell. Use o stt.providers.<nome> quando quiser vários motores de transcrição por shell, um nome escolhível em stt.provider, ou qualquer coisa que precise de language, model ou timeout por provedor.

Espaços reservados da transcrição​

O seu modelo de comando pode usar estes. O Hermes substitui na hora e coloca aspas conforme o contexto, então caminhos com espaço ficam seguros.

Espaço reservadoSignificado
{input_path}Caminho absoluto do áudio de entrada (local original, só leitura)
{output_path}Caminho absoluto em que o comando deve gravar a transcrição
{output_dir}Diretório pai do {output_path} (útil para ferramentas estilo whisper)
{format}Formato de saída configurado: txt, json, srt ou vtt
{language}Código de idioma configurado (padrão: en)
{model}stt.providers.<nome>.model, vazio quando não definido

Use {{ e }} para chaves literais (útil ao embutir JSON no comando).

Como a transcrição é lida de volta​

Depois que o seu comando termina com sucesso:

  1. Se o {output_path} existe e não está vazio → o Hermes o lê como texto UTF-8.
  2. Senão, se o comando escreveu na saída padrão → o Hermes usa isso.
  3. Senão → erro: "Command STT provider wrote no output file and produced no stdout".

Assim o registro serve tanto para CLIs que gravam arquivo (whisper-cli, parakeet-asr) quanto para comandos de uma linha, estilo curl, que despejam a transcrição na saída padrão (curl … | jq -r .text).

Para format: json, srt ou vtt, o Hermes devolve o conteúdo cru do arquivo no campo transcript. Extrair o .text de um JSON está fora do escopo do executor — configure format: txt, ou processe o JSON depois.

Chaves opcionais da transcrição por comando​

ChavePadrãoSignificado
timeout300Segundos; a árvore de processos é encerrada no vencimento (start_new_session no Unix, taskkill /T no Windows).
formattxtUm de txt, json, srt ou vtt. Define a extensão do {output_path}.
languageenRepassado ao {language}. Cai no stt.language e então em en.
modelvazioRepassado ao {model}. O argumento model= do transcribe_audio() sobrescreve.

Detalhes de comportamento​

  • Embutidos sempre vencem. Declarar stt.providers.openai: type: command NÃO substitui o manipulador real do Whisper da OpenAI. O nome embutido é resolvido antes de o resolvedor de comandos rodar.
  • Limpeza da árvore de processos. Um comando que passa do timeout tem a árvore inteira encerrada, não só o invólucro de shell. Pipelines de transcrição que criam subprocessos de carregamento de modelo são recolhidos de forma confiável.
  • As aspas são automáticas. Espaços reservados dentro de '…' recebem escape seguro para aspas simples; dentro de "…", escape de $, crase e "; fora de aspas, shlex.quote. Não coloque aspas você mesmo nos valores.

Segurança​

O comando roda com o mesmo usuário do Hermes, com acesso total ao sistema de arquivos — mesmo modelo de confiança do tts.providers.<nome>: type: command e do HERMES_LOCAL_STT_COMMAND. Só declare provedores por comando de fontes em que você confia.

Provedores de transcrição por plugin em Python​

Para motores que não são embutidos E não cabem num comando de shell (precisam de SDK Python, autenticação com renovação de OAuth, pedaços em transmissão contínua), registre um plugin com ctx.register_transcription_provider(). O plugin convive com os 8 provedores embutidos (local, local_command, groq, openai, mistral, xai, elevenlabs, deepinfra) e com o registro stt.providers.<nome>: type: command — os embutidos mantêm a implementação nativa e sempre vencem em caso de nome igual; provedores por comando vencem plugins de mesmo nome (configuração é mais local que instalação de plugin).

Qual escolher​

O seu backend tem…Use
Um comando de shell que recebe um áudio e emite textostt.providers.<nome>: type: command (sem Python)
Só o escape antigo, de um comando únicoA variável HERMES_LOCAL_STT_COMMAND (dividida em argumentos, sem shell implícito)
Um SDK Python sem CLIPlugin com register_transcription_provider()
Autenticação com renovação de OAuth, transmissão em pedaços, metadados de vozesPlugin com register_transcription_provider()
Um embutido já cobre (local, groq, openai…)Defina stt.provider: <nome> — os embutidos são internos

Ordem de resolução​

  1. stt.provider é um nome embutido → despacho embutido. Sempre vence.
  2. stt.provider casa com um stt.providers.<nome> que tem command: → executor de comando. Vence um plugin de mesmo nome.
  3. stt.provider casa com um TranscriptionProvider registrado por plugin → despacho pelo plugin:
    • se o is_available() do plugin devolve False (falta credencial ou SDK), a chamada devolve um erro de indisponibilidade identificando o plugin — não a mensagem genérica de "No STT provider available".
    • senão, o transcribe() do plugin é chamado com model (do argumento público model=, caindo em stt.<provedor>.model) e language (de stt.<provedor>.language).
  4. Nenhuma correspondência → erro "No STT provider available".

Espaço de configuração por provedor​

Os plugins leem a configuração deles em stt.<provedor>, no config.yaml, espelhando como os embutidos leem stt.openai.model e stt.mistral.model:

stt:
provider: my-stt
my-stt:
model: whisper-large-v3
language: pt # repassado como language= ao transcribe()
# outras chaves específicas do plugin vão aqui; leia pelo seu próprio
# acesso ao config.yaml, no __init__, no is_available ou no transcribe

O despachante repassa model e language dessa seção; o resto, o plugin lê sozinho.

Plugin mínimo​

Coloque em ~/.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" # com o que o stt.provider casa

@property
def display_name(self) -> str:
return "My Custom STT"

def is_available(self) -> bool:
# Devolva False quando faltar credencial ou dependência — o seletor
# pula essa linha, mas o despachante ainda roteia aqui se configurado.
import os
return bool(os.environ.get("MY_STT_API_KEY"))

def transcribe(self, file_path, *, model=None, language=None, **extra):
# Devolva o envelope padrão:
# {"success": bool, "transcript": str, "provider": str, "error": str}
# NÃO levante exceção — converta em envelope de erro, para quem chama
# (gateway ou CLI) ver um formato consistente na falha.
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())

Habilite (hermes plugins enable my-stt), defina stt.provider: my-stt no config.yaml, e a transcrição de mensagens de voz passa pelo seu plugin.

Ganchos opcionais​

Sobrescreva na sua classe, para uma integração mais rica:

  • list_models() → lista de {id, display, languages, max_audio_seconds}.
  • default_model() → o texto devolvido quando a pessoa não escolhe modelo.
  • get_setup_schema() → devolve {name, badge, tag, env_vars: [{key, prompt, url}]}, para montar as linhas do seletor em hermes tools e hermes setup (a categoria de transcrição ainda não foi publicada no seletor — esses metadados existem para compatibilidade futura).

Veja o agent/transcription_provider.py para a classe base completa.