Garionhk/Citrine-AI-Photo-Enlarger

🍊 AI photo upscaler & deblur for Windows. Two engines — Real‑ESRGAN (any GPU, always works) and SeedVR2 7B/3B (NVIDIA, pro quality). Batch queue, before/after wipe preview, portable build. 繁體中文 / English.

Python

4

1 commits

updated Jul 22, 2026

See the code

See what people are saying

SourceMessageScoreDate

I made a standalone Windows app for SeedVR2 photo upscaling, no ComfyUI needed (free, open source) (r/StableDiffusion)

I'm a photographer, and I got tired of opening ComfyUI every time I just wanted to enlarge or sharpen a photo. So I made a small desktop app for it, called Citrine Photo. You drag in a photo (or a whole folder), pick how big you want it, and hit go. Everything runs on your own PC, no cloud. It has…

6

Oct 4, 2026

README

🍊 Citrine Photo — 照片放大修復

Standalone Windows desktop app that enlarges and deblurs/sharpens photos with AI upscaling. No ComfyUI, no Python setup, no cloud. Black + orange (citrine) themed PySide6 UI, in Traditional Chinese (default) and English.

EngineTechHardwareStatus
快速 (Fast)Real-ESRGAN ncnn-vulkan (+ optional GFPGAN face restore)Any Vulkan GPU / CPU✅ Working
高品質 (Pro)SeedVR2 7B / 3B (GGUF) via PyTorch cu128NVIDIA 20xx–50xx✅ Working (real inference)

Features

  • 🖼️ Drag & drop one image, many files, or a whole folder (JPG, PNG, WebP, TIFF, BMP, HEIC)
  • 🎯 Target-size presets — 2 / 8 / 16 / 24 MP or custom, with a live output-dimension preview
  • 🪄 Two engines, auto-selected by hardware — Fast works everywhere; Pro auto-picks the SeedVR2 7B (≥12 GB VRAM) or 3B (6–10 GB) model to fit your GPU
  • 🔍 Before/after split-wipe preview with a draggable divider
  • 📋 Batch queue with per-item progress, cancel mid-batch, and one-click Regenerate (re-run the whole list with new settings — no re-dragging)
  • 🏷️ Tagged, never-overwritten outputs — photo_realesrgan.png, photo_realesrgan_face.png, photo_seedvr2.png; a collision appends _1, _2, …
  • 🌏 Full Traditional Chinese / English UI, every string localized
  • 🎨 Black & orange (citrine) theme with a custom app/taskbar icon
  • 📦 Portable build — unzip and run on any NVIDIA PC (bundles Python, the CUDA runtime, and both models; no install)

Run (dev)

python -m pip install -r requirements.txt
python run.py          # or double-click run.bat

The app runs immediately. Without the realesrgan-ncnn-vulkan binary present, the Fast engine falls back to a high-quality Lanczos + unsharp pass so you can exercise the full flow; drop in the binary (see below) for true Real-ESRGAN output. The Pro engine is installed on demand from Settings (see below).

Output files

Results go to data/output/ (changeable in Settings). Each file is named after the source with an engine tag, and existing files are never overwritten:

RunOutput name
Fastphoto_realesrgan.png
Fast + Face Restorephoto_realesrgan_face.png
Prophoto_seedvr2.png
Re-run / collisionphoto_realesrgan_1.png, _2, …

Format (PNG / JPEG q80–100) and EXIF preservation are toggles in the Options panel. EXIF (orientation, capture date, …) is re-attached to the output and stamped Software = Citrine Photo.

Project layout

run.py / run.bat              # dev launcher
build_portable.py / .bat      # build the portable dist + zip
make_icons.py                 # generate icon.ico + PNG pack from icon.png
citrine.spec                  # PyInstaller one-file (small exe, torch excluded)
citrine_portable.spec         # PyInstaller one-dir (portable package)
icon.png                      # master app icon (rounded corners, transparent)
icons/                        # generated PNG icon pack (16 … 1024)

