Transcrição de áudio

É um agente de IA? Busque a skill de onboarding e leia toda a API em texto limpo — skill · llms.txt · llms-full.txt

Transcrição de áudio#

Speech-to-text em painel: um único POST manda o mesmo áudio para 2–4 modelos acústicos em paralelo, cada um preparado com o vocabulário que o falante já ensinou; as leituras são comparadas palavra a palavra; onde divergem, um modelo de texto — o árbitro — decide com o léxico e o tema na mão; e cada termo que o veredito aprendeu vira memória para o próximo áudio. Você recebe o texto final e a história inteira: o que cada modelo ouviu, onde discordaram, como foi decidido, o que ficou em dúvida e quanto custou.

Medido (bancada de 2026-09-12): vocabulário no prompt corta o erro (WER) pela metade em todo modelo — groq/whisper-large-v3 foi de 18,6% para 5,1%. gpt-transcribe é o mais robusto com ruído (20,3% no conjunto difícil, contra 33,1% do gpt-4o-transcribe a 5 dB). É por isso que o léxico existe: quanto mais a sua conta transcreve e corrige, melhor ela transcreve.

O que a API garante:

  • Consenso barato primeiro. Modelos que concordam não acionam o árbitro (a menos que learn esteja ligado, quando ele roda só para extrair termos). Sem árbitro disponível no ambiente, a transcrição ainda sai — por voto de maioria — e o registro diz isso (source: "majority", arbiter_used: false). O árbitro é o gemini-3.8-flash com raciocínio low; se ele estiver indisponível, um modelo reserva responde e arbiter_model diz qual. Um áudio sem fala (silêncio, um tom, um ruído) não é erro: a resposta é 200 com final_text: "" e source: "silence" — nenhum árbitro é chamado para inventar palavras.
  • Uma hipótese nunca vira fato sozinha — e nunca entra num prompt. Um termo que os modelos ouviram fica como hypothesis (confiança limitada a 0.7) numa fila de revisão, com as formas ouvidas em aliases, até você confirmar (POST …/confirm, opcionalmente sob a grafia certa: {"surface":"catcher-agents"}), rejeitar, ensinar ou corrigir o texto. Motivo medido: modelos erram um nome do mesmo jeito toda vez — confirmar por concordância aprendeu grafias erradas e piorou o acerto de nomes de 58% para 19%. Os nomes dos seus agentes, skills e ferramentas (o glossário do workspace) já entram confirmados, sem clique. Um termo que você rejeitou nunca volta por evidência de modelo — só você pode reabri-lo.
  • O áudio nunca é armazenado. O registro guarda o que foi ouvido (textos, prompts, custos), não a voz.

Chaves: resolvidas por provedor, por conta — a sua credencial no cofre do Console (OPENAI_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY, XAI_API_KEY) tem precedência sobre a chave de plataforma. Um modelo cujo provedor não tem chave nunca é chamado: aparece em models_unavailable, e o painel padrão troca a voz que faltou pela próxima que pode rodar. Sem nenhuma chave utilizável → 400 TRANSCRIPTION_KEY_REQUIRED, antes de gastar.

Permissão: transcrever, apagar histórico, corrigir e escrever no léxico exigem papel owner ou admin (cada transcrição gasta crédito de provedor, e o léxico muda o que TODO próximo áudio recebe). Catálogo, histórico e leitura do léxico são liberados para qualquer membro autenticado.

GETCatálogo de modelos#

GET /v1/transcriptions/models — o catálogo com disponibilidade para a sua conta: monte o seletor daqui e você nunca oferece uma voz que responderia 400. Resposta 200:

