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
This commit is contained in:
+14
-24
@@ -1,25 +1,17 @@
|
||||
/** 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
|
||||
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);
|
||||
|
||||
// Parse CLI arguments
|
||||
const args = process.argv.slice(2);
|
||||
let voice = "f1";
|
||||
let speed = 100;
|
||||
@@ -32,8 +24,7 @@ for (let i = 0; i < args.length; i++) {
|
||||
}
|
||||
}
|
||||
|
||||
// Import aquestalk.js from sibling directory
|
||||
const aquestalkMod = pathToFileURL(join(__dirname, "..", "aquestalk.js", "dist", "index.js")).href;
|
||||
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}...`);
|
||||
@@ -48,6 +39,14 @@ try {
|
||||
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({
|
||||
@@ -59,28 +58,21 @@ const rl = createInterface({
|
||||
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));
|
||||
|
||||
const wav = synthesize(kanaText);
|
||||
writeFileSync(outputPath, wav);
|
||||
process.stdout.write(`OK:${outputPath}\n`);
|
||||
} catch (err) {
|
||||
process.stdout.write(`ERR:${err.message}\n`);
|
||||
@@ -89,7 +81,5 @@ rl.on("line", (line) => {
|
||||
|
||||
rl.on("close", () => {
|
||||
console.error("[bridge] stdin closed, shutting down.");
|
||||
aq.destroy().then(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
aq.destroy().then(() => process.exit(0));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user