Skip to content

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

Endpointwss://api.sippulse.ai/v1/asr/listen, ou /v1/listen
ÁudioPCM 16 bits com sinal, little endian, intercalado
Modelopulse-stt-streaming-v1
Idiomaspt-BR, pt
Canaisaté 8 numa mesma sessão
Cobrançasegundos 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âmetroValoresPadrãoObservação
encodinglinear16linear16PCM 16 bits com sinal, little endian
sample_rate8000 a 480008000Precisa bater com o áudio enviado
channels1 a 81Amostras intercaladas; cada canal é transcrito em separado
languagept-BR, ptpt-BR
interim_resultstrue, falsetruefalse envia só os resultados finais
endpointing560 a 1500700Milissegundos 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
modelnome do modelopadrão da contaUse pulse-stt-streaming-v1
tagaté 64 caracteresnenhumDevolvido 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:

  1. Parâmetro de query apiKey
  2. Header api-key
  3. Parâmetro de query accessToken
  4. Header Authorization: Bearer <token> ou Authorization: Token <token>
  5. Subprotocolo Sec-WebSocket-Protocol: token, <credencial>
  6. 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:

json
{ "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.

json
{ "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

json
{
  "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 }
        ]
      }
    ]
  }
}
CampoSignificado
channel_index[canal, total de canais]. Cada canal é transcrito de forma independente
start, durationSegundos desde o início da sessão
is_finalfalse é parcial e ainda vai ser reescrito; true é trecho que não muda mais
speech_finaltrue quando o trecho fechou o turno, ou seja, o silêncio atingiu o endpointing
metadata.request_idTrace id da sessão, o mesmo valor do trace_id no Metadata. Cite ao abrir um chamado
alternativesSempre 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:

json
{
  "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:

json
{ "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.

StatusCódigoO que fazer
400INVALID_PARAM, UNSUPPORTED_ENCODING, UNSUPPORTED_LANGUAGECorrigir a query; a mensagem diz qual campo
401UNAUTHORIZEDCredencial ausente, malformada ou inválida
402INSUFFICIENT_CREDITA organização não tem crédito para abrir a sessão
404MODEL_NOT_FOUNDO modelo não está no seu catálogo, está inativo ou não é de streaming
429TOO_MANY_SESSIONSA cota de canais simultâneos da organização está em uso. Faça backoff e tente de novo; vem com Retry-After
502UPSTREAM_UNAVAILABLE, UPSTREAM_REJECTEDO worker de fala não respondeu ou recusou a sessão
503POD_NOT_READY, CREDIT_AUTHORIZATION_UNAVAILABLE, SHUTTING_DOWNTemporário. Repita depois do Retry-After
500INTERNAL_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ódigoSignificado
1000Fim normal: o seu CloseStream, ou o teto de duas horas por sessão
1001O 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)
1008O crédito acabou no meio da sessão
1011Erro interno
1013Backpressure: 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.

python
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.

python
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: language aceita pt-BR e pt, que são o mesmo idioma. Qualquer outro valor retorna UNSUPPORTED_LANGUAGE (HTTP 400). Outros idiomas não estão disponíveis neste modelo.
  • endpointing fora da faixa: valores inteiros fora de [560, 1500] são normalizados para o limite mais próximo, não recusados. false, no ou zero significam "sem endpointing" e viram o maior turno, 1500. Apenas um valor que não é inteiro nem um desses retorna INVALID_PARAM (HTTP 400).

Streaming ou REST

StreamingREST
Resultado chegadurante a faladepois do arquivo inteiro
EntradaPCM ao vivoMP3, WAV, OGG, PCM até 25 MB
Idiomaspt-BRmultilíngue
Diarizaçãopor canalpor canal ou por locutor
Anonimização, Audio Intelligencenãosim

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.