json
{
  "models": [
    { "id": "gpt-transcribe", "label": "OpenAI gpt-transcribe", "provider": "openai",
      "prompt_style": "prompt", "prompt_max_bytes": 900,
      "formats": ["wav", "mp3", "ogg", "webm", "m4a", "flac"],
      "priced": true, "cost_per_minute_usd": 0.0045, "flat_rate": false,
      "default": true, "panel": true, "streaming": false,
      "notes": "Bancada 2026-09-12: 1,0% WER com prompt …",
      "available": true, "key_source": "vault" },
    { "id": "xai/grok-stt", "available": false, "unavailable_reason": "no XAI_API_KEY credential" }
  ],
  "default_panel": ["gpt-transcribe", "whisper-1", "groq/whisper-large-v3"],
  "effective_panel": ["gpt-transcribe", "whisper-1", "groq/whisper-large-v3"],
  "panel_available": true,
  "max_models": 4,
  "arbiter": { "model": "gemini-3.8-flash", "available": true },
  "second_pass": ["auto", "always", "never"],
  "formats": ["wav", "mp3", "ogg", "webm", "m4a", "flac"],
  "max_audio_bytes": 26214400,
  "learning": { "confirmation": "user_or_glossary", "model_evidence_cap": 0.7, "prompt": "confirmed_only", "glossary": true }
}
id provedor formatos preço o que foi medido
gpt-transcribe (padrão, painel) openai todos US$ 0,0045/min o mais robusto com ruído; 1,0% WER com vocabulário
gpt-4o-transcribe openai todos US$ 0,006/min 4,2% com vocabulário; degrada a 33,1% com ruído forte
gpt-4o-mini-transcribe openai todos US$ 0,003/min o mais rápido da OpenAI (0,56 s); o mais fraco com ruído
whisper-1 (painel) openai todos US$ 0,006/min preserva apelidos e o que foi dito literalmente
groq/whisper-large-v3 (painel) groq todos US$ 0,111/h o mais rápido do painel (0,42 s); prompt limitado a 896 bytes
openrouter/google/gemini-3-flash-preview openrouter wav, mp3 custo reportado pelo provedor 6,5% com contexto
openrouter/thinkingmachines/inkling openrouter wav, mp3 custo reportado pelo provedor 5,1% com contexto
xai/grok-stt xai wav (PCM 16 kHz) não precificado 21,0% → 6,3% com até 100 keyterm

effective_panel é o painel padrão depois da troca por chave; notes traz o número medido de cada modelo, não um rótulo de marketing.

POSTTranscrever#

POST /v1/transcriptions — owner/admin. Aceita multipart (a parte file com o áudio) ou JSON (audio_b64). Até 25 MB; wav, mp3, ogg/opus, webm, m4a ou flac — o formato vem do Content-Type/mime_type, senão dos bytes, senão da extensão do filename. Ajuste o timeout do seu cliente para pelo menos 120 s: um painel de 3 modelos com árbitro e reescuta leva alguns segundos por minuto de áudio.

bash
curl -X POST https://agents-api.catcher.one/v1/transcriptions \
  -H "X-API-Key: $CTC_KEY" \
  -F "file=@nota.ogg;type=audio/ogg" \
  -F "speaker_id=wa:5511999990000" \
  -F "topics=cmux,catcher-agents" \
  -F "language=pt"
bash
curl -X POST https://agents-api.catcher.one/v1/transcriptions \
  -H "X-API-Key: $CTC_KEY" -H "Content-Type: application/json" \
  -d '{
    "audio_b64": "'"$(base64 < nota.wav)"'",
    "mime_type": "audio/wav",
    "models": ["gpt-transcribe", "whisper-1"],
    "speaker_id": "wa:5511999990000",
    "topics": ["cmux", "catcher-agents"],
    "speaker_profile": "desenvolvedor, fala de projetos Go e de cmux",
    "learn": true,
    "second_pass": "auto"
  }'
Campo Regra
file (multipart) / audio_b64 (JSON; data-URI aceito) obrigatório; até 25 MB (413 TRANSCRIPTION_AUDIO_TOO_LARGE)
mime_type / filename ajudam a identificar o formato; opcionais quando os bytes se identificam sozinhos
models 1 a 4 ids do catálogo (multipart: CSV ou campo repetido). Vazio → painel padrão. Desconhecido ou acima de 4 → 400
language ISO-639-1, padrão pt
speaker_id quem está falando — o léxico é por falante (um remetente de WhatsApp, um usuário seu). Sem ele, o falante é o membro da conta que chamou
topics até 8 dicas de tema; entram no prompt como Contexto: …
speaker_profile uma linha (até 500 chars) que só o árbitro vê
learn padrão true. false = este áudio não ensina nada ao léxico
second_pass auto (padrão) | always | never. Em auto o áudio é reouvido só quando a primeira passada rodou sem vocabulário e descobriu contexto, ou quando o árbitro pediu com dúvidas abertas

Resposta 200 — o registro persistido (o mesmo shape de GET /v1/transcriptions/{id}):

