import { spawn } from "node:child_process"; import process from "node:process"; import ffmpegStatic from "ffmpeg-static"; import type { AssistantRuntimeConfig } from "./config.js"; import { Logger } from "./logger.js"; import { LocalVoiceSession } from "./audio/local-voice-session.js"; import { ElevenLabsSttService } from "./services/elevenlabs-stt.js"; import { ElevenLabsTtsService } from "./services/elevenlabs-tts.js"; import { OpenAiLlmService } from "./services/openai-llm.js"; function resolveFfmpegPath(): string { const ffmpegPath = ffmpegStatic as unknown as string | null; if (!ffmpegPath) { throw new Error("ffmpeg-static 경로를 찾지 못했습니다."); } return ffmpegPath; } export async function printLocalAudioDevices(): Promise { if (process.platform === "win32") { const ffmpegPath = resolveFfmpegPath(); console.log("\n=== ffmpeg dshow audio devices ==="); await new Promise((resolve) => { const child = spawn( ffmpegPath, ["-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "dummy"], { stdio: ["ignore", "ignore", "inherit"], }, ); child.on("exit", () => resolve()); child.on("error", (error) => { throw error; }); }); console.log("\n위 목록의 오디오 장치 이름을 `LOCAL_AUDIO_SOURCE` 에 그대로 넣으면 됩니다."); console.log("Windows 로컬 모드는 현재 출력 장치 직접 선택 대신 시스템 기본 출력 장치를 사용합니다."); return; } const runs = [ { label: "wpctl status", args: ["status"], }, { label: "wpctl status -n", args: ["status", "-n"], }, ] as const; for (const run of runs) { console.log(`\n=== ${run.label} ===`); await new Promise((resolve, reject) => { const child = spawn("wpctl", run.args, { stdio: ["ignore", "inherit", "inherit"], }); child.on("exit", (code) => { if (code === 0) { resolve(); return; } reject(new Error(`wpctl exited with code ${code ?? "null"}`)); }); child.on("error", reject); }); } } export async function runLocalAssistant(config: AssistantRuntimeConfig, logger: Logger): Promise { const stt = new ElevenLabsSttService(config); const tts = new ElevenLabsTtsService(config); const llm = new OpenAiLlmService(config); const session = new LocalVoiceSession({ config, logger, stt, tts, llm, }); console.log(session.statusSummary()); console.log("로컬 음성 테스트를 시작합니다. Ctrl+C 로 종료합니다."); if (process.platform === "win32") { console.log("Windows 로컬 모드는 현재 시스템 기본 출력 장치로 재생됩니다."); } if (config.DEBUG_TEXT_EVENTS) { console.log("텍스트 로그 출력이 켜져 있습니다."); } const shutdown = async (exitCode = 0) => { await session.destroy().catch((error) => { logger.warn("Local session shutdown failed", error); }); process.exit(exitCode); }; process.on("SIGINT", () => { void shutdown(0); }); process.on("SIGTERM", () => { void shutdown(0); }); await session.start(); }