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