Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Ermis Voice SDK para Python

CI

Prévia (0.4.0a1). A Voice API ainda está em desenvolvimento; nomes e campos podem mudar até o lançamento.

O SDK liga o seu backend à Ermis Voice API. Ele cria sessões, abre o canal de controle e entrega os eventos da ligação como objetos Python, com métodos para mandar o agente agir.

Novo por aqui? O guia Primeiros passos leva você da instalação a um número de telefone atendido pelo agente. No terceiro passo você já ouve o agente, sem telefone e sem navegador.

Vai montar um agente para um cenário? Os tutoriais montam, passo a passo, um atendente, uma URA, um agendamento, pedidos por telefone, um assistente conduzido pela sua IA e uma ligação ativa. O tutorial do atendente explica a configuração inteira: Twilio ou navegador, nome e voz do agente, calibragem de ruído, qualidade e latência, respostas pré-definidas e a sua IA externa (camada 3).

Prefere começar com um assistente de IA? Cole o prompt pronto no Claude, ChatGPT ou Cursor, descreva o seu negócio e receba o agente com testes.

Como funciona

flowchart LR
    cliente(["Cliente final"]) <-->|voz| origem["Twilio ou navegador"]
    origem <-->|"áudio (audio_url)"| api
    subgraph api ["Ermis Voice API · servidores da Ermis"]
        direction TB
        ouve["Detecta fala e interrupção"] --> transcreve["Transcreve e avalia"]
        transcreve --> decide["Decide a intenção"]
        fala["Fala com voz natural"]
        espera["Frases de espera"]
    end
    api <-->|"eventos e comandos (control_url)"| sdk
    subgraph backend ["Seu servidor · guarda a chave"]
        sdk["SDK ermis"] <-->|objetos Python| codigo["Seu código e sua IA"]
    end

São dois caminhos. O áudio vai do telefone (ou do navegador) direto para a Ermis e volta, sem passar pelo seu servidor. Os eventos e comandos ("o cliente disse X e quer Y", "fale Z") são os únicos que passam pelo SDK. Uma ligação é sempre o mesmo laço:

sequenceDiagram
    participant C as Cliente final
    participant E as Ermis Voice API
    participant S as Seu servidor (SDK)
    S->>E: sessions.create()
    C->>E: o áudio conecta
    E-->>S: SessionStarted
    S->>E: speak("Olá! Como posso ajudar?")
    C->>E: "quero marcar uma consulta"
    E-->>S: Transcript + Decision
    Note over S: seu roteiro ou sua IA escolhe a resposta
    S->>E: speak("Claro! Para qual dia?")
    E->>C: voz do agente (para se o cliente interromper)
    S->>E: end() depois da despedida
    E-->>S: SessionEnded

O que é de quem

A Voice API (nos servidores da Ermis) O SDK (no seu servidor) Você
Áudio da ligação Recebe do Twilio/navegador, detecta fala e interrupções Não toca no áudio (exceto no transporte websocket, onde só repassa quadros) Conecta o áudio à Ermis (TwiML, navegador)
O que o cliente disse Transcreve e avalia (transcript, assessment) Converte em Transcript Decide o que fazer
O que o cliente quer Classifica intenção, extrai dados (decision, slot) Decision; decide() e extract_slot() viram chamadas com retorno Usa para escolher a resposta
Resposta Sintetiza e fala, para no barge-in speak(), say() Escreve o texto (seu roteiro ou sua IA)
Espera Fala frases de espera e aplica o tempo limite WaitingOptions, thinking() Configura as frases
Cobrança Mede por segundo, desconta créditos usage(), balance(), exceções de crédito —

Resumindo: a Voice API conduz a ligação; o SDK é o controle remoto; a inteligência do negócio é sua. Tudo o que o SDK faz também pode ser feito com HTTP e WebSocket puros, seguindo a documentação da API. O SDK só tira o trabalho repetitivo: autenticação, montagem do JSON, ids de correlação, tipos, erros e o TwiML.

Instalação

pip install ermis-voice    # Python 3.10+
export ERMIS_API_KEY=ek_test_...

O pacote se chama ermis-voice no pip e é importado como ermis:

from ermis import Ermis

Para instalar direto do GitHub (por exemplo, uma versão ainda não publicada no PyPI):

pip install "git+https://github.com/Ermisai/ermis-voice-sdk.git"

Durante a prévia, a chave e a URL da Voice API são entregues pela equipe da Ermis. Defina ERMIS_API_URL se ela for diferente de https://api.ermis.ai.

Chaves ek_test_… usam o Sandbox; ek_live_…, produção. Use a chave só no servidor.

Início rápido (Twilio)

import asyncio

