Add realtime loopback STT prototype

This commit is contained in:
2026-05-02 20:20:54 +09:00
parent 10e0dd75db
commit 5775c4809a
17 changed files with 1034 additions and 0 deletions

107
src/index.ts Normal file
View File

@@ -0,0 +1,107 @@
import process from "node:process";
import { loadConfig } from "./config.js";
import { Logger } from "./logger.js";
import { printAudioDevices, spawnLoopbackCapture } from "./audio/capture.js";
import { RealtimeSegmenter } from "./audio/realtime-segmenter.js";
import { FasterWhisperSttService } from "./services/faster-whisper-stt.js";
const mode = process.argv[2] ?? "loopback";
async function runLoopback(): Promise<void> {
const config = loadConfig();
const logger = new Logger(config.LOG_LEVEL);
const stt = new FasterWhisperSttService(config, logger);
await stt.warmup();
const transcriptionQueue: Buffer[] = [];
let transcribing = false;
const runNext = async (): Promise<void> => {
if (transcribing) {
return;
}
const next = transcriptionQueue.shift();
if (!next) {
return;
}
transcribing = true;
try {
const text = await stt.transcribePcm16(next);
if (!text) {
logger.info("빈 전사 결과");
} else {
logger.info("Transcript", text);
if (config.DEBUG_TRANSCRIPTS) {
console.log(`\n[text] ${text}\n`);
}
}
} catch (error) {
logger.warn("STT failed", error);
} finally {
transcribing = false;
void runNext();
}
};
const segmenter = new RealtimeSegmenter({
onSegment: (pcm16) => {
transcriptionQueue.push(pcm16);
void runNext();
},
});
const capture = spawnLoopbackCapture(config, logger);
capture.stdout.on("data", (chunk: Buffer) => {
segmenter.pushChunk(chunk);
});
capture.stderr.on("data", (chunk: Buffer) => {
const text = chunk.toString().trim();
if (text) {
logger.debug("[capture]", text);
}
});
capture.on("exit", (code, signal) => {
logger.warn("capture exited", { code, signal });
});
console.log("실시간 출력장치 STT를 시작합니다. Ctrl+C 로 종료합니다.");
console.log(`source: ${config.AUDIO_SOURCE ?? "unset"}`);
console.log(`model: ${config.WHISPER_MODEL}`);
console.log(`language: ${config.WHISPER_LANGUAGE}`);
const shutdown = async (): Promise<void> => {
if (!capture.killed) {
capture.kill("SIGTERM");
}
await stt.destroy();
process.exit(0);
};
process.on("SIGINT", () => {
void shutdown();
});
process.on("SIGTERM", () => {
void shutdown();
});
}
async function main(): Promise<void> {
switch (mode) {
case "devices":
await printAudioDevices();
return;
case "loopback":
await runLoopback();
return;
default:
throw new Error(`알 수 없는 실행 모드입니다: ${mode}. 사용 가능: loopback, devices`);
}
}
void main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});