fix: handle missing ffmpeg on windows and add devices alias

This commit is contained in:
2026-04-30 02:57:49 +09:00
parent 24aa58fc42
commit 5d636e8619
5 changed files with 76 additions and 24 deletions

59
src/audio/ffmpeg-path.ts Normal file
View File

@@ -0,0 +1,59 @@
import { existsSync } from "node:fs";
import { spawnSync } from "node:child_process";
import process from "node:process";
import ffmpegStatic from "ffmpeg-static";
function firstExisting(paths: Array<string | null | undefined>): string | null {
for (const candidate of paths) {
if (candidate && existsSync(candidate)) {
return candidate;
}
}
return null;
}
function findOnPath(): string | null {
const locator = process.platform === "win32" ? "where" : "which";
const binaryName = process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg";
const result = spawnSync(locator, [binaryName], {
encoding: "utf8",
});
if (result.status !== 0) {
return null;
}
const match = result.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.length > 0 && existsSync(line));
return match ?? null;
}
export function resolveFfmpegPath(): string | null {
const staticPath = ffmpegStatic as unknown as string | null;
return firstExisting([
process.env.FFMPEG_PATH,
process.env.FFMPEG_BIN,
staticPath,
findOnPath(),
]);
}
export function requireFfmpegPath(): string {
const resolved = resolveFfmpegPath();
if (resolved) {
return resolved;
}
throw new Error(
[
"ffmpeg를 찾지 못했습니다.",
"1. `bun install` 재실행",
"2. 안 되면 `bun pm trust ffmpeg-static` 후 다시 `bun install`",
"3. 또는 시스템 ffmpeg를 설치해서 PATH에 추가",
].join("\n"),
);
}

View File

@@ -5,11 +5,11 @@ import os from "node:os";
import path from "node:path";
import type { Readable, Writable } from "node:stream";
import ffmpegStatic from "ffmpeg-static";
import { RealTimeVAD } from "avr-vad";
import type { AssistantRuntimeConfig } from "../config.js";
import { Logger } from "../logger.js";
import { requireFfmpegPath } from "./ffmpeg-path.js";
import { takeFrame, int16ArrayToFloat32, float32ToPcm16Buffer } from "./pcm.js";
import { ConversationMemory, type UserUtterance } from "../services/conversation.js";
import { ElevenLabsSttService } from "../services/elevenlabs-stt.js";
@@ -460,11 +460,7 @@ export class LocalVoiceSession {
}
private getFfmpegPath(): string {
const ffmpegPath = ffmpegStatic as unknown as string | null;
if (!ffmpegPath) {
throw new Error("ffmpeg-static 경로를 찾지 못했습니다.");
}
return ffmpegPath;
return requireFfmpegPath();
}
private describeSink(): string {

View File

@@ -1,29 +1,20 @@
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 { requireFfmpegPath } from "./audio/ffmpeg-path.js";
import { ElevenLabsSttService } from "./services/elevenlabs-stt.js";
import { ElevenLabsTtsService } from "./services/elevenlabs-tts.js";
import { OllamaLlmService } from "./services/ollama-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();
const ffmpegPath = requireFfmpegPath();
console.log("\n=== ffmpeg dshow audio devices ===");
await new Promise<void>((resolve) => {
await new Promise<void>((resolve, reject) => {
const child = spawn(
ffmpegPath,
["-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "dummy"],
@@ -31,10 +22,14 @@ export async function printLocalAudioDevices(): Promise<void> {
stdio: ["ignore", "ignore", "inherit"],
},
);
child.on("exit", () => resolve());
child.on("error", (error) => {
throw error;
child.on("exit", (code) => {
if (code === 0 || code === 1) {
resolve();
return;
}
reject(new Error(`ffmpeg exited with code ${code ?? "null"}`));
});
child.on("error", reject);
});
console.log("\n위 목록의 오디오 장치 이름을 `LOCAL_AUDIO_SOURCE` 에 그대로 넣으면 됩니다.");

View File

@@ -1,9 +1,9 @@
import { Readable } from "node:stream";
import ffmpegStatic from "ffmpeg-static";
import prism from "prism-media";
import type { AssistantRuntimeConfig } from "../config.js";
import { resolveFfmpegPath } from "../audio/ffmpeg-path.js";
export interface PreparedSpeechAudio {
stream: Readable;
@@ -12,7 +12,7 @@ export interface PreparedSpeechAudio {
export class ElevenLabsTtsService {
constructor(private readonly config: AssistantRuntimeConfig) {
const resolvedFfmpegPath = ffmpegStatic as unknown as string | null;
const resolvedFfmpegPath = resolveFfmpegPath();
if (resolvedFfmpegPath && !process.env.FFMPEG_PATH) {
process.env.FFMPEG_PATH = resolvedFfmpegPath;
}