Resolve Docker path for VSCode terminals

This commit is contained in:
2026-05-03 17:22:52 +09:00
parent c328ef517e
commit aa14ebc447
6 changed files with 108 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ LOCAL_AI_PYTHON=python
# Windows: ffmpeg dshow 장치 이름 # Windows: ffmpeg dshow 장치 이름
# Linux: pactl list sources short 에서 monitor/source 이름 # Linux: pactl list sources short 에서 monitor/source 이름
AUDIO_SOURCE= AUDIO_SOURCE=
DOCKER_BIN=
DEBUG=false DEBUG=false
TTS_ENABLED=true TTS_ENABLED=true

View File

@@ -59,6 +59,10 @@ bun run test:tts -- "안녕하세요. 로컬 티티에스 테스트입니다."
- `AUDIO_SOURCE` - `AUDIO_SOURCE`
- `bun run devices` 에서 보이는 `ffmpeg dshow` 오디오 장치 이름 - `bun run devices` 에서 보이는 `ffmpeg dshow` 오디오 장치 이름
- 보통 `Stereo Mix`, 오디오 인터페이스 loopback 채널, 가상 케이블 입력 같은 이름을 넣습니다 - 보통 `Stereo Mix`, 오디오 인터페이스 loopback 채널, 가상 케이블 입력 같은 이름을 넣습니다
- `DOCKER_BIN`
- 비워두면 자동 탐색
- VSCode가 오래 떠 있어서 `docker` PATH를 못 잡을 때만 설정
- 예: `C:\Program Files\Docker\Docker\resources\bin\docker.exe`
- `DEBUG` - `DEBUG`
- `true`면 상세 로그 출력 - `true`면 상세 로그 출력
- `false`면 전사 결과만 출력 - `false`면 전사 결과만 출력

View File

@@ -15,6 +15,7 @@ const envSchema = z.object({
LOCAL_AI_VENV_PATH: z.string().min(1).default(".local-ai/.venv"), LOCAL_AI_VENV_PATH: z.string().min(1).default(".local-ai/.venv"),
LOCAL_AI_PYTHON: emptyToUndefined, LOCAL_AI_PYTHON: emptyToUndefined,
AUDIO_SOURCE: emptyToUndefined, AUDIO_SOURCE: emptyToUndefined,
DOCKER_BIN: emptyToUndefined,
TTS_ENABLED: z TTS_ENABLED: z
.string() .string()
.optional() .optional()

93
src/docker-runtime.ts Normal file
View File

@@ -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<boolean> {
try {
await access(target, fsConstants.F_OK);
return true;
} catch {
return false;
}
}
async function captureStdout(command: string, args: string[]): Promise<string | null> {
return await new Promise<string | null>((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<string | null> {
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<string> {
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(" "),
);
}

View File

@@ -4,6 +4,7 @@ import { mkdir, rm } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { AppConfig } from "../config.js"; import type { AppConfig } from "../config.js";
import { resolveDockerCommand } from "../docker-runtime.js";
import type { Logger } from "../logger.js"; import type { Logger } from "../logger.js";
import { playWavFile } from "./audio-playback.js"; import { playWavFile } from "./audio-playback.js";
@@ -41,9 +42,10 @@ export class MeloTtsService {
async warmup(): Promise<void> { async warmup(): Promise<void> {
await mkdir(path.resolve(process.cwd(), this.config.TTS_CACHE_DIR), { recursive: true }); 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 }); 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, ["--version"]);
await run("docker", ["image", "inspect", this.config.TTS_IMAGE]); await run(docker, ["image", "inspect", this.config.TTS_IMAGE]);
} }
async speak(text: string): Promise<void> { async speak(text: string): Promise<void> {
@@ -115,6 +117,7 @@ export class MeloTtsService {
device: this.config.TTS_DEVICE, device: this.config.TTS_DEVICE,
}); });
await run("docker", args, "inherit"); const docker = await resolveDockerCommand(this.config);
await run(docker, args, "inherit");
} }
} }

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { loadConfig } from "./config.js"; import { loadConfig } from "./config.js";
import { resolveDockerCommand } from "./docker-runtime.js";
import { Logger } from "./logger.js"; import { Logger } from "./logger.js";
import { MeloTtsService } from "./services/melo-tts.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<void> { export async function setupTts(): Promise<void> {
const config = loadConfig(); const config = loadConfig();
const logger = new Logger(config.DEBUG ? config.LOG_LEVEL : "error"); const logger = new Logger(config.DEBUG ? config.LOG_LEVEL : "error");
const docker = await resolveDockerCommand(config);
const dockerContext = path.resolve(process.cwd(), "docker", "melotts"); const dockerContext = path.resolve(process.cwd(), "docker", "melotts");
const cacheDir = path.resolve(process.cwd(), config.TTS_CACHE_DIR); const cacheDir = path.resolve(process.cwd(), config.TTS_CACHE_DIR);
const outputDir = path.resolve(process.cwd(), config.TTS_OUTPUT_DIR); const outputDir = path.resolve(process.cwd(), config.TTS_OUTPUT_DIR);
@@ -44,7 +46,7 @@ export async function setupTts(): Promise<void> {
await mkdir(outputDir, { recursive: true }); await mkdir(outputDir, { recursive: true });
console.log(`MeloTTS Docker 이미지 빌드: ${config.TTS_IMAGE}`); 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 tts = new MeloTtsService(config, logger);
const warmupPath = path.join(outputDir, "warmup.wav"); const warmupPath = path.join(outputDir, "warmup.wav");