Grio43 commited on
Commit
071ea55
·
verified ·
1 Parent(s): 75fb688

launcher: portable Python fallback + auto-install VC++ runtime

Browse files

When no system Python is found, run.bat downloads an embeddable Python 3.12 into .pyenv/ and uses that. When onnxruntime fails to load due to missing Visual C++ 2015-2022 redistributable, app.py auto-downloads vc_redist.x64.exe, runs it elevated via UAC, then re-launches so the DLL load is retried in a fresh process.

Files changed (2) hide show
  1. web_interface/app.py +194 -1
  2. web_interface/run.bat +44 -10
web_interface/app.py CHANGED
@@ -4,6 +4,10 @@ Oppai ONNX tagger — single-file launcher.
4
  First run: creates `.venv/`, installs requirements, re-execs inside the venv,
5
  then starts a local Gradio web UI on http://127.0.0.1:7860 .
6
 
 
 
 
 
7
  Subsequent runs: skip install (marker file) and start the UI immediately.
8
 
9
  For most users:
@@ -34,6 +38,17 @@ ROOT = Path(__file__).resolve().parent
34
  VENV_DIR = ROOT / ".venv"
35
  MARKER = VENV_DIR / ".bootstrapped"
36
 
 
 
 
 
 
 
 
 
 
 
 
37
  # Default folder if it exists; otherwise the first auto-discovered folder
38
  # next to this script is used. Override with --model-dir or the UI picker.
39
  DEFAULT_MODEL_DIR = ROOT / "V1.1_onnx"
@@ -114,6 +129,170 @@ def _bootstrap(force_reinstall: bool) -> None:
114
  sys.exit(subprocess.call([str(py), str(Path(__file__).resolve()), *args]))
115
 
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  # ---------------------------------------------------------------------------
118
  # App
119
  # ---------------------------------------------------------------------------
@@ -284,7 +463,13 @@ def _run_app() -> None:
284
  import json
285
 
286
  import numpy as np
287
- import onnxruntime as ort
 
 
 
 
 
 
288
  import gradio as gr
289
  from PIL import Image
290
 
@@ -655,6 +840,14 @@ def main() -> None:
655
  pass
656
 
657
  force = "--reinstall" in sys.argv[1:]
 
 
 
 
 
 
 
 
658
  if not _in_target_venv():
659
  _bootstrap(force_reinstall=force)
660
  return # _bootstrap re-execs and exits
 
4
  First run: creates `.venv/`, installs requirements, re-execs inside the venv,
5
  then starts a local Gradio web UI on http://127.0.0.1:7860 .
6
 
7
+ If the user has no Python at all, `run.bat` instead drops a portable Python
8
+ into `.pyenv/` and uses that. In that case `app.py` skips venv creation and
9
+ installs requirements directly into the portable Python's site-packages.
10
+
11
  Subsequent runs: skip install (marker file) and start the UI immediately.
12
 
13
  For most users:
 
38
  VENV_DIR = ROOT / ".venv"
39
  MARKER = VENV_DIR / ".bootstrapped"
40
 
41
+ # Portable Python lives here when run.bat had to download one because no
42
+ # system Python was available. When app.py is launched by .pyenv/python.exe,
43
+ # we install requirements directly into that interpreter — no venv needed.
44
+ PYENV_DIR = ROOT / ".pyenv"
45
+ PYENV_MARKER = PYENV_DIR / ".bootstrapped"
46
+
47
+ # Microsoft Visual C++ 2015-2022 redistributable (x64). onnxruntime's native
48
+ # DLLs depend on this; if it's missing the import fails with
49
+ # "DLL load failed while importing onnxruntime_pybind11_state".
50
+ VC_REDIST_URL = "https://aka.ms/vs/17/release/vc_redist.x64.exe"
51
+
52
  # Default folder if it exists; otherwise the first auto-discovered folder
53
  # next to this script is used. Override with --model-dir or the UI picker.
54
  DEFAULT_MODEL_DIR = ROOT / "V1.1_onnx"
 
129
  sys.exit(subprocess.call([str(py), str(Path(__file__).resolve()), *args]))
130
 
131
 