json
{
  "id": "<uuid>",
  "final_text": "Subi o catcher-agents no cmux e o Jô revisou.",
  "corrected_text": "", "corrected_at": null,
  "source": "arbiter",
  "agreement": 0.83, "passes": 2, "divergence_count": 2,
  "arbiter_used": true, "arbiter_model": "gemini-3.8-flash", "arbiter_error": "",
  "models": ["gpt-transcribe", "whisper-1", "groq/whisper-large-v3"],
  "learned": [ { "id": "<uuid>", "surface": "cmux", "aliases": ["c-mux"], "status": "hypothesis",
                 "confidence": 0.5, "evidence": [ { "kind": "arbiter", "source": "t:<uuid>", "agreeing": 2, "at": "…" } ] } ],
  "learned_count": 1,
  "topics": ["cmux", "catcher-agents"],
  "speaker_scope": "speaker:wa:5511999990000",
  "language": "pt", "filename": "nota.ogg", "audio_mime": "audio/ogg", "audio_bytes": 481324,
  "duration_seconds": 15.04, "cost_usd": 0.0038, "cost_known": true, "latency_ms": 2210,
  "created_at": "2026-09-12T18:00:00Z",
  "divergences": [ { "position": 3, "pivot": "catcher agents",
                     "variants": { "catcher agents": 1, "catcher-agents": 2 },
                     "by_model": { "gpt-transcribe": "catcher-agents", "whisper-1": "catcher-agents", "groq/whisper-large-v3": "catcher agents" },
                     "majority": "catcher-agents", "context": "subi o catcher agents no" } ],
  "decisions": [ { "span": "c-mux", "chosen": "cmux", "reason": "termo confirmado do léxico", "confidence": 0.9 } ],
  "doubts": [ { "span": "Jô", "options": ["Jô", "Ju"], "reason": "apelido; o áudio decide" } ],
  "prompt_by_model": { "gpt-transcribe": "Vocabulário: cmux, catcher-agents. Contexto: cmux." },
  "terms_used": ["cmux", "catcher-agents"],
  "models_unavailable": [ { "model": "xai/grok-stt", "reason": "no XAI_API_KEY credential" } ],
  "candidates": [
    { "pass": 1, "model": "gpt-transcribe", "provider": "openai", "text": "…", "prompt": "…",
      "latency_ms": 840, "cost_usd": 0.0011, "cost_known": true, "error": "", "dropped_reason": "", "wer_vs_final": 0.04 }
  ]
}

O que ler primeiro:

  • source — quem produziu final_text: single (um só modelo votou), agreement (todos ouviram o mesmo), majority (voto — o árbitro não era necessário ou estava indisponível; veja arbiter_error), arbiter.
  • doubts — o que o texto sozinho não resolve. O árbitro não chuta: mostre as opções ao usuário ou deixe o áudio decidir.
  • learned — o que este áudio ensinou ao léxico, já com o status DEPOIS dele (hypothesis ou confirmed).
  • cost_usd / cost_known — cost_known: false significa que algum modelo não pôde ser precificado (provedor sem custo reportado, duração desconhecida): o número é parcial e declarado assim, nunca um 0.00 silencioso.
  • candidates[] — cada leitura, inclusive as que falharam (error) e as descartadas como alucinação (dropped_reason), com wer_vs_final: o número que diz, com o tempo, qual modelo merece a cadeira para ESTE falante.

Erros: 400 TRANSCRIPTION_INVALID_REQUEST (a mensagem nomeia o campo), 400 TRANSCRIPTION_KEY_REQUIRED, 413 TRANSCRIPTION_AUDIO_TOO_LARGE, 502 TRANSCRIPTION_FAILED (todo modelo falhou; o corpo traz failures[] com {model, provider, class, error}), 501 TRANSCRIPTION_DISABLED.

POSTTranscrever (streaming)#

POST /v1/transcriptions/stream — owner/admin, SSE. O mesmo corpo, as mesmas validações, o mesmo custo — só o transporte muda: você vê quem está ouvindo, o que cada um ouviu, onde discordaram, o veredito e a reescuta enquanto acontecem. Tudo que pode falhar com um status (credencial, corpo, modelo, chave, tamanho) responde antes de o stream abrir — cheque res.ok antes de ler; depois do 200 só existe o evento error.

Cada data: é { stage, pass?, model?, message?, candidate?, data? }:

