Files
bililive-touhou-tts/build.py
T
chun_qiu 22ee613352 feat: remove like event (unreliable via INTERACT_WORD_V2); restore portable build dir
- Drop 'like' event from rules, handler and web UI (Bilibili like messages
  are not reliably delivered as INTERACT_WORD_V2 msg_type=6)
- Keep enter_room / follow / interact_other
- event_rules.json rewritten without like key
- build.py keeps BUILD_DIR = dist/portable
2026-08-08 17:11:15 +08:00

185 lines
7.1 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",
"--noconfirm",
"--distpath", str(BUILD_DIR / "app"),
str(ROOT / "server.py"),
],
)
exe = BUILD_DIR / "app" / "bililive-tts" / "bililive-tts.exe"
internal = BUILD_DIR / "app" / "bililive-tts" / "_internal"
base_lib = internal / "base_library.zip"
if not exe.exists() or not base_lib.exists():
print(f"ERROR: PyInstaller build incomplete: exe={exe.exists()}, base_library.zip={base_lib.exists()}")
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")
if (ROOT / "event_rules.json").exists():
shutil.copy2(ROOT / "event_rules.json", BUILD_DIR / "event_rules.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())