citrine/
├── app.py                  # entry: single-instance, theme, icon, GPU detect, VRAM model auto-select
├── config.py               # data/config.json load/save
├── i18n.py                 # zh-Hant (default) + en locale dicts
├── theme.py                # black/orange palette + QSS
├── gpu.py                  # nvidia-smi compatibility gate
├── download.py             # resumable HTTP + SHA256 (installer / first-launch)
├── installer.py            # SeedVR2 install/uninstall flow (QThread, 6 steps)
├── paths.py                # runtime path + bundled-resource resolution
├── engines/
│   ├── protocol.py         # JSON-lines stdin/stdout protocol
│   ├── pipeline.py         # MP-scale, 3×3/15% tiling, feathered reassembly, EXIF
│   ├── ncnn_engine.py      # Real-ESRGAN subprocess wrapper (+ Lanczos fallback)
│   ├── seedvr2_engine.py   # GUI-side launcher for the embedded-Python engine
│   └── worker.py           # QThread bridge → progress/done/error signals
├── gui/
│   ├── main_window.py      # main window, run orchestration, batch driver, regenerate
│   ├── drop_zone.py        # drag-drop files/folders
│   ├── preview.py          # before/after split-wipe slider
│   ├── batch_queue.py      # per-item status list
│   ├── settings_dialog.py  # language, folders, engine mgmt, log viewer
│   ├── install_wizard.py   # SeedVR2 install wizard (progress, disk check, log)
│   └── widgets.py          # Card, SegmentedControl, PrimaryButton, …
└── resources/
    ├── manifest.json       # pinned download URLs / SHA256 / vendor pin
    ├── icon.png / icon.ico # runtime + exe icon
    └── seedvr2/            # Pro-engine code, extracted into the embedded env
        ├── engine.py       # JSON-protocol entry + tiling pipeline
        ├── seedvr2_infer.py# Runner + vendor-binding resolver
        ├── gguf_loader.py  # GGUF → torch state_dict
        ├── color_fix.py    # wavelet / AdaIN colour correction
        ├── fetch_vendor.py # pull the pinned numz inference code
        └── vendor/         # vendored SeedVR2 CLI (fetched, gitignored)

Adding the Real-ESRGAN binary

Download realesrgan-ncnn-vulkan (URL in citrine/resources/manifest.json) and place it at data/engines/realesrgan/realesrgan-ncnn-vulkan.exe with its models/ folder alongside. The Fast engine picks it up automatically. (The portable build bundles this for you.)

SeedVR2 (Pro) install

Settings → 引擎管理 → 安裝 SeedVR2 launches the install wizard (install_wizard.py driven by installer.py). It shows a model picker (7B/3B, defaulting to the GPU-recommended size), a required-vs-available disk-space check, live speed/ETA, and a streaming log, then runs 6 steps:

  1. download embeddable CPython 3.11 → extract → get-pip.py → enable Lib\site-packages in python3xx._pth
  2. pip install torch --index-url .../cu128 + safetensors/einops/gguf/…
  3. download GGUF (7B or 3B) + VAE — resumable, SHA256-verified
  4. extract the engine package (engine.py + adapters) from bundled resources
  5. fetch the vendored SeedVR2 inference code — pinned numz commit as a zip (no git), plus its extra deps (torchvision cu128, diffusers, peft, rotary_embedding_torch, opencv, …)
  6. smoke test on a 64×64 image; seedvr2.installed is set only after it passes

Everything supports cancel; interrupted downloads resume. Uninstall (same button once installed) deletes engines/seedvr2/ and reports freed space.

Total download is ~8–9 GB; the app warns and shows required vs available space before starting. Model weights come from Hugging Face (AInVFX/SeedVR2_comfyUI for the GGUFs, numz/SeedVR2_comfyUI for the VAE).

How the Pro engine works (Phase 0 — verified end-to-end)

The Pro engine's model code lives under citrine/resources/seedvr2/ and runs in the separate embedded Python (never in the GUI exe). Rather than importing numz's ComfyUI-coupled internals, the binding drives its standalone inference_cli.py as a subprocess over a directory of tiles (one model load per image). The image pipeline is a native port of the ComfyUI workflow Upscale_by_SeedVR2_v4:

target-MP scale → Lanczos pre-scale → 3×3 grid with 15% overlap → per-tile 0.25× downscale → SeedVR2 4× restore → wavelet colour-match → feathered 64px reassembly → EXIF re-attach.

FileRole
engine.pyJSON-protocol entry, tiling, SeedVR2Model → Runner
seedvr2_infer.pybatched Runner + VendorBinding resolver (numz-CLI → bytedance → fallback)
gguf_loader.pyDiT weights: GGUF → dequantized torch state_dict
color_fix.pywavelet / AdaIN colour correction (spec default: wavelet)
fetch_vendor.pypull the pinned upstream inference code into vendor/