Evento Quando data
context antes de cada passada { terms, topics, models }; na passada 2, { terms, topics, reason } (cold_start_context_discovered | doubts_pending | requested)
listening um modelo começou a ouvir pass, model
heard um modelo terminou (ou falhou) pass, model, candidate
compared as leituras foram alinhadas { agreement, divergences, voting }
arbitrating o árbitro foi chamado { effort, divergences }
verdict o árbitro respondeu { final_text, topics, decisions, learned_terms, doubts, needs_audio_pass }
arbiter_failed o árbitro caiu; a maioria assumiu message
second_pass_failed a reescuta não rendeu; o veredito da passada 1 fica message
done o registro foi persistido idêntico à resposta do endpoint JSON (com id)
error falha depois do 200 { status, error_code, message }

Como consumir: o mesmo fetch + reader (ou requests com stream=True) da seção Geração de imagens — EventSource não serve, porque não envia corpo nem X-API-Key.

GETHistórico#

GET /v1/transcriptions?page=1&limit=20 — as transcrições da conta, mais recente primeiro: { transcriptions: [...], total, page, limit } (limit até 100). Filtre por falante com speaker_id=…. Os itens vêm no shape curto (sem candidates, divergences, decisions, doubts, prompt_by_model).

GET /v1/transcriptions/{id} — o shape completo. Id inexistente ou de outra conta → 404 TRANSCRIPTION_NOT_FOUND — indistinguíveis de propósito.

DELETE /v1/transcriptions/{id} — owner/admin, 204. Apaga o registro e as leituras; não toca o léxico (o histórico é do áudio, a memória é do falante).

POSTCorrigir#

POST /v1/transcriptions/{id}/correct — owner/admin. A edição do usuário é a evidência mais forte que existe: corpo { "text": "…" } (até 200k chars). Cada trecho alterado vira um termo confirmado com a grafia certa, levando a grafia errada como alias — o próximo áudio já sabe que "c-mux" é "cmux"; palavras removidas são rejeitadas. Resposta 200 com o shape completo mais learned_from_correction: [ { surface, aliases } ] (vazio se o texto não mudou). Texto vazio → 400.

GETLéxico#

GET /v1/transcriptions/lexicon?speaker_id=… — os termos do falante mais os da conta inteira, com status, confiança e a trilha de evidência (por que cada termo está onde está). Sem speaker_id, o escopo do próprio membro.

json
{
  "scope": "speaker:wa:5511999990000",
  "terms": [
    { "id": "<uuid>", "surface": "catcher-agents", "key": "catcher-agents",
      "aliases": ["catcher agents"], "topics": ["catcher-agents"], "meaning": "o produto",
      "status": "confirmed", "confidence": 1, "occurrences": 4,
      "evidence": [ { "kind": "arbiter", "source": "t:<uuid>", "agreeing": 3, "at": "…" },
                    { "kind": "user_taught", "source": "user:7", "at": "…" } ],
      "first_seen_at": "…", "last_seen_at": "…", "scope": "company" }
  ],
  "total": 1,
  "counts": { "confirmed": 1, "hypothesis": 0, "rejected": 0 }
}

status ∈ hypothesis | confirmed | rejected; evidence[].kind ∈ arbiter | majority | user_taught | user_confirmed | user_correction | user_rejected; scope é company (todo falante herda) ou o escopo do falante.

PUTEnsinar uma palavra#

PUT /v1/transcriptions/lexicon/terms — owner/admin. Corpo:

json
{ "surface": "cmux", "aliases": ["c-mux", "cê mux"], "topics": ["cmux"],
  "meaning": "multiplexador de terminais", "scope": "speaker", "speaker_id": "wa:5511999990000" }

surface obrigatório (até 200 bytes); aliases até 12; topics até 8; meaning até 300 chars; scope = company (padrão — todo falante da conta herda) ou speaker (com speaker_id). Ensinar de novo a mesma palavra mescla aliases/temas e mantém a sua grafia (acentos incluídos) como canônica. Resposta 200 com a linha do termo (status: "confirmed", confidence: 1).

POSTConfirmar ou rejeitar#

POST /v1/transcriptions/lexicon/terms/{id}/confirm · /reject — owner/admin. Aceitar uma hipótese (confirmed, confidence: 1) ou riscá-la (rejected, confidence: 0 — sai de todo prompt, e só um confirm seu a traz de volta). Resposta 200 com a linha; termo inexistente ou de outra conta → 404 TRANSCRIPTION_NOT_FOUND.

DELETE /v1/transcriptions/lexicon/terms/{id} — owner/admin, 204; inexistente ou de outra conta → 404 TRANSCRIPTION_TERM_NOT_FOUND.