- Vendor english-kana-matcher.js (english-to-kana, MIT, 49216 words) - Rewrite bridge: dictionary lookup + letter-by-letter fallback for OOV words - Convert English punctuation (,.!?) to Japanese pauses (、。) - Fix Chinese fullwidth comma being stripped by filter_kana - Preserve decimal points in numbers (3.5 -> スリーファイブ) - main.py now applies filter_kana (matches web UI behavior) - Remove wanakana dependency; rebuild portable zip (82.5 MB)
180 lines
6.8 KiB
Python
180 lines
6.8 KiB
Python
"""Build portable zip package: bililive-touhou-tts-portable.zip
|
|
|
|
Usage:
|
|
python build.py
|
|
|
|
Output: dist/bililive-touhou-tts-portable.zip
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).parent
|
|
DIST = ROOT / "dist"
|
|
BUILD_DIR = DIST / "portable"
|
|
|
|
|
|
def step(msg: str) -> None:
|
|
print(f"\n=== {msg} ===")
|
|
|
|
|
|
def safe_copy_tree(src: Path, dst: Path, ignore_patterns: tuple = ()) -> None:
|
|
"""Copy directory tree, skipping symlinks/recursion."""
|
|
if dst.exists():
|
|
shutil.rmtree(dst, ignore_errors=True)
|
|
dst.mkdir(parents=True, exist_ok=True)
|
|
|
|
to_visit = [(src, dst)]
|
|
while to_visit:
|
|
s, d = to_visit.pop()
|
|
try:
|
|
for entry in s.iterdir():
|
|
if entry.name.startswith(".") and entry.name != ".npmignore":
|
|
continue
|
|
if entry.name in ignore_patterns:
|
|
continue
|
|
if entry.name == "__pycache__":
|
|
continue
|
|
|
|
target = d / entry.name
|
|
|
|
if entry.is_symlink():
|
|
continue # skip symlinks entirely
|
|
|
|
if entry.is_dir():
|
|
if target.exists():
|
|
try:
|
|
shutil.rmtree(target, ignore_errors=True)
|
|
except OSError:
|
|
continue
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
to_visit.append((entry, target))
|
|
else:
|
|
try:
|
|
shutil.copy2(entry, target)
|
|
except OSError:
|
|
pass # skip unreadable files
|
|
except OSError:
|
|
pass # skip unreadable dirs
|
|
|
|
|
|
def main() -> int:
|
|
DIST.mkdir(exist_ok=True)
|
|
if BUILD_DIR.exists():
|
|
print(" Cleaning previous build...")
|
|
for item in BUILD_DIR.iterdir():
|
|
try:
|
|
if item.is_dir():
|
|
shutil.rmtree(item, ignore_errors=True)
|
|
else:
|
|
item.unlink(missing_ok=True)
|
|
except OSError:
|
|
pass
|
|
BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# ── 1. PyInstaller ─────────────────────────────────────────────────
|
|
step("Building Python EXE with PyInstaller")
|
|
subprocess.run(
|
|
[
|
|
sys.executable, "-m", "PyInstaller",
|
|
"--onedir",
|
|
"--name", "bililive-tts",
|
|
"--add-data", f"{ROOT / 'static'}{os.pathsep}static",
|
|
"--hidden-import", "blivedm",
|
|
"--hidden-import", "blivedm.clients",
|
|
"--hidden-import", "blivedm.clients.web",
|
|
"--hidden-import", "blivedm.clients.ws_base",
|
|
"--hidden-import", "blivedm.handlers",
|
|
"--hidden-import", "blivedm.models",
|
|
"--hidden-import", "blivedm.models.web",
|
|
"--hidden-import", "blivedm.utils",
|
|
"--hidden-import", "pypinyin",
|
|
"--hidden-import", "sounddevice",
|
|
"--hidden-import", "soundfile",
|
|
"--hidden-import", "numpy",
|
|
"--hidden-import", "aiohttp",
|
|
"--hidden-import", "Brotli",
|
|
"--hidden-import", "pure_protobuf",
|
|
"--hidden-import", "yarl",
|
|
"--hidden-import", "chinese2kana",
|
|
"--hidden-import", "pinyin2kana_data",
|
|
"--hidden-import", "audio_player",
|
|
"--hidden-import", "danmaku_handler",
|
|
"--hidden-import", "tts",
|
|
"--hidden-import", "bili_login",
|
|
"--distpath", str(BUILD_DIR / "app"),
|
|
str(ROOT / "server.py"),
|
|
],
|
|
)
|
|
exe = BUILD_DIR / "app" / "bililive-tts" / "bililive-tts.exe"
|
|
if not exe.exists():
|
|
print(f"ERROR: PyInstaller failed, {exe} not found")
|
|
return 1
|
|
print(f" OK: {exe} ({exe.stat().st_size // 1024 // 1024} MB)")
|
|
|
|
# ── 2. Portable Node.js ────────────────────────────────────────────
|
|
step("Copying portable Node.js")
|
|
node_src = Path(os.environ.get("NODE_PATH", r"C:\nvm4w\nodejs\node.exe"))
|
|
shutil.copy2(node_src, BUILD_DIR / "node.exe")
|
|
print(f" OK: node.exe")
|
|
|
|
# ── 3. aquestalk.js (only what's needed) ───────────────────────────
|
|
step("Copying aquestalk.js runtime")
|
|
aq_src = ROOT / "aquestalk.js"
|
|
aq_dst = BUILD_DIR / "aquestalk.js"
|
|
|
|
print(" dist/ ...")
|
|
safe_copy_tree(aq_src / "dist", aq_dst / "dist")
|
|
print(" voices/ ...")
|
|
safe_copy_tree(aq_src / "voices", aq_dst / "voices")
|
|
print(" node_modules/ (symlink-safe) ...")
|
|
safe_copy_tree(aq_src / "node_modules", aq_dst / "node_modules",
|
|
ignore_patterns=("bililive-touhou-tts",))
|
|
|
|
# ── 4. node_modules (if any) ───────────────────────────────────────
|
|
if (ROOT / "node_modules").exists():
|
|
step("Copying node_modules")
|
|
safe_copy_tree(ROOT / "node_modules", BUILD_DIR / "node_modules")
|
|
|
|
# ── 5. Bridge + config ─────────────────────────────────────────────
|
|
step("Copying bridge files")
|
|
shutil.copy2(ROOT / "tts_bridge.js", BUILD_DIR / "tts_bridge.js")
|
|
shutil.copy2(ROOT / "english-kana-matcher.js", BUILD_DIR / "english-kana-matcher.js")
|
|
shutil.copy2(ROOT / "package.json", BUILD_DIR / "package.json")
|
|
print(" OK")
|
|
|
|
# ── 6. Launcher ────────────────────────────────────────────────────
|
|
step("Creating launcher")
|
|
bat = BUILD_DIR / "start.bat"
|
|
bat.write_text(
|
|
'@echo off\r\n'
|
|
'cd /d "%~dp0"\r\n'
|
|
'start "" "app\\bililive-tts\\bililive-tts.exe"\r\n',
|
|
encoding="ascii",
|
|
)
|
|
print(f" OK")
|
|
|
|
# ── 7. Zip ─────────────────────────────────────────────────────────
|
|
step("Creating zip")
|
|
zip_path = DIST / "bililive-touhou-tts-portable.zip"
|
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
for file in BUILD_DIR.rglob("*"):
|
|
if ".git" in file.parts or "__pycache__" in file.parts:
|
|
continue
|
|
if file.is_dir():
|
|
continue
|
|
arcname = str(file.relative_to(BUILD_DIR))
|
|
zf.write(file, arcname)
|
|
|
|
size_mb = zip_path.stat().st_size / (1024 * 1024)
|
|
print(f"\n Done: {zip_path} ({size_mb:.1f} MB)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|