from fastapi import FastAPI, Form, Response
from ermis import Ermis, SessionStarted, Decision

app = FastAPI()
ermis = Ermis()  # lê ERMIS_API_KEY

@app.post("/twilio/voz")
async def ligacao(CallSid: str = Form(...)):
    session = await ermis.sessions.create(transport="twilio", metadata={"call_sid": CallSid})
    asyncio.create_task(conversar(session))
    return Response(session.twiml(), media_type="application/xml")   # o áudio vai direto para a Ermis

async def conversar(session):
    async with ermis.connect(session) as call:
        async for event in call:
            if isinstance(event, SessionStarted):
                await call.speak("Olá! Como posso ajudar?", calibrate=True)
            elif isinstance(event, Decision):
                await call.speak(await sua_ia(event))

Exemplo completo, com respostas rápidas, confirmação, desistência e despedida: examples/atendente_twilio.py.

Exemplos

Exemplo O que mostra
primeira_ligacao.py Fale com o agente sem telefone: envia um WAV gravado e salva a resposta em resposta.wav
atendente_twilio.py Atendente geral por telefone: frases prontas, sua IA, confirmação, desistência e despedida
agendamento.py Marcar, remarcar e cancelar consultas: intenções do negócio na sessão, horários como opções faladas (set_custom_intents), telefone com expect_slot, confirmação
ura.py URA por voz: o cliente diz o motivo e é transferido para financeiro, suporte ou vendas; CPF com expect_slot
clinica.py O resultado do tutorial: atendente com URA por voz, perfis de qualidade e latência, calibragem de ruído, guard, respostas prontas e IA na camada 3
assistente_ia.py Suporte técnico conduzido pela sua IA: CPF com ask(slot=...), ferramentas que consultam o seu sistema, guard com drift e off_topic vazios
confirmacao.py Ligação ativa: o agente liga pelo Twilio, confirma a identidade e a consulta e registra o resultado
pizzaria.py Pedidos de pizza por objetivos (nome, pizzas, endereço): vocabulary do cardápio, endereço com expect_slot, dúvidas no meio do pedido, unclear e off_topic

Os exemplos de telefone rodam com uvicorn <arquivo>:app e um número do Twilio; o passo a passo está em Primeiros passos.

Referência

Ermis(api_key=None, *, base_url=None, timeout=10.0, http_client=None)

Cliente assíncrono. Sem api_key, lê ERMIS_API_KEY; sem base_url, lê ERMIS_API_URL ou usa https://api.ermis.ai. Feche com await ermis.aclose() ou use async with Ermis() as ermis.

Método Chamada da API Retorno
await ermis.sessions.create(**opções) POST /v1/voice/sessions Session
await ermis.sessions.get(session_id) GET /v1/voice/sessions/{id} dict
await ermis.sessions.end(session_id) DELETE /v1/voice/sessions/{id} None
await ermis.usage(limit=50, before=None) GET /v1/usage dict com entries e next_cursor
await ermis.balance() GET /v1/account/balance dict
await ermis.voices.list() GET /v1/voice/voices list[Voice] (id, name, gender, production, default)
ermis.connect(session_or_control_url, normalize_text=True) WebSocket control_url Call (use com async with)

usage() devolve os lançamentos de crédito em páginas: passe o next_cursor recebido em before para buscar a página seguinte (limit de 1 a 100). balance() devolve balance, reserved_credits (reservado por ligações em andamento) e available_credits, em créditos inteiros.

Opções de sessions.create (todas opcionais; o que você não passar usa o padrão da API): transport ("twilio", "browser", "websocket"), audio_format ("mulaw_8k", "pcm16_48k"), language, voice (um id de voices.list()), auto_decide, transcription_priority ("balanced", "latency", "accuracy", "local_first", "local_only"), turn_detection ("smart_turn", "silence"), early_turn={"speculate": True, "close_on_complete_text": True}, custom_intents=[CustomIntent(…)] (as intenções do seu negócio, respondidas em cada Decision), vocabulary=[…] (palavras do seu negócio: a transcrição chega corrigida), vad=VadOptions(min_silence_ms=…), barge_in=BargeInOptions(enabled=…, min_speech_ms=…), waiting=WaitingOptions(…), max_duration_s, metadata.

from ermis import WaitingOptions

waiting = WaitingOptions(
    filler_after_ms=800,                       # None desliga a frase de espera
    filler_messages=["Certo, um momento."],
    filler_by_intent={"complaint": ["Entendo, sinto muito por isso."]},   # frase pela intenção da fala
    gap_after_ms=600,                          # pausa antes da sua resposta; None desliga
    patience_after_ms=3500,
    patience_every_ms=8000,
    patience_messages=["Ainda estou verificando…"],
    timeout_ms=20000,                          # None: espera sem limite
    timeout_message="Desculpe, não consegui essa informação agora.",
    timeout_action="apologize",                # ou "end"
)