132
+ # ---------------------------------------------------------------------------
133
+ # Portable-Python bootstrap (run.bat path when no system Python exists)
134
+ # ---------------------------------------------------------------------------
135
+
136
+ def _running_portable_python() -> bool:
137
+ """True when sys.executable lives inside ROOT/.pyenv/."""
138
+ try:
139
+ return Path(sys.executable).resolve().parent == PYENV_DIR.resolve()
140
+ except OSError:
141
+ return False
142
+
143
+
144
+ def _bootstrap_portable(force_reinstall: bool) -> None:
145
+ """Install pip + requirements directly into the embedded Python.
146
+
147
+ Embeddable Python ships without pip and disables `import site` by default,
148
+ so we fix both before pip-installing the rest. No re-exec is needed —
149
+ we're already running inside the target interpreter.
150
+ """
151
+ py = Path(sys.executable)
152
+
153
+ # 1) Enable site-packages by uncommenting `import site` in pythonXY._pth.
154
+ pth_files = sorted(py.parent.glob("python*._pth"))
155
+ for pth in pth_files:
156
+ try:
157
+ text = pth.read_text(encoding="utf-8")
158
+ except OSError:
159
+ continue
160
+ new_text = text
161
+ for needle in ("#import site", "# import site"):
162
+ if needle in new_text:
163
+ new_text = new_text.replace(needle, "import site")
164
+ if new_text != text:
165
+ try:
166
+ pth.write_text(new_text, encoding="utf-8")
167
+ print(f"[bootstrap] Enabled site-packages in {pth.name}.")
168
+ except OSError as e:
169
+ print(f"[bootstrap] Could not edit {pth}: {e}")
170
+
171
+ # 2) Install pip if missing.
172
+ has_pip = subprocess.call(
173
+ [str(py), "-m", "pip", "--version"],
174
+ stdout=subprocess.DEVNULL,
175
+ stderr=subprocess.DEVNULL,
176
+ ) == 0
177
+ if not has_pip:
178
+ print("[bootstrap] Installing pip into portable Python ...")
179
+ import urllib.request
180
+ getpip = py.parent / "_get-pip.py"
181
+ try:
182
+ urllib.request.urlretrieve("https://bootstrap.pypa.io/get-pip.py", getpip)
183
+ subprocess.check_call([str(py), str(getpip), "--no-warn-script-location"])
184
+ finally:
185
+ try:
186
+ getpip.unlink()
187
+ except OSError:
188
+ pass
189
+
190
+ # 3) Install requirements (skipped if marker says we already did it).
191
+ needs_install = force_reinstall or not PYENV_MARKER.exists()
192
+ if needs_install:
193
+ print("[bootstrap] Upgrading pip ...")
194
+ subprocess.check_call([str(py), "-m", "pip", "install", "--upgrade", "pip"])
195
+ print(f"[bootstrap] Installing: {', '.join(REQUIREMENTS)}")
196
+ subprocess.check_call([str(py), "-m", "pip", "install", *REQUIREMENTS])
197
+ PYENV_MARKER.write_text("ok\n", encoding="utf-8")
198
+ else:
199
+ print("[bootstrap] Requirements already installed (delete .pyenv/.bootstrapped to redo).")
200
+
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # onnxruntime / Visual C++ runtime recovery
204
+ # ---------------------------------------------------------------------------
205
+
206
+ def _looks_like_vcredist_failure(err: BaseException) -> bool:
207
+ """Heuristic: did onnxruntime fail to load because VC++ runtime DLLs
208
+ aren't on the box? The exact wording varies slightly by Windows version
209
+ but always mentions the failed DLL by name."""
210
+ if os.name != "nt":
211
+ return False
212
+ msg = str(err)
213
+ return "DLL load failed" in msg and "onnxruntime_pybind11_state" in msg
214
+
215
+
216
+ def _install_vcredist_with_uac() -> bool:
217
+ """Download vc_redist.x64.exe and run it elevated. Returns True on
218
+ apparent success (exit 0 or 3010 = success-pending-reboot)."""
219
+ if os.name != "nt":
220
+ return False
221
+ import urllib.request
222
+ dest = ROOT / ".vc_redist.x64.exe"
223
+ print(f"[runtime] Downloading {VC_REDIST_URL} ...")
224
+ try:
225
+ urllib.request.urlretrieve(VC_REDIST_URL, dest)
226
+ except Exception as e: # noqa: BLE001
227
+ print(f"[runtime] Download failed: {e}")
228
+ return False
229
+
230
+ # Start-Process -Verb RunAs is what triggers the UAC prompt; -Wait blocks
231
+ # until the installer exits so we can read its exit code. Apostrophes in
232
+ # the path are escaped by doubling for PowerShell's single-quote syntax.
233
+ ps_dest = str(dest).replace("'", "''")
234
+ ps_cmd = (
235
+ "$ErrorActionPreference='Stop';"
236
+ f"$p = Start-Process -FilePath '{ps_dest}' "
237
+ "-ArgumentList '/install','/quiet','/norestart' "
238
+ "-Verb RunAs -Wait -PassThru;"
239
+ "exit $p.ExitCode"
240
+ )
241
+ print("[runtime] Launching installer — accept the UAC prompt to continue.")
242
+ try:
243
+ rc = subprocess.call(
244
+ ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd]
245
+ )
246
+ except OSError as e:
247
+ print(f"[runtime] Could not launch installer: {e}")
248
+ rc = -1
249
+ finally:
250
+ try:
251
+ dest.unlink()
252
+ except OSError:
253
+ pass
254
+
255
+ if rc in (0, 3010):
256
+ if rc == 3010:
257
+ print("[runtime] Install succeeded but Windows requested a reboot.")
258
+ return True
259
+ if rc == 1602 or rc == 1223:
260
+ print("[runtime] Install was cancelled at the UAC prompt.")
261
+ else:
262
+ print(f"[runtime] Installer exited with code {rc}.")
263
+ return False
264
+
265
+
266
+ def _recover_and_relaunch_after_vcredist() -> None:
267
+ """Try to install the VC++ redistributable, then re-exec the launcher so
268
+ the onnxruntime DLL gets a fresh load attempt. Windows caches failed DLL
269
+ loads within a process, so re-importing in-place doesn't work."""
270
+ print()
271
+ print("=" * 60)
272
+ print(" onnxruntime failed to load")
273
+ print("=" * 60)
274
+ print("This almost always means the Microsoft Visual C++ 2015-2022")
275
+ print("Redistributable (x64) is missing on this Windows machine.")
276
+ print("Trying to install it for you now — you'll see a UAC prompt.")
277
+ print()
278
+
279
+ if not _install_vcredist_with_uac():
280
+ print()
281
+ print("Manual fix:")
282
+ print(f" 1. Download {VC_REDIST_URL}")
283
+ print(" 2. Run it (accept the UAC prompt)")
284
+ print(" 3. Re-run this script")
285
+ print()
286
+ print("If install keeps failing, your CPU may lack AVX2 support, which")
287
+ print("modern onnxruntime wheels require. Most CPUs from 2014+ have it.")
288
+ sys.exit(1)
289
+
290
+ args = [a for a in sys.argv[1:] if a != "--reinstall"]
291
+ print("\n[runtime] Visual C++ runtime installed. Re-launching the app ...\n")
292
+ rc = subprocess.call([sys.executable, str(Path(__file__).resolve()), *args])
293
+ sys.exit(rc)
294
+
295
+
296
  # ---------------------------------------------------------------------------
