41 KiB
Gallery architecture and contracts
The current runtime preserves the contracts below. Installation, task API examples and recovery procedures are in the 1.6.0 foundation guide. Historical RC notes describe their own versions.
py/gallery/execution.py/runtime.py: lifecycle-owned read (2/32 slots), mutation (1/16 slots), scan (1), analysis (2), upload (2 sessions) and thumbnail (2) lanes. HTTP reads use query-only connections and per-request source snapshots; cancellation does not abort submitted file commits.- Read-source validation snapshots live for at most one second and are invalidated immediately by source-config mtime/size or default-root changes. Mutations and scans validate directly; batch annotations use one fresh configuration snapshot with containment/existence checks per target. Signing normalized source roots does not repeat filesystem canonicalization. Each HTTP read shares one read-only SQLite handle across its helpers and closes it when the worker finishes.
py/gallery/file_transactions.py: stage/transfer/state commit, durable recovery evidence, conservative rollback; not cross-filesystem ACID.py/gallery/catalog.py/jobs.py: authoritative asset IDs and persistent, bounded jobs; catalog corruption is read-only degradation, never index-style automatic rebuild. JSON remains the user-state authority.py/gallery/media.py: provider contract, currently image-only.py/gallery/similarity.py: exact-radius candidate generation using disjoint Hamming bands.py/nodes/prompt_snapshot.py: optional resolved-text PNG metadata pass-through.gallery_ui/src/components/shared/DisclosureMenu.tsx: accessible native disclosure for progressive actions.gallery_ui/src/styles/base.css: global visual tokens and shared controls;minimal.cssandworkspace-system.cssown navigation and workspace presentation.UNIVERSAL_EXTRACTOR_DATA_DIR: opt-in external data root, no automatic migration.- Release builds now clean dist and validate Vite manifest; old hash names are never rewritten.
sync-distis a non-mutating compatibility alias for the audit. scripts/check_release.pyis the portable metadata checker; PowerShell delegates to it.
See the 1.6.0 foundation guide and validation record for supported transactions, limitations, and platform validation status.
Universal Gallery Architecture
This document summarizes the runtime architecture that matters for future backend and frontend work.
Request Flow
- ComfyUI serves the committed frontend bundle from
gallery_ui/dist/at/gallery/. /gallery/servesindex.htmlwithCache-Control: no-store; hashed files under/gallery/assets/*are served with long-lived immutable cache headers so unchanged chunks can be reused by the browser.- Gallery API routes live under
/universal_gallery/api/inpy/gallery/routes.py. - Gallery behavior is exposed through the compatibility facade in
py/gallery/service.py.sources.py,indexing.py,file_ops.py,libraries.pyandmedia_service.pyreceive host-owned environment bindings and do not import the facade. Infrastructure is owned byGalleryRuntime; adjacent helpers remain:refs.pyowns source/image/folder ref normalization,source_security.pyowns source path hardening and scope checks,metadata.pyowns Pillow metadata reads,recipe.pyowns structured Comfy prompt recipe extraction,update_checker.pyowns GitHub release update status, andstate_store.pyowns persisted image state and boards. - File APIs must stay scoped to registered gallery sources and supported image extensions.
- Route-level safety is covered by
tests/test_gallery_routes.py: API handlers must reject cross-originOrigin,Referer, orSec-Fetch-Siterequests, import endpoints must enforce configured size/count limits with 413 responses, and/gallery/assets/*must not serve files outside the committed dist assets directory. /gallery/and/gallery/assets/*responses carry browser hardening headers throughpy/gallery/routes.py, includingnosniffand a same-origin CSP compatible with the React app's dynamic inline styles.
Persistent task API
POST /universal_gallery/api/jobs accepts only refresh_index or fingerprint_assets with validated source-scoped payloads; returns 202 and a job. Fingerprint payload { "whole_library": true } prepares all enabled sources in keyset batches and cannot be combined with scoped parameters. GET /jobs paginates active/recent records, GET /jobs/{id} reads one, and POST /jobs/{id}/cancel requests checkpoint cancellation. Internal colors/dimensions tasks share the analysis lane. Queue capacity is 256 waiting jobs; active identical versioned requests coalesce. Transient I/O failures retry twice (2s, 10s); unfinished replay-safe jobs resume after restart. File transaction journals are never automatically replayed.
Reverse image retrieval (POST /universal_gallery/api/images/search-by-image) accepts a single bounded in-memory reference, searches committed fingerprints across enabled sources, and returns validated paths and ranked matches. Decode and ranking use the analysis lane, leaving ordinary read workers available. The lazy modal supports paste/drop/file selection, coverage/progress, full-path copy and navigation into the source folder. Fingerprint v3 adds EXIF-normalized pHash/dHash, color layout and aspect alongside file SHA-256; exact copies retain separate asset IDs. See image-search usage, API and limitations.
The old forced-refresh/prewarm/variant APIs await the same scheduler and retain their response contracts. Progress is shown through the existing status center; visibility-aware combined polling stops on completion. Task HTTP cancellation leaves committed writes intact. Analysis computes outside write transactions and validates file, asset ID, location and content revision before short commits. The runtime retains idle WAL anchors to avoid a checkpoint on every connection close; all transactions and anchors close on shutdown.
Update Status Flow
/universal_gallery/api/update-status reports whether the installed plugin is behind the latest stable GitHub release for Tera-Dark/ComfyUI-Universal-Extractor. py/gallery/update_checker.py reads the local version from pyproject.toml, fetches up to five GitHub Releases, ignores drafts and prereleases, compares semantic version parts, and returns current_version, latest_version, update_available, release links, changelog bodies, checked_at, and a soft error string. If the installed version has a CHANGELOG.md section and GitHub has not published that release body yet, the checker uses the local section as the release note fallback; if the installed version is newer than GitHub's latest stable release, it is reported as the latest version rather than showing a false downgrade.
The checker keeps a process-local cache for 30 minutes. Normal topbar loads call the route without force; the update popover's manual recheck uses force=true. Network, GitHub, timeout, and malformed-response failures should not break the Gallery: the route returns the cached status with an error when possible, or a no-update fallback if no cache exists.
Because Gallery routes are registered during ComfyUI startup, adding or changing /universal_gallery/api/update-status requires a ComfyUI restart before the running server exposes the route. A frontend-only rebuild is not enough for backend route changes.
Startup Diagnostics
/universal_gallery/api/context and /universal_gallery/api/images are first-screen routes and should fail soft for index/source startup problems. If source loading, SQLite initialization, or index refresh raises unexpectedly, py/gallery/routes.py logs the traceback server-side and returns a shape-compatible fallback payload with index_error and source diagnostics instead of a bare 500. The frontend surfaces index_error through the normal gallery inline error state while still allowing the user to open Settings and inspect source diagnostics.
Gallery Index Data Model
Runtime gallery data is stored under data/. User state remains JSON-backed, while image listing uses SQLite:
gallery_state.json: editable image state, categories, pins, notes, and boards.gallery_state.json.bak-*: timestamped backups created before automatic state recovery migrations.gallery_sources.json: configured gallery sources.catalog.sqlite3: authoritative identity and jobs (assets,jobs,catalog_meta,catalog_migrations), independent schema/backup, never deleted as a cache. Asset IDs survive index rebuilds and plugin moves/trash/restoration; duplicate content and external moves do not automatically merge identities.upload_journal/: session-owned temporary upload recovery records.gallery_index.sqlite3: derived image index, schema 11 with cached asset IDs/content revisions and physical nanosecond mtimes, versioned fingerprints, committedgallery_folderssnapshots and covering/stable-order indexes. Fingerprints also include the search descriptor and physical nanosecond mtime. Hot context requests never traverse directories; initial upgrades may discover empty folders asynchronously after serving existing image rows.library_summary_cache.json: derived prompt-library counts keyed by filename, file size, andmtime_ns; it is runtime state, not a user library.thumb_cache/: generated thumbnails.trash/: plugin-managed trash storage.
gallery_index.sqlite3 uses PRAGMA user_version for internal schema migration, opens in WAL mode with a configurable busy timeout, and keeps schema changes auditable through gallery_schema_migrations. The SQLite file is a rebuildable derived cache: if the main database header or initialization reports corruption, the index and any -wal/-shm side files are moved aside with .corrupt-* suffixes and rebuilt from registered sources plus gallery_state.json.
The important tables are:
gallery_images: primary image index keyed byrelative_path; stores source, folder, file size, real image width/height, created/modified timestamps, baked state fields, and color profile fields.gallery_index_meta: source signature, built timestamp, color index version, state digest, auxiliary-table version, and FTS availability.gallery_schema_migrations: append-only migration audit rows recording schema version bumps.gallery_images_fts: FTS5 search table for filename/path/title/category/notes text. If FTS5 is unavailable, search falls back to the existing LIKE expression.gallery_fts_rows: derived B-tree mapping from path to FTS rowid. Incremental updates delete FTS entries by rowid, avoiding a full scan per UNINDEXED path. Initialize existing rowids once; clear the map together with a full FTS rebuild, never reuse the unrelatedgallery_imagesrowid.gallery_image_color_family: normalized color-family rows used by color filtering, while legacy color fields remain ingallery_imagesfor response compatibility.gallery_image_fingerprints: local-only derived fingerprints used by variant organization. It stores file hashes, lightweight visual dHash values, prompt/workflow metadata hashes, filename sequence keys, and soft-failure diagnostics keyed byrelative_path.
The index has composite indexes for common list views: source plus folder plus creation time, pinned plus creation time, category plus creation time, color family plus creation time, filename, and size.
Loading And Sync Strategy
HTTP image listing prepares its index outside the read pool, then uses list_images_page() in a query-only snapshot with stable sort tie-breaks. Legacy direct Python facade calls retain synchronous maintenance semantics. Listing queries SQLite directly with pagination. It does not need to load the whole image index into Python.
The first gallery load should use the existing SQLite index without force_refresh=true. Forced image refresh is reserved for user-triggered refreshes and freshness-detected changes, where the backend can prefer incremental sync over a full rebuild.
Automatic gallery sync shares the existing durable refresh coordinator with manual refresh. A visible gallery reads /api/sync-status once per second for bounded terminal output hints, discovered through the injected ComfyUI history reader or same-origin bridge. Browser broadcasts are an optional fast path. Prompt IDs deduplicate these transports; one-second bursts merge to a common registered-source scope. API clients with private sockets do not require an ordinary ComfyUI page. Startup history is primed without replaying old runs.
Known output goes straight to a refresh_index job without an extra physical freshness scan. Refresh jobs capture the output boot instance/sequence: a running scan predating new output cannot satisfy it, while a queued covering scan can absorb the latest sequence. Frontend shared promises are invalidated by new output. Visible-only 15-second physical freshness remains the fallback for external file edits/old hosts; acknowledge its fingerprint only after successful synchronization. Initial indexed browsing is followed by an asynchronous reconciliation. The auto-sync preference controls all automatic paths.
Incremental sync scans only the affected scope, upserts new or modified files, deletes missing files, and queues only changed paths for color backfill. Full rebuild remains the cold-start and repair fallback. Scan commits validate the source mutation generation; inaccessible subtrees do not become deletion signals. Identity is registered in bounded batches, independently of decoding; failed analysis remains soft.
get_gallery_context() avoids full image reads and directory discovery. It derives source counts, pinned count, subfolders, and move targets from committed SQLite snapshots. Its forced refresh awaits the shared index task; board summaries reuse the resulting database. list_boards() selects only actual indexed board members, ordered by creation time for automatic covers, rather than building a potentially over-limit IN query for every pinned/note state path. Frontend context snapshots retain unchanged collection identities so count updates do not invalidate folder/move calculations. Folder sorting reuses an Intl.Collator and one per-sort key cache instead of parsing pins/dates and constructing locale options for each comparison. Only the active tree or list presentation is built.
The freshness endpoint still stats in-scope image files (file-in-place edits must be detectable); it now hashes in deterministic directory order without retaining the entire image record list. The in-process cache retains the 32 most recently visited scopes. Disabling live gallery refresh from Settings is appropriate for exceptionally large or slow network-mounted sources.
list_libraries() does not parse every prompt-library JSON file on each request. It uses library_summary_cache.json for list counts when filename, file size, and mtime_ns match, then invalidates the affected summary when a library is imported, saved, deleted, or otherwise invalidated through the service facade.
Library page and artist-search routes clamp client-requested limits so large JSON libraries cannot produce unbounded responses. Import routes stream multipart files with backpressure into target-volume .ue-uploads/{session}/*.part files, enforce per-file/total/count limits, then revalidate and exclusively publish using hard links. Journals permit startup cleanup only with verifiable ownership; unsupported target filesystems fail safely. File/JSON/catalog operations use compensation journals, not cross-store ACID.
GalleryContext.subfolders keeps the legacy string list for API compatibility. GalleryContext.subfolder_details adds source id, relative path, and directory modified time for frontend source-scoped folder trees and default modified-time sorting.
State And Color Indexing
Editable image state remains persisted in gallery_state.json. The state store serializes load-modify-save operations through a module-level lock and writes JSON atomically by replacing a same-directory temporary file, so interrupted writes do not leave partial JSON. The SQLite index caches those fields for fast list queries, but _sync_image_state_to_index_db() first compares gallery_state_mtime and gallery_state_digest; unchanged state skips DB writes. When the state file does change, the service compares indexed fields for all affected paths in batches of at most 900 bind parameters, updates only differing rows and rebuilds FTS/color helpers only for those rows. Removed state paths clear prior indexed fields. py/gallery/sqlite_batch.py is shared with variant fingerprint queries and prewarming, so those queries remain portable to bundled SQLite builds with a 999-parameter limit.
Pin and board state is path-keyed. For legacy paths never registered in the catalog, refresh/build preserves conservative suffix recovery after current rows exist: exactly one full-suffix match, target metadata priority and a gallery_state.json.bak-* backup. Known catalog paths, including old namespaces and tombstones, are excluded. External moves of known assets leave old state/identity at the missing location and create an independent asset; no suffix, hash or basename guess transfers notes.
Color indexing is intentionally asynchronous:
- Base file rows are available before pixel analysis completes.
- Current page paths are prioritized for color backfill.
- Failed or unreadable images are marked with a fallback color profile so the background worker does not loop forever.
/universal_gallery/api/color-index/statusreports backfill progress.
Image dimensions are indexed separately from thumbnails. New or changed files read their real Pillow dimensions during full or incremental index sync, and older rows with missing dimensions are filled by a low-priority background pass. List responses expose optional width and height values; missing dimensions must not block image listing or force the frontend to use thumbnail naturalWidth/naturalHeight.
Pillow decoding is centralized through py/gallery/image_safety.py. By default it sets Image.MAX_IMAGE_PIXELS from UNIVERSAL_EXTRACTOR_MAX_IMAGE_PIXELS=160000000; setting the variable to 0 or an empty value leaves Pillow's default untouched. DecompressionBombWarning is treated as an error for metadata, dimensions, color analysis, and thumbnail generation. Oversized files remain valid gallery entries, while derived data fails softly and records a diagnostic instead of crashing index or prewarm workers.
Variant organization is a derived cache over the same image index. /api/images/variant-groups applies the current Gallery view filters, computes missing fingerprints for a bounded batch, and groups images as exact duplicates, near duplicates, same prompt, same workflow, or filename series. /api/images/variant-group returns the images inside a selected group using the normal ImageRecord shape, while /api/images/fingerprints/prewarm and /api/images/fingerprints/status let the UI analyze more images and display progress. Fingerprints are rebuildable and must not become the source of truth for user state.
Frontend Contract
The public response shapes for ImageListResponse and GalleryContext are preserved. Frontend code should keep using galleryApi.listImages, galleryApi.getContext, and galleryApi.getImageFreshness rather than reaching into backend implementation details.
Large frontend surfaces are split along stable helper boundaries. WorkspaceSidebar imports folder-scope and sorting helpers from components/shared/folderTree.ts, while GalleryWorkspace delegates image prefetch state and card image loading to components/gallery/galleryImagePrefetch.ts and GalleryCardImage.tsx, selection math to components/gallery/gallerySelectionModel.ts, and dual-folder selection/drag/shortcut rules to components/gallery/dualFolderModel.ts. These modules are implementation details; UI behavior and public API calls remain unchanged.
First-screen loading is intentionally gallery-first. GalleryWorkspace and its ordinary grid stay statically imported, while LibraryWorkspace, WorkbenchWorkspace, SettingsWorkspace, ImageDetailModal, and optional Gallery surfaces (dual-folder organizer, variant groups, board sharing, metadata viewer) use React.lazy/Suspense on demand. useLibraryData remains disabled until library or workbench is active; useGalleryData still fetches context for shared navigation but defers image-list requests on those tabs. Browse-scope changes abort obsolete read requests via AbortSignal, while backend file mutations remain shielded and must never be canceled halfway through. Disabling image prefetch in Settings also disables optional thumbnail prewarming, not just IntersectionObserver prefetch. Preference reads/writes use utils/safeStorage.ts so blocked browser storage does not crash navigation; hooks/useUiPreferences.ts owns the typed App preference snapshot and persists outside state updaters.
The update bell lives in TopNavigation beside the refresh action. It should remain a compact icon button with a red dot only when UpdateStatus.update_available is true. The popover uses the shared floating layer placement and dismiss helpers, displays current/latest versions plus release notes, and treats UpdateStatus.error as a non-blocking diagnostic rather than a full-screen failure.
WorkbenchWorkspace is a lazy-loaded creative-tool console rather than a Gallery-internal mode. Its frontend contract is visual and organizational: a status strip summarizes active tool/library/queue state, the left rail chooses tools, and the stage hosts the active tool with a staged generator flow plus queue/source panels. At medium desktop widths the generator must stay wide and supporting panels stack below it before the whole workspace collapses to a single column on mobile. Adding future workbench utilities should extend this rail/stage model without changing Gallery first-load data fetching.
Shared interaction primitives live in gallery_ui/src/utils/interaction.ts. Context menus in gallery, dual-folder, sidebar, and library surfaces should use the shared placement and dismiss helpers so menus stay near their trigger, flip inside the viewport, and close consistently on Escape, outside click, resize, or scroll. Keyboard shortcuts should use the shared editable-target guard so input fields and folder searches keep normal text-editing behavior.
First-run onboarding is owned by gallery_ui/src/components/shared/OnboardingTour.tsx. It is a frontend-only overlay that highlights real UI anchors through data-tour-id, may request tab changes through App.tsx, and stores completion in localStorage under universal-extractor:onboarding-tour-v1-completed. Skipping and finishing both mark the tour complete; the Settings restart button opens the tour for the current session without clearing or mixing that state into UiPreferences.
The main gallery grid is rendered through the useVirtualMasonry helper, backed by @tanstack/react-virtual. The right Inspector is a fixed overlay with reserved viewport space: desktop layouts add 320px of right margin to the main gallery scroll container, while layouts at or below 960px reserve 32dvh below it. Opening and closing it intentionally changes the available grid width (and may reflow the effective masonry column count), but must preserve page, selected paths and the user's browsing position; the newly sized virtual grid must be remeasured. It must not obscure cards or the bottom pagination. Box selection in the virtual grid must not depend only on mounted DOM cards: it should freeze the drag-start layout, use estimated rectangles for offscreen masonry items, compensate scroll delta while dragging, and auto-scroll near the main scroll container edges.
Normal gallery list view is also card-based rather than a plain table. At desktop widths of 1240px and above, it lays out two compact list cards per row; below that it returns to one column. Each list card keeps the same image actions and selection behavior as grid cards, uses a larger thumbnail, shows a local timestamp with seconds, and displays real image dimensions when the backend provides width and height.
Variant organization is a Gallery-internal workspace, not a top-level tab. GalleryToolbar toggles it from the action group, VariantGroupsView renders group cards and type filters, and selected groups reuse normal gallery cards, selection, context menus, board actions, move, delete-to-trash, metadata, and workflow-send behavior. The view may trigger fingerprint prewarm, but it must not change ordinary first-load behavior or fetch prompt libraries.
Dual-folder organization lives in DualFolderWorkspace. It is a frontend-only file-management surface over the existing image APIs: folder panes call galleryApi.listImages, internal drags use a private application/x-universal-gallery-image payload, moves use the existing /api/images/move wrapper, and delete/state/board actions reuse the same callbacks as normal gallery selection. It must keep external file drops separate from internal image drags so imports continue to receive the browser-provided File objects unchanged. Dual-folder cards should mirror normal-gallery interaction polish: clipped text, real-resolution chips when dimensions are available, hover lift, image scale, and stable selected/focused/drop states.
Switching into dual-folder mode unmounts the normal virtual masonry grid. Switching back must remeasure the newly mounted .ue-gallery-grid--virtual before normal gallery layout resumes: GalleryWorkspace clears stale gridWidth while dual-folder mode is active and reattaches ResizeObserver measurement when dualFolderMode or galleryViewMode changes. This prevents old grid widths from carrying over into the normal gallery and causing clipped or shifted columns.
When the gallery is on page 1 sorted by newest first, live sync may replace the list automatically. In other views, the frontend shows a pending-refresh pill so browsing position is not interrupted.
Sidebar source shortcuts use folder refs as scopes. default_output:: means the output source root, default_input:: means the input source root, and source_id::relative/path targets a folder inside a registered source. The empty string is only a compatibility fallback and should not be used as the normal output shortcut.
The folder panel should render only the active source tree. Pinned folders sort first, then folders sort by modified time descending unless the user switches to name sorting. For sibling folders with valid date-like names (YYYY-MM-DD, YYYY.MM.DD, or YYYY_MM_DD), the embedded date sorts ahead of raw directory mtime so generated date folders stay chronologically stable even after later file writes. Legacy pinned output paths without the default_output:: prefix should still be treated as aliases for their canonical source refs.
Trash grid mode is presentation-only and remains backed by /api/trash. It uses a responsive masonry-style card layout, keeps restore/purge actions local to the trash page, and clips long names and original paths inside the card bounds.
Trash storage paths are resolved through resolve_trash_storage_path() in py/gallery/service.py before preview, restore, or purge. The helper keeps all operations inside data/trash/, even if runtime trash state is corrupted.
Image Recipe Contract
/universal_gallery/api/metadata returns the embedded metadata and workflow as before, plus a derived recipe object from py/gallery/recipe.py. The recipe is intentionally local and best-effort: it reads Comfy prompt graphs when present and extracts positive/negative prompt text, checkpoint name, LoRA names and strengths, latent dimensions, seed, steps, CFG, sampler, scheduler, denoise, and whether a workflow payload exists. It also exposes recipe.lora_manager when ComfyUI-Lora-Manager nodes such as Lora Loader (LoraManager) or Lora堆 are detected, including a normalized LoRA stack for one-click application. Missing or non-Comfy metadata should produce empty fields rather than errors.
Future integrations, including one-click recipe application through external LoRA tooling, should consume this structured recipe contract instead of scraping raw metadata or adding more graph heuristics to metadata.py. UI flows may still display the legacy summary field for compatibility; recipe is the cleaner handoff shape for automation.
ComfyUI Workflow Handoff
The image detail action for opening a workflow in ComfyUI is a same-origin handoff, not a window-creation path. gallery_ui/src/App.tsx asks for confirmation, wraps the send in the global operation status center, then sends a universal-extractor:workflow-probe over the universal-extractor-workflow BroadcastChannel. web/comfyui/top_menu_extension.js responds with an instanceId, visibility state, and focus state. The gallery then sends one targeted universal-extractor:workflow-message to a single acknowledged ComfyUI instance and waits for universal-extractor:workflow-delivered.
LoRA Manager stack application uses the same probe/target channel but sends universal-extractor:lora-stack-message and awaits universal-extractor:lora-stack-delivered from the selected instance, including a target/count summary or an error. The shared right-click/detail confirmation reads the image recipe first and offers three choices: append, replace, or cancel. The ComfyUI bridge writes to one existing compatible node in the current graph, preferring a selected node, otherwise the first compatible graph node. Only active LoRAs from the image are sent. mode: "append" retains existing entries (including disabled/locked state and unrelated text), updates same-name LoRAs, and adds new names without duplicates; mode: "replace" removes only the target node's old LoRAs and preserves non-LoRA text; the bridge does not directly rewrite other graph nodes. Native LoRA Manager callbacks may still synchronize connected trigger-word nodes as part of its normal behavior. Incoming entries use explicit expanded: false so CLIP controls start collapsed even when CLIP and model weights differ; the independent value, including zero, is preserved. The confirmation warns that editing model strength while collapsed in upstream LoRA Manager subsequently synchronizes CLIP. Writes call the third-party widget callbacks, wait for their asynchronous list/text sync, verify the resulting list, and report failure instead of premature success; widget/property snapshots permit best-effort rollback on a callback error. The target node and canvas are marked dirty after a change.
The gallery must not call window.open() as a fallback for workflow handoff. If no refreshed ComfyUI page responds, the gallery stores the payload under universal-extractor:pending-workflow and shows a refresh/retry error so the user can refresh an existing ComfyUI tab. This avoids one click opening multiple ComfyUI tabs or broadcasting the same workflow into every open ComfyUI page.
ComfyUI Nodes
Node registration starts in the repository root __init__.py, which loads classes from py/nodes/extractor_node.py through py/plugin.py. The generation node is UniversalJsonSegmentRandomizer, displayed as Universal Artist/Tag Randomizer; UniversalPromptSnapshot is the additional resolved-text pass-through node.
UniversalJsonSegmentRandomizer reads the same data/ libraries but resolves explicit field paths such as name, other_names, meta.style, or tags.*, flattens nested string/number values, optionally filters entries by another field path, and returns both the joined prompt string and a JSON string of selected segments. It supports random, polling, and sequential selection; duplicate policy control; and direct output formats for Anima artist strings, artist:name, weighted artist prompts, NAI-style weights, generic tag strings, and custom templates. Polling state is process-local and keyed by node id, library filename, field paths, filters, pool size/content hash, and seed, so separate nodes or changed libraries do not share offsets; restarting ComfyUI resets polling back to the seed-derived offset. Node file access is constrained to plain .json filenames in data/ and excludes runtime state files listed in RUNTIME_STATE_FILENAMES.
Release Guardrails
scripts/verify.ps1 remains the preferred full local check. It runs Python tests, backend compileall, Node bridge tests, frontend typecheck/lint/i18n/tests/security audit/build, dist asset audit, and release metadata checks. scripts/check-release.ps1 checks that pyproject.toml, gallery_ui/package.json, and package-lock.json versions agree and built assets referenced by index.html exist. Clean builds remove previous hashed files; do not rewrite old hashes with new content. Release versions should also add a matching ## x.y.z section in CHANGELOG.md so the update popover can display local notes before or without a populated GitHub Release body.
The manual Comfy registry workflow validates release metadata, passes the installed English structured release notes through COMFY_NODE_CHANGELOG, checks all registry statuses for an existing version, and skips publication when that version already exists (including pending review). A failed registry check or publish fails the workflow; publication runs are serialized per repository. Dispatch the verified immutable release tag. A successful upload does not imply an active/approved Registry version.
The production frontend does not use the default Vite/React starter assets, so gallery_ui/src/assets/hero.png, react.svg, and vite.svg should stay absent unless a real UI surface references replacement assets.
Browse URLs encode the stable workspace tab and gallery source/board, search, filters, sort and page, plus the selected prompt library when that workspace is active. Meaningful scope changes push browser history; typing a query debounces the API request and replaces (rather than appends) its current history entry. popstate restores the encoded scope after running the same dirty-library, dirty-source-settings and image-detail leave guards as workspace navigation. A cancelled navigation restores the previous URL without discarding the draft. Detail image links, transient dual-pane folders and variant group analyses are intentionally not deep links: they require their own independently validated loading/permission semantics and are never reconstructed from untrusted query strings. Detail navigation and the batch Inspector use the visible page, variant group, or loaded dual-pane records, not the unrelated first gallery page.
Explicit gallery refresh waits for both context and image/trash requests to finish before marking its operation complete; a superseding view change reports an interrupted request. Live freshness replaces the newest first page only while the scroll viewport is near its top, otherwise it offers the existing pending-update pill. At mobile widths (up to 560px) grid density is an independent, persistent comfortable single-column or compact two-column choice, subject to the actual available width; the density popover shows the effective column count. The workflow-send confirmation is always required for safety; there is no confirmWorkflowSend UI preference.
Workspace visual system (1.8.1)
The 1.8.1 workspace alignment keeps the confirmed Gallery as its visual baseline. Global tokens and shared action styles are defined in styles/base.css, with cross-workspace layout rules consolidated in workspace-system.css. Settings feature CSS must not define shared button geometry. Standalone Metadata and image detail both import the independently owned metadata.css, so styles do not depend on visit order. Lazy JavaScript entry points and gallery loading gates remain unchanged.
Library search lives beside its content, independently from sidebar filename filtering; overview/help are optional disclosures. Workbench advanced recipes retain their effective settings in a compact summary, and the 1320px stacking breakpoint keeps its generator usable. Settings has relevant sidebar navigation without requesting library data. ModalSurface portals ordinary light dialogs outside the inert application root, with stack-aware keyboard ownership, deliberate backdrop dismissal and opener restoration through interaction.ts. Nested confirmations stay above ordinary dialogs; pending work and entry-draft discard guards remain explicit.
1.7 QoL contracts
GET /universal_gallery/api/release-notes?locale=zh-CN|en: installedcurrent_version,source, and up to fivereleases, current first. Structured bilingual files ship indocs/releases; older CHANGELOG sections remain readable offline.update-statuspreserves old fields and addscheck_status(checked,cached,failed,preview),notes_source,last_success_at. Remote calls share one in-flight check and cache successful results for 30 minutes. Safe Markdown uses React text, never raw HTML or remote images.GET /universal_gallery/api/images/locate?asset_id=...&limit=48&sort_by=created_at&sort_order=desc: returnsasset_id,relative_path, canonical parentsubfolder, one-basedpage, zero-based pageindex, andlimit. A legacyrelative_pathcan replace the ID. Source resolution, namespace checks, same-origin guards and the list's stable SQL ordering apply. The folder page includes descendants, matching ordinary list semantics.- ComfyUI extension listens to
execution_success, terminalexecuting,execution_errorandexecution_interrupted. Channeluniversal-extractor-gallery-eventscarries{version:1,type:"outputs-changed",prompt_id:string}(1–200 chars); both ends deduplicate a bounded 256 prompt IDs. The bridge now relays bounded output folder hints through/sync-notifications; server validation maps them to registered-source scopes. Unknown scope falls back to all registered sources. No arbitrary path is accepted from an event. useGalleryAutoSyncowns dirty generations, visible-only fallback checks, one initial all-source reconciliation even when an old folder is open, and 2s/10s retry for transient failures.gallerySynccoalesces persistent scan jobs across manual and automatic callers, cancelling polling when all subscribers leave; submitted backend jobs retain their normal durable lifecycle. A new event during a scan requires a trailing sync. Fingerprints are acknowledged only after success.- The global task observer and refresh waiters share one
galleryJobMonitorsubscription. Modern servers return a revision and wake one asynchronous long poll on task changes; older servers retain the visible-only two-second fallback. Waiting holds no query worker/SQLite transaction; no active jobs means no continuing task polling. UI feedback retains errors/active operations and at most 50 completed session records. Feedback links only explicit job IDs. Each new job resets its scope record's quiet timer and hidden state. Ordinary toast history is not persisted. UiPreferences.interfaceDensityis optional for older callers and normalizes tostandardorcompact. It never changes saved column count. Shared dimensions set card gaps to 14/10px and estimated chrome to 50/44px, respectively. Image search retains its in-memory session through detail/locate; explicit close releases URLs and requests.- Virtual masonry measures its scroll margin below the toolbar, uses fixed estimated card heights, and shares actual cached lane positions with offscreen box selection. Media ratios remain stable for existing content versions during the loaded page; a new content version or page entry can use newly available dimensions. Width/density changes restore the visible asset and pixel offset. Browser anchoring is disabled for the virtual surface to avoid double correction. Thumbnail load/retry state is keyed by source URL/content version; cached complete images are recognized at attachment, prefetch waits for decode and does not clear successful sources. Placeholders are static and photos do not fade in on every mount. Legacy trash previews reserve a fixed 1/1.36 frame with
object-fit:containrather than growing CSS columns after decode. - Trash batches execute sequentially, stop at the first failed commit, retain unfinished selections and perform a single snapshot reload without forcing a source scan. Each file retains the existing recovery journal semantics. Refresh deduplication ignores the enqueue-time catalog generation: each attempt captures a fresh generation after acquiring the scan lock and validates it before commit. I/O/conflict retries remain bounded to three attempts in the same task. A terminal auto-sync failure blocks timer/focus retries for that scope and dirty generation; a user retry or new output permits another attempt. Historical refresh jobs coalesce into one UI record per scope rather than restoring a stack of identical failures. On scheduler startup, equivalent queued legacy refresh jobs retain only the newest executable request; older records become cancelled with a
superseded_byresult. No file-operation journal, asset identity or versioned analysis work is consolidated.