| # -*- mode: python ; coding: utf-8 -*- | |
| # | |
| # InsightUX PyInstaller build spec β onedir, not onefile. | |
| # | |
| # Onefile re-extracts the whole bundle (torch alone is ~1.2GB) to a temp | |
| # directory on EVERY launch, which is a bad startup-time tradeoff on top of | |
| # an already-heavy ML stack. Onedir installs once as a persistent folder | |
| # (InsightUX.exe + _internal/), which is also just what installer.iss wants | |
| # to package anyway. | |
| # | |
| # Build (from the repo root, inside the project venv β this project's venv | |
| # uses Python 3.10, which is the proven-working interpreter; PyInstaller | |
| # must be installed into that same venv first: `pip install pyinstaller`): | |
| # pyinstaller packaging/InsightUX.spec --noconfirm | |
| # | |
| # Output lands in dist/InsightUX/ β that whole folder is what | |
| # packaging/installer.iss packages into the installer. | |
| # | |
| # Known risk areas likely to need iteration on the FIRST real build (this is | |
| # normal for this dependency stack, not a sign anything here is wrong): | |
| # - mediapipe ships its own .tflite/.binarypb data files as package data; | |
| # collect_all() below should catch them, but if face_mesh fails to load | |
| # at runtime with a "file not found", that's the first place to check. | |
| # - onnxruntime/torch/torchvision ship native DLLs; "DLL load failed" at | |
| # startup means collect_all() missed one β check dist/InsightUX/_internal | |
| # for what's actually present vs. what the traceback wants. | |
| # - pywebview + pythonnet's `clr` bridge needs the Microsoft Edge WebView2 | |
| # Runtime on the TARGET machine. Usually preinstalled on modern Windows | |
| # 10/11, but installer.iss bundles + silently runs Microsoft's WebView2 | |
| # bootstrapper as a safety net for older/locked-down machines. | |
| import os | |
| import dis | |
| from PyInstaller.utils.hooks import collect_all | |
| import PyInstaller.lib.modulegraph.util as _mg_util | |
| # A handful of very large single-file modules in this dependency tree | |
| # (bottle.py, matplotlib/pyplot.py, and possibly others not yet hit) trip a | |
| # real dis.get_instructions() bug on this Python 3.10.0 build β an IndexError | |
| # walking co_consts for certain EXTENDED_ARG-heavy bytecode in big modules. | |
| # Confirmed this is purely a build-time INTROSPECTION bug, not a real problem: | |
| # `import bottle` / `import matplotlib.pyplot` both work completely normally | |
| # at actual runtime; only PyInstaller's static bytecode scan of them (used to | |
| # discover further hidden imports) crashes. | |
| # | |
| # Patched once here instead of excluding modules one at a time as each is | |
| # discovered (there's no way to know in advance how many more of these exist | |
| # inside torch's ~1.2GB of dependencies). Worst case if this ever masks a | |
| # real import PyInstaller would otherwise have auto-discovered: the | |
| # hiddenimports/collect_all() list below already covers every package this | |
| # app actually needs bundled, so a skipped scan on one giant module isn't | |
| # depended on for correctness. | |
| _orig_iterate_instructions = _mg_util.iterate_instructions | |
| def _safe_iterate_instructions(code_object): | |
| try: | |
| yield from _orig_iterate_instructions(code_object) | |
| except Exception as e: | |
| print(f"[InsightUX.spec] bytecode scan skipped on a module " | |
| f"(dis bug workaround): {type(e).__name__}: {e}") | |
| return | |
| _mg_util.iterate_instructions = _safe_iterate_instructions | |
| PACKAGING_DIR = os.path.abspath(os.path.dirname(os.path.abspath(SPEC))) | |
| REPO_ROOT = os.path.dirname(PACKAGING_DIR) | |
| datas = [ | |
| (os.path.join(REPO_ROOT, "models", "gaze_cnn_v4.onnx"), "models"), | |
| (os.path.join(REPO_ROOT, "models", "gaze_cnn_v4.onnx.data"), "models"), | |
| (os.path.join(REPO_ROOT, "checkpoints", "best_model_v4.pt"), "checkpoints"), | |
| ] | |
| binaries = [] | |
| # models.model_v4 is only reached via a try/except-guarded dynamic import | |
| # inside calibrate.py's fine-tune function β spell it out explicitly rather | |
| # than trust static analysis to follow it through the try/except. | |
| hiddenimports = ["models.model_v4"] | |
| # The blunt-but-reliable safety net for packages that ship native binaries | |
| # and/or non-.py package data PyInstaller's built-in hooks don't fully catch. | |
| for pkg in ("mediapipe", "onnxruntime", "torch", "torchvision", "cv2"): | |
| pkg_datas, pkg_binaries, pkg_hiddenimports = collect_all(pkg) | |
| datas += pkg_datas | |
| binaries += pkg_binaries | |
| hiddenimports += pkg_hiddenimports | |
| a = Analysis( | |
| [os.path.join(REPO_ROOT, "browser_session.py")], | |
| pathex=[REPO_ROOT], | |
| binaries=binaries, | |
| datas=datas, | |
| hiddenimports=hiddenimports, | |
| hookspath=[], | |
| hooksconfig={}, | |
| runtime_hooks=[], | |
| excludes=[], | |
| noarchive=False, | |
| ) | |
| pyz = PYZ(a.pure) | |
| exe = EXE( | |
| pyz, | |
| a.scripts, | |
| [], | |
| exclude_binaries=True, | |
| name="InsightUX", | |
| debug=False, | |
| bootloader_ignore_signals=False, | |
| strip=False, | |
| upx=False, | |
| # Keep the console. calibrate.py's live lighting/blink/quality readouts | |
| # and the correlation "VERDICT" summary are the primary calibration | |
| # feedback today β hiding the console would silently drop the one thing | |
| # a user needs to read after every calibration run. | |
| console=True, | |
| disable_windowed_traceback=False, | |
| target_arch=None, | |
| codesign_identity=None, | |
| entitlements_file=None, | |
| ) | |
| coll = COLLECT( | |
| exe, | |
| a.binaries, | |
| a.zipfiles, | |
| a.datas, | |
| strip=False, | |
| upx=False, | |
| upx_exclude=[], | |
| name="InsightUX", | |
| ) | |