: implement modular model management system with support for LLM and Kokoro text-to-speech integration across platforms
This commit is contained in:
1 parent
d5d0c0ac19
commit
bd89699994
18 files changed
+1693
-41
No files matched your search
+24
-1
@@ -39,6 +39,13 @@ if (Test-Path $speechBackendDir) {
|
||||
Remove-Item $speechBackendDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Delete Kokoro TTS runtime dependencies
|
||||
$ttsRuntimeDir = Join-Path $appDir "tts-runtime"
|
||||
if (Test-Path $ttsRuntimeDir) {
|
||||
Write-Host " >> Removing Kokoro TTS runtime dependencies..." -ForegroundColor Cyan
|
||||
Remove-Item $ttsRuntimeDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Delete dist
|
||||
$distDir = Join-Path $appDir "dist"
|
||||
if (Test-Path $distDir) {
|
||||
@@ -64,12 +71,28 @@ if (Test-Path $speechModelsDir) {
|
||||
Write-Host " >> Preserving speech models in app/speech-models." -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Preserve TTS models
|
||||
$ttsModelsDir = Join-Path $appDir "tts-models"
|
||||
if (Test-Path $ttsModelsDir) {
|
||||
Write-Host " >> Preserving TTS models in app/tts-models." -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Preserve transcriptions
|
||||
$transcriptionsDir = Join-Path $appDir "transcriptions"
|
||||
if (Test-Path $transcriptionsDir) {
|
||||
Write-Host " >> Preserving speech transcripts in app/transcriptions." -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Preserve TTS outputs/cache
|
||||
$ttsOutputsDir = Join-Path $appDir "tts-outputs"
|
||||
if (Test-Path $ttsOutputsDir) {
|
||||
Write-Host " >> Preserving TTS outputs in app/tts-outputs." -ForegroundColor Cyan
|
||||
}
|
||||
$ttsCacheDir = Join-Path $appDir "tts-cache"
|
||||
if (Test-Path $ttsCacheDir) {
|
||||
Write-Host " >> Preserving TTS model cache in app/tts-cache." -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Preserve OpenVINO models
|
||||
$openVinoModelsDir = Join-Path $appDir "openvino-models"
|
||||
if (Test-Path $openVinoModelsDir) {
|
||||
@@ -101,7 +124,7 @@ if (Test-Path $lockFile) {
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " ============================================================" -ForegroundColor Green
|
||||
Write-Host " Reset complete. Image models, text models, speech models, transcripts, OpenVINO models, and generated outputs were preserved." -ForegroundColor Green
|
||||
Write-Host " Reset complete. Image models, text models, speech models, TTS models, transcripts, TTS outputs, OpenVINO models, and generated outputs were preserved." -ForegroundColor Green
|
||||
Write-Host " ============================================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Read-Host " Press Enter to close..."
|
||||
+20
-1
@@ -40,6 +40,12 @@ if [[ -d "$APP_DIR/speech-backend" ]]; then
|
||||
rm -rf "$APP_DIR/speech-backend"
|
||||
fi
|
||||
|
||||
# Delete Kokoro TTS runtime dependencies
|
||||
if [[ -d "$APP_DIR/tts-runtime" ]]; then
|
||||
echo " >> Removing Kokoro TTS runtime dependencies..."
|
||||
rm -rf "$APP_DIR/tts-runtime"
|
||||
fi
|
||||
|
||||
# Delete dist
|
||||
if [[ -d "$APP_DIR/dist" ]]; then
|
||||
echo " >> Removing dist/ build folder..."
|
||||
@@ -61,11 +67,24 @@ if [[ -d "$APP_DIR/speech-models" ]]; then
|
||||
echo " >> Preserving speech models in app/speech-models."
|
||||
fi
|
||||
|
||||
# Preserve TTS models
|
||||
if [[ -d "$APP_DIR/tts-models" ]]; then
|
||||
echo " >> Preserving TTS models in app/tts-models."
|
||||
fi
|
||||
|
||||
# Preserve transcriptions
|
||||
if [[ -d "$APP_DIR/transcriptions" ]]; then
|
||||
echo " >> Preserving speech transcripts in app/transcriptions."
|
||||
fi
|
||||
|
||||
# Preserve TTS outputs/cache
|
||||
if [[ -d "$APP_DIR/tts-outputs" ]]; then
|
||||
echo " >> Preserving TTS outputs in app/tts-outputs."
|
||||
fi
|
||||
if [[ -d "$APP_DIR/tts-cache" ]]; then
|
||||
echo " >> Preserving TTS model cache in app/tts-cache."
|
||||
fi
|
||||
|
||||
# Preserve OpenVINO models
|
||||
if [[ -d "$APP_DIR/openvino-models" ]]; then
|
||||
echo " >> Preserving OpenVINO models in app/openvino-models."
|
||||
@@ -92,7 +111,7 @@ fi
|
||||
|
||||
echo ""
|
||||
echo " ============================================================"
|
||||
echo " Reset complete. Image models, text models, speech models, transcripts, OpenVINO models, and generated outputs were preserved."
|
||||
echo " Reset complete. Image models, text models, speech models, TTS models, transcripts, TTS outputs, OpenVINO models, and generated outputs were preserved."
|
||||
echo " ============================================================"
|
||||
echo ""
|
||||
read -rp " Press Enter to close..."
|
||||
+537
-5
@@ -31,9 +31,11 @@ const PORT_FRONTEND = readPort(process.env.PORT || process.env.FRONTEND_PORT, 14
|
||||
const PREFERRED_BACKEND_PORT = readPort(process.env.BACKEND_PORT || process.env.SD_BACKEND_PORT, 8080);
|
||||
const PREFERRED_LLM_PORT = readPort(process.env.LLM_PORT, 10086);
|
||||
const PREFERRED_SPEECH_PORT = readPort(process.env.SPEECH_PORT, 10088);
|
||||
const PREFERRED_TTS_PORT = readPort(process.env.TTS_PORT, 10089);
|
||||
let PORT_BACKEND = PREFERRED_BACKEND_PORT;
|
||||
let PORT_LLM = PREFERRED_LLM_PORT;
|
||||
let PORT_SPEECH = PREFERRED_SPEECH_PORT;
|
||||
let PORT_TTS = PREFERRED_TTS_PORT;
|
||||
const MAX_JSON_BODY_BYTES = 64 * 1024 * 1024;
|
||||
const SERVER_BUILD = "text-image-v1";
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
@@ -115,6 +117,20 @@ const TRANSCRIPTIONS = path.join(ROOT, "app", "transcriptions");
|
||||
if (!fs.existsSync(TRANSCRIPTIONS)) {
|
||||
fs.mkdirSync(TRANSCRIPTIONS, { recursive: true });
|
||||
}
|
||||
const TTS_MODELS = path.join(ROOT, "app", "tts-models");
|
||||
if (!fs.existsSync(TTS_MODELS)) {
|
||||
fs.mkdirSync(TTS_MODELS, { recursive: true });
|
||||
}
|
||||
const TTS_OUTPUTS = path.join(ROOT, "app", "tts-outputs");
|
||||
if (!fs.existsSync(TTS_OUTPUTS)) {
|
||||
fs.mkdirSync(TTS_OUTPUTS, { recursive: true });
|
||||
}
|
||||
const TTS_CACHE = path.join(ROOT, "app", "tts-cache");
|
||||
if (!fs.existsSync(TTS_CACHE)) {
|
||||
fs.mkdirSync(TTS_CACHE, { recursive: true });
|
||||
}
|
||||
const TTS_RUNTIME = path.join(ROOT, "app", "tts-runtime");
|
||||
const TTS_WORKER = path.join(ROOT, "scripts", "tts-kokoro-worker.mjs");
|
||||
const SPEECH_BACKEND_PATHS = {
|
||||
winCli: path.join(ROOT, "app", "speech-backend", "win", "whisper-cli.exe"),
|
||||
winServer: path.join(ROOT, "app", "speech-backend", "win", "whisper-server.exe"),
|
||||
@@ -135,6 +151,38 @@ const SPEECH_MODEL_CATALOG = [
|
||||
{ id: "base-q5_1", name: "Whisper Base Multilingual Q5", filename: "ggml-base-q5_1.bin", size: "57 MB", language: "Multilingual", recommended: false },
|
||||
{ id: "small", name: "Whisper Small Multilingual", filename: "ggml-small.bin", size: "466 MB", language: "Multilingual", recommended: false },
|
||||
];
|
||||
const TTS_MODEL_CATALOG = [
|
||||
{
|
||||
id: "kokoro-onnx-q8",
|
||||
name: "Kokoro 82M ONNX Q8",
|
||||
filename: "kokoro-onnx-q8.json",
|
||||
modelId: "onnx-community/Kokoro-82M-v1.0-ONNX",
|
||||
dtype: "q8",
|
||||
size: "Model cache managed by kokoro-js",
|
||||
format: "Kokoro ONNX",
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
id: "kokoro-onnx-fp32",
|
||||
name: "Kokoro 82M ONNX FP32",
|
||||
filename: "kokoro-onnx-fp32.json",
|
||||
modelId: "onnx-community/Kokoro-82M-v1.0-ONNX",
|
||||
dtype: "fp32",
|
||||
size: "Model cache managed by kokoro-js",
|
||||
format: "Kokoro ONNX",
|
||||
recommended: false,
|
||||
},
|
||||
];
|
||||
const TTS_VOICES = [
|
||||
{ id: "af_heart", name: "Heart", language: "en-us", gender: "Female", recommended: true },
|
||||
{ id: "af_bella", name: "Bella", language: "en-us", gender: "Female", recommended: false },
|
||||
{ id: "af_nicole", name: "Nicole", language: "en-us", gender: "Female", recommended: false },
|
||||
{ id: "af_sarah", name: "Sarah", language: "en-us", gender: "Female", recommended: false },
|
||||
{ id: "am_michael", name: "Michael", language: "en-us", gender: "Male", recommended: false },
|
||||
{ id: "am_fenrir", name: "Fenrir", language: "en-us", gender: "Male", recommended: false },
|
||||
{ id: "bf_emma", name: "Emma", language: "en-gb", gender: "Female", recommended: false },
|
||||
{ id: "bm_george", name: "George", language: "en-gb", gender: "Male", recommended: false },
|
||||
];
|
||||
const OPENVINO_MODELS = path.join(ROOT, "app", "openvino-models");
|
||||
if (!fs.existsSync(OPENVINO_MODELS)) {
|
||||
fs.mkdirSync(OPENVINO_MODELS, { recursive: true });
|
||||
@@ -189,6 +237,24 @@ let speechTranscriptionState = {
|
||||
model: "",
|
||||
filename: "",
|
||||
};
|
||||
let ttsReady = false;
|
||||
let ttsError = null;
|
||||
let ttsOperationQueue = Promise.resolve();
|
||||
let ttsSettings = {
|
||||
model: null,
|
||||
voice: "af_heart",
|
||||
speed: 1,
|
||||
dtype: "q8",
|
||||
backendMode: "Kokoro ONNX",
|
||||
};
|
||||
let ttsGenerationState = {
|
||||
active: false,
|
||||
phase: "",
|
||||
progress: 0,
|
||||
model: "",
|
||||
voice: "",
|
||||
output: "",
|
||||
};
|
||||
let llmSettings = {
|
||||
model: null,
|
||||
threads: Math.max(1, Math.min(16, os.cpus().length || 4)),
|
||||
@@ -1674,6 +1740,300 @@ function resolveSpeechModel(value) {
|
||||
return { ...model, path: modelPath };
|
||||
}
|
||||
|
||||
function getTtsManifestPath(filename) {
|
||||
const manifestPath = path.join(TTS_MODELS, path.basename(String(filename || "")));
|
||||
return pathInside(manifestPath, TTS_MODELS) ? manifestPath : "";
|
||||
}
|
||||
|
||||
function readTtsManifest(filename) {
|
||||
const manifestPath = getTtsManifestPath(filename);
|
||||
if (!manifestPath || !fs.existsSync(manifestPath)) return null;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function installTtsCatalogModel(modelIdOrFilename) {
|
||||
const raw = String(modelIdOrFilename || "").trim();
|
||||
const catalogModel = TTS_MODEL_CATALOG.find((model) => model.id === raw || model.filename === raw) || TTS_MODEL_CATALOG[0];
|
||||
if (!catalogModel) throw new Error("Unknown TTS model.");
|
||||
const manifestPath = getTtsManifestPath(catalogModel.filename);
|
||||
const manifest = {
|
||||
...catalogModel,
|
||||
installed: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
notes: "Kokoro model files are cached under app/tts-cache by kokoro-js on first generation.",
|
||||
};
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
|
||||
return manifest;
|
||||
}
|
||||
|
||||
function getTtsModels() {
|
||||
const catalog = TTS_MODEL_CATALOG.map((model) => {
|
||||
const manifest = readTtsManifest(model.filename);
|
||||
const installed = Boolean(manifest);
|
||||
const manifestPath = getTtsManifestPath(model.filename);
|
||||
const sizeBytes = installed ? getPathSize(manifestPath) : 0;
|
||||
return {
|
||||
...model,
|
||||
...(manifest || {}),
|
||||
installed,
|
||||
sizeBytes,
|
||||
localSize: installed ? formatBytes(sizeBytes) : "",
|
||||
url: `kokoro://install/${model.id}`,
|
||||
};
|
||||
});
|
||||
|
||||
const known = new Set(catalog.map((model) => model.filename.toLowerCase()));
|
||||
let custom = [];
|
||||
try {
|
||||
custom = fs.readdirSync(TTS_MODELS)
|
||||
.filter((filename) => filename.toLowerCase().endsWith(".json") && !known.has(filename.toLowerCase()))
|
||||
.map((filename) => {
|
||||
const manifest = readTtsManifest(filename) || {};
|
||||
const sizeBytes = getPathSize(path.join(TTS_MODELS, filename));
|
||||
return {
|
||||
id: manifest.id || filename,
|
||||
name: manifest.name || filename,
|
||||
filename,
|
||||
modelId: manifest.modelId || "onnx-community/Kokoro-82M-v1.0-ONNX",
|
||||
dtype: manifest.dtype || "q8",
|
||||
format: manifest.format || "Kokoro ONNX",
|
||||
size: formatBytes(sizeBytes),
|
||||
localSize: formatBytes(sizeBytes),
|
||||
sizeBytes,
|
||||
installed: true,
|
||||
recommended: false,
|
||||
custom: true,
|
||||
};
|
||||
});
|
||||
} catch (_) {}
|
||||
return [...catalog, ...custom];
|
||||
}
|
||||
|
||||
function resolveTtsModel(value) {
|
||||
const raw = String(value || "").trim();
|
||||
const model = getTtsModels().find((item) => item.id === raw || item.filename === raw) ||
|
||||
getTtsModels().find((item) => item.installed);
|
||||
if (!model) throw new Error("Download or import a Kokoro TTS model from Model Manager first.");
|
||||
const manifestPath = getTtsManifestPath(model.filename);
|
||||
if (!manifestPath || !fs.existsSync(manifestPath)) {
|
||||
throw new Error(`TTS model is not installed: ${model.filename}`);
|
||||
}
|
||||
return { ...model, path: manifestPath };
|
||||
}
|
||||
|
||||
function getTtsRuntimeStatus() {
|
||||
const nodeModules = path.join(TTS_RUNTIME, "node_modules");
|
||||
const installed = fs.existsSync(path.join(nodeModules, "kokoro-js"));
|
||||
return {
|
||||
installed: installed && fs.existsSync(TTS_WORKER),
|
||||
worker: TTS_WORKER,
|
||||
runtime: TTS_RUNTIME,
|
||||
cache: TTS_CACHE,
|
||||
backendMode: "Kokoro ONNX",
|
||||
};
|
||||
}
|
||||
|
||||
function runExclusiveTtsOperation(operation) {
|
||||
const run = ttsOperationQueue.catch(() => {}).then(operation);
|
||||
ttsOperationQueue = run.catch(() => {});
|
||||
return run;
|
||||
}
|
||||
|
||||
async function startTts(settings = {}) {
|
||||
const runtime = getTtsRuntimeStatus();
|
||||
if (!runtime.installed) {
|
||||
throw new Error("Kokoro TTS runtime is not installed. Run scripts/setup-tts for this platform.");
|
||||
}
|
||||
const model = resolveTtsModel(settings.model || ttsSettings.model);
|
||||
await killBackend();
|
||||
await killOpenVinoWorker();
|
||||
await killLlm();
|
||||
await stopSpeech();
|
||||
PORT_TTS = PREFERRED_TTS_PORT;
|
||||
ttsError = null;
|
||||
ttsReady = true;
|
||||
ttsSettings = {
|
||||
...ttsSettings,
|
||||
model: model.filename,
|
||||
voice: settings.voice || ttsSettings.voice || "af_heart",
|
||||
speed: Math.max(0.5, Math.min(2, Number(settings.speed) || ttsSettings.speed || 1)),
|
||||
dtype: model.dtype || settings.dtype || ttsSettings.dtype || "q8",
|
||||
backendMode: runtime.backendMode,
|
||||
};
|
||||
}
|
||||
|
||||
async function stopTts() {
|
||||
ttsReady = false;
|
||||
ttsError = null;
|
||||
ttsGenerationState = { active: false, phase: "", progress: 0, model: "", voice: "", output: "" };
|
||||
}
|
||||
|
||||
function saveTtsOutput(result) {
|
||||
const createdAt = new Date().toISOString();
|
||||
const stamp = createdAt.replace(/[:.]/g, "-");
|
||||
const base = `tts-${stamp}-${safeOutputName(result.voice || "voice")}`;
|
||||
const wavFilename = `${base}.wav`;
|
||||
const jsonFilename = `${base}.json`;
|
||||
const wavPath = path.join(TTS_OUTPUTS, wavFilename);
|
||||
fs.copyFileSync(result.wavPath, wavPath);
|
||||
const metadata = {
|
||||
text: result.text || "",
|
||||
model: result.model,
|
||||
modelName: result.modelName,
|
||||
voice: result.voice,
|
||||
voiceName: result.voiceName,
|
||||
speed: result.speed,
|
||||
durationMs: result.durationMs,
|
||||
sampleRate: result.sampleRate || 24000,
|
||||
createdAt,
|
||||
audioFile: wavFilename,
|
||||
metadata: jsonFilename,
|
||||
displayName: result.displayName || `${result.voiceName || result.voice} - ${createdAt}`,
|
||||
};
|
||||
fs.writeFileSync(path.join(TTS_OUTPUTS, jsonFilename), JSON.stringify(metadata, null, 2), "utf8");
|
||||
return {
|
||||
...metadata,
|
||||
filename: jsonFilename,
|
||||
url: `/tts-outputs/${encodeURIComponent(wavFilename)}`,
|
||||
};
|
||||
}
|
||||
|
||||
function listTtsOutputs() {
|
||||
try {
|
||||
return fs.readdirSync(TTS_OUTPUTS)
|
||||
.filter((file) => file.toLowerCase().endsWith(".json"))
|
||||
.map((file) => {
|
||||
const filePath = path.join(TTS_OUTPUTS, file);
|
||||
try {
|
||||
const metadata = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
const stat = fs.statSync(filePath);
|
||||
return {
|
||||
...metadata,
|
||||
filename: file,
|
||||
displayName: metadata.displayName || metadata.text?.slice(0, 48) || metadata.audioFile || file,
|
||||
sizeBytes: stat.size,
|
||||
size: formatBytes(stat.size),
|
||||
modifiedAt: stat.mtime.toISOString(),
|
||||
createdAt: metadata.createdAt || stat.mtime.toISOString(),
|
||||
url: metadata.audioFile ? `/tts-outputs/${encodeURIComponent(metadata.audioFile)}` : "",
|
||||
};
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function synthesizeTts(text, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cleanedText = String(text || "").trim();
|
||||
if (!cleanedText) {
|
||||
reject(new Error("Enter text to synthesize."));
|
||||
return;
|
||||
}
|
||||
if (cleanedText.length > 5000) {
|
||||
reject(new Error("TTS text is too long. Limit is 5000 characters for V1."));
|
||||
return;
|
||||
}
|
||||
const runtime = getTtsRuntimeStatus();
|
||||
if (!runtime.installed) {
|
||||
reject(new Error("Kokoro TTS runtime is not installed."));
|
||||
return;
|
||||
}
|
||||
const model = resolveTtsModel(options.model || ttsSettings.model);
|
||||
const voice = String(options.voice || ttsSettings.voice || "af_heart");
|
||||
const voiceInfo = TTS_VOICES.find((item) => item.id === voice) || { id: voice, name: voice };
|
||||
const speed = Math.max(0.5, Math.min(2, Number(options.speed) || ttsSettings.speed || 1));
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const tempWav = path.join(TTS_OUTPUTS, `.tts-${stamp}.wav`);
|
||||
const payload = {
|
||||
text: cleanedText,
|
||||
modelId: model.modelId || "onnx-community/Kokoro-82M-v1.0-ONNX",
|
||||
dtype: model.dtype || "q8",
|
||||
voice,
|
||||
speed,
|
||||
output: tempWav,
|
||||
cacheDir: TTS_CACHE,
|
||||
};
|
||||
|
||||
ttsGenerationState = {
|
||||
active: true,
|
||||
phase: "Generating speech...",
|
||||
progress: -1,
|
||||
model: model.filename,
|
||||
voice,
|
||||
output: "",
|
||||
};
|
||||
|
||||
const startedAt = Date.now();
|
||||
const proc = spawn(process.execPath, [TTS_WORKER], {
|
||||
cwd: ROOT,
|
||||
stdio: "pipe",
|
||||
windowsHide: true,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_PATH: path.join(TTS_RUNTIME, "node_modules"),
|
||||
TTS_RUNTIME,
|
||||
},
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
proc.stdout.on("data", (data) => { stdout += data.toString(); });
|
||||
proc.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
process.stderr.write(" [tts] " + data.toString());
|
||||
});
|
||||
proc.on("error", (err) => {
|
||||
ttsGenerationState = { active: false, phase: "Failed", progress: 0, model: model.filename, voice, output: "" };
|
||||
reject(err);
|
||||
});
|
||||
proc.on("exit", (code) => {
|
||||
ttsGenerationState = { active: false, phase: code === 0 ? "Complete" : "Failed", progress: code === 0 ? 100 : 0, model: model.filename, voice, output: "" };
|
||||
if (code !== 0) {
|
||||
try { fs.unlinkSync(tempWav); } catch (_) {}
|
||||
const message = (stderr || stdout || `Kokoro TTS exited with code ${code}`).trim().slice(-1400);
|
||||
ttsError = message;
|
||||
reject(new Error(message));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const workerResult = JSON.parse(stdout.trim() || "{}");
|
||||
if (!workerResult.ok || !fs.existsSync(tempWav)) {
|
||||
throw new Error(workerResult.error || "Kokoro TTS did not produce a WAV file.");
|
||||
}
|
||||
const saved = saveTtsOutput({
|
||||
text: cleanedText,
|
||||
model: model.filename,
|
||||
modelName: model.name,
|
||||
voice,
|
||||
voiceName: voiceInfo.name,
|
||||
speed,
|
||||
durationMs: Date.now() - startedAt,
|
||||
sampleRate: workerResult.sampleRate || 24000,
|
||||
wavPath: tempWav,
|
||||
displayName: `${voiceInfo.name || voice} - ${cleanedText.slice(0, 40)}`,
|
||||
});
|
||||
try { fs.unlinkSync(tempWav); } catch (_) {}
|
||||
ttsError = null;
|
||||
resolve(saved);
|
||||
} catch (err) {
|
||||
try { fs.unlinkSync(tempWav); } catch (_) {}
|
||||
ttsError = err.message || String(err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
proc.stdin.end(JSON.stringify(payload));
|
||||
});
|
||||
}
|
||||
|
||||
function runExclusiveSpeechOperation(operation) {
|
||||
const run = speechOperationQueue.catch(() => {}).then(operation);
|
||||
speechOperationQueue = run.catch(() => {});
|
||||
@@ -2091,13 +2451,25 @@ async function getHealth() {
|
||||
totalBytes: getPathSize(OUTPUTS),
|
||||
totalSize: formatBytes(getPathSize(OUTPUTS)),
|
||||
},
|
||||
tts: {
|
||||
models: fs.existsSync(TTS_MODELS) ? fs.readdirSync(TTS_MODELS).filter((file) => file.toLowerCase().endsWith(".json")).length : 0,
|
||||
outputs: fs.existsSync(TTS_OUTPUTS) ? fs.readdirSync(TTS_OUTPUTS).filter((file) => file.toLowerCase().endsWith(".json")).length : 0,
|
||||
runtimeInstalled: getTtsRuntimeStatus().installed,
|
||||
totalBytes: getPathSize(TTS_MODELS) + getPathSize(TTS_OUTPUTS) + getPathSize(TTS_CACHE),
|
||||
totalSize: formatBytes(getPathSize(TTS_MODELS) + getPathSize(TTS_OUTPUTS) + getPathSize(TTS_CACHE)),
|
||||
},
|
||||
issues,
|
||||
};
|
||||
}
|
||||
|
||||
function addCleanupCandidate(candidates, id, targetPath, reason, options = {}) {
|
||||
if (!fs.existsSync(targetPath)) return;
|
||||
if (!options.allowUserData && (pathInside(targetPath, MODELS) || pathInside(targetPath, OUTPUTS))) return;
|
||||
if (!options.allowUserData && (
|
||||
pathInside(targetPath, MODELS) ||
|
||||
pathInside(targetPath, OUTPUTS) ||
|
||||
pathInside(targetPath, TTS_MODELS) ||
|
||||
pathInside(targetPath, TTS_OUTPUTS)
|
||||
)) return;
|
||||
const sizeBytes = getPathSize(targetPath);
|
||||
candidates.push({
|
||||
id,
|
||||
@@ -4072,7 +4444,13 @@ function cancelModelDownload() {
|
||||
try { fs.unlinkSync(activeDownload.tempPath || activeDownload.destPath); } catch (_) {}
|
||||
}
|
||||
} else if (filename) {
|
||||
const targetDir = downloadState.kind === "text" ? LLM_MODELS : downloadState.kind === "speech" ? SPEECH_MODELS : MODELS;
|
||||
const targetDir = downloadState.kind === "text"
|
||||
? LLM_MODELS
|
||||
: downloadState.kind === "speech"
|
||||
? SPEECH_MODELS
|
||||
: downloadState.kind === "tts"
|
||||
? TTS_MODELS
|
||||
: MODELS;
|
||||
try { fs.unlinkSync(`${path.join(targetDir, filename)}.part`); } catch (_) {}
|
||||
}
|
||||
activeDownload = null;
|
||||
@@ -4397,9 +4775,17 @@ function streamModelUpload(req, filename, targetDir = MODELS, mode = "image") {
|
||||
? lowerName.endsWith(".gguf")
|
||||
: mode === "speech"
|
||||
? lowerName.endsWith(".bin")
|
||||
: mode === "tts"
|
||||
? lowerName.endsWith(".json")
|
||||
: isModelFile(lowerName);
|
||||
if (!safeFilename || !valid) {
|
||||
reject(new Error(mode === "text" ? "Filename must end with .gguf" : mode === "speech" ? "Filename must end with .bin" : "Filename must end with .gguf, .safetensors, or .ckpt"));
|
||||
reject(new Error(mode === "text"
|
||||
? "Filename must end with .gguf"
|
||||
: mode === "speech"
|
||||
? "Filename must end with .bin"
|
||||
: mode === "tts"
|
||||
? "Filename must end with .json"
|
||||
: "Filename must end with .gguf, .safetensors, or .ckpt"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4771,6 +5157,136 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (req.url === "/api/tts/status" && req.method === "GET") {
|
||||
const runtime = getTtsRuntimeStatus();
|
||||
return json(res, 200, {
|
||||
ok: true,
|
||||
ready: ttsReady,
|
||||
running: ttsReady,
|
||||
port: PORT_TTS,
|
||||
preferredPort: PREFERRED_TTS_PORT,
|
||||
runtimeInstalled: runtime.installed,
|
||||
runtimePath: runtime.runtime,
|
||||
workerPath: runtime.worker,
|
||||
cachePath: runtime.cache,
|
||||
backendMode: runtime.backendMode,
|
||||
error: ttsError,
|
||||
settings: ttsSettings,
|
||||
generation: ttsGenerationState,
|
||||
voices: TTS_VOICES,
|
||||
});
|
||||
}
|
||||
|
||||
if (req.url === "/api/tts/models" && req.method === "GET") {
|
||||
return json(res, 200, { ok: true, models: getTtsModels(), voices: TTS_VOICES });
|
||||
}
|
||||
|
||||
if (req.url === "/api/tts/start" && req.method === "POST") {
|
||||
const body = await readJsonBody(req, res);
|
||||
if (!body) return;
|
||||
try {
|
||||
await runExclusiveTtsOperation(() => startTts(body));
|
||||
return json(res, 200, { ok: true, ready: ttsReady, settings: ttsSettings, port: PORT_TTS });
|
||||
} catch (err) {
|
||||
ttsError = err.message || String(err);
|
||||
return json(res, 500, { ok: false, error: ttsError });
|
||||
}
|
||||
}
|
||||
|
||||
if (req.url === "/api/tts/stop" && req.method === "POST") {
|
||||
await stopTts();
|
||||
return json(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
if (req.url === "/api/tts/speak" && req.method === "POST") {
|
||||
const body = await readJsonBody(req, res, 512 * 1024);
|
||||
if (!body) return;
|
||||
try {
|
||||
if (!ttsReady || (body.model && ttsSettings.model !== body.model)) {
|
||||
await runExclusiveTtsOperation(() => startTts(body));
|
||||
}
|
||||
const output = await runExclusiveTtsOperation(() => synthesizeTts(body.text, body));
|
||||
return json(res, 200, { ok: true, output });
|
||||
} catch (err) {
|
||||
ttsError = err.message || String(err);
|
||||
return json(res, 500, { ok: false, error: ttsError });
|
||||
}
|
||||
}
|
||||
|
||||
if (req.url === "/api/tts/download-model" && req.method === "POST") {
|
||||
const body = await readJsonBody(req, res);
|
||||
if (!body) return;
|
||||
try {
|
||||
const model = installTtsCatalogModel(body.modelId || body.model_id || body.model || body.filename);
|
||||
return json(res, 200, { ok: true, message: "TTS model manifest installed", filename: model.filename, model });
|
||||
} catch (err) {
|
||||
return json(res, 500, { ok: false, error: err.message || String(err) });
|
||||
}
|
||||
}
|
||||
|
||||
if (req.url.startsWith("/api/tts/import-model") && req.method === "POST") {
|
||||
try {
|
||||
const parsed = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
||||
const result = await streamModelUpload(req, parsed.searchParams.get("filename"), TTS_MODELS, "tts");
|
||||
return json(res, 200, { ok: true, message: `Imported ${result.filename}`, model: result });
|
||||
} catch (err) {
|
||||
console.error(" [api] Failed to import TTS model:", err);
|
||||
return json(res, 500, { ok: false, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (req.url === "/api/tts/delete-model" && req.method === "POST") {
|
||||
const body = await readJsonBody(req, res);
|
||||
if (!body) return;
|
||||
const filename = path.basename(String(body.filename || ""));
|
||||
const modelPath = path.join(TTS_MODELS, filename);
|
||||
if (!filename || !filename.endsWith(".json") || !pathInside(modelPath, TTS_MODELS)) {
|
||||
return json(res, 400, { ok: false, error: "Invalid filename" });
|
||||
}
|
||||
if (ttsSettings.model === filename) await stopTts();
|
||||
try {
|
||||
fs.unlinkSync(modelPath);
|
||||
return json(res, 200, { ok: true });
|
||||
} catch (err) {
|
||||
return json(res, err.code === "ENOENT" ? 404 : 500, { ok: false, error: err.code === "ENOENT" ? "TTS model not found" : err.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (req.url === "/api/tts/outputs" && req.method === "GET") {
|
||||
return json(res, 200, { ok: true, outputs: listTtsOutputs() });
|
||||
}
|
||||
|
||||
if (req.url === "/api/tts/delete-output" && req.method === "POST") {
|
||||
const body = await readJsonBody(req, res);
|
||||
if (!body) return;
|
||||
const filename = path.basename(String(body.filename || ""));
|
||||
if (!filename || !filename.endsWith(".json")) {
|
||||
return json(res, 400, { ok: false, error: "Invalid filename" });
|
||||
}
|
||||
const jsonPath = path.join(TTS_OUTPUTS, filename);
|
||||
if (!pathInside(jsonPath, TTS_OUTPUTS)) {
|
||||
return json(res, 400, { ok: false, error: "Invalid path" });
|
||||
}
|
||||
try {
|
||||
if (!fs.existsSync(jsonPath)) {
|
||||
return json(res, 404, { ok: false, error: "TTS output metadata not found" });
|
||||
}
|
||||
const metadata = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
const audioFile = metadata.audioFile ? path.basename(String(metadata.audioFile)) : "";
|
||||
fs.unlinkSync(jsonPath);
|
||||
if (audioFile) {
|
||||
const audioPath = path.join(TTS_OUTPUTS, audioFile);
|
||||
if (pathInside(audioPath, TTS_OUTPUTS) && fs.existsSync(audioPath)) {
|
||||
fs.unlinkSync(audioPath);
|
||||
}
|
||||
}
|
||||
return json(res, 200, { ok: true });
|
||||
} catch (err) {
|
||||
console.error(" [api] Failed to delete TTS output:", err);
|
||||
return json(res, 500, { ok: false, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
function isOomError(msg) {
|
||||
if (!msg) return false;
|
||||
const lower = String(msg).toLowerCase();
|
||||
@@ -5307,6 +5823,21 @@ async function getLlmfitRecommendations(useCase = "chat", limit = 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url.startsWith("/tts-outputs/") && req.method === "GET") {
|
||||
const filename = path.basename(decodeURIComponent(req.url.replace(/^\/tts-outputs\//, "").split("?")[0] || ""));
|
||||
const filePath = path.join(TTS_OUTPUTS, filename);
|
||||
if (!filename || !pathInside(filePath, TTS_OUTPUTS) || !fs.existsSync(filePath)) {
|
||||
return json(res, 404, { ok: false, error: "TTS output not found" });
|
||||
}
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) return json(res, 500, { ok: false, error: err.message });
|
||||
res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream", "Access-Control-Allow-Origin": "*" });
|
||||
res.end(data);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// POST /api/save-output
|
||||
if (req.url === "/api/save-output" && req.method === "POST") {
|
||||
try {
|
||||
@@ -5436,6 +5967,7 @@ server.listen(PORT_FRONTEND, "0.0.0.0", () => {
|
||||
console.log(" Image API: http://127.0.0.1:" + PORT_BACKEND);
|
||||
console.log(" Text API : starts on http://127.0.0.1:" + PREFERRED_LLM_PORT);
|
||||
console.log(" Speech : managed locally, API on frontend port");
|
||||
console.log(" TTS : Kokoro ONNX managed locally, API on frontend port");
|
||||
console.log(" ============================================================");
|
||||
console.log("");
|
||||
|
||||
@@ -5444,5 +5976,5 @@ server.listen(PORT_FRONTEND, "0.0.0.0", () => {
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on("SIGINT", async () => { await killBackend(); await killOpenVinoWorker(); await killLlm(); await stopSpeech(); process.exit(0); });
|
||||
process.on("SIGTERM", async () => { await killBackend(); await killOpenVinoWorker(); await killLlm(); await stopSpeech(); process.exit(0); });
|
||||
process.on("SIGINT", async () => { await killBackend(); await killOpenVinoWorker(); await killLlm(); await stopSpeech(); await stopTts(); process.exit(0); });
|
||||
process.on("SIGTERM", async () => { await killBackend(); await killOpenVinoWorker(); await killLlm(); await stopSpeech(); await stopTts(); process.exit(0); });
|
||||
@@ -0,0 +1,52 @@
|
||||
# Local AI Studio - Kokoro TTS setup for Windows
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$rootDir = Split-Path -Parent $scriptDir
|
||||
$appDir = Join-Path $rootDir "app"
|
||||
$toolsDir = Join-Path $appDir "tools"
|
||||
$nodeDir = Join-Path $toolsDir "node-win"
|
||||
$nodeExe = Join-Path $nodeDir "node.exe"
|
||||
$npmCmd = Join-Path $nodeDir "npm.cmd"
|
||||
$runtimeDir = Join-Path $appDir "tts-runtime"
|
||||
$modelsDir = Join-Path $appDir "tts-models"
|
||||
$outputsDir = Join-Path $appDir "tts-outputs"
|
||||
$cacheDir = Join-Path $appDir "tts-cache"
|
||||
|
||||
function Print-OK { param([string]$m); Write-Host " OK $m" -ForegroundColor Green }
|
||||
function Print-Info { param([string]$m); Write-Host " >> $m" -ForegroundColor Cyan }
|
||||
function Print-Fail { param([string]$m); Write-Host " XX $m" -ForegroundColor Red }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " ============================================================" -ForegroundColor Cyan
|
||||
Write-Host " Setting up Kokoro ONNX Text-to-Speech runtime" -ForegroundColor Cyan
|
||||
Write-Host " ============================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $runtimeDir, $modelsDir, $outputsDir, $cacheDir | Out-Null
|
||||
|
||||
if (-not (Test-Path $nodeExe) -or -not (Test-Path $npmCmd)) {
|
||||
Print-Fail "Portable Node.js is missing. Run scripts/setup.ps1 first."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$packageJson = Join-Path $runtimeDir "package.json"
|
||||
if (-not (Test-Path $packageJson)) {
|
||||
'{"private":true,"type":"module","dependencies":{"kokoro-js":"^1.2.1"}}' | Out-File -FilePath $packageJson -Encoding utf8
|
||||
}
|
||||
|
||||
Push-Location $runtimeDir
|
||||
$oldPath = $env:PATH
|
||||
try {
|
||||
$env:PATH = "$nodeDir;$env:PATH"
|
||||
Print-Info "Installing kokoro-js into app/tts-runtime..."
|
||||
& $npmCmd install --prefer-offline 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Print-Fail "kokoro-js install failed."
|
||||
exit 1
|
||||
}
|
||||
} finally {
|
||||
$env:PATH = $oldPath
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Print-OK "Kokoro TTS runtime is ready."
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Local AI Studio - Kokoro TTS setup for Linux/macOS
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
APP_DIR="$ROOT_DIR/app"
|
||||
TOOLS_DIR="$APP_DIR/tools"
|
||||
PLATFORM="$(uname -s)"
|
||||
|
||||
if [[ "$PLATFORM" == "Darwin" ]]; then
|
||||
NODE_DIR="$TOOLS_DIR/node-mac"
|
||||
else
|
||||
NODE_DIR="$TOOLS_DIR/node-linux"
|
||||
fi
|
||||
|
||||
NODE_BIN="$NODE_DIR/bin/node"
|
||||
NPM_BIN="$NODE_DIR/bin/npm"
|
||||
RUNTIME_DIR="$APP_DIR/tts-runtime"
|
||||
MODELS_DIR="$APP_DIR/tts-models"
|
||||
OUTPUTS_DIR="$APP_DIR/tts-outputs"
|
||||
CACHE_DIR="$APP_DIR/tts-cache"
|
||||
|
||||
print_ok() { echo " OK $1"; }
|
||||
print_info() { echo " >> $1"; }
|
||||
print_fail() { echo " XX $1"; }
|
||||
|
||||
echo ""
|
||||
echo " ============================================================"
|
||||
echo " Setting up Kokoro ONNX Text-to-Speech runtime"
|
||||
echo " ============================================================"
|
||||
echo ""
|
||||
|
||||
mkdir -p "$RUNTIME_DIR" "$MODELS_DIR" "$OUTPUTS_DIR" "$CACHE_DIR"
|
||||
|
||||
if [[ ! -x "$NODE_BIN" || ! -x "$NPM_BIN" ]]; then
|
||||
print_fail "Portable Node.js is missing. Run scripts/setup.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$RUNTIME_DIR/package.json" ]]; then
|
||||
cat > "$RUNTIME_DIR/package.json" <<'JSON'
|
||||
{"private":true,"type":"module","dependencies":{"kokoro-js":"^1.2.1"}}
|
||||
JSON
|
||||
fi
|
||||
|
||||
cd "$RUNTIME_DIR"
|
||||
export PATH="$NODE_DIR/bin:$PATH"
|
||||
print_info "Installing kokoro-js into app/tts-runtime..."
|
||||
"$NPM_BIN" install --prefer-offline
|
||||
|
||||
print_ok "Kokoro TTS runtime is ready."
|
||||
+10
-3
@@ -158,7 +158,7 @@ function Expand-WithProgress {
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
Print-Header
|
||||
|
||||
$steps = 6
|
||||
$steps = 7
|
||||
|
||||
# ── Step 1: Portable Node.js ──────────────────────────────────────────────────
|
||||
Print-Step 1 $steps "Setting up portable Node.js (app/tools/node-win/)"
|
||||
@@ -434,7 +434,14 @@ if (-not $?) {
|
||||
Read-Host; exit 1
|
||||
}
|
||||
|
||||
Print-Step 5 $steps "Installing frontend dependencies (app/frontend/)"
|
||||
Print-Step 5 $steps "Setting up Kokoro ONNX text-to-speech runtime"
|
||||
& (Join-Path $scriptDir "setup-tts.ps1")
|
||||
if (-not $?) {
|
||||
Print-Fail "Kokoro TTS setup failed."
|
||||
Read-Host; exit 1
|
||||
}
|
||||
|
||||
Print-Step 6 $steps "Installing frontend dependencies (app/frontend/)"
|
||||
Write-Host ""
|
||||
|
||||
if (-not (Test-Path $npmCmd)) {
|
||||
@@ -488,7 +495,7 @@ try {
|
||||
Print-OK "Dependencies installed!"
|
||||
|
||||
# ── Step 4: Build frontend ────────────────────────────────────────────────
|
||||
Print-Step 6 $steps "Building frontend -> app/dist/"
|
||||
Print-Step 7 $steps "Building frontend -> app/dist/"
|
||||
Write-Host ""
|
||||
|
||||
& $npmCmd run build 2>&1
|
||||
|
||||
+6
-3
@@ -258,7 +258,7 @@ if [[ "$PLATFORM" == "Linux" ]]; then
|
||||
check_glibc
|
||||
fi
|
||||
|
||||
TOTAL_STEPS=6
|
||||
TOTAL_STEPS=7
|
||||
|
||||
# ── Step 1: Portable Node.js ────────────────────────────────────────────────
|
||||
print_step 1 $TOTAL_STEPS "Setting up portable Node.js ($NODE_DIR/)"
|
||||
@@ -547,7 +547,10 @@ bash "$SCRIPT_DIR/setup-llama.sh"
|
||||
print_step 4 $TOTAL_STEPS "Setting up whisper.cpp speech backend"
|
||||
bash "$SCRIPT_DIR/setup-whisper.sh"
|
||||
|
||||
print_step 5 $TOTAL_STEPS "Installing frontend dependencies (app/frontend/)"
|
||||
print_step 5 $TOTAL_STEPS "Setting up Kokoro ONNX text-to-speech runtime"
|
||||
bash "$SCRIPT_DIR/setup-tts.sh"
|
||||
|
||||
print_step 6 $TOTAL_STEPS "Installing frontend dependencies (app/frontend/)"
|
||||
|
||||
if [[ ! -x "$NPM_BIN" ]]; then
|
||||
print_fail "Portable npm was not found at $NPM_BIN"
|
||||
@@ -645,7 +648,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Step 4: Build frontend ──────────────────────────────────────────────────
|
||||
print_step 6 $TOTAL_STEPS "Building frontend -> app/dist/"
|
||||
print_step 7 $TOTAL_STEPS "Building frontend -> app/dist/"
|
||||
|
||||
if [ "$USE_SYMLINKS" = false ]; then
|
||||
# If symlinks are disabled, run vite directly using the local node executable.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { createRequire } from "node:module";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function readStdin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => { data += chunk; });
|
||||
process.stdin.on("end", () => resolve(data));
|
||||
process.stdin.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function resolveRuntimePackage(name) {
|
||||
const runtime = process.env.TTS_RUNTIME || "";
|
||||
if (runtime) {
|
||||
const pkg = path.join(runtime, "node_modules", name, "dist", "kokoro.js");
|
||||
return pathToFileUrl(pkg);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function pathToFileUrl(filePath) {
|
||||
const resolved = path.resolve(filePath).replace(/\\/g, "/");
|
||||
return `file:///${resolved.replace(/^\/+/, "")}`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const payload = JSON.parse(await readStdin());
|
||||
process.env.TRANSFORMERS_CACHE = payload.cacheDir || process.env.TRANSFORMERS_CACHE || "";
|
||||
process.env.HF_HOME = payload.cacheDir || process.env.HF_HOME || "";
|
||||
|
||||
const kokoro = await import(resolveRuntimePackage("kokoro-js"));
|
||||
const { KokoroTTS } = kokoro;
|
||||
|
||||
const tts = await KokoroTTS.from_pretrained(payload.modelId, {
|
||||
dtype: payload.dtype || "q8",
|
||||
device: "cpu",
|
||||
});
|
||||
const audio = await tts.generate(payload.text, {
|
||||
voice: payload.voice || "af_heart",
|
||||
speed: Number(payload.speed) || 1,
|
||||
});
|
||||
await fs.mkdir(path.dirname(payload.output), { recursive: true });
|
||||
audio.save(payload.output);
|
||||
process.stdout.write(JSON.stringify({
|
||||
ok: true,
|
||||
output: payload.output,
|
||||
sampleRate: audio.sampling_rate || audio.sampleRate || 24000,
|
||||
}));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(err?.stack || err?.message || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in new issue
Block a user