112 lines
3.2 KiB
TypeScript
112 lines
3.2 KiB
TypeScript
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<void> {
|
|
if (process.platform === "win32") {
|
|
const ffmpegPath = resolveFfmpegPath();
|
|
|
|
console.log("\n=== ffmpeg dshow audio devices ===");
|
|
await new Promise<void>((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<void>((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<void> {
|
|
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();
|
|
}
|