Transcrição em Tempo Real (STT Streaming)
O endpoint de streaming transcreve o áudio enquanto ele está sendo falado, por uma única conexão WebSocket. Você envia PCM cru e recebe resultados parciais, que vão sendo corrigidos conforme mais áudio chega, e um resultado final para cada trecho.
Use para legendagem ao vivo, agentes de voz, monitoria de call center e qualquer caso em que esperar a gravação terminar não é opção. Para arquivos que já existem, o endpoint REST de transcrição é mais simples e suporta mais formatos, mais idiomas e pós-processamento.
Beta fechado
O pulse-stt-streaming-v1 ainda não aparece no catálogo público de modelos. Peça ao suporte para habilitá-lo na sua organização antes de integrar.
Resumo
| Endpoint | wss://api.sippulse.ai/v1/asr/listen, ou /v1/listen |
| Áudio | PCM 16 bits com sinal, little endian, intercalado |
| Modelo | pulse-stt-streaming-v1 |
| Idiomas | pt-BR, pt |
| Canais | até 8 numa mesma sessão |
| Cobrança | segundos de áudio por canal, silêncio incluído |
Handshake
Tudo o que pode falhar é checado antes de a conexão ser aceita. Por isso uma sessão recusada volta como resposta HTTP normal no mesmo socket, com status e corpo legível. Depois que o 101 Switching Protocols chega, a sessão está viva e o pod já está conectado.
Parâmetros de query
| Parâmetro | Valores | Padrão | Observação |
|---|---|---|---|
encoding | linear16 | linear16 | PCM 16 bits com sinal, little endian |
sample_rate | 8000 a 48000 | 8000 | Precisa bater com o áudio enviado |
channels | 1 a 8 | 1 | Amostras intercaladas; cada canal é transcrito em separado |
language | pt-BR, pt | pt-BR | |
interim_results | true, false | true | false envia só os resultados finais |
endpointing | 560 a 1500 | 700 | Milissegundos de silêncio que fecham um turno. Valores fora da faixa são normalizados para ela; false, no ou 0 significa sem endpointing e vira 1500 |
model | nome do modelo | padrão da conta | Use pulse-stt-streaming-v1 |
tag | até 64 caracteres | nenhum | Devolvido no Metadata; serve para correlacionar com o seu log |
Parâmetro desconhecido é ignorado, não recusado: o SDK da Deepgram manda punctuate, smart_format e diarize por padrão, e derrubar a conexão por causa deles não ajudaria ninguém. Já valor inválido num parâmetro conhecido é 400, com o campo problemático na mensagem, com uma exceção: endpointing fora da faixa é normalizado em vez de recusado.
Não existe equivalente para smart_format (a pontuação vem do próprio modelo) nem para multichannel (use channels).
Autenticação
A credencial é lida nesta ordem, e vale a primeira encontrada:
- Parâmetro de query
apiKey - Header
api-key - Parâmetro de query
accessToken - Header
Authorization: Bearer <token>ouAuthorization: Token <token> - Subprotocolo
Sec-WebSocket-Protocol: token, <credencial> - Header
x-guest-token
As opções 1 e 3 existem porque o navegador não consegue mandar header no handshake de WebSocket; a 5 é o mesmo truque que o SDK web da Deepgram usa, e o servidor devolve o subprotocolo token no 101 para o navegador aceitar. A opção 4 aceita os dois prefixos, então um cliente Deepgram mantém o header original.
API key é a credencial certa para integração de servidor. Chave em query string acaba no log de proxy e de navegador, então prefira o header sempre que o seu cliente conseguir mandar um.
Enviando áudio
Mande frames binários com PCM cru. Blocos de 20 a 100 ms são o ponto ideal; o limite duro é 64 KB por frame. Numa chamada estéreo a 16 kHz, 40 ms são 16000 * 0,04 * 2 canais * 2 bytes = 2560 bytes.
Não é preciso cadenciar o áudio com precisão, mas enviar muito mais rápido que o tempo real acaba disparando a proteção de backpressure e fecha a sessão com 1013.
Dois frames de texto são aceitos:
{ "type": "Finalize" }Fecha o turno atual na hora, sem encerrar a sessão: use quando a sua própria detecção de voz souber que o interlocutor parou de falar. O áudio pendente é transcrito e sai como resultado final.
{ "type": "CloseStream" }Encerra a sessão. Os finais que faltam e o frame Metadata são enviados, e só então a conexão fecha com 1000. Termine sempre assim, em vez de derrubar o socket, ou o último trecho e o resumo de consumo se perdem.
Qualquer outro frame de texto é descartado com um aviso, inclusive o KeepAlive da Deepgram. Ele conta como atividade do cliente para o timer de ociosidade, então deixá-lo num cliente portado não faz mal.
Recebendo resultados
Results
{
"type": "Results",
"metadata": { "request_id": "4f1a9c2e8b7d4a6f" },
"channel_index": [0, 2],
"start": 4.32,
"duration": 1.86,
"is_final": true,
"speech_final": true,
"channel": {
"alternatives": [
{
"transcript": "qual é o horário de funcionamento",
"confidence": 0.8594,
"words": [
{ "word": "qual", "start": 4.32, "end": 4.48, "confidence": 0.8594 },
{ "word": "é", "start": 4.48, "end": 4.56, "confidence": 0.8203 }
]
}
]
}
}| Campo | Significado |
|---|---|
channel_index | [canal, total de canais]. Cada canal é transcrito de forma independente |
start, duration | Segundos desde o início da sessão |
is_final | false é parcial e ainda vai ser reescrito; true é trecho que não muda mais |
speech_final | true quando o trecho fechou o turno, ou seja, o silêncio atingiu o endpointing |
metadata.request_id | Trace id da sessão, o mesmo valor do trace_id no Metadata. Cite ao abrir um chamado |
alternatives | Sempre exatamente uma alternativa. O confidence é o do modelo, de 0 a 1, e existe também em cada item de words |
Regra de renderização: substitua a linha atual enquanto is_final for false, fixe-a quando is_final for true, e trate speech_final como fim do turno de quem fala.
Metadata
Enviado uma vez, logo antes de a sessão fechar:
{
"type": "Metadata",
"trace_id": "4f1a9c2e8b7d4a6f",
"channels": 2,
"duration": [61.44, 61.44],
"model": "pulse-stt-streaming-v1",
"tag": "call-8821"
}duration é o áudio cobrado por canal, em segundos. O trace_id é o identificador a citar ao abrir um chamado sobre uma sessão específica.
Error
Tanto o corpo HTTP anterior ao handshake quanto qualquer frame de erro usam o mesmo envelope:
{ "type": "Error", "code": "INVALID_PARAM", "message": "sample_rate must be between 8000 and 48000, got 96000" }Quando a sessão é recusada
Antes do 101, as falhas são respostas HTTP, não códigos de fechamento. A sua biblioteca cliente vai reportá-las como erro de handshake com um status.
| Status | Código | O que fazer |
|---|---|---|
| 400 | INVALID_PARAM, UNSUPPORTED_ENCODING, UNSUPPORTED_LANGUAGE | Corrigir a query; a mensagem diz qual campo |
| 401 | UNAUTHORIZED | Credencial ausente, malformada ou inválida |
| 402 | INSUFFICIENT_CREDIT | A organização não tem crédito para abrir a sessão |
| 404 | MODEL_NOT_FOUND | O modelo não está no seu catálogo, está inativo ou não é de streaming |
| 429 | TOO_MANY_SESSIONS | A cota de canais simultâneos da organização está em uso. Faça backoff e tente de novo; vem com Retry-After |
| 502 | UPSTREAM_UNAVAILABLE, UPSTREAM_REJECTED | O worker de fala não respondeu ou recusou a sessão |
| 503 | POD_NOT_READY, CREDIT_AUTHORIZATION_UNAVAILABLE, SHUTTING_DOWN | Temporário. Repita depois do Retry-After |
| 500 | INTERNAL_ERROR, POD_NOT_CONFIGURED | É do nosso lado. Abra um chamado com o horário |
Atenção: channels=2 consome dois canais da cota, não um.
Quando a sessão termina
Depois do 101, o motivo chega como código de fechamento do WebSocket.
| Código | Significado |
|---|---|
1000 | Fim normal: o seu CloseStream, ou o teto de duas horas por sessão |
1001 | O servidor está sendo derrubado para um deploy, ou o seu cliente parou de responder aos pings (ping a cada 20 s, ociosidade de 120 s) |
1008 | O crédito acabou no meio da sessão |
1011 | Erro interno |
1013 | Backpressure: o áudio chegou mais rápido do que dava para processar, ou o seu cliente parou de ler os resultados |
Em 1001 e 1013, reconecte e continue enviando áudio. Em 1008, recarregue o saldo antes: reconectar na hora só vai dar 402.
Cobrança
Streaming é cobrado por segundos de áudio por canal, pela regra transcription, pelo tempo real da sessão: sem bloco mínimo e sem arredondar para o minuto. Uma sessão de 12 segundos custa 12 segundos. Silêncio conta: uma sessão que fica aberta sem ninguém falando ainda é áudio que o modelo processou.
O preço aparece por minuto na tabela de preços por convenção do mercado, mas a medição é por segundo. Uma chamada estéreo custa o dobro de uma mono de mesma duração. O array duration do Metadata é exatamente o que é cobrado, e cada sessão aparece no dashboard atribuída ao projeto e à API key que a abriu.
Sessões longas são cobradas em janelas ao longo da execução, não só no fim, então o consumo aparece no dashboard enquanto a chamada ainda está acontecendo.
Um exemplo que roda
Envia um arquivo WAV em tempo real e imprime a transcrição. Precisa de pip install websockets.
import asyncio
import json
import wave
import websockets
URL = "wss://api.sippulse.ai/v1/asr/listen"
API_KEY = "sp-..."
BLOCK_MS = 40
async def main(path: str) -> None:
audio = wave.open(path, "rb")
rate, channels = audio.getframerate(), audio.getnchannels()
url = (
f"{URL}?encoding=linear16&sample_rate={rate}&channels={channels}"
f"&language=pt-BR&endpointing=700&model=pulse-stt-streaming-v1"
)
async with websockets.connect(url, additional_headers={"api-key": API_KEY}) as ws:
async def receive() -> None:
async for message in ws:
frame = json.loads(message)
if frame["type"] == "Results":
text = frame["channel"]["alternatives"][0]["transcript"]
if not text:
continue
mark = "final" if frame["is_final"] else " "
print(f"{mark} [ch{frame['channel_index'][0]}] {text}")
elif frame["type"] == "Metadata":
print(f"cobrado: {sum(frame['duration']):.1f}s")
elif frame["type"] == "Error":
print(f"erro: {frame['code']} - {frame['message']}")
reader = asyncio.create_task(receive())
block = int(rate * BLOCK_MS / 1000)
while chunk := audio.readframes(block):
await ws.send(chunk)
await asyncio.sleep(BLOCK_MS / 1000) # cadência de tempo real
await ws.send(json.dumps({"type": "CloseStream"}))
await asyncio.wait_for(reader, timeout=10)
asyncio.run(main("chamada.wav"))Para transcrever o microfone em vez de um arquivo, troque o laço de leitura pela sua biblioteca de captura e mantenha o resto: o protocolo não se importa de onde o PCM veio.
Vindo da Deepgram
O protocolo é no formato da Deepgram de propósito, então um cliente que já existe precisa de muito pouco. Tanto o livekit-plugins-deepgram quanto o SDK oficial Python conectam apontando a base URL para este endpoint e usando uma API key como credencial; o resto da configuração não muda.
from livekit.plugins import deepgram
stt = deepgram.STT(
base_url="wss://api.sippulse.ai/v1/asr/listen",
api_key="sp-...",
model="pulse-stt-streaming-v1",
language="pt-BR",
endpointing_ms=700,
)O SDK oficial acrescenta /v1/listen ao host que recebe, e esse path também é aceito: wss://api.sippulse.ai/v1/listen e wss://api.sippulse.ai/v1/asr/listen abrem a mesma sessão. O esquema Authorization: Token <chave>, que os dois clientes usam, é aceito exatamente como Bearer.
Endpointing
endpointing fora da faixa suportada não fecha mais a conexão: ele é normalizado para o limite mais próximo de [560, 1500] milissegundos. O valor em vigor é o que fecha o turno, então um cliente que pede 25, o padrão do plugin, recebe 560. endpointing=false, no ou 0 (o plugin manda false quando endpointing_ms é 0) significa "sem endpointing" na Deepgram e vira o maior turno disponível, 1500.
O teto de 1500 não é arbitrário: acima dele o turno fecha longe demais do fim da fala e sai sem a pontuação terminal.
Um valor que não é inteiro nem false/no retorna INVALID_PARAM (HTTP 400).
Parâmetros ignorados
Parâmetros que não conhecemos, inclusive os que os clientes da Deepgram mandam por padrão - punctuate, smart_format, diarize, no_delay, vad_events, filler_words, profanity_filter, numerals - são ignorados em silêncio. Eles não fecham a sessão e não aparecem nos resultados.
Mensagens que não enviamos
SpeechStarted e UtteranceEnd não são emitidos. O plugin do LiveKit funciona sem eles: ele os trata como opcionais e segue transcrevendo só com os frames Results e Metadata.
Limitações conhecidas
- Um idioma por vez:
languageaceitapt-BRept, que são o mesmo idioma. Qualquer outro valor retornaUNSUPPORTED_LANGUAGE(HTTP 400). Outros idiomas não estão disponíveis neste modelo. endpointingfora da faixa: valores inteiros fora de[560, 1500]são normalizados para o limite mais próximo, não recusados.false,noou zero significam "sem endpointing" e viram o maior turno,1500. Apenas um valor que não é inteiro nem um desses retornaINVALID_PARAM(HTTP 400).
Streaming ou REST
| Streaming | REST | |
|---|---|---|
| Resultado chega | durante a fala | depois do arquivo inteiro |
| Entrada | PCM ao vivo | MP3, WAV, OGG, PCM até 25 MB |
| Idiomas | pt-BR | multilíngue |
| Diarização | por canal | por canal ou por locutor |
| Anonimização, Audio Intelligence | não | sim |
Um arranjo comum é usar os dois: streaming para a experiência ao vivo e uma transcrição REST da gravação depois, para analytics, anonimização e insights.
