This guide explains how ImgTagPlus is structured today and how its major runtime paths fit together. It complements the user-facing overview in README.md and the behavioral contract in SPEC.md.
imgtagplus/cli.py— command-line entry point and server daemon helpers- starts/stops/restarts the local web server daemon
- dispatches headless tagging runs into the shared application pipeline
imgtagplus/tui.py+imgtagplus/tui.tcss— Textual terminal UI launched whenimgtagplusis run with no arguments- keyboard-driven dashboard: arrow keys (↑/↓) navigate action buttons; Tab/Shift-Tab also works
- command palette disabled (
ENABLE_COMMAND_PALETTE = False) — Ctrl+P does nothing - shows a "Server detected" toast at startup when the web UI is already running
- exit confirmation dialog (
ExitConfirmScreen) when quitting with the server running — lets the user stop the server, leave it running, or cancel
imgtagplus/server.py— FastAPI app for the local web UI- serves
/,/static/*, and FastAPI's generated/docs - exposes local API endpoints under
/api/* - runs at most one tagging job at a time
- streams logs and progress to the browser over SSE
- serves
imgtagplus/app.py— orchestration layer for a tagging run- resolves the requested model
- scans the input path
- starts resource monitoring
- loads the tagger backend
- tags each image and writes XMP sidecars
- prints the end-of-run summary and returns an exit code
imgtagplus/scanner.py— image discovery for a single file or directory treeimgtagplus/metadata.py— XMP sidecar read/merge/write logicimgtagplus/logger.py— file + console logging setupimgtagplus/monitor.py— per-process CPU/RAM sampling during a run
imgtagplus/tagger.py— CLIP-based zero-shot tagger using ONNX Runtimeimgtagplus/vlm.py— Florence-2 caption-driven taggerimgtagplus/profiler.py— hardware detection and model recommendation dataimgtagplus/tags.py— curated CLIP vocabulary used for zero-shot tagging
imgtagplus/static/index.html— local single-page UI shellimgtagplus/static/main.js— API calls, SSE connection management, browser-side state
Typical example:
imgtagplus -i ./photos -r --model-id clipExecution path:
imgtagplus/cli.pyparses arguments.- For headless runs, the CLI imports
imgtagplus.app.run()and calls it directly. imgtagplus/app.py:- configures logging
- resolves the requested model key or Hugging Face model ID
- scans for images
- starts the resource monitor
- instantiates either the CLIP tagger or Florence tagger
- loops over images, tags them, writes XMP files, and records summary stats
imgtagplus/metadata.pywrites a sidecar per image, either:- alongside the source image, or
- under
--output-dir
app.run()prints a summary and returns:0when all images succeeded2when one or more images failed during processing1for setup failures such as scan or model-load errors
- Scan failures return early before any work starts.
- Per-image failures are logged and then routed through
_prompt_on_error()inapp.py. --continue-on-errorskips prompts and keeps going.--silentsuppresses the prompt and aborts on the first image failure.--input-timeout(default 30 s) controls how long the prompt waits before auto-skipping.
When the scanner finds no supported images at the input path:
app.run()callsprogress_callback(0, 0, "")to signal an empty result, then returns0.- The web server emits a WARNING log and the frontend shows a yellow "No Images Found" state.
When imgtagplus runs with no arguments, tui.py launches a Textual terminal UI (ImgTagPlusApp) that can:
- start the web server in sandbox mode or with full file system access
- stop or restart the existing server daemon
- open a tagging form (
TaggingScreen) and run the sameimgtagplus.app.run()pipeline headlessly, with a live progress view
Key UX details:
- arrow keys (↑/↓) navigate the action button list; first button is auto-focused on load
qand Ctrl+C both route throughImgTagPlusApp.action_quit()— if the server is running, anExitConfirmScreenmodal asks whether to stop it, leave it running, or cancel- a "Server detected" notification is shown at startup when the web UI is already active
- the command palette is disabled (
ENABLE_COMMAND_PALETTE = False)
The TUI is intentionally thin. It does not implement its own tagging logic.
Typical example:
imgtagplus --start-serverRuntime path:
imgtagplus/cli.pylaunchesimgtagplus/server.pyas a detached subprocess and waits for/health.imgtagplus/server.pystarts FastAPI and serves the local UI.imgtagplus/static/main.jsloads:/api/systemfor hardware and model metadata/api/statusto restore run state after refresh
- When the user starts a job, the browser
POSTs/api/tag. - The server validates inputs, acquires the single worker lock, drains old SSE queues, and starts one background thread.
- That thread builds an
argparse.Namespaceand calls the sameimgtagplus.app.run()used by the CLI. - Progress updates are pushed into
progress_queue; log records are mirrored intolog_queue. - The browser stays connected to
/api/streamand updates the progress bar and log view from those events.
input path
-> scanner.scan()
-> list[Path]
-> selected tagger backend
-> list[(tag, score)]
-> metadata.write_xmp()
-> .xmp sidecar file
scanner.scan()resolves files with supported image extensions.Tagger.precompute_tag_embeddings()builds or loads cached text embeddings for the curatedTAGSlist.Tagger.tag_image():- preprocesses the image
- runs the ONNX visual encoder
- compares the image embedding against cached tag embeddings
- ranks tags and applies threshold / max tag limits
app.pystrips scores down to tag names for XMP writing.
scanner.scan()resolves the input images.FlorenceTagger.tag_image():- generates a
<DETAILED_CAPTION> - post-processes the generated text
- extracts keywords from the caption
- preserves adjacent word pairs as compound tags (e.g. "blue sky")
- returns
(keyword, 1.0)tuples because this path does not expose per-tag confidence scores
- generates a
app.pywrites the extracted keyword list to XMP.
The web server is intentionally single-tenant:
_job_lockinimgtagplus/server.pyallows only one active job at a time- a second
POST /api/tagwhile busy returns an error payload instead of queueing work
This keeps the local UI simple and avoids overlapping model loads and filesystem writes.
imgtagplus/server.py uses two in-memory queues:
log_queuefor formatted log recordsprogress_queuefor progress and completion events
Important details:
- queues are bounded
- when full, the oldest event is dropped so the UI sees current state
- old queue contents are cleared before a new run begins
- SSE emits log events first, then progress events, then an idle heartbeat when nothing is running
imgtagplus/monitor.py samples:
- process CPU percentage
- process RSS memory
The resulting summary is appended to the end-of-run output shown in the CLI and mirrored to the UI logs.
imgtagplus/logger.py configures:
- a DEBUG file log in the current working directory by default
- a console handler at INFO, or WARNING in
--silentmode
The web server also adds an extra handler so application logs appear in the browser's live stream.
The browser file picker is server-mediated; it does not read the filesystem directly.
- default mode: sandboxed
- sandbox root:
IMGTAGPLUS_SANDBOX_DIRor./sandbox - unrestricted mode: set
IMGTAGPLUS_FFSA=1
Both input and output_dir are checked against the sandbox boundary before a web job starts.
- CLIP model assets and embedding caches live under the configured model directory
- Florence downloads also use that model directory and set
HF_HOMEto the same location - XMP sidecars persist on disk next to the source images or under the chosen output directory
- log files are written into the current working directory unless
--log-fileoverrides the path
The server applies several layers of protection appropriate for a local tool:
- Request validation: CSRF origin checking on mutating requests (POST/PUT/DELETE) — only
localhost/127.0.0.1origins are accepted. - Rate limiting: Per-client-IP sliding window limits on browse (100/10 s), tag (10/10 s), and a semaphore cap on SSE connections (5 concurrent).
- Response hardening:
Content-Security-Policy,X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: no-referreron every response. - Frontend escaping:
escapeHtml()sanitises all server-provided strings before innerHTML interpolation. - Model trust: Florence-2
trust_remote_codeis only enabled for model IDs in the pinned revision allowlist; community ONNX processor revisions are pinned.
Some current design choices are deliberate:
- one shared pipeline for CLI and web requests keeps behavior aligned
- local-only web serving avoids a separate API/auth layer
- sandbox enforcement lives in the server, not in the frontend
- model recommendations are advisory; unsupported models may still appear, but the UI flags them
For exact endpoint contracts, see docs/API.md. For model-specific runtime behavior, see docs/MODELS.md.