Status: verified working — real 7B and 3B Q4_K_M inference runs the full VAE→DiT→VAE→post pipeline on an RTX A4000. Until the vendored code is present the Runner resolves to an honest on-GPU FallbackBinding (bicubic 4× + unsharp, logs backend=fallback) so the app degrades gracefully instead of failing.

Low-VRAM mode passes --blocks_to_swap/--dit_offload_device for block-swap offload. The optional last step is the spec §6.3 parity spike (compare a 100% crop against the original ComfyUI workflow).

App icon

icon.png (repo root) is the master. Regenerate the pack any time:

python make_icons.py

This writes citrine/resources/icon.ico (multi-size, 16→256), the 256px runtime icon.png, and icons/icon_<N>.png (16…1024). The exe embeds icon.ico; the running app sets it as the window/taskbar icon (with a Windows AppUserModelID so dev runs show it too). build_portable.py regenerates it automatically before building.

Build the exe

python -m pip install pyinstaller
pyinstaller citrine.spec

torch is explicitly excluded — the exe stays small (target ≤ 70 MB); the Pro engine lives in its own on-demand embedded Python.

Build a portable package (runs on any NVIDIA PC)

python build_portable.py          # -> dist/CitrinePhoto/  (~11 GB)
python build_portable.py --zip    # also -> dist/CitrinePhoto-portable.zip
# or: build_portable.bat [--no-zip | --only-zip | --skip-engines | --skip-exe]

Produces a self-contained folder: CitrinePhoto.exe + _internal/ + data/engines/{realesrgan,seedvr2} with the embedded Python, torch cu128, both GGUF models (7B + 3B), the VAE, and the vendored inference code. Unzip anywhere and run — no Python, no CUDA toolkit, no install. The cu128 wheels carry the CUDA runtime; the target machine needs only an NVIDIA driver (≥ ~570).

Notes:

  • Extract somewhere writable (Desktop, Documents, a data drive) — not C:\Program Files. The app uses data/ beside the exe only when that folder is writable; otherwise it falls back to %LOCALAPPDATA%\CitrinePhoto, where the bundled engines wouldn't be found and the Pro engine would show as not installed.
  • One-dir, never one-file. A one-file build would re-extract ~11 GB to %TEMP% on every launch (and torch is deliberately never bundled in the exe).
  • --skip-engines reuses the already-copied 11 GB engines for a fast rebuild; --only-zip re-zips without re-copying.
  • Both models ship; app._auto_select_model() picks 7B on ≥12 GB VRAM and 3B on 6–10 GB at startup, until the user pins one in the Options panel.
  • The build writes a clean config.json with no absolute paths and seedvr2.installed = true, and prunes torch dev files (test/, include/, *.lib).
  • Redistribution: the portable package bundles the SeedVR2 weights (ByteDance) and the vendored numz code — check the upstream licenses before sharing the zip publicly. (Not an issue for your own machines.)

Robustness notes

  • All embedded-Python subprocesses are forced to UTF-8 (PYTHONUTF8/ PYTHONIOENCODING + UTF-8 decode) so emoji in vendored logs and non-ASCII (e.g. Chinese) filenames never trigger a cp1252 UnicodeEncodeError.
  • Batch workers are Qt-parented QThreads freed via deleteLater, so advancing between images can't garbage-collect a still-running thread.

Phase status

  • Phase 1 — Pipeline parity: ✅ MP-scale, 3×3/15% tiling, feathered reassembly
  • Phase 2 — ncnn engine: ✅ wrapper + protocol + Lanczos fallback (GFPGAN hook)
  • Phase 3 — GUI: ✅ main window, split-wipe preview, batch queue, regenerate, settings, i18n
  • Phase 4 — Installer flow: ✅ wizard (embedded-Python bootstrap, pip cu128, resumable+SHA256 downloads, vendor fetch, smoke test, uninstall)
  • Phase 0 — SeedVR2 seam: ✅ real 7B/3B inference verified end-to-end on an RTX A4000
  • Phase 5 — Packaging: ✅ portable one-dir build + zip; multi-GPU QA / parity spike remain

