LocalLens
Estender

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

src/qvac.ts
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:

src/locallens.ts
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

src/server.ts
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:

src/ui/app.js
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.

Referências externas

On this page