LocalLens
Extender

Agregar preguntas por voz con transcripción de QVAC

Graba audio en el navegador, transcríbelo localmente a través de @qvac/sdk, entrega el texto al endpoint de chat existente.

Dónde pertenece: la UI para grabar, el gateway de QVAC para la llamada de transcripción y una ruta del servidor delgada que los conecte.

Una pregunta por voz es solo una pregunta de texto con un paso extra al frente. QVAC trae Whisper out-of-the-box, así que no hay un modelo de terceros que conectar. El mismo SDK que potencia chat y embeddings transcribe un chunk de audio al string de pregunta que el endpoint de chat espera.

La referencia oficial de transcripción de QVAC vive en docs.qvac.tether.io/sdk/examples/ai-tasks/transcription.

La superficie de transcripción de QVAC

import {
  loadModel,
  transcribe,
  unloadModel,
  WHISPER_TINY,
} from "@qvac/sdk";

transcribe({ modelId, audioChunk }) devuelve la transcripción completa como un solo string. audioChunk acepta una ruta de archivo o un buffer en memoria, así que el gateway no necesita escribir el blob grabado a disco antes de transcribir.

WHISPER_TINY es el modelo más chico y el default correcto para preguntas por voz. La accuracy es más que suficiente para prompts cortos y el load time se mantiene rápido. Para captura multilenguaje, cambia por alguna de las constantes Parakeet TDT de la referencia de QVAC.

La receta de cuatro pasos

1. Agregar un método transcribe al 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,
    });
  }
}

Misma forma que los loaders existentes de chat y embedding: carga lazy en el primer uso, una sola promesa en vuelo vía required, un método por tarea. El modelo de transcripción es independiente del modelo de chat, así que cargarlo no demora el próximo round-trip de chat.

2. Exponerlo a través de LocalLensApp

Un forwarder chico mantiene encapsulado al gateway:

src/locallens.ts
async transcribe(audio: Buffer): Promise<string> {
  return this.qvac.transcribe(audio);
}

Ese es todo el cambio en el workflow. Las preguntas por voz reutilizan el pipeline existente de askBrain, así que las transiciones de estado multi-paso se quedan en un solo lugar.

3. Agregar una ruta /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 });
}

Un pass-through delgado, exactamente como los endpoints existentes de chat y cerebro. Los errores arrojados por LocalLensApp.transcribe fluyen por el helper compartido errorResponse sin cambios. Los AppError mantienen sus status codes; cualquier otra cosa aflora como un 500.

4. Grabar y enviar desde la UI

Usa la API estándar MediaRecorder para capturar:

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();
      }),
  };
}

Agrega un botón de micrófono al lado del input del chat. Cuando la grabación termina, hace POST del blob a /api/transcribe, mete el texto devuelto en el input del chat y envía el 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();

El form de chat ya sabe cómo llamar a /api/brains/:id/chat, así que el resto del round-trip no cambia. El usuario ve sus palabras aparecer como una pregunta, y la respuesta vuelve en stream de la misma forma.

Whisper espera WAV a 16 kHz

MediaRecorder viene por default en un contenedor que puede no coincidir con lo que Whisper quiere. Los docs de QVAC cubren la perilla audio_format: "f32le" y recomiendan audio a 16 kHz. Si la calidad de transcripción es pobre, resamplea en el cliente (o en el servidor) antes de llamar a transcribe.

Lo que no necesitas cambiar

  • rag.ts — el mismo builder de prompts.
  • El endpoint de chat — el mismo body JSON, la misma forma de response.
  • store.ts — sin campos nuevos.
  • domain.ts — sin tipos nuevos más allá de los que el camino de chat ya necesita.

Ese es el payoff de rutear la voz por el camino de chat existente. Cada mejora al lado de la respuesta beneficia a las preguntas por voz gratis.

Referencias externas

On this page