297
  # App
298
  # ---------------------------------------------------------------------------
 
463
  import json
464
 
465
  import numpy as np
466
+ try:
467
+ import onnxruntime as ort
468
+ except ImportError as e:
469
+ if _looks_like_vcredist_failure(e):
470
+ _recover_and_relaunch_after_vcredist()
471
+ sys.exit(1) # only reached if recover failed without exiting
472
+ raise
473
  import gradio as gr
474
  from PIL import Image
475
 
 
840
  pass
841
 
842
  force = "--reinstall" in sys.argv[1:]
843
+
844
+ if _running_portable_python():
845
+ # run.bat downloaded an embeddable Python because the user had none.
846
+ # Install requirements directly into it; no venv re-exec needed.
847
+ _bootstrap_portable(force_reinstall=force)
848
+ _run_app()
849
+ return
850
+
851
  if not _in_target_venv():
852
  _bootstrap(force_reinstall=force)
853
  return # _bootstrap re-execs and exits
web_interface/run.bat CHANGED
@@ -8,11 +8,17 @@ chcp 65001 >nul 2>nul
8
  set "PYTHONUTF8=1"
9
  set "PYTHONIOENCODING=utf-8"
10
 
11
- rem Pick a Python launcher. Try py first (works on most python.org installs),
12
- rem then `python`, then `python3`. enabledelayedexpansion + !ERRORLEVEL! is
13
- rem required so each check reads the value AFTER the preceding `where` runs.
 
