fix: OOV english words now transcribed via phonemize G2P + IPA

- Add phonemize (MIT) rule-based G2P as second fallback tier
- IPA phoneme -> katakana CV-syllable transcription table
- proposal -> プラポーザル, bilibili -> バイリーバイリー (no more letter-spelling)
- Letter spelling kept as last resort only
- Bridge logs SYNTH IN/OUT to stderr; tts.py drains stderr to Python logs
  so conversion results are visible in the server log
- Rebuild portable zip (83.8 MB)
This commit is contained in:
2026-08-08 15:21:00 +08:00
parent 0c0e63161d
commit cb22e074af
4 changed files with 217 additions and 16 deletions
+26
View File
@@ -45,6 +45,7 @@ class TTSBridge:
self._process: asyncio.subprocess.Process | None = None
self._bridge_script = bridge_script or _resolve_path("tts_bridge.js")
self._node_exe = _get_node_exe()
self._stderr_task: asyncio.Task | None = None
async def start(self) -> None:
if not pathlib.Path(self._bridge_script).exists():
@@ -61,6 +62,7 @@ class TTSBridge:
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
self._stderr_task = asyncio.create_task(self._drain_stderr())
line = await asyncio.wait_for(self._process.stdout.readline(), timeout=60)
line_str = line.decode("utf-8").strip()
@@ -73,6 +75,23 @@ class TTSBridge:
logger.info("Node.js TTS bridge ready.")
async def _drain_stderr(self) -> None:
"""Continuously read bridge stderr and forward to Python logs."""
if self._process is None or self._process.stderr is None:
return
try:
while True:
raw = await self._process.stderr.readline()
if not raw:
break
line = raw.decode("utf-8", errors="replace").rstrip()
if line:
logger.info("[bridge] %s", line)
except asyncio.CancelledError:
pass
except Exception:
logger.debug("Bridge stderr drain stopped", exc_info=True)
async def synthesize(self, kana_text: str) -> bytes:
if self._process is None or self._process.stdin is None:
raise RuntimeError("Bridge not started. Call start() first.")
@@ -114,6 +133,13 @@ class TTSBridge:
async def stop(self) -> None:
if self._process is not None:
logger.info("Stopping TTS bridge...")
if self._stderr_task is not None:
self._stderr_task.cancel()
try:
await self._stderr_task
except (asyncio.CancelledError, Exception):
pass
self._stderr_task = None
try:
if self._process.stdin is not None:
self._process.stdin.close()