fix: handle missing ffmpeg on windows and add devices alias
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
"start": "bun src/index.ts discord",
|
"start": "bun src/index.ts discord",
|
||||||
"start:discord": "bun src/index.ts discord",
|
"start:discord": "bun src/index.ts discord",
|
||||||
"start:local": "bun src/index.ts local",
|
"start:local": "bun src/index.ts local",
|
||||||
|
"devices": "bun src/index.ts local-devices",
|
||||||
"audio:devices": "bun src/index.ts local-devices",
|
"audio:devices": "bun src/index.ts local-devices",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit",
|
||||||
"build": "tsc -p tsconfig.json"
|
"build": "tsc -p tsconfig.json"
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"onnxruntime-node"
|
"onnxruntime-node",
|
||||||
|
"ffmpeg-static"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/audio/ffmpeg-path.ts
Normal file
59
src/audio/ffmpeg-path.ts
Normal 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"),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,11 +5,11 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { Readable, Writable } from "node:stream";
|
import type { Readable, Writable } from "node:stream";
|
||||||
|
|
||||||
import ffmpegStatic from "ffmpeg-static";
|
|
||||||
import { RealTimeVAD } from "avr-vad";
|
import { RealTimeVAD } from "avr-vad";
|
||||||
|
|
||||||
import type { AssistantRuntimeConfig } from "../config.js";
|
import type { AssistantRuntimeConfig } from "../config.js";
|
||||||
import { Logger } from "../logger.js";
|
import { Logger } from "../logger.js";
|
||||||
|
import { requireFfmpegPath } from "./ffmpeg-path.js";
|
||||||
import { takeFrame, int16ArrayToFloat32, float32ToPcm16Buffer } from "./pcm.js";
|
import { takeFrame, int16ArrayToFloat32, float32ToPcm16Buffer } from "./pcm.js";
|
||||||
import { ConversationMemory, type UserUtterance } from "../services/conversation.js";
|
import { ConversationMemory, type UserUtterance } from "../services/conversation.js";
|
||||||
import { ElevenLabsSttService } from "../services/elevenlabs-stt.js";
|
import { ElevenLabsSttService } from "../services/elevenlabs-stt.js";
|
||||||
@@ -460,11 +460,7 @@ export class LocalVoiceSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getFfmpegPath(): string {
|
private getFfmpegPath(): string {
|
||||||
const ffmpegPath = ffmpegStatic as unknown as string | null;
|
return requireFfmpegPath();
|
||||||
if (!ffmpegPath) {
|
|
||||||
throw new Error("ffmpeg-static 경로를 찾지 못했습니다.");
|
|
||||||
}
|
|
||||||
return ffmpegPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private describeSink(): string {
|
private describeSink(): string {
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
|
|
||||||
import ffmpegStatic from "ffmpeg-static";
|
|
||||||
|
|
||||||
import type { AssistantRuntimeConfig } from "./config.js";
|
import type { AssistantRuntimeConfig } from "./config.js";
|
||||||
import { Logger } from "./logger.js";
|
import { Logger } from "./logger.js";
|
||||||
import { LocalVoiceSession } from "./audio/local-voice-session.js";
|
import { LocalVoiceSession } from "./audio/local-voice-session.js";
|
||||||
|
import { requireFfmpegPath } from "./audio/ffmpeg-path.js";
|
||||||
import { ElevenLabsSttService } from "./services/elevenlabs-stt.js";
|
import { ElevenLabsSttService } from "./services/elevenlabs-stt.js";
|
||||||
import { ElevenLabsTtsService } from "./services/elevenlabs-tts.js";
|
import { ElevenLabsTtsService } from "./services/elevenlabs-tts.js";
|
||||||
import { OllamaLlmService } from "./services/ollama-llm.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> {
|
export async function printLocalAudioDevices(): Promise<void> {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
const ffmpegPath = resolveFfmpegPath();
|
const ffmpegPath = requireFfmpegPath();
|
||||||
|
|
||||||
console.log("\n=== ffmpeg dshow audio devices ===");
|
console.log("\n=== ffmpeg dshow audio devices ===");
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const child = spawn(
|
const child = spawn(
|
||||||
ffmpegPath,
|
ffmpegPath,
|
||||||
["-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "dummy"],
|
["-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "dummy"],
|
||||||
@@ -31,10 +22,14 @@ export async function printLocalAudioDevices(): Promise<void> {
|
|||||||
stdio: ["ignore", "ignore", "inherit"],
|
stdio: ["ignore", "ignore", "inherit"],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
child.on("exit", () => resolve());
|
child.on("exit", (code) => {
|
||||||
child.on("error", (error) => {
|
if (code === 0 || code === 1) {
|
||||||
throw error;
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(new Error(`ffmpeg exited with code ${code ?? "null"}`));
|
||||||
});
|
});
|
||||||
|
child.on("error", reject);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("\n위 목록의 오디오 장치 이름을 `LOCAL_AUDIO_SOURCE` 에 그대로 넣으면 됩니다.");
|
console.log("\n위 목록의 오디오 장치 이름을 `LOCAL_AUDIO_SOURCE` 에 그대로 넣으면 됩니다.");
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Readable } from "node:stream";
|
import { Readable } from "node:stream";
|
||||||
|
|
||||||
import ffmpegStatic from "ffmpeg-static";
|
|
||||||
import prism from "prism-media";
|
import prism from "prism-media";
|
||||||
|
|
||||||
import type { AssistantRuntimeConfig } from "../config.js";
|
import type { AssistantRuntimeConfig } from "../config.js";
|
||||||
|
import { resolveFfmpegPath } from "../audio/ffmpeg-path.js";
|
||||||
|
|
||||||
export interface PreparedSpeechAudio {
|
export interface PreparedSpeechAudio {
|
||||||
stream: Readable;
|
stream: Readable;
|
||||||
@@ -12,7 +12,7 @@ export interface PreparedSpeechAudio {
|
|||||||
|
|
||||||
export class ElevenLabsTtsService {
|
export class ElevenLabsTtsService {
|
||||||
constructor(private readonly config: AssistantRuntimeConfig) {
|
constructor(private readonly config: AssistantRuntimeConfig) {
|
||||||
const resolvedFfmpegPath = ffmpegStatic as unknown as string | null;
|
const resolvedFfmpegPath = resolveFfmpegPath();
|
||||||
if (resolvedFfmpegPath && !process.env.FFMPEG_PATH) {
|
if (resolvedFfmpegPath && !process.env.FFMPEG_PATH) {
|
||||||
process.env.FFMPEG_PATH = resolvedFfmpegPath;
|
process.env.FFMPEG_PATH = resolvedFfmpegPath;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user