14
  set "PYCMD="
15
 
 
 
 
 
 
16
  where py >nul 2>nul
17
  if !ERRORLEVEL! EQU 0 set "PYCMD=py"
18
 
@@ -26,20 +32,48 @@ if not defined PYCMD (
26
  if !ERRORLEVEL! EQU 0 set "PYCMD=python3"
27
  )
28
 
29
- if not defined PYCMD (
30
- echo [run.bat] No Python found on PATH.
31
- echo [run.bat] Install Python 3.10+ from https://www.python.org/downloads/
32
- echo [run.bat] During install, tick "Add Python to PATH".
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  pause
34
  exit /b 1
35
  )
36
 
 
 
 
37
  echo [run.bat] Using !PYCMD! to launch app.py.
38
- echo [run.bat] First run creates .venv and installs ~500 MB of packages.
39
- echo [run.bat] That can take several minutes; it only happens once.
40
  echo.
41
 
42
- !PYCMD! "%~dp0app.py" %*
43
  set "EXITCODE=!ERRORLEVEL!"
44
 
45
  if !EXITCODE! NEQ 0 (
 
8
  set "PYTHONUTF8=1"
9
  set "PYTHONIOENCODING=utf-8"
10
 
11
+ rem Pick a Python launcher. Order:
12
+ rem 1. .pyenv\python.exe portable Python downloaded by a previous run
13
+ rem 2. py / python / python3 user's system Python
14
+ rem 3. Download a portable Python on the fly (no admin, no PATH changes)
15
  set "PYCMD="
16
 
17
+ if exist "%~dp0.pyenv\python.exe" (
18
+ set "PYCMD=%~dp0.pyenv\python.exe"
19
+ goto :launch
20
+ )
21
+
22
  where py >nul 2>nul
23
  if !ERRORLEVEL! EQU 0 set "PYCMD=py"
24
 
 
32
  if !ERRORLEVEL! EQU 0 set "PYCMD=python3"
33
  )
34
 
35
+ if defined PYCMD goto :launch
36
+
37
+ echo [run.bat] No Python detected on this machine.
38
+ echo [run.bat] Downloading a portable Python (~30 MB) into .pyenv\ next to this script.
39
+ echo [run.bat] Nothing is installed system-wide; delete .pyenv\ to remove it.
40
+ echo.
41
+
42
+ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
43
+ "$ErrorActionPreference='Stop';" ^
44
+ "$ver='3.12.7';" ^
45
+ "$dest='%~dp0.pyenv';" ^
46
+ "$zip=Join-Path $env:TEMP 'oppai-py-embed.zip';" ^
47
+ "[void](New-Item -ItemType Directory -Force -Path $dest);" ^
48
+ "Invoke-WebRequest -UseBasicParsing -Uri \"https://www.python.org/ftp/python/$ver/python-$ver-embed-amd64.zip\" -OutFile $zip;" ^
49
+ "Expand-Archive -Path $zip -DestinationPath $dest -Force;" ^
50
+ "Remove-Item $zip -Force"
51
+
52
+ if !ERRORLEVEL! NEQ 0 (
53
+ echo.
54
+ echo [run.bat] Failed to download portable Python.
55
+ echo [run.bat] Check your internet connection, or install Python 3.10-3.12
56
+ echo from https://www.python.org/downloads/ ^(tick "Add Python to PATH"^)
57
+ echo and re-run this script.
58
+ pause
59
+ exit /b 1
60
+ )
61
+
62
+ if not exist "%~dp0.pyenv\python.exe" (
63
+ echo [run.bat] Portable Python download finished but python.exe is missing.
64
+ echo [run.bat] Try deleting .pyenv\ and re-running, or install Python manually.
65
  pause
66
  exit /b 1
67
  )
68
 
69
+ set "PYCMD=%~dp0.pyenv\python.exe"
70
+
71
+ :launch
72
  echo [run.bat] Using !PYCMD! to launch app.py.
73
+ echo [run.bat] First run installs ~500 MB of packages; that only happens once.
 
74
  echo.
75
 
76
+ "!PYCMD!" "%~dp0app.py" %*
77
  set "EXITCODE=!ERRORLEVEL!"
78
 
79
  if !EXITCODE! NEQ 0 (