Garionhk/Citrine-AI-Photo-Enlarger

🍊 AI photo upscaler & deblur for Windows. Two engines — Real‑ESRGAN (any GPU, always works) and SeedVR2 7B/3B (NVIDIA, pro quality). Batch queue, before/after wipe preview, portable build. 繁體中文 / English.

Python

4

1 commits

updated Jul 22, 2026

See the code

See what people are saying

SourceMessageScoreDate

I made a standalone Windows app for SeedVR2 photo upscaling, no ComfyUI needed (free, open source) (r/StableDiffusion)

I'm a photographer, and I got tired of opening ComfyUI every time I just wanted to enlarge or sharpen a photo. So I made a small desktop app for it, called Citrine Photo. You drag in a photo (or a whole folder), pick how big you want it, and hit go. Everything runs on your own PC, no cloud. It has…

6

Oct 4, 2026

README

🍊 Citrine Photo — 照片放大修復

Standalone Windows desktop app that enlarges and deblurs/sharpens photos with AI upscaling. No ComfyUI, no Python setup, no cloud. Black + orange (citrine) themed PySide6 UI, in Traditional Chinese (default) and English.

EngineTechHardwareStatus
快速 (Fast)Real-ESRGAN ncnn-vulkan (+ optional GFPGAN face restore)Any Vulkan GPU / CPU✅ Working
高品質 (Pro)SeedVR2 7B / 3B (GGUF) via PyTorch cu128NVIDIA 20xx–50xx✅ Working (real inference)

Features

  • 🖼️ Drag & drop one image, many files, or a whole folder (JPG, PNG, WebP, TIFF, BMP, HEIC)
  • 🎯 Target-size presets — 2 / 8 / 16 / 24 MP or custom, with a live output-dimension preview
  • 🪄 Two engines, auto-selected by hardware — Fast works everywhere; Pro auto-picks the SeedVR2 7B (≥12 GB VRAM) or 3B (6–10 GB) model to fit your GPU
  • 🔍 Before/after split-wipe preview with a draggable divider
  • 📋 Batch queue with per-item progress, cancel mid-batch, and one-click Regenerate (re-run the whole list with new settings — no re-dragging)
  • 🏷️ Tagged, never-overwritten outputs — photo_realesrgan.png, photo_realesrgan_face.png, photo_seedvr2.png; a collision appends _1, _2, …
  • 🌏 Full Traditional Chinese / English UI, every string localized
  • 🎨 Black & orange (citrine) theme with a custom app/taskbar icon
  • 📦 Portable build — unzip and run on any NVIDIA PC (bundles Python, the CUDA runtime, and both models; no install)

Run (dev)

python -m pip install -r requirements.txt
python run.py          # or double-click run.bat

The app runs immediately. Without the realesrgan-ncnn-vulkan binary present, the Fast engine falls back to a high-quality Lanczos + unsharp pass so you can exercise the full flow; drop in the binary (see below) for true Real-ESRGAN output. The Pro engine is installed on demand from Settings (see below).

Output files

Results go to data/output/ (changeable in Settings). Each file is named after the source with an engine tag, and existing files are never overwritten:

RunOutput name
Fastphoto_realesrgan.png
Fast + Face Restorephoto_realesrgan_face.png
Prophoto_seedvr2.png
Re-run / collisionphoto_realesrgan_1.png, _2, …

Format (PNG / JPEG q80–100) and EXIF preservation are toggles in the Options panel. EXIF (orientation, capture date, …) is re-attached to the output and stamped Software = Citrine Photo.

Project layout

run.py / run.bat              # dev launcher
build_portable.py / .bat      # build the portable dist + zip
make_icons.py                 # generate icon.ico + PNG pack from icon.png
citrine.spec                  # PyInstaller one-file (small exe, torch excluded)
citrine_portable.spec         # PyInstaller one-dir (portable package)
icon.png                      # master app icon (rounded corners, transparent)
icons/                        # generated PNG icon pack (16 … 1024)

