Add loopback STT debug logging
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
interface RealtimeSegmenterOptions {
|
interface RealtimeSegmenterOptions {
|
||||||
onSegment: (pcm16: Buffer) => void;
|
onSegment: (pcm16: Buffer) => void;
|
||||||
|
onLevel?: (peak: number) => void;
|
||||||
|
onSpeechStart?: (peak: number) => void;
|
||||||
|
onSpeechDiscarded?: (samples: number) => void;
|
||||||
|
onSpeechReady?: (samples: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RealtimeSegmenter {
|
export class RealtimeSegmenter {
|
||||||
@@ -44,6 +48,8 @@ export class RealtimeSegmenter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.options.onLevel?.(peak);
|
||||||
|
|
||||||
if (!this.speechActive) {
|
if (!this.speechActive) {
|
||||||
appendWithCap(this.preRoll, frame, this.preRollSamples);
|
appendWithCap(this.preRoll, frame, this.preRollSamples);
|
||||||
if (peak >= this.speechStartThreshold) {
|
if (peak >= this.speechStartThreshold) {
|
||||||
@@ -60,6 +66,7 @@ export class RealtimeSegmenter {
|
|||||||
this.silenceFrames = 0;
|
this.silenceFrames = 0;
|
||||||
this.speech.splice(0, this.speech.length, ...this.preRoll);
|
this.speech.splice(0, this.speech.length, ...this.preRoll);
|
||||||
this.preRoll.splice(0, this.preRoll.length);
|
this.preRoll.splice(0, this.preRoll.length);
|
||||||
|
this.options.onSpeechStart?.(peak);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.speech.push(...frame);
|
this.speech.push(...frame);
|
||||||
@@ -81,9 +88,11 @@ export class RealtimeSegmenter {
|
|||||||
this.speechCandidateFrames = 0;
|
this.speechCandidateFrames = 0;
|
||||||
|
|
||||||
if (speechPcm.length < this.minSpeechSamples * 2) {
|
if (speechPcm.length < this.minSpeechSamples * 2) {
|
||||||
|
this.options.onSpeechDiscarded?.(speechPcm.length / 2);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.options.onSpeechReady?.(speechPcm.length / 2);
|
||||||
this.options.onSegment(speechPcm);
|
this.options.onSegment(speechPcm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
src/index.ts
56
src/index.ts
@@ -14,6 +14,13 @@ async function runLoopback(): Promise<void> {
|
|||||||
const stt = new FasterWhisperSttService(config, logger);
|
const stt = new FasterWhisperSttService(config, logger);
|
||||||
let capture = null as ReturnType<typeof spawnLoopbackCapture> | null;
|
let capture = null as ReturnType<typeof spawnLoopbackCapture> | null;
|
||||||
let shuttingDown: Promise<void> | null = null;
|
let shuttingDown: Promise<void> | null = null;
|
||||||
|
let receivedChunks = 0;
|
||||||
|
let receivedBytes = 0;
|
||||||
|
let maxPeak = 0;
|
||||||
|
let lastChunkAt = 0;
|
||||||
|
let lastLevelLogAt = 0;
|
||||||
|
let sawSpeechStart = false;
|
||||||
|
let emittedSegmentCount = 0;
|
||||||
|
|
||||||
const shutdown = async (exitCode: number, reason: string, error?: unknown): Promise<void> => {
|
const shutdown = async (exitCode: number, reason: string, error?: unknown): Promise<void> => {
|
||||||
if (shuttingDown) {
|
if (shuttingDown) {
|
||||||
@@ -60,6 +67,7 @@ async function runLoopback(): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await stt.warmup();
|
await stt.warmup();
|
||||||
|
logger.info("STT warmup finished");
|
||||||
|
|
||||||
const transcriptionQueue: Buffer[] = [];
|
const transcriptionQueue: Buffer[] = [];
|
||||||
let transcribing = false;
|
let transcribing = false;
|
||||||
@@ -93,14 +101,50 @@ async function runLoopback(): Promise<void> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const segmenter = new RealtimeSegmenter({
|
const segmenter = new RealtimeSegmenter({
|
||||||
|
onLevel: (peak) => {
|
||||||
|
if (peak > maxPeak) {
|
||||||
|
maxPeak = peak;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastLevelLogAt >= 3000) {
|
||||||
|
lastLevelLogAt = now;
|
||||||
|
logger.info("Audio input heartbeat", {
|
||||||
|
chunks: receivedChunks,
|
||||||
|
bytes: receivedBytes,
|
||||||
|
peak: maxPeak,
|
||||||
|
speech_started: sawSpeechStart,
|
||||||
|
emitted_segments: emittedSegmentCount,
|
||||||
|
});
|
||||||
|
maxPeak = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSpeechStart: (peak) => {
|
||||||
|
sawSpeechStart = true;
|
||||||
|
logger.info("Speech start detected", { peak });
|
||||||
|
},
|
||||||
|
onSpeechDiscarded: (samples) => {
|
||||||
|
logger.info("Discarded short speech segment", { samples });
|
||||||
|
},
|
||||||
|
onSpeechReady: (samples) => {
|
||||||
|
emittedSegmentCount += 1;
|
||||||
|
logger.info("Speech segment ready", { index: emittedSegmentCount, samples });
|
||||||
|
},
|
||||||
onSegment: (pcm16) => {
|
onSegment: (pcm16) => {
|
||||||
transcriptionQueue.push(pcm16);
|
transcriptionQueue.push(pcm16);
|
||||||
|
logger.info("Queued segment for STT", {
|
||||||
|
queue: transcriptionQueue.length,
|
||||||
|
bytes: pcm16.length,
|
||||||
|
});
|
||||||
void runNext();
|
void runNext();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
capture = spawnLoopbackCapture(config, logger);
|
capture = spawnLoopbackCapture(config, logger);
|
||||||
capture.stdout.on("data", (chunk: Buffer) => {
|
capture.stdout.on("data", (chunk: Buffer) => {
|
||||||
|
receivedChunks += 1;
|
||||||
|
receivedBytes += chunk.length;
|
||||||
|
lastChunkAt = Date.now();
|
||||||
segmenter.pushChunk(chunk);
|
segmenter.pushChunk(chunk);
|
||||||
});
|
});
|
||||||
capture.stderr.on("data", (chunk: Buffer) => {
|
capture.stderr.on("data", (chunk: Buffer) => {
|
||||||
@@ -123,6 +167,18 @@ async function runLoopback(): Promise<void> {
|
|||||||
console.log(`source: ${config.AUDIO_SOURCE ?? "unset"}`);
|
console.log(`source: ${config.AUDIO_SOURCE ?? "unset"}`);
|
||||||
console.log(`model: ${config.WHISPER_MODEL}`);
|
console.log(`model: ${config.WHISPER_MODEL}`);
|
||||||
console.log(`language: ${config.WHISPER_LANGUAGE}`);
|
console.log(`language: ${config.WHISPER_LANGUAGE}`);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (lastChunkAt === 0 && !shuttingDown) {
|
||||||
|
logger.warn("아직 캡처 PCM 데이터가 들어오지 않았습니다. AUDIO_SOURCE 가 잘못됐거나 loopback 입력이 아닌 장치일 수 있습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastChunkAt > 0 && now - lastChunkAt >= 5000 && !shuttingDown) {
|
||||||
|
logger.warn("최근 5초 동안 새 PCM chunk 가 들어오지 않았습니다.");
|
||||||
|
}
|
||||||
|
}, 5000).unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user