From aa14ebc44759c4bb1cb4de22c34b839ef9f973d3 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sun, 3 May 2026 17:22:52 +0900 Subject: [PATCH] Resolve Docker path for VSCode terminals --- .env.example | 1 + README.md | 4 ++ src/config.ts | 1 + src/docker-runtime.ts | 93 ++++++++++++++++++++++++++++++++++++++++ src/services/melo-tts.ts | 9 ++-- src/setup-tts.ts | 4 +- 6 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 src/docker-runtime.ts diff --git a/.env.example b/.env.example index 2dc385e..3635da9 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ LOCAL_AI_PYTHON=python # Windows: ffmpeg dshow 장치 이름 # Linux: pactl list sources short 에서 monitor/source 이름 AUDIO_SOURCE= +DOCKER_BIN= DEBUG=false TTS_ENABLED=true diff --git a/README.md b/README.md index 48e06b9..c185732 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,10 @@ bun run test:tts -- "안녕하세요. 로컬 티티에스 테스트입니다." - `AUDIO_SOURCE` - `bun run devices` 에서 보이는 `ffmpeg dshow` 오디오 장치 이름 - 보통 `Stereo Mix`, 오디오 인터페이스 loopback 채널, 가상 케이블 입력 같은 이름을 넣습니다 +- `DOCKER_BIN` + - 비워두면 자동 탐색 + - VSCode가 오래 떠 있어서 `docker` PATH를 못 잡을 때만 설정 + - 예: `C:\Program Files\Docker\Docker\resources\bin\docker.exe` - `DEBUG` - `true`면 상세 로그 출력 - `false`면 전사 결과만 출력 diff --git a/src/config.ts b/src/config.ts index 51002f5..edc1e9b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,6 +15,7 @@ const envSchema = z.object({ LOCAL_AI_VENV_PATH: z.string().min(1).default(".local-ai/.venv"), LOCAL_AI_PYTHON: emptyToUndefined, AUDIO_SOURCE: emptyToUndefined, + DOCKER_BIN: emptyToUndefined, TTS_ENABLED: z .string() .optional() diff --git a/src/docker-runtime.ts b/src/docker-runtime.ts new file mode 100644 index 0000000..5e6c1db --- /dev/null +++ b/src/docker-runtime.ts @@ -0,0 +1,93 @@ +import { spawn } from "node:child_process"; +import { constants as fsConstants } from "node:fs"; +import { access } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; + +import type { AppConfig } from "./config.js"; + +async function fileExists(target: string): Promise { + try { + await access(target, fsConstants.F_OK); + return true; + } catch { + return false; + } +} + +async function captureStdout(command: string, args: string[]): Promise { + return await new Promise((resolve) => { + const child = spawn(command, args, { + stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, + }); + + let stdout = ""; + child.stdout.on("data", (chunk: Buffer) => { + stdout += chunk.toString(); + }); + + child.on("error", () => resolve(null)); + child.on("exit", (code) => { + if (code === 0) { + resolve(stdout); + return; + } + resolve(null); + }); + }); +} + +async function resolveWithWhere(): Promise { + const stdout = await captureStdout("cmd.exe", ["/d", "/s", "/c", "where docker"]); + if (!stdout) { + return null; + } + + const candidates = stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.length > 0); + + for (const candidate of candidates) { + if (await fileExists(candidate)) { + return candidate; + } + } + + return null; +} + +export async function resolveDockerCommand(config: AppConfig): Promise { + if (config.DOCKER_BIN && await fileExists(config.DOCKER_BIN)) { + return config.DOCKER_BIN; + } + + if (process.platform !== "win32") { + return "docker"; + } + + const commonPaths = [ + "C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe", + "C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker-cli.exe", + ]; + + for (const candidate of commonPaths) { + if (await fileExists(candidate)) { + return candidate; + } + } + + const found = await resolveWithWhere(); + if (found) { + return found; + } + + throw new Error( + [ + "Docker 실행 파일을 찾지 못했습니다.", + "VSCode를 완전히 다시 열어 PATH를 새로 고치거나,", + ".env에 DOCKER_BIN=C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe 를 넣어주세요.", + ].join(" "), + ); +} diff --git a/src/services/melo-tts.ts b/src/services/melo-tts.ts index 3ef04bd..bc3dcb1 100644 --- a/src/services/melo-tts.ts +++ b/src/services/melo-tts.ts @@ -4,6 +4,7 @@ import { mkdir, rm } from "node:fs/promises"; import path from "node:path"; import type { AppConfig } from "../config.js"; +import { resolveDockerCommand } from "../docker-runtime.js"; import type { Logger } from "../logger.js"; import { playWavFile } from "./audio-playback.js"; @@ -41,9 +42,10 @@ export class MeloTtsService { async warmup(): Promise { await mkdir(path.resolve(process.cwd(), this.config.TTS_CACHE_DIR), { recursive: true }); await mkdir(path.resolve(process.cwd(), this.config.TTS_OUTPUT_DIR), { recursive: true }); + const docker = await resolveDockerCommand(this.config); - await run("docker", ["--version"]); - await run("docker", ["image", "inspect", this.config.TTS_IMAGE]); + await run(docker, ["--version"]); + await run(docker, ["image", "inspect", this.config.TTS_IMAGE]); } async speak(text: string): Promise { @@ -115,6 +117,7 @@ export class MeloTtsService { device: this.config.TTS_DEVICE, }); - await run("docker", args, "inherit"); + const docker = await resolveDockerCommand(this.config); + await run(docker, args, "inherit"); } } diff --git a/src/setup-tts.ts b/src/setup-tts.ts index 58ddae2..c979aa8 100644 --- a/src/setup-tts.ts +++ b/src/setup-tts.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { spawn } from "node:child_process"; import { loadConfig } from "./config.js"; +import { resolveDockerCommand } from "./docker-runtime.js"; import { Logger } from "./logger.js"; import { MeloTtsService } from "./services/melo-tts.js"; @@ -36,6 +37,7 @@ async function run(command: string, args: string[], cwd = process.cwd()): Promis export async function setupTts(): Promise { const config = loadConfig(); const logger = new Logger(config.DEBUG ? config.LOG_LEVEL : "error"); + const docker = await resolveDockerCommand(config); const dockerContext = path.resolve(process.cwd(), "docker", "melotts"); const cacheDir = path.resolve(process.cwd(), config.TTS_CACHE_DIR); const outputDir = path.resolve(process.cwd(), config.TTS_OUTPUT_DIR); @@ -44,7 +46,7 @@ export async function setupTts(): Promise { await mkdir(outputDir, { recursive: true }); console.log(`MeloTTS Docker 이미지 빌드: ${config.TTS_IMAGE}`); - await run("docker", ["build", "-t", config.TTS_IMAGE, dockerContext]); + await run(docker, ["build", "-t", config.TTS_IMAGE, dockerContext]); const tts = new MeloTtsService(config, logger); const warmupPath = path.join(outputDir, "warmup.wav");