Switch local TTS to Kokoro ONNX
This commit is contained in:
@@ -26,8 +26,10 @@ const envSchema = z.object({
|
||||
LOCAL_STT_DEVICE: z.string().min(1).default("auto"),
|
||||
LOCAL_STT_COMPUTE_TYPE: z.string().min(1).default("auto"),
|
||||
LOCAL_STT_BEAM_SIZE: z.coerce.number().int().min(1).max(8).default(1),
|
||||
LOCAL_TTS_LANGUAGE: z.string().min(1).default("KR"),
|
||||
LOCAL_TTS_SPEAKER: z.string().min(1).default("KR"),
|
||||
LOCAL_TTS_MODEL_PATH: z.string().min(1).default(".local-ai/models/kokoro-v1.0.onnx"),
|
||||
LOCAL_TTS_VOICES_PATH: z.string().min(1).default(".local-ai/models/voices-v1.0.bin"),
|
||||
LOCAL_TTS_LANGUAGE: z.string().min(1).default("ko"),
|
||||
LOCAL_TTS_SPEAKER: z.string().min(1).default("af_heart"),
|
||||
LOCAL_TTS_DEVICE: z.string().min(1).default("auto"),
|
||||
LOCAL_TTS_SPEED: z.coerce.number().min(0.8).max(1.6).default(1.12),
|
||||
BOT_DEFAULT_LANGUAGE: z.string().min(2).default("ko"),
|
||||
|
||||
@@ -16,7 +16,7 @@ import { GuildVoiceSession } from "./audio/guild-voice-session.js";
|
||||
import { type DiscordRuntimeConfig } from "./config.js";
|
||||
import { Logger } from "./logger.js";
|
||||
import { LocalFasterWhisperSttService } from "./services/local-stt.js";
|
||||
import { LocalMeloTtsService } from "./services/local-tts.js";
|
||||
import { LocalKokoroTtsService } from "./services/local-tts.js";
|
||||
import { OllamaLlmService } from "./services/ollama-llm.js";
|
||||
|
||||
export async function runDiscordBot(config: DiscordRuntimeConfig, logger: Logger): Promise<void> {
|
||||
@@ -38,7 +38,7 @@ export async function runDiscordBot(config: DiscordRuntimeConfig, logger: Logger
|
||||
});
|
||||
|
||||
const stt = new LocalFasterWhisperSttService(config, logger);
|
||||
const tts = new LocalMeloTtsService(config, logger);
|
||||
const tts = new LocalKokoroTtsService(config, logger);
|
||||
const llm = new OllamaLlmService(config);
|
||||
const sessions = new Map<string, GuildVoiceSession>();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Logger } from "./logger.js";
|
||||
import { LocalVoiceSession } from "./audio/local-voice-session.js";
|
||||
import { requireFfmpegPath } from "./audio/ffmpeg-path.js";
|
||||
import { LocalFasterWhisperSttService } from "./services/local-stt.js";
|
||||
import { LocalMeloTtsService } from "./services/local-tts.js";
|
||||
import { LocalKokoroTtsService } from "./services/local-tts.js";
|
||||
import { OllamaLlmService } from "./services/ollama-llm.js";
|
||||
|
||||
export async function printLocalAudioDevices(): Promise<void> {
|
||||
@@ -68,7 +68,7 @@ export async function printLocalAudioDevices(): Promise<void> {
|
||||
|
||||
export async function runLocalAssistant(config: AssistantRuntimeConfig, logger: Logger): Promise<void> {
|
||||
const stt = new LocalFasterWhisperSttService(config, logger);
|
||||
const tts = new LocalMeloTtsService(config, logger);
|
||||
const tts = new LocalKokoroTtsService(config, logger);
|
||||
const llm = new OllamaLlmService(config);
|
||||
|
||||
await stt.warmup();
|
||||
|
||||
@@ -30,6 +30,14 @@ export function resolveLocalAiCachePath(config: AppConfig): string {
|
||||
return path.resolve(process.cwd(), config.LOCAL_AI_CACHE_DIR);
|
||||
}
|
||||
|
||||
export function resolveLocalAiTtsModelPath(config: AppConfig): string {
|
||||
return path.resolve(process.cwd(), config.LOCAL_TTS_MODEL_PATH);
|
||||
}
|
||||
|
||||
export function resolveLocalAiTtsVoicesPath(config: AppConfig): string {
|
||||
return path.resolve(process.cwd(), config.LOCAL_TTS_VOICES_PATH);
|
||||
}
|
||||
|
||||
export function resolveVenvPythonPath(config: AppConfig): string {
|
||||
const venvPath = resolveLocalAiVenvPath(config);
|
||||
return process.platform === "win32"
|
||||
|
||||
@@ -7,12 +7,13 @@ import type { Logger } from "../logger.js";
|
||||
import { resolveFfmpegPath } from "../audio/ffmpeg-path.js";
|
||||
import { PythonJsonWorker } from "./python-json-worker.js";
|
||||
import type { PreparedSpeechAudio, TtsService } from "./tts.js";
|
||||
import { resolveLocalAiTtsModelPath, resolveLocalAiTtsVoicesPath } from "../python-runtime.js";
|
||||
|
||||
interface SynthesizeResult {
|
||||
wav_base64?: string;
|
||||
}
|
||||
|
||||
export class LocalMeloTtsService implements TtsService {
|
||||
export class LocalKokoroTtsService implements TtsService {
|
||||
private readonly worker: PythonJsonWorker;
|
||||
|
||||
constructor(config: AssistantRuntimeConfig, logger: Logger) {
|
||||
@@ -22,6 +23,8 @@ export class LocalMeloTtsService implements TtsService {
|
||||
}
|
||||
|
||||
this.worker = new PythonJsonWorker(config, logger, "local_tts_worker.py", "local-tts", {
|
||||
LOCAL_TTS_MODEL_PATH: resolveLocalAiTtsModelPath(config),
|
||||
LOCAL_TTS_VOICES_PATH: resolveLocalAiTtsVoicesPath(config),
|
||||
LOCAL_TTS_LANGUAGE: config.LOCAL_TTS_LANGUAGE,
|
||||
LOCAL_TTS_SPEAKER: config.LOCAL_TTS_SPEAKER,
|
||||
LOCAL_TTS_DEVICE: config.LOCAL_TTS_DEVICE,
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
|
||||
import { loadConfig } from "./config.js";
|
||||
import { resolveLocalAiCachePath, resolveLocalAiVenvPath, resolvePythonLaunch, resolveVenvPythonPath } from "./python-runtime.js";
|
||||
import {
|
||||
resolveLocalAiCachePath,
|
||||
resolveLocalAiTtsModelPath,
|
||||
resolveLocalAiTtsVoicesPath,
|
||||
resolveLocalAiVenvPath,
|
||||
resolvePythonLaunch,
|
||||
resolveVenvPythonPath,
|
||||
} from "./python-runtime.js";
|
||||
|
||||
const KOKORO_MODEL_URL =
|
||||
"https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/kokoro-v1.0.onnx";
|
||||
const KOKORO_VOICES_URL =
|
||||
"https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/voices-v1.0.bin";
|
||||
|
||||
async function run(command: string, args: string[], extraEnv?: NodeJS.ProcessEnv): Promise<void> {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@@ -48,11 +60,28 @@ async function ensurePip(pythonBin: string, env: NodeJS.ProcessEnv): Promise<voi
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureDownload(url: string, filePath: string): Promise<void> {
|
||||
if (existsSync(filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`다운로드 실패: ${url} (${response.status})`);
|
||||
}
|
||||
|
||||
const bytes = Buffer.from(await response.arrayBuffer());
|
||||
await writeFile(filePath, bytes);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const venvPath = resolveLocalAiVenvPath(config);
|
||||
const venvPython = resolveVenvPythonPath(config);
|
||||
const cachePath = resolveLocalAiCachePath(config);
|
||||
const ttsModelPath = resolveLocalAiTtsModelPath(config);
|
||||
const ttsVoicesPath = resolveLocalAiTtsVoicesPath(config);
|
||||
const requirementsPath = path.resolve(process.cwd(), "python", "requirements.txt");
|
||||
const baseEnv = {
|
||||
HF_HOME: cachePath,
|
||||
@@ -77,6 +106,9 @@ async function main(): Promise<void> {
|
||||
console.log("로컬 AI 의존성 설치를 시작합니다.");
|
||||
await run(venvPython, ["-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"], baseEnv);
|
||||
await run(venvPython, ["-m", "pip", "install", "-r", requirementsPath], baseEnv);
|
||||
console.log("로컬 TTS 모델 파일을 확인합니다.");
|
||||
await ensureDownload(KOKORO_MODEL_URL, ttsModelPath);
|
||||
await ensureDownload(KOKORO_VOICES_URL, ttsVoicesPath);
|
||||
|
||||
console.log("설치가 끝났습니다.");
|
||||
console.log("다음 순서:");
|
||||
|
||||
Reference in New Issue
Block a user