A small Windows desktop app for generating images, with a unified UX over both cloud APIs and local inference. Cloud side: full control over OpenAI's gpt-image-2 (moderation, batch retries, live cost). Local side: a bundled stable-diffusion.cpp runtime with SDXL / Flux 1 / Flux 2 / SD3 support and a curated catalog of pre-vetted models - no Python dependencies, no GPU drivers to fight, just a single .exe.
Built because ChatGPT's image moderation is inconsistent and ComfyUI is too technical for casual creative work. Espresso aims for the middle ground: simpler than ComfyUI, more capable than the ChatGPT image dialog.
More screenshots - model library, settings
The catalog ships 25 pre-vetted models across SDXL, Flux 1, Flux 2, SD3, Z-Image and Qwen-Image, with free-text search on CivitAI and HuggingFace next to it:
API keys go to the Windows Credential Manager, never to disk in plaintext:
- All
gpt-image-2parameters exposed: model, size (up to 3840×2160), quality, moderation (low/auto), format + compression, background, n - API key in Windows Credential Manager (DPAPI-backed via
keyring) - no plain JSON - Mode INSISTER - retries through moderation rejections until N images collected, with cancel + max-retries safety net
- Mode THINKING - routes through OpenAI's Responses API with a thin-proxy orchestrator that forwards your prompt verbatim to
gpt-image-2(no paraphrase) for ChatGPT-quality results - Live cost estimate per generation (text + image input + output tokens at current OpenAI rates)
- Bundled
stable-diffusion.cppruntime - installer dialog auto-detects NVIDIA, fetches the right CUDA / AVX2 / Vulkan binary archive plus the separate CUDA runtime DLLs, atomic install - Multi-file model support - Flux 1 / Flux 2 (Klein 4B / Klein 9B / dev) / SD3 with the right
--diffusion-model+--t5xxl/--clip_l/--vae/--llmplumbing per family - Per-variant LLM auto-resolution for Flux 2 - Klein 4B → Qwen3 4B, Klein 9B → Qwen3 8B, dev → Mistral 24B, picked from the DiT filename
- VRAM-aware
--offload-to-cpuauto-detect - queriesnvidia-smiat model selection, auto-ticks the offload toggle when the model + auxiliaries don't fit free VRAM (with explicit override) - Sampler / scheduler presets ("Pipeline" combo) - Auto, Portraits / réalisme (DPM++ 2M Karras), Illustration / art (DPM++ 2S Ancestral Karras), Vitesse (Euler Ancestral Karras), Reproductible
- Live step counter - sd.cpp's
step N/Mparsed from stdout and surfaced in the progress bar
- "Sélection Espresso" - curated catalog of 7 pre-vetted models: SDXL Base 1.0, Juggernaut XL v9, Flux 2 Klein 4B GGUF, Flux 1 schnell GGUF, Flux 1 dev GGUF, SD3 Medium, Flux 2 Klein 9B GGUF (⚡ very-big-card)
- Free-text search on CivitAI + HuggingFace with NSFW opt-in toggle
- HuggingFace token in Settings (separate keyring slot) for gated repos like Black Forest Labs FLUX.2-dev - auth header attached only to real
huggingface.cohosts - Auxiliary files dialog - sequential download of T5XXL FP8 / CLIP-L / VAE / Qwen3 / Mistral GGUFs with KiB-scaled progress (no Qt int32 overflow on 5+ GiB downloads)
- Unified ☁/⚙ dropdown - every model from every backend in one combo, params panel adapts (cloud → Quality / N / INSISTER / THINKING / Modération ; local → Pipeline / Steps / CFG / Seed / Décharger sur RAM)
- Auto-save with timestamped filenames (millisecond precision, no collisions) + a
.txtsidecar per image with the prompt and parameters used - Live preview with mouse-wheel zoom, click+drag pan, double-click to reset
- Multi-image thumbnail strip for batches - click a thumb to swap the main view
- Task queue with snapshot of params at enqueue time, drag-reorder, auto-drain when idle
- Visual feedback: progress bar (determinate or indeterminate), elapsed-time counter, pulsing status dot
- Keyboard shortcuts - Ctrl+Enter (generate), Esc (cancel if running), Ctrl+, (settings)
- Accessibility - accessible names + descriptions on every actionable widget for NVDA / JAWS / Narrator
- Dark, flat, arty UI - single accent color (#ff4d80), sharp corners, animated pixel-art logo on the header, editorial typography
- Network defenses: streaming response cap, HTTPS-only image fallback, UNC path rejection, redacted on-disk logs, filename sanitisation on every external trust boundary
- Windows taskbar identity so the app appears under its own icon (not the Python interpreter)
| Phase | Scope | Status |
|---|---|---|
| A | Identity (rename to Espresso, logo, brand font) | ✅ done |
| B0 | PySide6 port + split main.py into espresso/ package, audit B0 |
✅ done |
| B1 | Backend abstraction + sd.cpp scaffold | ✅ done |
| B2 | Model & LoRA registry + Bibliothèque dialog | ✅ done |
| B3 | Unified ☁/⚙ dropdown | ✅ done |
| B3.1 | sd.cpp installer with NVIDIA detection + 2-archive auto-fetch | ✅ done |
| B4 | Multi-file model support (Flux 1, Flux 2 Klein, SD3) - live-validated on RTX 3080 8 GiB + RTX 4090 24 GiB | ✅ done |
| B4.x | VRAM-saver --offload-to-cpu toggle |
✅ done |
| B4.y | Per-variant LLM auto-resolution for Flux 2 | ✅ done |
| B4.z | VRAM-aware auto-tick of --offload-to-cpu |
✅ done |
| B4.aa | Sampler / scheduler presets (Pipeline combo) | ✅ done |
| B4.bb | Featured curated catalog ("Sélection Espresso") | ✅ done |
| B5 | Distribution polish - code signing, GitHub Releases auto-update, signed installer | next |
Requires Python 3.11+ and uv.
uv sync
uv run python -m espressoOn first launch click Réglages and paste:
- your OpenAI
sk-…key (required for cloud generations) - your HuggingFace
hf_…token (optional - required for gated repos like FLUX.2-dev; create one at https://huggingface.co/settings/tokens withReadpermission)
For local generation, click Bibliothèque then TÉLÉCHARGER on a Featured entry. The first launch on a multi-file model (Flux / SD3) opens a second dialog to fetch the auxiliary files (T5XXL / CLIP-L / VAE / LLM encoder), which are reused across runs.
If sd.cpp's binary isn't installed yet, picking a ⚙ local model and clicking GÉNÉRER opens the install dialog - auto-detects NVIDIA, picks the CUDA variant, fetches both archives. AMD / Intel users can pick Vulkan manually.
For verbose logs (recommended when reporting a bug):
set ESPRESSO_DEBUG_VERBOSE=1
uv run python -m espressoLogs land in %APPDATA%\Espresso\app.log (rolling, redacted) - share that file when reporting an issue.
uv sync --group dev
uv run pytest
uv run ruff checkThe repo carries 1200+ tests covering the registry layer, sd.cpp wrapper, format detection, multi-file resolution, VRAM auto-detect, sampler presets, the Featured catalog, the task queue, and the 3D print-prep pipeline. Ruff is clean.
Prerequisites: Windows x86-64 and uv (it fetches the right Python by itself). Then:
build.batThe script syncs the dependencies including the build group (PyInstaller), wipes any previous build/ and dist/ output, runs PyInstaller in one-dir windowed mode with the app resources bundled (Pillow data, the app icon, the animated logo), archives the result, compiles the installer, and writes a SHA-256 for each artifact. The Qt runtime is collected by PyInstaller's own PySide6 hooks, which pull in only what the three imported modules need. The installer step needs Inno Setup (winget install --id JRSoftware.InnoSetup --exact) - the build fails without it rather than shipping an incomplete artifact set.
You get:
| Path | What it is |
|---|---|
dist\Espresso\Espresso.exe |
the app, runnable right there |
dist\Espresso-Setup-<version>-win-x64.exe |
the installer (single file) |
dist\Espresso-<version>-win-x64.zip |
the portable archive |
dist\*.sha256 |
one checksum per artifact |
No Python, no CUDA toolkit and no drivers are needed on the target machine.
- Easiest path: grab
Espresso-Setup-<version>-win-x64.exefrom the Releases page and run it. Per-user install (no admin rights), Start Menu shortcut, optional Desktop shortcut, clean uninstaller. Being a single file, the Setup exe is also fully scannable on VirusTotal - the multi-file zip is not (VirusTotal does not inspect archives over 3 MB). Uninstalling never touches%APPDATA%\Espresso: your downloaded models and settings survive. - Portable path: grab the zip, extract it fully, then run
Espresso\Espresso.exe. Verify the download first withGet-FileHash .\Espresso-<version>-win-x64.zip -Algorithm SHA256against the checksum published with the release. - With the zip, do not launch the exe from inside the zip viewer, and do not copy
Espresso.exeout on its own: Windows then extracts or copies the exe without its_internalfolder, and it dies with "Failed to load Python DLL ... python311.dll". The exe and_internalmust stay side by side - to launch from the Desktop, make a shortcut (right-click → Send to → Desktop), not a copy. (The installer has none of these traps.) - The build is unsigned, so Windows may show a SmartScreen prompt on first run ("More info" → "Run anyway"). Code signing is on the roadmap (B5).
- The sd.cpp runtime and model weights are never bundled - they are downloaded on demand into
%APPDATA%\Espresso\(see the paths table below), which is why the archive stays around 69 MB instead of tens of gigabytes.
| Path | Purpose |
|---|---|
%APPDATA%\Espresso\config.json |
last-used parameters (plain JSON, no API key) |
%APPDATA%\Espresso\models\ |
downloaded model files (.safetensors, .gguf) |
%APPDATA%\Espresso\models\aux\ |
shared auxiliary files (T5XXL, CLIP-L) |
%APPDATA%\Espresso\models\aux\flux1\ |
Flux 1 VAE |
%APPDATA%\Espresso\models\aux\flux2\ |
Flux 2 VAE + per-variant LLM (Qwen3 / Mistral) |
%APPDATA%\Espresso\models\aux\sd3\ |
SD3 VAE bundle |
%APPDATA%\Espresso\sd.exe + DLLs |
bundled sd.cpp runtime |
%APPDATA%\Espresso\registry.json |
cached CivitAI / HF browse results (24h TTL) |
Windows Credential Manager: Espresso / openai_api_key |
OpenAI API key (DPAPI-encrypted, per-user) |
Windows Credential Manager: Espresso / huggingface_token |
HF read token (DPAPI-encrypted, per-user) |
%APPDATA%\Espresso\app.log |
rolling diagnostic log (sk-… redacted) |
%APPDATA%\Espresso\last_error.log |
stack trace of the last API failure (redacted) |
%APPDATA%\Espresso\crash.log |
stack trace of the last app-level crash (redacted) |
~\Pictures\Espresso\ |
default output folder |
The previous location %APPDATA%\OpenAI Image GUI\ is migrated automatically on first launch - your existing config and API key carry over without re-paste.
- The API rejects
response_formatas an unknown parameter forgpt-image-2, despite some docs showing it. Confirmed by live test. The code does not send it.gpt-image-2always returns base64 indata[].b64_json. - The API also rejects
quality_modeandthinkingbody parameters on/v1/images/generations(claimed by third-party blogs but not real). Thinking mode is reachable only via/v1/responseswith theimage_generationtool. gpt-image-2does not supportbackground: "transparent"(onlyauto/opaque); the UI exposes only the supported values.gpt-image-2requires organization verification in the OpenAI console.gpt-image-1works without verification and acceptsmoderation: "low"too - useful as a fallback while waiting for verification.- ChatGPT web GPT-5 silently rephrases prompts before invoking
image_generation- that's why some borderline prompts pass on web but get blocked by Espresso (which forwards verbatim by design, seeTHINKING_PROXY_SYSTEM). When you hit a moderation block here that ChatGPT accepted, try rewording in neutral terms or fall back to the web UI.
- Multi-file architectures (Flux 1 / Flux 2 / SD3) need the
--diffusion-modelflag (NOT-m) for the DiT file, plus separate--t5xxl/--clip_l/--vae/--llmflags for the auxiliaries. The wrapper handles this automatically based on the model family detection. - Each Flux 2 variant uses a different LLM encoder with incompatible tensor structures. Klein 4B → Qwen3 4B (~2.4 GB), Klein 9B → Qwen3 8B (~4.7 GB), dev → Mistral Small 3.2 24B (~14 GB). The wrapper picks the right one from the DiT filename.
- Community FP8 quants of Flux 1 dev that strip the
model.diffusion_model.*tensor name prefix (Forge / A1111 convention) are NOT compatible with sd.cpp. Stick to the leejet GGUF mirrors (leejet/FLUX.1-dev-gguf,leejet/FLUX.1-schnell-gguf) - pre-pinned in the Featured catalog. - The CUDA binary archive does NOT include the CUDA runtime DLLs. The installer fetches them as a second archive (
cudart-sd-...zip) automatically.
| Layer | Choice |
|---|---|
| Language | Python 3.11+ |
| Package manager | uv |
| UI | PySide6 (Qt 6) - LGPL, dynamically linked |
| HTTP | httpx |
| Image processing | Pillow |
| Secret storage | keyring (Windows Credential Manager via DPAPI) |
| Subprocess | builtin (sd.cpp wrapping, nvidia-smi probing) |
| Local inference | stable-diffusion.cpp (leejet) - bundled binary, GGUF + safetensors |
| Model registry | CivitAI public API + HuggingFace Hub API |
| Tests / lint | pytest + ruff |
| Packaging | PyInstaller one-dir, windowed + Inno Setup installer |
Espresso's visual identity lives in assets/: the animated pixel-art logo (header of the app and of this README) and the red coffee cup (window icon) are original creations by the project author, and are covered by the same MIT license as the code. See assets/README.md for details.
MIT. Use it, fork it, ship it commercially - just keep the copyright notice.
Third-party components keep their own terms. Everything the app depends on is permissively licensed (BSD-3, MIT, Apache-2.0) with one exception worth naming: Qt, via PySide6, is LGPL-3.0. It is dynamically linked and its DLLs sit as separate replaceable files in _internal/, which is what the LGPL asks for. If you redistribute a modified build, that arrangement is your obligation to preserve. stable-diffusion.cpp (MIT) is downloaded at runtime rather than bundled, and the models the catalog points to carry their own licenses - several are noncommercial, so check before selling anything you generate.



