- Python main entry with CLI (--room-id, --voice, --speed) - blivedm integration for real-time danmaku receiving - Chinese-to-Japanese katakana conversion (pypinyin + mapping table) - Node.js persistent bridge for aquestalk.js TTS synthesis - Audio playback via sounddevice - Message queue for sequential TTS playback
96 lines
2.5 KiB
JavaScript
96 lines
2.5 KiB
JavaScript
/** tts_bridge.js — Persistent Node.js bridge for aquestalk.js TTS synthesis.
|
|
|
|
Protocol (via stdin/stdout):
|
|
- On startup, prints "READY" to stdout.
|
|
- Reads lines from stdin in format: INPUT_PATH|OUTPUT_PATH
|
|
- Reads kana text from INPUT_PATH, synthesizes WAV, writes to OUTPUT_PATH.
|
|
- Prints "OK:OUTPUT_PATH" on success, "ERR:message" on failure.
|
|
- Exits when stdin is closed.
|
|
|
|
Usage:
|
|
node tts_bridge.js --voice f1 --speed 100
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync } from "fs";
|
|
import { createInterface } from "readline";
|
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
import { dirname, join } from "path";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// Parse CLI arguments
|
|
const args = process.argv.slice(2);
|
|
let voice = "f1";
|
|
let speed = 100;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === "--voice" && i + 1 < args.length) {
|
|
voice = args[++i];
|
|
} else if (args[i] === "--speed" && i + 1 < args.length) {
|
|
speed = parseInt(args[++i], 10);
|
|
}
|
|
}
|
|
|
|
// Import aquestalk.js from sibling directory
|
|
const aquestalkMod = pathToFileURL(join(__dirname, "..", "aquestalk.js", "dist", "index.js")).href;
|
|
const { load } = await import(aquestalkMod);
|
|
|
|
console.error(`[bridge] Loading aquestalk.js with voice="${voice}", speed=${speed}...`);
|
|
|
|
let aq;
|
|
try {
|
|
aq = await load(voice);
|
|
console.error("[bridge] aquestalk.js loaded successfully.");
|
|
} catch (err) {
|
|
console.error("[bridge] Failed to load aquestalk.js:", err.message);
|
|
process.stdout.write("ERR:FAILED_TO_LOAD\n");
|
|
process.exit(1);
|
|
}
|
|
|
|
process.stdout.write("READY\n");
|
|
|
|
const rl = createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
terminal: false,
|
|
});
|
|
|
|
rl.on("line", (line) => {
|
|
line = line.trim();
|
|
if (!line) return;
|
|
|
|
const sepIdx = line.indexOf("|");
|
|
if (sepIdx === -1) {
|
|
process.stdout.write(`ERR:INVALID_FORMAT:${line}\n`);
|
|
return;
|
|
}
|
|
|
|
const inputPath = line.substring(0, sepIdx);
|
|
const outputPath = line.substring(sepIdx + 1);
|
|
|
|
try {
|
|
const kanaText = readFileSync(inputPath, "utf-8").trim();
|
|
|
|
if (!kanaText) {
|
|
process.stdout.write(`ERR:EMPTY_TEXT\n`);
|
|
return;
|
|
}
|
|
|
|
const wav = aq.run(kanaText, speed);
|
|
|
|
writeFileSync(outputPath, Buffer.from(wav));
|
|
|
|
process.stdout.write(`OK:${outputPath}\n`);
|
|
} catch (err) {
|
|
process.stdout.write(`ERR:${err.message}\n`);
|
|
}
|
|
});
|
|
|
|
rl.on("close", () => {
|
|
console.error("[bridge] stdin closed, shutting down.");
|
|
aq.destroy().then(() => {
|
|
process.exit(0);
|
|
});
|
|
});
|