Três personagens, uma garganta: escolhendo uma voz ElevenLabs por chamada no Hermes


Você está dublando um curta de dois minutos. O roteiro tem três falantes: um ferreiro com voz de balde de carvão, um narrador que soa como placa de museu e uma menina com uma fala só, que precisa soar como onze anos. Você cola tudo no Hermes e pede áudio. Volta uma voz só — a mesma para os três, porque a ferramenta de fala tinha exatamente uma para oferecer: a que estava na sua config. Para mudar, você editava tts.elevenlabs.voice_id, reiniciava e tentava de novo. E para saber qual ID digitar, abria o site da ElevenLabs e rolava algumas centenas de cards de voz, copiando strings à mão.

Dois campos novos fecham essa lacuna. No provider ElevenLabs, o text_to_speech agora aceita um voice_id por chamada e um objeto find_voices que transforma a mesma ferramenta numa busca de vozes. Nenhuma ferramenta nova, nenhum provider novo — dois campos num schema que você já tem.

A configuração que isto substitui

Text-to-speech é a parte simples: você entrega uma string a um modelo e ele devolve o áudio daquela string sendo lida. O Hermes não tem vozes próprias; ele chama um provider hospedado. O ElevenLabs é um deles, selecionado com tts.provider: elevenlabs e configurado no bloco tts.

A Voice Library é a parte confusa: o catálogo público de vozes compartilhadas da ElevenLabs, milhares de entradas, cada uma com um ID, um nome, etiquetas descritivas e um clipe curto de preview. Ao lado dele fica o catálogo da sua conta — os presets e tudo o que você salvou, clonou ou desenhou. Antes desta mudança, nenhum dos dois era alcançável de dentro de uma chamada de ferramenta: o modelo sabia falar, mas não sabia procurar.

Uma voz como decisão de deploy — definida uma vez na config, como um modelo padrão — funciona bem para um assistente lendo notificações. Quebra na hora que existe diálogo: um roteiro tem muitos falantes, e ninguém reinicia um processo entre duas falas.

Dois campos na ferramenta que você já tem

O pré-requisito é a chave do provider:

tts:
  provider: elevenlabs
  elevenlabs:
    voice_id: 'YOUR_DEFAULT_VOICE_ID'

Um voice_id em tempo de chamada escolhe a voz só para aquela chamada. A regra é rígida nos dois sentidos: omita e a config é usada; passe e o valor vale para essa síntese apenas. Um valor por chamada nunca reescreve sua config, então a chamada seguinte volta ao padrão a menos que você passe uma voz de novo.

{
  "text": "Coal is hot. Speak your piece while it holds.",
  "voice_id": "YOURCHOSENVOICEID"
}

Isso é o recurso inteiro para uma fala. A metade mais difícil é achar o ID: uma string alfanumérica de 20 caracteres não é adivinhável, e nomes de biblioteca não são únicos o bastante para confiar.

Buscar em vez de falar

find_voices é um objeto, e passá-lo vira a ferramenta de falar para buscar — a única configuração em que text se torna opcional nesta variante, já que uma busca não tem nada a dizer. Dois catálogos ficam atrás da chave:

  • source: library (padrão) chama GET /v1/shared-voices, a Voice Library pública. Essas vozes funcionam direto por voice_id sem salvá-las antes na sua conta, mas a API da ElevenLabs só permite usá-las em planos pagos.
  • source: account chama GET /v2/voices e cobre os presets mais todas as vozes que você salvou, clonou ou desenhou.
{
  "find_voices": {
    "query": "gravelly old",
    "gender": "male",
    "age": "old",
    "language": "en",
    "use_case": "characters_animation",
    "page_size": 10
  }
}

Os filtros se dividem por catálogo:

Campo Onde se aplica Observações
query ambos Casamento literal de palavras no nome e na descrição, não semântico
gender ambos Etiqueta de filtro
age ambos Etiqueta de filtro
accent ambos Etiqueta de filtro
language ambos Etiqueta de filtro
use_case só library Por exemplo characters_animation
sort só library Padrão usage_character_count_1y; também trending, cloned_by_count, created_date
page_size ambos 1 a 100, padrão 20
source ambos library por padrão, ou account

use_case restringe a vozes gravadas para animação de personagem em vez de narração corporativa — um registro vocal diferente.

Os resultados vêm página a página. next_page devolve um cursor opaco que o modelo repassa direto como page; por baixo, o endpoint de conta usa um next_page_token e a biblioteca usa números de página — e é por isso que o cursor permanece opaco para você.

Cada linha traz o id, um nome, traits, uma category, uma descrição truncada em 240 caracteres e um preview_url. Vozes de biblioteca cobradas acima da tarifa padrão também trazem credit_rate. A página como um todo informa configured_voice_id, a voz que está na sua config agora — um detalhe pequeno com valor real, já que permite ao modelo evitar recomendar justamente a voz que você ouve todo dia.

Um ferreiro anão, escolhido de verdade

