#!/usr/bin/env python3 """Собирает готовый к запуску ZIP для Windows. Собирается на любой ОС: скачиваются встраиваемый Python для Windows, колёса под win_amd64 и ffmpeg.exe. Устанавливать Python на целевой машине не требуется - распаковал и запустил start.bat. uv run --with pip python build/make_windows_zip.py """ import argparse import io import shutil import subprocess import sys import urllib.request import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent OUT = ROOT / "dist" PY_VERSION = "3.11.9" PY_URL = f"https://www.python.org/ftp/python/{PY_VERSION}/python-{PY_VERSION}-embed-amd64.zip" # LGPL-сборка: нам нужен только декодер аудио, GPL-компоненты не требуются. FFMPEG_URL = ("https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/" "ffmpeg-master-latest-win64-lgpl.zip") # Caddy сам получает и продлевает сертификаты Let's Encrypt - для машины без # постоянного администратора это надёжнее ручной возни с сертификатами. CADDY_URL = ("https://github.com/caddyserver/caddy/releases/download/v2.10.2/" "caddy_2.10.2_windows_amd64.zip") PACKAGES = [ "fastapi", "uvicorn", "python-multipart", "onnx-asr", "onnxruntime", "sherpa-onnx", "numpy", "huggingface-hub", "tqdm", "requests", ] def fetch(url: str) -> bytes: print(f" скачиваю {url.rsplit('/', 1)[-1]}") with urllib.request.urlopen(url) as r: return r.read() def add_python(stage: Path) -> None: print("Встраиваемый Python") data = fetch(PY_URL) py_dir = stage / "python" py_dir.mkdir(parents=True) with zipfile.ZipFile(io.BytesIO(data)) as z: z.extractall(py_dir) # Встраиваемая сборка по умолчанию не видит site-packages: включаем, # иначе установленные колёса не импортируются. for pth in py_dir.glob("python*._pth"): text = pth.read_text(encoding="utf-8").replace("#import site", "import site") if "Lib\\site-packages" not in text: text += "\nLib\\site-packages\n..\n" pth.write_text(text, encoding="utf-8") def add_wheels(stage: Path) -> None: print("Колёса под win_amd64") site = stage / "python" / "Lib" / "site-packages" site.mkdir(parents=True, exist_ok=True) wheels = stage / "_wheels" subprocess.run([ sys.executable, "-m", "pip", "download", "--quiet", "--only-binary=:all:", "--platform", "win_amd64", "--python-version", "3.11", "--implementation", "cp", "--abi", "cp311", "-d", str(wheels), *PACKAGES, ], check=True) for whl in sorted(wheels.glob("*.whl")): with zipfile.ZipFile(whl) as z: z.extractall(site) shutil.rmtree(wheels) # Тесты и заголовки в поставке не нужны и весят прилично for junk in list(site.rglob("__pycache__")) + list(site.glob("*/include")): shutil.rmtree(junk, ignore_errors=True) def add_ffmpeg(stage: Path) -> None: print("ffmpeg") data = fetch(FFMPEG_URL) bin_dir = stage / "bin" bin_dir.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(io.BytesIO(data)) as z: name = next(n for n in z.namelist() if n.endswith("bin/ffmpeg.exe")) (bin_dir / "ffmpeg.exe").write_bytes(z.read(name)) def add_caddy(stage: Path) -> None: print("Caddy для HTTPS") data = fetch(CADDY_URL) bin_dir = stage / "bin" bin_dir.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(io.BytesIO(data)) as z: name = next(n for n in z.namelist() if n.endswith("caddy.exe")) (bin_dir / "caddy.exe").write_bytes(z.read(name)) def add_app(stage: Path) -> None: print("Код сервиса") shutil.copytree(ROOT / "app", stage / "app", ignore=shutil.ignore_patterns("__pycache__")) for name in ("replacements.txt", "README.md"): src = ROOT / name if src.is_file(): shutil.copy(src, stage / name) write_bat(stage / "start.bat", START_BAT) write_bat(stage / "download_models.bat", DOWNLOAD_BAT) write_bat(stage / "start-https.bat", HTTPS_BAT) (stage / "download_models.py").write_text( (ROOT / "build" / "download_models.py").read_text(encoding="utf-8"), encoding="utf-8") def write_bat(path: Path, text: str) -> None: """Пишет .bat с переводами строк CRLF и без кириллицы. cmd.exe читает файлы с одними LF криво - теряет символы в начале строк, из-за чего `set TALKSCORE_ASR_HOME` превращается в `ALKSCORE_ASR_HOME`. Кириллицы в .bat тоже быть не должно: кодировка консоли заранее неизвестна, поэтому все сообщения для человека печатает Python. """ path.write_text(text, encoding="ascii", newline="\r\n") # chcp 65001 переводит консоль в UTF-8, и Python дальше пишет в той же кодировке. # Без этой пары русский текст в выводе превращается в мусор. # .bat - трёхстрочные заглушки: вся логика запуска живёт в app/launcher.py, # который обновление доставляет штатно. Раньше логика была здесь, и чтобы # изменить порядок запуска, требовалась переустановка. # chcp 65001 переводит консоль в UTF-8, иначе русский текст станет мусором. START_BAT = """@echo off chcp 65001 >nul cd /d "%~dp0" set "TALKSCORE_ASR_HOME=%~dp0" set "PYTHONPATH=%~dp0" "%~dp0python\\python.exe" -m app.launcher pause """ HTTPS_BAT = """@echo off chcp 65001 >nul cd /d "%~dp0" set "TALKSCORE_ASR_HOME=%~dp0" set "PYTHONPATH=%~dp0" "%~dp0python\\python.exe" -m app.launcher https pause """ DOWNLOAD_BAT = """@echo off chcp 65001 >nul cd /d "%~dp0" set "TALKSCORE_ASR_HOME=%~dp0" set "PYTHONPATH=%~dp0" "%~dp0python\\python.exe" download_models.py pause """ def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--skip-ffmpeg", action="store_true", help="не вкладывать ffmpeg (нужен свой в PATH)") args = ap.parse_args() stage = OUT / "talkscore-asr" if stage.exists(): shutil.rmtree(stage) stage.mkdir(parents=True) add_python(stage) add_wheels(stage) if not args.skip_ffmpeg: add_ffmpeg(stage) add_caddy(stage) add_app(stage) archive = OUT / "talkscore-asr-windows.zip" archive.unlink(missing_ok=True) print("Упаковка") with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z: for path in sorted(stage.rglob("*")): if path.is_file(): z.write(path, Path("talkscore-asr") / path.relative_to(stage)) size_mb = archive.stat().st_size / 1024 / 1024 print(f"\nГотово: {archive} ({size_mb:.0f} МБ)") return 0 if __name__ == "__main__": sys.exit(main())