# -*- 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", )