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
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:
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
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:
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
Agregar parsing de PDF e imágenes con OCR de QVAC
Reutiliza la tarea de OCR de QVAC para convertir páginas escaneadas e imágenes en LocalDocuments.
Agregar mejores diagnósticos
Aflora el estado de los modelos, el progreso de indexación, las cuentas de archivos y chunks, y números reales de latencia del profiler de QVAC.