Adicionar perguntas por voz com transcrição QVAC
Grave áudio no navegador, transcreva localmente via @qvac/sdk, passe o texto para o endpoint de chat existente.
Onde ela pertence: a UI para gravação, o gateway QVAC para a chamada de transcrição e uma rota fina no servidor que conecta eles.
Uma pergunta por voz é só uma pergunta de texto com um passo extra na frente. O QVAC entrega Whisper out of the box, então não tem modelo de terceiro para conectar. O mesmo SDK que alimenta chat e embeddings transcreve um chunk de áudio na string de pergunta que o endpoint de chat espera.
A referência oficial de transcrição QVAC está em
docs.qvac.tether.io/sdk/examples/ai-tasks/transcription.
A superfície de transcrição QVAC
import {
loadModel,
transcribe,
unloadModel,
WHISPER_TINY,
} from "@qvac/sdk";transcribe({ modelId, audioChunk }) retorna a transcrição inteira
como uma única string. audioChunk aceita um caminho de arquivo
ou um buffer em memória, então o gateway não precisa escrever o blob
gravado em disco antes de transcrever.
WHISPER_TINY é o menor modelo e o default certo para
perguntas por voz. A acurácia é mais que suficiente para prompts curtos e o tempo de
load fica rápido. Para captura multilíngue, troca para uma das
constantes Parakeet TDT da referência QVAC.
A receita em quatro passos
1. Adiciona um método transcribe ao gateway
import {
loadModel,
transcribe,
unloadModel,
WHISPER_TINY,
} from "@qvac/sdk";
export class QvacGateway {
// existing fields…
private sttModelId: string | undefined;
private async ensureSttReady(): Promise<void> {
if (this.sttModelId) return;
this.sttModelId = await loadModel({
modelSrc: WHISPER_TINY,
modelType: "whisper",
modelConfig: { language: "en" },
});
}
async transcribe(audio: Buffer | string): Promise<string> {
await this.ensureSttReady();
return transcribe({
modelId: required(this.sttModelId, "QVAC transcription model is not loaded."),
audioChunk: audio,
});
}
}Mesmo formato dos loaders de chat e embedding existentes: load lazy
no primeiro uso, promise em voo única via required, um
método por tarefa. O modelo de transcrição é independente do
modelo de chat, então carregar ele não atrasa a próxima volta de chat.
2. Expõe via LocalLensApp
Um forwarder pequeno mantém o gateway encapsulado:
async transcribe(audio: Buffer): Promise<string> {
return this.qvac.transcribe(audio);
}Essa é a mudança inteira no workflow. Perguntas por voz reutilizam o
pipeline askBrain existente, então transições de estado multi-passo
ficam num lugar só.
3. Adiciona uma rota /api/transcribe
if (url.pathname === "/api/transcribe" && request.method === "POST") {
const arrayBuffer = await request.arrayBuffer();
const audio = Buffer.from(arrayBuffer);
const text = await app.transcribe(audio);
return json({ text });
}Um pass-through fino, exatamente como os endpoints de chat e brain
existentes. Erros lançados por LocalLensApp.transcribe fluem
pelo helper compartilhado errorResponse sem mudança. AppErrors
mantêm seus status codes; tudo mais vira 500.
4. Grava e envia da UI
Usa a API padrão MediaRecorder para captura:
async function startRecording() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream);
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.start();
return {
stop: () =>
new Promise((resolve) => {
recorder.onstop = () => {
stream.getTracks().forEach((t) => t.stop());
resolve(new Blob(chunks, { type: "audio/wav" }));
};
recorder.stop();
}),
};
}Adiciona um botão de microfone ao lado do input de chat. Quando a
gravação termina, faz POST do blob para /api/transcribe, joga o
texto retornado no input de chat e submete o form de chat
existente:
const recorder = await startRecording();
// … wait for the user to release the button …
const audio = await recorder.stop();
const { text } = await fetch("/api/transcribe", {
method: "POST",
body: audio,
}).then((r) => r.json());
elements.chatInput.value = text;
elements.chatForm.requestSubmit();O form de chat já sabe como chamar /api/brains/:id/chat,
então o resto da round-trip fica igual. O usuário vê suas
palavras aparecerem como pergunta, e a resposta vem em stream do mesmo
jeito.
Whisper espera WAV 16 kHz
MediaRecorder faz default para um container que pode não bater com o que
o Whisper quer. Os docs do QVAC cobrem o botão audio_format: "f32le"
e recomendam áudio 16 kHz. Se a qualidade da transcrição estiver
ruim, resampleie no cliente (ou no servidor) antes de chamar
transcribe.
O que você não precisa mudar
rag.ts— mesmo construtor de prompt.- O endpoint de chat — mesmo body JSON, mesmo formato de resposta.
store.ts— sem campos novos.domain.ts— sem tipos novos além do que o caminho de chat já precisa.
Esse é o retorno de rotear voz pelo caminho de chat existente. Toda melhoria do lado da resposta beneficia perguntas por voz de graça.