Dados importantes (endereço, e-mail, CPF)

A transcrição rápida erra mais em dados ditados. Avise antes que a próxima resposta traz o dado: a Ermis lê essa fala também com o reconhecimento de maior precisão, em segundo plano, sem atrasar a conversa.

Para uma resposta que precisa ser exata e decide o próximo passo (um sabor, um tamanho), use await call.expect_precise() logo depois da pergunta: a Transcript e a Decision dessa fala já saem da leitura precisa, com alguns décimos de segundo a mais só nela.

await call.speak("Pra qual endereço eu mando?")
endereco = await call.expect_slot("endereco", "address", description="rua, número e bairro")
...
elif isinstance(event, Decision):          # a decisão da resposta chega no ritmo normal
    resultado = await endereco             # o valor, lido pelo reconhecimento preciso
    if resultado.found:
        await call.speak(f"Anotado: {resultado.value}. Tá certo?")

Fluxo da conversa: o roteiro pergunta, a Ermis conduz

O seu código guarda as perguntas e as respostas do negócio; a Ermis decide o que acontece entre elas. Pergunte com ask (a pergunta vira a pergunta atual, com as respostas esperadas) e ligue o guard na sessão com as frases do seu negócio:

session = await ermis.sessions.create(
    transport="twilio",
    guard={"enabled": True, "steer": "Vamos voltar ao seu pedido?", "drift": "Tá bom! Voltando ao seu pedido:",
           "off_topic": "Isso eu não sei te dizer, aqui eu cuido dos pedidos."},
)
...
await call.ask("De qual sabor?", precise=True, choices=[CustomIntent("calabresa", "calabresa"),
                                                         CustomIntent("chocolate", "chocolate", "a pizza doce")])
...
elif isinstance(event, Decision):
    if event.handled:                 # a Ermis já respondeu (fala confusa, divagação, fora do tema)
        continue
    sabor = event.chosen()            # "chocolate", mesmo com "aquela doce" ou "pit sapequena dissocolate"
    if event.answer == "no": ...      # sim/não à pergunta que o agente fez

Fala confusa: a Ermis pede para repetir, na segunda volta à pergunta atual e na terceira se despede. Divagação e assunto fora do tema: a frase do negócio e a pergunta atual de novo.

Voz natural

Toda fala do agente passa por normalize_for_speech() antes de ir para a voz, então você pode escrever como escreveria numa tela:

await call.speak("Seu pedido de 2 pizzas fica **R$ 109,80** e chega às 20h30 🍕")
# a voz fala: "Seu pedido de duas pizzas fica cento e nove reais e oitenta centavos e chega às vinte horas e trinta"

Números e valores por extenso (com gênero: "duas pizzas"), datas, horas, porcentagens, ordinais, CPF, CEP e telefone lidos em grupos, e-mails e sites ("arroba", "ponto"), abreviações comuns, e sem markdown nem emojis. Desligue com ermis.connect(session, normalize_text=False) ou só numa fala com speak(..., normalize=False). Para usar no seu próprio texto: from ermis import normalize_for_speech.

Session

session_id, audio_url, control_url, session_token, e:

  • session.twiml(): TwiML pronto para o transporte twilio, com o token escapado.
  • session.browser_credentials(): {"audio_url", "session_token"}, o que entregar ao navegador no transporte browser. A chave de API nunca sai do servidor.

Call: o canal de controle

async for event in call recebe os eventos até SessionEnded. Os comandos podem ser chamados a qualquer momento, inclusive enquanto você trata um evento.

Método Mensagem enviada Retorno
await call.speak(text, *, voice=, allow_barge_in=, calibrate=, id=) speak o id da fala, na hora
await call.say(text, **mesmas_opções) speak e espera True se falou tudo, False se o cliente interrompeu
await call.interrupt() interrupt —
await call.decide(text, custom_intents=[CustomIntent(key, label, description)]) decide Decision
await call.extract_slot(text, name, type="text", *, description=) extract_slot SlotResult
await call.thinking(message=None, *, timeout_ms=None) thinking —
await call.done_processing() done_processing —
await call.end() end —
await call.send_audio(frame) quadro binário (só websocket) —
await call.playback_finished(speak_id) playback_finished (só websocket) —
await call.send(dict) qualquer mensagem crua —

decide() e extract_slot() geram o id, esperam a resposta com esse id (padrão: 10 s) e devolvem o resultado. Essas respostas não aparecem na iteração. Depois do fim, call.ended guarda o SessionEnded.

