Resolve Docker path for VSCode terminals
This commit is contained in:
@@ -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