Pular para o conteúdo
int8labs

Avatar

O Gustavo ou a Helena fala no seu site: voz e boca sincronizadas, desenhadas no navegador do seu usuário pelo SDK web. O avatar lê um texto com a própria voz ou dubla um áudio seu. R$ 0,20 por minuto de fala, com a voz incluída.

4 min de leitura · atualizado em 13/10/2026

Os avatares

  • Gustavo (avatar: "gustavo", o padrão): o anfitrião. Voz masculina, tom direto e amigável.
  • Helena (avatar: "helena"): a atendente, o exemplo de avatar de atendimento para o seu produto. Voz feminina, tom acolhedor.

A opção avatar do SDK escolhe o rosto, a voz e a boca; sem ela, fala o Gustavo. No WebSocket, é o parâmetro ?avatar= da URL. O preço é o mesmo para os dois.

Como funciona

  1. O seu servidor pede um token temporário com o escopo avatar (POST /v1/tokens), depois de conferir o login do seu usuário.
  2. A página carrega o SDK de https://int8labs.com/sdk/avatar/v1.js e cria o avatar num elemento seu.
  3. O SDK abre o WebSocket /v1/avatar/stream com o token e o avatar, toca o som e move a boca no tempo certo.

A chave nunca vai para o navegador: só o token, que vence em minutos.

No seu site

import { createAvatar } from "https://int8labs.com/sdk/avatar/v1.js";
​
// O token vem do seu backend (POST /v1/tokens com o escopo avatar). A chave nunca vai para o navegador.
// Passe uma função: o SDK a chama ao conectar e de novo quando o token vence, e reconecta sozinho.
// Se ela lançar um erro, a fala rejeita com o código "token" e o status do erro (402, 401…).
const pedirToken = async () => {
const resposta = await fetch("/int8/token", { method: "POST" });
if (!resposta.ok) throw Object.assign(new Error("Não deu para pegar o token."), { status: resposta.status });
return (await resposta.json()).token;
};
​
const avatar = await createAvatar({
container: document.querySelector("#avatar"),
avatar: "gustavo", // ou "helena": o rosto, a voz e a boca
token: pedirToken,
apiUrl: "https://api.int8labs.com",
});
​
// O done chega com o custo enquanto o som ainda toca; o speak() resolve quando o som termina.
avatar.on("done", ({ audioMs, costBrl }) => console.log(`${audioMs} ms, R$ ${costBrl}`));
avatar.on("error", ({ code, status, message }) => console.error(code, status, message));
​
// O navegador só deixa o som tocar depois de um gesto: fale dentro de um clique.
// Sem o clique, a fala rejeita com o código "autoplay".
document.querySelector("#falar").addEventListener("click", async () => {
await avatar.speak("Olá! Como posso ajudar?");
await avatar.speak("Posso confirmar a sua consulta?"); // fala depois da primeira, sem cortar
});

O token vem de uma rota sua, como esta:

curl https://api.int8labs.com/v1/tokens \
-H "Authorization: Bearer $INT8_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ttl_seconds": 600, "scopes": ["avatar"]}'

Passe o token como uma função, como no exemplo: o SDK a chama ao conectar e de novo quando o token vence (fechamento 4401), e reconecta sozinho. Com um token fixo (texto), o 4401 vira o erro api_key_expired e a conversa acaba.

Quando a função de token falha, a fala rejeita (e o evento error sai) com o código token, a mensagem e o status numérico do erro que ela lançou: um 402, por exemplo, quando o seu backend recusa o token por falta de saldo. Por isso o exemplo lança o erro com o status da resposta.

Um avatar fora da lista rejeita o createAvatar com o código invalid_request, antes de baixar qualquer coisa.

Modo texto

avatar.speak(texto) fala até 1.000 caracteres por vez, com a voz do avatar escolhido. A promessa resolve quando o som termina de tocar, junto com o evento end ({ interrupted: false }), e devolve a duração (audioMs), o custo (costBrl) e o saldo (balanceBrl). O evento done chega antes, com esses números e o som ainda tocando. Assim, dois await avatar.speak() seguidos falam um depois do outro, sem corte.

Se stop() ou uma nova fala interromper o speak(), a promessa rejeita com o código interrupted e o end sai com { interrupted: true }. Trate-o como um fim normal, não como falha. A parte que já foi falada é cobrada. O SDK também recusa uma chave int8_live_: use sempre o token temporário int8_tmp_, gerado no seu servidor.

O navegador só deixa o som tocar depois de um gesto do usuário, como um clique. Por isso o exemplo chama speak() dentro de um clique. Sem o gesto, o som fica bloqueado: o done sai, porque a fala foi gerada e é cobrada, e logo depois a promessa rejeita com o código autoplay e a mensagem "O navegador bloqueou o som: chame speak() depois de um clique.". O evento error sai junto. O speakAudio() segue a mesma regra.

Modo áudio próprio

Com um áudio seu (gravado, ou de outro motor de voz), o avatar só mexe a boca:

// A boca dubla um áudio seu: o SDK decodifica no navegador, reamostra e toca sincronizado com o rosto.
// Como o speak(), chame dentro de um clique: sem o gesto, o navegador bloqueia o som.
document.querySelector("#falar").addEventListener("click", async () => {
const resposta = await fetch("/audios/boas-vindas.mp3");
await avatar.speakAudio(await resposta.arrayBuffer());
});
​
// Quando a página sair: fecha a conexão e libera o WebGL.
avatar.destroy();

O SDK decodifica o arquivo no navegador, reamostra e manda no modo áudio próprio; o som toca do seu lado, sincronizado com o rosto. Até 5 minutos por fala.

Protocolo

Para usar sem o SDK (num app nativo, por exemplo), o WebSocket manda o áudio (0x01) e os pacotes de rosto (0x02, 25 quadros por segundo). A sincronia da boca com o som fica do seu lado, pela posição do player.

import asyncio
import json
import os
​
import websockets # pip install "websockets>=14"
​
​
async def main() -> None:
# avatar=gustavo (o padrão) ou helena: o rosto, a voz e a boca.
async with websockets.connect(
"wss://api.int8labs.com/v1/avatar/stream?avatar=helena",
additional_headers={"Authorization": f"Bearer {os.environ['INT8_API_KEY']}"},
) as ws:
await ws.send(json.dumps({"type": "speak", "text": "Olá! Como posso ajudar?"}))
audio, pacotes = bytearray(), 0
async for mensagem in ws:
if isinstance(mensagem, bytes):
if mensagem[0] == 0x01:
audio += mensagem[3:] # PCM16 LE mono, 44100 Hz
elif mensagem[0] == 0x02:
pacotes += 1 # pacote de rosto, para o renderizador
continue
evento = json.loads(mensagem)
if evento["type"] == "done":
print(evento["audio_ms"], "ms;", pacotes, "pacotes de rosto; R$", evento["cost_brl"])
break
​
​
asyncio.run(main())

As mensagens estão na referência do WebSocket do avatar.

Limites e cobrança

  • Até 5 conexões de avatar abertas por organização; a sexta fecha com 4429.
  • Texto de até 1.000 caracteres; áudio próprio de até 5 minutos por fala.
  • A cobrança é por segundo de fala, voz incluída. O saldo é conferido antes de cada fala.