citrine/
├── app.py                  # entry: single-instance, theme, icon, GPU detect, VRAM model auto-select
├── config.py               # data/config.json load/save
├── i18n.py                 # zh-Hant (default) + en locale dicts
├── theme.py                # black/orange palette + QSS
├── gpu.py                  # nvidia-smi compatibility gate
├── download.py             # resumable HTTP + SHA256 (installer / first-launch)
├── installer.py            # SeedVR2 install/uninstall flow (QThread, 6 steps)
├── paths.py                # runtime path + bundled-resource resolution
├── engines/
│   ├── protocol.py         # JSON-lines stdin/stdout protocol
│   ├── pipeline.py         # MP-scale, 3×3/15% tiling, feathered reassembly, EXIF
│   ├── ncnn_engine.py      # Real-ESRGAN subprocess wrapper (+ Lanczos fallback)
│   ├── seedvr2_engine.py   # GUI-side launcher for the embedded-Python engine
│   └── worker.py           # QThread bridge → progress/done/error signals
├── gui/
│   ├── main_window.py      # main window, run orchestration, batch driver, regenerate
│   ├── drop_zone.py        # drag-drop files/folders
│   ├── preview.py          # before/after split-wipe slider
│   ├── batch_queue.py      # per-item status list
│   ├── settings_dialog.py  # language, folders, engine mgmt, log viewer
│   ├── install_wizard.py   # SeedVR2 install wizard (progress, disk check, log)
│   └── widgets.py          # Card, SegmentedControl, PrimaryButton, …
└── resources/
    ├── manifest.json       # pinned download URLs / SHA256 / vendor pin
    ├── icon.png / icon.ico # runtime + exe icon
    └── seedvr2/            # Pro-engine code, extracted into the embedded env
        ├── engine.py       # JSON-protocol entry + tiling pipeline
        ├── seedvr2_infer.py# Runner + vendor-binding resolver
        ├── gguf_loader.py  # GGUF → torch state_dict
        ├── color_fix.py    # wavelet / AdaIN colour correction
        ├── fetch_vendor.py # pull the pinned numz inference code
        └── vendor/         # vendored SeedVR2 CLI (fetched, gitignored)

Adding the Real-ESRGAN binary

Download realesrgan-ncnn-vulkan (URL in citrine/resources/manifest.json) and place it at data/engines/realesrgan/realesrgan-ncnn-vulkan.exe with its models/ folder alongside. The Fast engine picks it up automatically. (The portable build bundles this for you.)

SeedVR2 (Pro) install

Settings → 引擎管理 → 安裝 SeedVR2 launches the install wizard (install_wizard.py driven by installer.py). It shows a model picker (7B/3B, defaulting to the GPU-recommended size), a required-vs-available disk-space check, live speed/ETA, and a streaming log, then runs 6 steps:

  1. download embeddable CPython 3.11 → extract → get-pip.py → enable Lib\site-packages in python3xx._pth
  2. pip install torch --index-url .../cu128 + safetensors/einops/gguf/…
  3. download GGUF (7B or 3B) + VAE — resumable, SHA256-verified
  4. extract the engine package (engine.py + adapters) from bundled resources
  5. fetch the vendored SeedVR2 inference code — pinned numz commit as a zip (no git), plus its extra deps (torchvision cu128, diffusers, peft, rotary_embedding_torch, opencv, …)
  6. smoke test on a 64×64 image; seedvr2.installed is set only after it passes

Everything supports cancel; interrupted downloads resume. Uninstall (same button once installed) deletes engines/seedvr2/ and reports freed space.

Total download is ~8–9 GB; the app warns and shows required vs available space before starting. Model weights come from Hugging Face (AInVFX/SeedVR2_comfyUI for the GGUFs, numz/SeedVR2_comfyUI for the VAE).

How the Pro engine works (Phase 0 — verified end-to-end)

The Pro engine's model code lives under citrine/resources/seedvr2/ and runs in the separate embedded Python (never in the GUI exe). Rather than importing numz's ComfyUI-coupled internals, the binding drives its standalone inference_cli.py as a subprocess over a directory of tiles (one model load per image). The image pipeline is a native port of the ComfyUI workflow Upscale_by_SeedVR2_v4:

target-MP scale → Lanczos pre-scale → 3×3 grid with 15% overlap → per-tile 0.25× downscale → SeedVR2 4× restore → wavelet colour-match → feathered 64px reassembly → EXIF re-attach.

FileRole
engine.pyJSON-protocol entry, tiling, SeedVR2Model → Runner
seedvr2_infer.pybatched Runner + VendorBinding resolver (numz-CLI → bytedance → fallback)
gguf_loader.pyDiT weights: GGUF → dequantized torch state_dict
color_fix.pywavelet / AdaIN colour correction (spec default: wavelet)
fetch_vendor.pypull the pinned upstream inference code into vendor/

