Resolve Docker path for VSCode terminals
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`면 전사 결과만 출력
|
||||
|
||||
@@ -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()
|
||||
|
||||
93
src/docker-runtime.ts
Normal file
93
src/docker-runtime.ts
Normal 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(" "),
|
||||
);
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user