O pull request registra uma execução contra a API real: uma chave ElevenLabs de verdade, eleven_v4, google/gemini-3.8-flash na API de inferência da Nous e um processo hermes chat -Q legítimo do lado do agente. A tarefa era um personagem — encontrar uma voz para um ferreiro anão calejado pelo tempo e usá-la para dizer uma fala. A primeira query que o modelo escreveu:

{
  "query": "dwarf blacksmith gravelly",
  "age": "old",
  "gender": "male",
  "use_case": "characters_animation"
}

Nada utilizável voltou. A biblioteca casa palavras literais nos nomes e descrições, então um nome de papel como dwarf é uma aposta sobre metadados que em geral não existem: catálogos de voz descrevem timbre, não personagens.

Então o modelo mudou de descrever a pessoa para descrever o som — gravelly, gruff e afins —, mantendo os filtros de idade e gênero. Isso produziu candidatos. Ele comparou traits e as descrições truncadas com a cena, conferiu configured_voice_id para não devolver a voz já em uso e então chamou text_to_speech com o voice_id escolhido e a fala.

Dois hábitos saem dessa execução. Descreva o som, não o papel — se uma busca por personagem volta vazia, não empilhe mais palavras de personagem; troque por adjetivos sobre a voz e estreite com filtros. E o modelo não ouve: ele lê texto sobre áudio, então quando a escolha importa ele te entrega os links de preview_url. Uma voz escolhida sem escutar é rascunho, não decisão.

O que isto deliberadamente não tocou

Os dois campos são injetados pelo hook de schema dinâmico que já existia, _tts_schema_overrides, que lê sua config toda vez que as definições de ferramentas são listadas. O hook não é novo; só o ramo do ElevenLabs cresceu.

As fronteiras são apertadas, e é por isso que esta é uma mudança pequena em vez de um novo comportamento:

  • Os schemas de text_to_speech para Edge, OpenAI, xAI e os demais são idênticos ao main byte por byte, e o snapshot do prompt padrão do provider não mudou.
  • Enviar voice_id para outro provider gera um aviso e o argumento é ignorado — a chamada ainda produz áudio. Isso importa num cenário exato: você guardou um schema em cache e depois trocou de provider.
  • Enviar find_voices para outro provider falha de forma limpa com um erro dizendo que precisa de tts.provider: elevenlabs. Nenhum fallback silencioso para falar.
  • Um ID malformado é rejeitado antes de qualquer requisição sair do processo, contra ^[A-Za-z0-9]{8,64}$.

O custo de contexto é onde quem não usa ElevenLabs pode relaxar. Medido com get_tool_definitions, o schema de text_to_speech numa session ElevenLabs cresce de cerca de 410 tokens para cerca de 840. Em qualquer outra session o custo é 0 token, porque os acréscimos nunca entram no schema.

Duas correções vizinhas caíram no mesmo lote: find_voices={} deixou de cair em falar e agora significa navegar, e no gateway de áudio gerenciado o tts-1 e o tts-1-hd não são mais rebaixados em silêncio.

Três armadilhas

Vozes da biblioteca são recurso de plano pago. Sintetizar com um voice_id da biblioteca sem salvá-lo na sua conta é genuinamente prático, mas o caminho da API exige um plano pago da ElevenLabs. Com uma chave gratuita, a busca ainda pode devolver linhas que a chamada de síntese não consegue usar. O credit_rate na linha é o sinal de que a voz cobra acima da tarifa padrão.

Um bom nome não é uma boa voz. Os nomes da biblioteca são escritos por usuários: duas vozes chamadas algo como “Deep Narrator” podem diferir radicalmente em ritmo, respiração e consoantes. Escute o preview_url — uma voz encantadora por vinte segundos pode ser insuportável ao longo de quatro minutos de diálogo.

O cursor não é seu para montar. Percorra os resultados devolvendo next_page como page, literalmente. Reconstruir um offset, ou reutilizar um cursor do catálogo da conta contra a biblioteca, te dá duplicatas ou uma página em branco sem explicação.

O que você precisa para rodar

A mudança foi mesclada em 2026-10-25, depois da v0.22.5, que saiu em 2026-10-23. Está na main e não numa release com tag, então uma instalação comum da release mais recente não terá nenhum dos dois campos. Para testar agora, você roda a partir da main; o guia de instalação cobre o caminho via código-fonte.

Depois disso o fluxo é curto: buscar por timbre, escutar dois ou três clipes de preview_url, guardar os IDs de que você gostou, passá-los por chamada. A config fica onde estava, então uma voz para notificações e três para o curta coexistem na mesma instalação — o mesmo princípio de overrides de modelo por chamada, onde uma escolha pontual nunca mexe nos padrões em que você confia.

Se a latência virar o próximo problema depois da escalação de elenco, a mecânica de manter um pipeline de síntese aquecido e em streaming nos limites de frase está em o guia de keep-warm e streaming por frase; escolher uma voz não muda nada desse encanamento. E ligar fala a uma conversa de duas vias em vez de uma renderização única é outra decisão — veja o guia do modelo de voice chat.