/* * Veskslop daemon — capture pipeline. * * Reads the client output monitor as raw PCM, splits it into utterances with * the VAD, and writes one WAV per utterance. The caller stores the metadata. */ import { spawn, type ChildProcess } from "node:child_process"; import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { pcmToWav, Vad, type VadConfig, type VadSegment } from "./vad.js"; export interface CaptureSegment { file: string; durationMs: number; startedAt: string; endedAt: string; } export interface CapturePipelineOptions { ffmpeg: string; target: string; directory: string; sessionId: string; vad: VadConfig; onSegment: (segment: CaptureSegment) => void; onError: (error: Error) => void; } export class CapturePipeline { private child?: ChildProcess; private vad?: Vad; private counter = 0; private stopped = false; constructor(private readonly options: CapturePipelineOptions) {} start() { mkdirSync(this.options.directory, { recursive: true }); this.vad = new Vad(this.options.vad, segment => this.emit(segment)); this.child = spawn( this.options.ffmpeg, [ "-hide_banner", "-loglevel", "error", "-f", "pulse", "-i", this.options.target, "-ac", "1", "-ar", String(this.options.vad.sampleRate), "-f", "s16le", "-" ], { stdio: ["ignore", "pipe", "pipe"] } ); let stderr = ""; this.child.stdout?.on("data", chunk => this.vad?.push(chunk)); this.child.stderr?.on("data", chunk => (stderr += chunk)); this.child.on("error", error => this.options.onError(new Error(String(error)))); this.child.on("close", code => { if (!this.stopped && code) { this.options.onError(new Error(stderr.trim() || `ffmpeg exited with ${code}`)); } }); } stop() { this.stopped = true; this.vad?.flush(); this.vad = undefined; try { this.child?.kill("SIGTERM"); } catch { // already gone } this.child = undefined; } private emit(segment: VadSegment) { this.counter++; const endedAt = new Date(); const startedAt = new Date(endedAt.getTime() - segment.durationMs); const file = join(this.options.directory, `${this.options.sessionId}-${this.counter}.wav`); writeFileSync(file, pcmToWav(segment.pcm, this.options.vad?.sampleRate ?? 16000)); this.options.onSegment({ file, durationMs: segment.durationMs, startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString() }); } }