Eventos

Classe Evento da API Campos
SessionStarted session.started session_id, limits
SpeechStarted / SpeechEnded speech.started / speech.ended duration_ms
Transcript transcript utterance_id, text, uncertain, assessment, latency_ms
Decision decision intent, action, is_escape, complexity, custom_intent, extracted, utterance_id, id
SlotResult slot id, value, status, found
SpeakStarted / SpeakDone speak.started / speak.done id, chars
BargeIn barge_in id
WaitingSpoken waiting utterance_id, kind, text
ReplyTimeout reply_timeout utterance_id, action
Usage usage billed_seconds, credits, balance
WarningEvent warning code, message
ErrorEvent error code, message
SessionEnded session.ended reason, billed_seconds, credits
Audio quadro binário (só websocket) data
UnknownEvent qualquer outro —

Todo evento tem type e raw (o JSON original). Eventos novos da API chegam como UnknownEvent em vez de quebrar o seu código.

Transcript.assessment traz ignore, ask_to_repeat, unproductive, confirm e o atalho usable (nem ignore, nem ask_to_repeat, nem unproductive).

Erros

O mesmo erro chega como status HTTP ou código de fechamento do WebSocket; o SDK levanta a mesma exceção nos dois casos. Todas herdam de ErmisError (message, code, status, request_id). Guarde o request_id para falar com o suporte.

Exceção HTTP / WebSocket
AuthenticationError 401 / 4001
InsufficientCreditsError 402 / 4002
ChannelLimitError 429 / 4029
RateLimitError 429 com código rate_limited (muitas requisições em pouco tempo; tente de novo em instantes)
InvalidRequestError 400 / 4400 (também quando decide/extract_slot recebem error)
SessionTimeoutError 4008 (60 s sem áudio)
MaxDurationError 4013
APIError 500 / 1011
ConnectionClosedError o canal fechou antes de session.ended

Outros transportes

Navegador. Crie a sessão com transport="browser", audio_format="pcm16_48k" e envie session.browser_credentials() ao navegador. O navegador abre audio_url?session_token=… e troca áudio direto com a Ermis; o seu backend conduz a conversa com ermis.connect(session), como no Twilio.

Seu próprio WebSocket. Com transport="websocket", o mesmo Call leva áudio e controle: envie com call.send_audio(frame), receba Audio na iteração e avise o fim de cada fala com call.playback_finished(speak_id).

Documentação

  • Primeiros passos: da instalação a uma ligação de verdade, com lista de conferência para produção.
  • Tutoriais: atendente, URA, agendamento, pedidos, assistente com IA e ligação ativa, passo a passo.
  • Prompt para assistentes de IA: gere um agente com o seu assistente de código. Para assistentes que leem a documentação sozinhos, há também o llms.txt.
  • Referência da Voice API: sessões, canal de controle, decisões, mensagens de espera, áudio e erros.
  • Na plataforma da Ermis: Documentação API → Ermis Voice e a aba Ligação da Área de Teste, para conversar com o agente pelo microfone.

Desenvolvimento

uv venv && uv pip install -e ".[dev]"
.venv/bin/python -m pytest -q

Os testes rodam contra uma Voice API simulada (REST com httpx.MockTransport, controle com um servidor WebSocket local).

Licença

MIT. Veja LICENSE.

Metadata

Release files for ermis-voice 0.4.0a1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ermis-voice 0.4.0a1
File Size Uploaded Provenance
ermis_voice-0.4.0a1.tar.gz 117.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for ermis-voice 0.4.0a1
File Interpreter ABI Platform
ermis_voice-0.4.0a1-py3-none-any.whl Python 3 none any Details

Total release size: 149.8 kB

Release files / ermis_voice-0.4.0a1.tar.gz

Download URL ermis_voice-0.4.0a1.tar.gz
Size 117.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ea75ea064654bcca4be1e54276df2f9edb44533b33b9b1a24ce75c479ba29bda
BLAKE2b-256 checksum
How to use checksums
fbe4640696706041ac526a3b152f620a8e2fc65e419ab01c630c0aed5937900b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / ermis_voice-0.4.0a1-py3-none-any.whl

Download URL ermis_voice-0.4.0a1-py3-none-any.whl
Size 32.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
03a291f76c456fc7f86b5159f9809f082d2408316db5ac2f3d831ed25dc13590
BLAKE2b-256 checksum
How to use checksums
d5125eba037a076a56d6fcebdd53e41143fdaf62547c29316fbbbcae4ea02e1f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for ermis-voice 0.4.0a1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ermis_voice-0.4.0a1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page