Resolve Docker path for VSCode terminals
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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`면 전사 결과만 출력
|
||||||
|
|||||||
@@ -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
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 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user