Files
bililive-touhou-tts/tts_bridge.js
T
chun_qiu fd5c732d8a feat: web UI + QR login + portable build
- Add web UI panel (dark theme) at / with REST API
- Add Bilibili QR code scan login with browser header emulation
- Add wanakana for English -> katakana conversion
- Add volume control (0%-200%)
- Add message format with '说、' pause marker
- PyInstaller --onedir build for portable exe
- Rate-limit danmaku logging (1/20)
- Fix aiohttp.access log spam
- Add README.md
2026-08-08 13:22:58 +08:00

86 lines
2.4 KiB
JavaScript

/** tts_bridge.js — Persistent Node.js bridge for aquestalk.js TTS synthesis.
Converts English/romaji to katakana via wanakana before synthesis.
*/
import { readFileSync, writeFileSync } from "fs";
import { createInterface } from "readline";
import { fileURLToPath, pathToFileURL } from "url";
import { dirname, join } from "path";
import wanakana from "wanakana";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
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);
}
}
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);
}
function synthesize(kanaText) {
const trimmed = kanaText.trim();
if (!trimmed) throw new Error("EMPTY_TEXT");
const finalText = wanakana.toKatakana(trimmed, { convertLongVowelMark: true });
const wav = aq.run(finalText, speed);
return Buffer.from(wav);
}
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 = synthesize(kanaText);
writeFileSync(outputPath, 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));
});