Files
oh-my-pi/packages/coding-agent/src/stt/transcriber.ts
T

92 lines
2.7 KiB
TypeScript

import { logger } from "@oh-my-pi/pi-utils";
import transcribeScript from "./transcribe.py" with { type: "text" };
export interface TranscribeOptions {
modelName?: string;
language?: string;
signal?: AbortSignal;
}
const TRANSCRIBE_TIMEOUT_MS = 120_000;
/**
* Find a usable Python command.
*/
export function resolvePython(): string | null {
for (const cmd of ["python", "py", "python3"]) {
if (Bun.which(cmd)) return cmd;
}
return null;
}
/**
* Transcribe a WAV file using Python openai-whisper.
*
* Reads the WAV via Python's built-in `wave` module (no ffmpeg needed),
* resamples to 16 kHz mono, and passes the numpy array directly to whisper.
*/
export async function transcribe(audioPath: string, options?: TranscribeOptions): Promise<string> {
const audioFile = Bun.file(audioPath);
if (audioFile.size < 100) {
throw new Error(`Audio file is empty or too small (${audioFile.size} bytes). Check microphone.`);
}
const pythonCmd = resolvePython();
if (!pythonCmd) {
throw new Error("Python not found. Install Python 3.8+ from https://python.org");
}
const modelName = options?.modelName ?? "base.en";
const language = options?.language ?? "en";
logger.debug("Transcribing with Python whisper", { pythonCmd, audioPath, modelName, language });
const proc = Bun.spawn([pythonCmd, "-c", transcribeScript, audioPath, modelName, language], {
stdout: "pipe",
stderr: "pipe",
});
if (options?.signal?.aborted) {
proc.kill();
options.signal.throwIfAborted();
}
const onAbort = () => proc.kill();
options?.signal?.addEventListener("abort", onAbort, { once: true });
let timedOut = false;
const killTimer = setTimeout(() => {
timedOut = true;
logger.error("Python whisper transcription timed out, killing process", { timeoutMs: TRANSCRIBE_TIMEOUT_MS });
proc.kill();
}, TRANSCRIBE_TIMEOUT_MS);
const exitCode = await proc.exited;
clearTimeout(killTimer);
options?.signal?.removeEventListener("abort", onAbort);
options?.signal?.throwIfAborted();
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
if (timedOut) {
throw new Error(`Transcription timed out after ${Math.round(TRANSCRIBE_TIMEOUT_MS / 1000)}s`);
}
if (exitCode !== 0) {
logger.error("Python whisper transcription failed", { exitCode, stderr: stderr.trim() });
if (stderr.includes("No module named 'whisper'")) {
throw new Error("openai-whisper not installed. Run: pip install openai-whisper");
}
// Show last line of stderr (the actual error, not the full traceback)
const lastLine = stderr.trim().split("\n").pop() ?? "";
throw new Error(`Transcription failed: ${lastLine}`);
}
const text = stdout.trim();
logger.debug("Transcription complete", { length: text.length });
return text;
}