Status: verified working — real 7B and 3B Q4_K_M inference runs the full VAE→DiT→VAE→post pipeline on an RTX A4000. Until the vendored code is present the Runner resolves to an honest on-GPU FallbackBinding (bicubic 4× + unsharp, logs backend=fallback) so the app degrades gracefully instead of failing.

Low-VRAM mode passes --blocks_to_swap/--dit_offload_device for block-swap offload. The optional last step is the spec §6.3 parity spike (compare a 100% crop against the original ComfyUI workflow).

App icon

icon.png (repo root) is the master. Regenerate the pack any time:

python make_icons.py

This writes citrine/resources/icon.ico (multi-size, 16→256), the 256px runtime icon.png, and icons/icon_<N>.png (16…1024). The exe embeds icon.ico; the running app sets it as the window/taskbar icon (with a Windows AppUserModelID so dev runs show it too). build_portable.py regenerates it automatically before building.

Build the exe

python -m pip install pyinstaller
pyinstaller citrine.spec

torch is explicitly excluded — the exe stays small (target ≤ 70 MB); the Pro engine lives in its own on-demand embedded Python.

Build a portable package (runs on any NVIDIA PC)

python build_portable.py          # -> dist/CitrinePhoto/  (~11 GB)
python build_portable.py --zip    # also -> dist/CitrinePhoto-portable.zip
# or: build_portable.bat [--no-zip | --only-zip | --skip-engines | --skip-exe]

Produces a self-contained folder: CitrinePhoto.exe + _internal/ + data/engines/{realesrgan,seedvr2} with the embedded Python, torch cu128, both GGUF models (7B + 3B), the VAE, and the vendored inference code. Unzip anywhere and run — no Python, no CUDA toolkit, no install. The cu128 wheels carry the CUDA runtime; the target machine needs only an NVIDIA driver (≥ ~570).

Notes:

  • Extract somewhere writable (Desktop, Documents, a data drive) — not C:\Program Files. The app uses data/ beside the exe only when that folder is writable; otherwise it falls back to %LOCALAPPDATA%\CitrinePhoto, where the bundled engines wouldn't be found and the Pro engine would show as not installed.
  • One-dir, never one-file. A one-file build would re-extract ~11 GB to %TEMP% on every launch (and torch is deliberately never bundled in the exe).
  • --skip-engines reuses the already-copied 11 GB engines for a fast rebuild; --only-zip re-zips without re-copying.
  • Both models ship; app._auto_select_model() picks 7B on ≥12 GB VRAM and 3B on 6–10 GB at startup, until the user pins one in the Options panel.
  • The build writes a clean config.json with no absolute paths and seedvr2.installed = true, and prunes torch dev files (test/, include/, *.lib).
  • Redistribution: the portable package bundles the SeedVR2 weights (ByteDance) and the vendored numz code — check the upstream licenses before sharing the zip publicly. (Not an issue for your own machines.)

Robustness notes

  • All embedded-Python subprocesses are forced to UTF-8 (PYTHONUTF8/ PYTHONIOENCODING + UTF-8 decode) so emoji in vendored logs and non-ASCII (e.g. Chinese) filenames never trigger a cp1252 UnicodeEncodeError.
  • Batch workers are Qt-parented QThreads freed via deleteLater, so advancing between images can't garbage-collect a still-running thread.

Phase status

  • Phase 1 — Pipeline parity: ✅ MP-scale, 3×3/15% tiling, feathered reassembly
  • Phase 2 — ncnn engine: ✅ wrapper + protocol + Lanczos fallback (GFPGAN hook)
  • Phase 3 — GUI: ✅ main window, split-wipe preview, batch queue, regenerate, settings, i18n
  • Phase 4 — Installer flow: ✅ wizard (embedded-Python bootstrap, pip cu128, resumable+SHA256 downloads, vendor fetch, smoke test, uninstall)
  • Phase 0 — SeedVR2 seam: ✅ real 7B/3B inference verified end-to-end on an RTX A4000
  • Phase 5 — Packaging: ✅ portable one-dir build + zip; multi-GPU QA / parity spike remain

Languages

Python

98.6%

Batchfile

1.4%