File size: 5,339 Bytes
1b83c9c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | # -*- 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",
)
|