A deeper walk-through of how the machine fits together. For the quick orientation see
../context/architecture.md; for module-wiring rules see
../reference/esm-patterns.md.
This is the macro picture. Per-layer deep-dives live alongside it (see README.md): core-engine.md and gui.md describe the current app; cli.md and server.md document the removed pre-revival CLI + classic server for historical reference.
The pre-revival 2022–2023 system (a yargs CLI + an Express/Pug server) was removed from the tree. A sibling CLI is planned but not built yet; today the only front end is the
targets/web/SPA.
The surfaces
There is one isomorphic engine (engine/core/) driven by one front end — the React/Vite SPA
(targets/web/), which builds into two editions:
- The SPA (browser) — composes a DPL prompt, runs the engine in the browser (via the Vite loader), and calls provider adapters directly with the user's BYOK key.
- The local
/apibackend (targets/web/backend/) — present only in the local/desktop edition. Serves the built SPA plus an/api/*surface (image save + feed, the Manager's on-disk file ops, ImageMagick convert). The online edition ships no server. - The engine under Node — the same
engine/core/engine runs headless under Node for the test suite and the/apiruntime, which also proves the browser and Node produce the same output.
One engine, two loaders
engine/core/ has no fs / framework dependency; content is supplied by an injected loader:
- Node —
nodeLoader.js:fs+createRequire(import.meta.url)for synchronous.jsgenerator loads (Node 24 canrequire()an ES module), resolving the content root module-relative (new URL("../../", import.meta.url)= the repo root, cwd-independent). - Browser —
browserLoader.js: a Viteimport.meta.glob("../../engine/data/blocks/**/*.js")build-time macro; the lists ship code-split (browserCatalogData.js).
Both call each generator's default(...) and read its full / suggestion_exclude flags. See
../reference/esm-patterns.md.
The prompt pipeline
The engine runs settings.promptModules in order over each prompt string — the stages are in
engine/core/stages/:
prompt → block → prompt-salt → list → emphasis → cleanup
- block (
{#name}) — call the generator fromengine/data/blocks/<category>/; nesting re-expands up to ~10 passes; honors the per-token intensity / focus dials; NSFW-gated by name token. (The one flat catalog replaced the oldv1/v2generations, and the legacy<expansion>stage was removed.) - prompt-salt (
{salt}) — inject a random / incrementing number (a subseed alternative). - list (
{name}) — pull a random line fromengine/data/lists/<name>, with emphasis / editing / alternating randomization (engine/helpers/randomEmphasis|Editing|Alternating.js) and once-only depletion. - emphasis — render typed
()/[]emphasis into the active provider dialect (SD/MJ weights, NAI braces, or plain words). - cleanup — collapse stray spaces / commas.
The DPL parser + renderer (engine/core/dpl/) compile and roll each template; a seeded Rng
(engine/core/rng.js) makes runs reproducible.
Dynamic-prompt classification
engine/promptFilesAndSuggestions.js scans engine/data/blocks/ and splits generators into full vs
partial by reading each module's full export, excluding suggestion_exclude ones. These drive the
random promptSuggestion()s and the SPA's block pickers. Tag metadata comes from
engine/blockManifest.js; NSFW gating from engine/gatedLists.js (isGatedBlock).
Image generation
Images are generated by the provider adapters in targets/shared/<id>/ — roughly 40 of them behind one
interface, over a shared transport in providers/_shared/. Calls go straight from the browser to the
chosen provider with the user's key (no server relay), so a provider that can't be called from a browser is
disabled in the online build. Stable Diffusion is still supported — it's where the project started — but it
is now one option among many.
In the local edition, POST /api/image (targets/web/backend/apiHandler.js) writes each PNG plus a .json
metadata sidecar (the prompt layers, the deterministic engine roll, the AI rewrite, the provider, and a
key-stripped settings snapshot) into output/; GET /api/feed reads them back for the Gallery. The in-app
Manager edits the real data/lists + data/blocks files through /api/manage/*
(targets/web/backend/manageFs.js) and hot-applies them via the runtime loader (targets/web/frontend/lib/runtimeLoader.js).
Settings as the spine
Everything reads from the merged settings — engine/settings.js defaults ⊕ the user's overrides
(user-settings.json on disk / localStorage in the browser). It carries the pipeline order, the content
paths, the active provider + dialect, gating (includeAdult), the intensity default, and the per-provider
BYOK keys (kept only on the device and stripped from any saved image sidecar). The online build boots the
default-settings shell and settles stored settings in via a guarded two-pass store, so the SSR prerender
and the client-first render match.