diff --git a/TESTING.md b/TESTING.md index 603f53e..3303d20 100644 --- a/TESTING.md +++ b/TESTING.md @@ -398,6 +398,13 @@ naming it, here and in a detailer's image ancestry; a latent that is not a link reaches its own sampler is `malformed`; running out of budget is `work-limit` or `depth-limit`, never single stage. Each sampler's walk runs once per resolution; selector branch runs share it and other candidates are charged its visit cost. +A UI workflow node whose `type` is a list or an object (read only for the overwritten +PromptManager check) is skipped like a non-object node, since it can be neither a +subgraph instance nor a PromptManager node, so a prompt that resolves still resolves +and the other nodes still feed the check; a list or object `class_type` is never a +preview output. Before #286 both raised and the inspect route answered 500 +(`test_unhashable_*` in `tests/test_api_prompt_inspector.py`, the `workflow-type-*` +golden cases on both sides). Text encoders: `single` encoders (the Qwen image encoders, `TextEncodeQwenImage21` slot 0 `prompt` and slot 1 `negative_prompt`, `KikoTextEncode`) read one string input unchanged. `parts-equal` encoders (CLIPTextEncodeSDXL, Flux, SD3) resolve when @@ -1581,7 +1588,43 @@ AVI/WMV preview only), chosen or dropped, previews inline. The heading's one act the shared viewer with the full inspector on a record read entirely in this browser (`web/js/workspace/inspect.mjs`, `local-session.mjs`, `local-inspection.mjs` and the L1 readers `png-metadata.js`, `local-video.mjs`, `local-projection.mjs`). Nothing is -uploaded, imported or stored. Only HTML and JS change, so no ComfyUI restart is needed. +uploaded, imported or stored. PRs L1 and L2 change only HTML and JS, so they need no +ComfyUI restart; PR L3 does (below). + +Local roles (#249 PR L3, decision LI-1): the prompts and resolution of a local record are +the server inspector's for the same records. `web/js/workspace/local-resolver.mjs` (with +`local-resolver-support.mjs`, `local-resolver-stages.mjs`, `local-parameters.mjs` and +`py-json.mjs`) ports `inspect_prompt_evidence` with `INSPECTION_PROFILE`: execution +evidence, graph tracing, every bound, several sampler candidates with the +identical-branch rule (#270), preview-only stages, detailer stages (multistage) and A1111 +`parameters`. It reads the server's own node table, `web/js/inspection-semantics.json`, +which the page fetches once through the API client as it mounts, before any selection +(`local-semantics.mjs`; `/prompt_manager/js/inspection-semantics.json` is the one JSON +file the static route serves, by exact path). A table that cannot be fetched answers +"The inspector's node table is unavailable." as the server does. The legacy fill in +`projectInspectionPayload` then applies to a local record exactly where it applies to the +Images answer for the same bytes. Python is the oracle: +`tests/test_local_inspect_parity.py` `LocalResolverParityTests` compares the local +evidence byte for byte with `inspect_prompt_evidence` for every parity PNG and inspection +vector (prompts, resolution and facts), the golden and selection record sets +(`tests/local_resolver_cases.py`, `tests/local_resolver_selection_cases.py`), every work +bound and 1,200 seeded fuzz cases; `tests/fixtures/local-inspect/resolver-vectors.json` +holds the committed answers the Node unit test reads. The browser spec `chooser shows the +Images roles and resolution for every parity PNG` compares the rail with Images for all +25 committed parity PNGs. The static route change and the resolver fixes (Python) need a +ComfyUI restart. + +Main thread (M4): the resolver runs on the page's thread, not a worker. On a desktop +(Node 26) `resolveLocalEvidence` on maximal 2 MiB graphs (a 19,990-sampler chain, 6,600 +independent samplers, a consumer-dense scan, a 100,000-node PromptManager workflow) took +130–455 ms; the server took 80–905 ms on the same inputs. `localFileAdapter.read` runs in +bounded steps (read, table, parse and resolve, project, fit) with a turn of the event +loop between them, every wait ends with the selection's signal (a stalled table request +included), and the inspector carries `aria-busy="true"` with "Loading file details…" +until the answer lands. `tests/js/workspace-local-inspection.test.js` pins a 2,500 ms +budget per step on maximal independent-sampler and stage-chain graphs, checks that the +stepped read answers byte for byte as one synchronous build, and that an abort at any +step ends the read. Bounds (the server's own budgets, ported by PR L1; see "Follow-up: local Inspect parity" under "Chronological Library history" for the known divergences): @@ -1600,7 +1643,8 @@ under "Chronological Library history" for the known divergences): Privacy, asserted by `local inspection makes no content request or storage write` and `local roles send exactly` in `tests/browser/workspace-inspect.spec.mjs`, and by the -managed-host spec below: no request carries the file's bytes, name or text (fetch, XHR, +managed-host spec below: the only static file Inspect reads is its node table, once per +page and before any selection; no request carries the file's bytes, name or text (fetch, XHR, beacon, WebSocket, EventSource); Inspect writes nothing to `localStorage`, `sessionStorage` or IndexedDB; only an explicit Send crosses the host bridge, and its payload carries neither the `blob:` URL nor the filename; no console message names the @@ -1627,19 +1671,27 @@ mapped differences"): `image/png,image/jpeg,image/webp` plus `video/*,.mp4,.mov,.m4v,.webm,.mkv,.avi,.wmv`. A video previews in the same `#local-preview` slot as `video.local-file-preview` with native `controls`, `playsInline`, `preload="metadata"` and `muted`, never autoplays - inline, and is paused, has its `src` removed and is removed from the page on replacement - or dispose, before its URL is revoked. Each preview owns its listeners, so the page never - pins a released node. A settled video (metadata read, or failed) is then reset with - `load()` at once; one still starting is reset only once it reports metadata or an error, - because WebKit (GStreamer) blocked the page when such a pipeline was reset (`replacing a - video that just started loading never blocks the page`, `a video replaced while still - starting is reset once it settles`). A WebKit load cut off by the revoked URL usually - stalls and never settles; a fallback then only drops the waiting listeners. The viewer's - own video teardown was checked the same way (`closing the viewer on a video that just - started never blocks the page`, 15 fresh pages on WebKit) and needed no change. - Follow-up: on WebKit, a video released exactly at its `loadstart` sometimes blocks the - page, with or without any reset and also before the deferred reset (2 of 24 runs in the - pinned image), so the `loadstart` spec is skipped on WebKit until that is investigated. + inline. On replacement or dispose it is paused, has its `src` removed, is reset with + `load()` (which frees its decoder) and is removed from the page, before its URL is + revoked. Each preview owns its listeners, so the page never pins a released node. + A video replaced while still starting (a source, but neither metadata nor an error yet) + stays where it is, inert and `aria-hidden`, with the new preview inserted after it, and + the session's revoke of its URL waits; once it reports metadata or an error (or after + 15 s) it is released as above and only then is its URL revoked. Page leave and dispose + release it at once. Cause, found with native stacks of the hung WPE web process in the + pinned image: WebKit (GStreamer) deadlocks when a video leaves the page or the viewport + before its pipeline has started. The main thread changes the pipeline state and waits + for the source's streaming thread, which is itself waiting on the main thread in a + synchronous bus handler. Removing the video, hiding it, or only pushing it below the + fold with a taller preview inserted before it all hung (7 to 21 of 120 fresh pages each, + replaced at `loadstart` + 0–7 ms), and none of the earlier steps (`pause()`, removing + `src`, `load()`, revoking the URL) was the cause on its own; pushing it out after + `loadedmetadata` hung 0 of 180. Specs: `replacing a video that just started loading + never blocks the page`, and `a video replaced while still starting is reset once it + settles`, which now runs on WebKit too (15 fresh pages, replaced at `loadstart` + 0–4 + ms). The viewer's own video teardown was checked the same way (`closing the viewer on a + video that just started never blocks the page`, 15 fresh pages on WebKit) and needed no + change. - **D63** (status line): one `p.small.spaced-16` in `#local-preview`, for states the concept never shows: "Drop one file at a time.", "Folders cannot be inspected. Choose one image or video file.", "This file type cannot be previewed or inspected here. @@ -1664,6 +1716,29 @@ mapped differences"): Modified with no Path row and no Show in folder, and ends with "Local file; no saved library association was checked." and Download original. There is no Prompt history action and no category or tag chips. +- **D117** (Copy settings summary, LI-4): the legacy metadata + page's COPY ALL, consolidated into the shared full inspector (the viewer's + `#viewer-details`, so Images, the recorded history's current file and Inspect all have + it). The concept has no such action. One plain button "Copy settings summary" follows + the six facts (before "File & provenance"); the Images side panel keeps the concept's + footer and has none. Its text is the legacy `copyAllData` template, line for line: + `Checkpoint`, `Positive Prompt`, `Negative Prompt`, `Steps`, `CFG Scale`, `Sampler`, + `Seed`, with the legacy placeholders ("Unknown", "No prompt found", "No negative prompt + found"). The values are the ones the inspector shows: a confirmed prompt and a known + fact verbatim, anything else (an explicitly empty prompt included, as the legacy parse + treats '') the placeholder. Against the legacy page, for the same file, only these + differ: numbers keep the server's spelling ("8.0" where the legacy page wrote 8); a fact + the legacy parse could not read can be known; and a negative the #249 ZeroOut guard + zeroed reads "No negative prompt found" where the legacy parse read the positive's text + through the zero-out. `workspace-file-inspector.test.js` runs the template taken from + `web/metadata.html` itself on the legacy parse of every parity PNG: 22 of 25 are + byte-equal, f01b and f23 differ by the ZeroOut guard and f17 by facts the legacy parse + missed. Copying works as the concept's prompt copy: the toast "Settings summary + copied.", or, without a clipboard or when it is refused, the select-to-copy dialog + titled "Copy settings summary". `.ca-tools #viewer-details > div > button` wraps + (`white-space: normal`) in the narrow rail at 200% zoom, as D20's buttons do. The concept + comparisons add the button to the concept's viewer at runtime (a copy of its own plain + button) and to the Inspect rail's section order. The chooser keeps naming the chosen file, as the concept's does: it is cleared as the picker opens (so choosing the same file again is a new selection), not after `change`, @@ -1677,9 +1752,8 @@ Legacy floor (`web/metadata.html`), dispositions: - Full text (the old "Show more"): the prompt blocks show the whole text. - Raw workflow view and Download workflow JSON: kept, in "Workflow & raw metadata". - Copy all summary and filename copy (LI-4): the filename is selectable text in the rail - and Download original saves the file under its name. The ruled "Copy settings summary" - button after the six facts of the shared full inspector is not part of PR L2; until it - lands, the legacy page keeps COPY ALL and stays reachable from Overview. + and Download original saves the file under its name. COPY ALL is the shared full + inspector's "Copy settings summary" (D117), with the legacy text. - Fully browser-local reading: kept and asserted (privacy above). - Case-variant keys `Workflow|comfy|ComfyUI|Prompt|Parameters` (LI-5): a separate backend issue; Inspect follows the server's readers, so the two never disagree. @@ -2272,12 +2346,41 @@ skip without ffmpeg). The remaining known divergences, checked against ffprobe 9 `unreadable`); ffprobe reads such files. - An EBML DocType other than `matroska`/`webm` is an invalid container locally; ffmpeg only warns and reads it. -- Envelope JSON deeper than 1,000 levels is refused locally as Python's default - recursion limit refuses it; the server's real limit depends on the interpreter and - call stack. A duplicate key in an object enclosing a nest deeper than the running +- Deep JSON (local roles, PR L3, and video envelopes): both browser CPython-JSON + parsers (`py-json.mjs`, `local-video.mjs`) share one cap, `JSON_MAX_DEPTH` = 1,000 + nested arrays or objects (each counts, an empty one too); past it a record is invalid + JSON locally. The server's `json.loads` raises RecursionError at a limit that depends + on the Python, measured on Linux x86-64: 993 levels from a bare script on 3.10 and + 3.11 (the 1,000 recursion limit minus the caller's frames, so about 990 inside the + server), 9,997 on 3.12 and 9,998 on 3.13 (the C recursion counter; lower on Windows + builds), and about 52,150 on 3.14 (stack-based, varies with the thread's stack). + The record kinds answer: a deep `prompt` graph (or `comfyassets_prompt_pairs` + envelope) is malformed; a deep `workflow` is ignored, so the overwritten PromptManager + check (R1) does not run and a prompt can resolve; a deep video `comment` or + `description` envelope makes the file unreadable on both sides (the server's + RecursionError reaches `read_inspection_file`). That gives two directions: + - 3.10/3.11, between the server's limit and 1,000: the server refuses and the local + page reads, so a deep prompt resolves locally while the server says malformed, and + a deep workflow can make R1 refuse locally where the server resolves. + - 3.12 and newer, from 1,001 up to the server's limit: the local page refuses and + the server reads, so a deep prompt is malformed locally while the server resolves, + and a deep workflow is ignored locally while the server can refuse it under R1. + Past both limits they agree. `test_local_json_depth_cap_at_its_boundary` checks the + local answer at 999, 1,000 and 1,001 for both kinds and that any server difference + is only one of the two directions above. +- Local roles (PR L3): `\d` in the A1111 settings footer is any Unicode decimal + digit on both sides, but the browser's Unicode version can be newer than the server + Python's, so a footer whose step count uses a digit added after Unicode 15 can differ. + `str.lower()` and `toLowerCase()` differ only for characters no node class name uses. +- A duplicate key in an object enclosing a nest deeper than the running Python's json limit is `malformed` locally and `limited` on that Python. - Decimal exponent limits assume CPython's C `_decimal`; on a `_pydecimal` interpreter the server would read the over-limit exponents the local projection refuses. +- Follow-up (two CPython-JSON emulations, review M6): `local-video.mjs` + (`pythonJsonLoads`/`pythonFloatRepr`, numbers kept as lexemes) and `py-json.mjs` + (`pyJsonLoads`/`pyFloatRepr`, Python value kinds) both emulate `json.loads`; only the + depth cap is shared so far. A fix to one parser can miss the other, so the video + reader should move onto `py-json.mjs` in a later PR. Preserve raw logs, failed traces, paired PNGs, geometry, runtime versions and source hashes before later managed commands clear their output directories. diff --git a/py/api/__init__.py b/py/api/__init__.py index fbebf5c..d97b359 100644 --- a/py/api/__init__.py +++ b/py/api/__init__.py @@ -32,6 +32,7 @@ from .saved_views import SavedViewRoutesMixin from .history import HistoryRoutesMixin from .images import ImageRoutesMixin from .admin import AdminRoutesMixin +from .settings_routes import SettingsRoutesMixin from .job_routes import JobRoutesMixin from .image_tag_jobs import ImageTagJobsMixin from .jobs import JobRegistry @@ -107,16 +108,20 @@ STATIC_MIME_TYPES = { ".html": "text/html", } _STATIC_SEPARATORS = re.compile(r"[\\/]+") +# Served by exact request path beside the extension allow-list: the browser's +# copy of the inspection node table (local Inspect resolves with it, #249). +STATIC_EXACT_FILES = {("js", "inspection-semantics.json"): "application/json"} -def _resolve_static_file(root, filepath): +def _resolve_static_file(root, filepath, allowed=STATIC_MIME_TYPES): """Real path of ``filepath`` under ``root``, or None when it must not be served. The request path is untrusted. It is rejected when it is empty, carries a ``..`` segment, is absolute on any platform (a leading slash or backslash, a drive letter such as ``C:`` or a UNC ``\\\\server\\share`` prefix all re-anchor ``os.path.join`` on Windows), has an extension outside - STATIC_MIME_TYPES, or resolves (symlinks included) outside ``root``. + ``allowed`` (STATIC_MIME_TYPES), or resolves (symlinks included) outside + ``root``. """ if not isinstance(filepath, str) or not filepath: return None @@ -129,7 +134,7 @@ def _resolve_static_file(root, filepath): segments = _STATIC_SEPARATORS.split(filepath) if any(segment in ("", ".", "..") for segment in segments): return None - if os.path.splitext(filepath)[1].lower() not in STATIC_MIME_TYPES: + if os.path.splitext(filepath)[1].lower() not in allowed: return None try: root_real = Path(os.path.normcase(os.path.realpath(root))) @@ -261,6 +266,7 @@ class PromptManagerAPI( PromptRoutesMixin, ImageRoutesMixin, AdminRoutesMixin, + SettingsRoutesMixin, JobRoutesMixin, ImageTagJobsMixin, LoggingRoutesMixin, @@ -361,6 +367,24 @@ class PromptManagerAPI( """409 envelope for a long job that is already running.""" return _json_error(f"{label} already running", 409) + async def _run_held(self, kind, title, admission, operation): + """Run a legacy-page operation under a registry hold (D80). + + While ``operation()`` runs, the registry lists it as a running, + non-cancellable job, so workspace jobs its admission class conflicts + with are refused, naming it. When the registry refuses the hold, the + legacy 409 envelope names the running job and nothing is done. A + response of 400 or above ends the hold ``failed``. + """ + busy = self._jobs.blocker(kind, admission=admission) + if busy is not None: + return self._long_job_busy_response(busy.title) + async with self._jobs.hold(kind, title, admission=admission) as job: + response = await operation() + if getattr(response, "status", 200) >= 400: + job.finish("failed") + return response + async def _run_in_executor(self, func, *args, **kwargs): """Run a blocking function in the default thread pool executor. @@ -456,6 +480,7 @@ class PromptManagerAPI( self._register_prompt_routes(routes) self._register_image_routes(routes) self._register_admin_routes(routes) + self._register_settings_routes(routes) self._register_job_routes(routes) self._register_logging_routes(routes) self._register_autotag_routes(routes) @@ -478,15 +503,20 @@ class PromptManagerAPI( def _serve_static_file(self, subdir, filepath): """Serve ``web//``; 403 outside it, 404 when missing.""" root = os.path.join(_get_project_root(), "web", subdir) - file_path = _resolve_static_file(root, filepath) + exact = STATIC_EXACT_FILES.get((subdir, filepath)) + # An exact file passes the same containment check, its extension only. + allowed = STATIC_MIME_TYPES if exact is None else {".json"} + file_path = _resolve_static_file(root, filepath, allowed) if file_path is None: return web.Response(text="Forbidden", status=403) + content_type = ( + exact or STATIC_MIME_TYPES[os.path.splitext(file_path)[1].lower()] + ) if not os.path.isfile(file_path): return web.Response(text="Not Found", status=404) - ext = os.path.splitext(file_path)[1].lower() with open(file_path, "rb") as f: content = f.read() - return web.Response(body=content, content_type=STATIC_MIME_TYPES[ext]) + return web.Response(body=content, content_type=content_type) def _serve_html_page(self, filename, title, cache=True): """Serve ``web/``; 404 when missing, 500 when unreadable.""" diff --git a/py/api/admin.py b/py/api/admin.py index f4a6af9..7ad05da 100644 --- a/py/api/admin.py +++ b/py/api/admin.py @@ -22,16 +22,17 @@ from .jobs import ( SCAN_GROUP_NAMES, SCAN_GROUPS, SCAN_SCOPES, + DUPLICATE_CLEANUP_JOB_KIND, + MAINTENANCE_JOB_KIND, + RESTORE_JOB_KIND, Job, JobBusy, ) try: from ...utils.parallel import map_parallel - from ...utils.validators import validate_result_timeout, validate_worker_threads except ImportError: from utils.parallel import map_parallel - from utils.validators import validate_result_timeout, validate_worker_threads try: from ...database.operations import PromptDatabase @@ -502,14 +503,6 @@ class AdminRoutesMixin: async def get_stats_route(request): return await self.get_statistics(request) - @routes.get("/prompt_manager/settings") - async def get_settings_route(request): - return await self.get_settings(request) - - @routes.post("/prompt_manager/settings") - async def save_settings_route(request): - return await self.save_settings(request) - @routes.get("/prompt_manager/backup") async def backup_database_route(request): return await self.backup_database(request) @@ -651,7 +644,15 @@ class AdminRoutesMixin: job.finish() async def cleanup_duplicates_endpoint(self, request): - """Cleanup duplicate prompts endpoint.""" + """Cleanup duplicate prompts endpoint (holds the registry: records).""" + return await self._run_held( + MAINTENANCE_JOB_KIND, + "Prompt cleanup (admin page)", + "records", + self._cleanup_duplicates, + ) + + async def _cleanup_duplicates(self): try: removed_count = await self._run_in_executor(self.db.cleanup_duplicates) @@ -766,26 +767,39 @@ class AdminRoutesMixin: return hash_sha256.hexdigest() async def delete_duplicate_images_endpoint(self, request): - """Delete duplicate image files from disk (single flight with the scan).""" - job = self._acquire_long_job(self.LONG_JOB_DUPLICATES) - if job is None: + """Delete duplicate image files from disk. + + Single flight with the duplicate scan, and a records hold on the + registry, so no scan, restore or tagging job runs meanwhile. A body + without paths is refused before the hold (it is no job). + """ + data, error_response = await _read_json(request) + if error_response is not None: + return error_response + image_paths = data.get("image_paths", []) + if not image_paths or not isinstance(image_paths, list): + return web.json_response( + {"success": False, "error": "No image paths provided"}, + status=400, + ) + guard = self._acquire_long_job(self.LONG_JOB_DUPLICATES) + if guard is None: return self._long_job_busy_response("Duplicate scan or delete") - async with job: - return await self._delete_duplicate_images(request) - async def _delete_duplicate_images(self, request): + async def delete(): + # No await since the check above: the lock is still free. + async with guard: + return await self._delete_duplicate_images(image_paths) + + return await self._run_held( + DUPLICATE_CLEANUP_JOB_KIND, + "Duplicate image delete (gallery page)", + "records", + delete, + ) + + async def _delete_duplicate_images(self, image_paths): try: - data, error_response = await _read_json(request) - if error_response is not None: - return error_response - image_paths = data.get("image_paths", []) - - if not image_paths or not isinstance(image_paths, list): - return web.json_response( - {"success": False, "error": "No image paths provided"}, - status=400, - ) - output_dir = await self._run_in_executor(self._find_comfyui_output_dir) result = await self._run_in_executor( self._delete_duplicate_images_sync, image_paths, output_dir @@ -882,208 +896,6 @@ class AdminRoutesMixin: status=500, ) - async def get_settings(self, request): - """Get current settings.""" - try: - from ..config import PromptManagerConfig, GalleryConfig - - root_paths = [ - self._public_path(d) for d in GalleryConfig.MONITORING_DIRECTORIES - ] - return web.json_response( - { - "success": True, - "settings": { - "result_timeout": PromptManagerConfig.RESULT_TIMEOUT, - "webui_display_mode": PromptManagerConfig.WEBUI_DISPLAY_MODE, - "infinite_scroll": PromptManagerConfig.INFINITE_SCROLL, - "worker_threads": PromptManagerConfig.WORKER_THREADS, - "cpu_count": PromptManagerConfig.max_worker_threads(), - "gallery_root_paths": root_paths, - "gallery_root_path": root_paths[0] if root_paths else "", - "monitored_directories": [ - self._public_path(d) for d in self._monitored_directories() - ], - }, - } - ) - except Exception as e: - self.logger.error(f"Get settings error: {e}", exc_info=True) - return web.json_response( - {"success": False, "error": "Failed to get settings"}, - status=500, - ) - - def _monitored_directories(self): - """Directories the running image monitor watches, else the configured roots.""" - from ..config import GalleryConfig - - monitor = _current_image_monitor() - if monitor is not None: - return list(getattr(monitor, "monitored_directories", [])) - return list(GalleryConfig.MONITORING_DIRECTORIES) - - async def save_settings(self, request): - """Save settings: the whole payload is validated before anything changes.""" - try: - from ..config import PromptManagerConfig - - data, error_response = await _read_json(request) - if error_response is not None: - return error_response - - try: - updates = self._validate_settings(data) - except ValueError as ve: - return web.json_response( - {"success": False, "error": str(ve)}, status=400 - ) - - new_roots, error = self._parse_gallery_roots(data) - if error: - return web.json_response({"success": False, "error": error}, status=400) - - for name, value in updates.items(): - setattr(PromptManagerConfig, name, value) - restart_required = False - if new_roots is not None: - restart_required = self._apply_gallery_roots(new_roots) - - self._persist_settings() - - return web.json_response( - { - "success": True, - "message": "Settings saved successfully", - "restart_required": restart_required, - } - ) - except Exception as e: - self.logger.error(f"Save settings error: {e}", exc_info=True) - return web.json_response( - {"success": False, "error": "Failed to save settings"}, - status=500, - ) - - @staticmethod - def _validate_settings(data): - """Scalar settings of a payload as ``{CONFIG_ATTR: value}``. - - Raises ValueError with a user-facing message on the first bad value; - nothing is applied until every value passed. - """ - from ..config import PromptManagerConfig - - updates = {} - if "result_timeout" in data: - updates["RESULT_TIMEOUT"] = validate_result_timeout(data["result_timeout"]) - if "webui_display_mode" in data: - mode = data["webui_display_mode"] - if mode not in ("popup", "newtab"): - raise ValueError("webui_display_mode must be 'popup' or 'newtab'") - updates["WEBUI_DISPLAY_MODE"] = mode - if "infinite_scroll" in data: - if not isinstance(data["infinite_scroll"], bool): - raise ValueError("infinite_scroll must be true or false") - updates["INFINITE_SCROLL"] = data["infinite_scroll"] - if "worker_threads" in data: - updates["WORKER_THREADS"] = validate_worker_threads( - data["worker_threads"], PromptManagerConfig.max_worker_threads() - ) - return updates - - def _parse_gallery_roots(self, data): - """Extract and validate gallery roots from a settings payload. - - Accepts ``gallery_root_paths`` (list, preferred) or the legacy single - ``gallery_root_path`` string. Blank entries are dropped. - - Returns: - (roots, error): ``roots`` is the validated list, or ``None`` when - the payload carries no gallery root key; ``error`` is a message - when validation failed (and ``roots`` is then ``None``). - """ - if "gallery_root_paths" in data: - raw = data["gallery_root_paths"] - if not isinstance(raw, list): - return None, "gallery_root_paths must be a list" - elif "gallery_root_path" in data: - raw = [data["gallery_root_path"]] - else: - return None, None - - from ..config import GalleryConfig - - roots = [] - for entry in raw: - if not isinstance(entry, str): - return None, "Gallery root paths must be strings" - entry = entry.strip() - if not entry: - continue - ok, reason = GalleryConfig.validate_gallery_root(entry) - if not ok: - return ( - None, - f"Invalid gallery root '{os.path.basename(entry)}': {reason}", - ) - roots.append(GalleryConfig.resolve_gallery_root(entry)) - return roots, None - - def _apply_gallery_roots(self, roots): - """Install validated gallery roots; returns True when they changed.""" - from ..config import GalleryConfig - - if roots == list(GalleryConfig.MONITORING_DIRECTORIES): - return False - GalleryConfig.MONITORING_DIRECTORIES = roots - self._cached_output_dir = None - self.reset_gallery_roots_cache() - return True - - def _persist_settings(self): - """Merge the user-editable settings into the configured config.json. - - Only the keys this endpoint owns are rewritten; every other section - (integrations, database, performance, ...) and every other key in - ``web_ui``/``gallery`` is kept as it was. A config file that cannot - be parsed is replaced. - """ - from ..config import GalleryConfig, PromptManagerConfig, write_private_json - - config_file = PromptManagerConfig.get_config_path() - existing = self._read_config_file(config_file) - web_ui = dict(existing.get("web_ui") or {}) - web_ui["result_timeout"] = PromptManagerConfig.RESULT_TIMEOUT - web_ui["webui_display_mode"] = PromptManagerConfig.WEBUI_DISPLAY_MODE - web_ui["infinite_scroll"] = PromptManagerConfig.INFINITE_SCROLL - gallery = dict(existing.get("gallery") or {}) - monitoring = dict(gallery.get("monitoring") or {}) - monitoring["directories"] = list(GalleryConfig.MONITORING_DIRECTORIES) - gallery["monitoring"] = monitoring - performance = dict(existing.get("performance") or {}) - performance["worker_threads"] = PromptManagerConfig.WORKER_THREADS - config_data = { - **existing, - "web_ui": web_ui, - "gallery": gallery, - "performance": performance, - } - try: - write_private_json(config_file, config_data) - self.logger.info(f"Settings saved to {config_file}") - except OSError as save_err: - self.logger.warning(f"Could not save config file: {save_err}") - - def _read_config_file(self, config_file): - """Current contents of config.json as a dict, or {} when unusable.""" - try: - with open(config_file, "r", encoding="utf-8") as f: - data = json.load(f) - except (OSError, ValueError): - return {} - return data if isinstance(data, dict) else {} - async def run_diagnostics(self, request): """Run comprehensive system diagnostics and health checks.""" try: @@ -1176,6 +988,11 @@ class AdminRoutesMixin: "vacuum", "cleanup_orphaned_images", ) + # Operations that only read; a run of only these holds the registry as + # "read", every other run as "records". + READ_ONLY_MAINTENANCE_OPERATIONS = frozenset( + {"check_hash_duplicates", "statistics", "check_consistency"} + ) async def run_maintenance(self, request): """Perform database maintenance operations and report each outcome.""" @@ -1188,7 +1005,27 @@ class AdminRoutesMixin: operations = data.get( "operations", list(self.DEFAULT_MAINTENANCE_OPERATIONS) ) + except Exception as e: + self.logger.error(f"Maintenance error: {e}", exc_info=True) + return web.json_response( + {"success": False, "error": "Maintenance failed"}, status=500 + ) + writes = not ( + isinstance(operations, list) + and all( + isinstance(name, str) and name in self.READ_ONLY_MAINTENANCE_OPERATIONS + for name in operations + ) + ) + return await self._run_held( + MAINTENANCE_JOB_KIND, + "Maintenance (admin page)", + "records" if writes else "read", + lambda: self._run_maintenance(operations), + ) + async def _run_maintenance(self, operations): + try: results = await self._run_in_executor( self._run_maintenance_operations, operations ) @@ -1342,8 +1179,18 @@ class AdminRoutesMixin: The upload is streamed to a temp file (capped at restore_max_bytes), verified with PromptModel.verify_database_file and only then copied - over the live database, which is backed up first. + over the live database, which is backed up first. The whole request + holds the registry (records): it is refused while a scan or another + record-changing job runs, and those are refused while it runs. """ + return await self._run_held( + RESTORE_JOB_KIND, + "Database restore (admin page)", + "records", + lambda: self._restore_database(request), + ) + + async def _restore_database(self, request): max_bytes = getattr(self, "restore_max_bytes", 100 * 1024 * 1024) temp_path = None try: diff --git a/py/api/autotag_routes.py b/py/api/autotag_routes.py index 999b941..f72db79 100644 --- a/py/api/autotag_routes.py +++ b/py/api/autotag_routes.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import Optional from urllib.parse import quote +from .jobs import AUTOTAG_BATCH_JOB_KIND from .prompts import thumbnail_url_for from .. import model_gate @@ -63,6 +64,12 @@ def path_is_within(candidate, root) -> bool: return child != parent and child.is_relative_to(parent) +async def _refusal(title): + """The single error event of an auto-tag batch refused by another job.""" + message = f"{title} already running" + yield {"type": "error", "status": "error", "message": message} + + class AutotagRoutesMixin: """Mixin providing auto-tagging API endpoints.""" @@ -425,6 +432,8 @@ class AutotagRoutesMixin: "general_threshold and character_threshold must be numeric" ) + errors = [] # an error event ends the run; Activity lists it failed + async def events(): try: from ..autotag import get_autotag_service @@ -432,14 +441,40 @@ class AutotagRoutesMixin: async for event in self._autotag_batch_events( get_autotag_service(), opts ): + if event.get("type") == "error": + errors.append(event) yield event except Exception: self.logger.exception("AutoTag error") + errors.append(_INTERNAL_ERROR) yield { "type": "error", "message": _INTERNAL_ERROR, } + # The batch writes prompt tags: a records hold on the registry, so it + # never runs beside a scan, restore or the workspace tagging job. + busy = self._jobs.blocker(AUTOTAG_BATCH_JOB_KIND, admission="records") + if busy is not None: + if busy.kind == AUTOTAG_BATCH_JOB_KIND: # unchanged payload + return web.json_response( + {"success": False, "error": _BUSY_ERROR}, status=409 + ) + # Both legacy pages read this route as a stream, where a 409 reads + # as a lost connection: refuse with one error frame instead (the + # admin page keys on ``type``, the gallery on ``status``). + return await self._stream_sse(request, _refusal(busy.title)) + async with self._jobs.hold( + AUTOTAG_BATCH_JOB_KIND, + "Auto-tag images (legacy page)", + admission="records", + ) as job: + response = await self._start_autotag_batch(request, opts, events) + if response.status >= 400 or errors: + job.finish("failed") + return response + + async def _start_autotag_batch(self, request, opts, events): if not _autotag_batch_lock.acquire(blocking=False): return web.json_response( {"success": False, "error": _BUSY_ERROR}, status=409 diff --git a/py/api/images.py b/py/api/images.py index 6edfdc1..69ad96f 100644 --- a/py/api/images.py +++ b/py/api/images.py @@ -19,6 +19,7 @@ from typing import Callable, NamedTuple from aiohttp import web from PIL import Image, UnidentifiedImageError +from .jobs import THUMBNAILS_JOB_KIND from .prompts import bad_request, parse_page_params, publish_image_paths from .prompts import thumbnail_url_for from .prompts import safe_error_message as _safe_error @@ -104,6 +105,10 @@ def _card_header(path: Path) -> tuple: return inspection.width, inspection.height, records +# The legacy gallery page's thumbnail generation, as Activity lists it (D80). +THUMBNAIL_GENERATION_TITLE = "Thumbnail generation (gallery page)" + + class _CardFacts(NamedTuple): """One image file's card facts, derived from its header alone. @@ -1736,11 +1741,24 @@ class ImageRoutesMixin: return cls.THUMBNAIL_SIZES.get(quality, cls.THUMBNAIL_SIZES["medium"]) async def generate_thumbnails(self, request): - """Generate every thumbnail in one executor job (POST, blocking).""" + """Generate every thumbnail in one executor job (POST, blocking). + + Holds the registry as guarded work: refused while a record-changing + job runs, and such jobs are refused while it runs. A body that cannot + be read is refused before the hold, so it is never listed as a job. + """ + data, error = await _json_body_or_error(request) + if error is not None: + return error + return await self._run_held( + THUMBNAILS_JOB_KIND, + THUMBNAIL_GENERATION_TITLE, + "guarded", + lambda: self._generate_thumbnails(data), + ) + + async def _generate_thumbnails(self, data): try: - data, error = await _json_body_or_error(request) - if error is not None: - return error thumbnail_size = self._thumbnail_size(data.get("quality", "medium")) output_dirs = self._get_all_output_dirs() @@ -1935,14 +1953,32 @@ class ImageRoutesMixin: "Connection": "keep-alive", }, ) - await response.prepare(request) + busy = self._jobs.blocker(THUMBNAILS_JOB_KIND, admission="guarded") + if busy is not None: + # The gallery reads this stream: refuse with its error event. + await response.prepare(request) + message = f"{busy.title} already running" + await self._emit_sse( + response, "error", {"error": message, "message": message} + ) + return response + async with self._jobs.hold( + THUMBNAILS_JOB_KIND, THUMBNAIL_GENERATION_TITLE, admission="guarded" + ) as job: + await response.prepare(request) + if not await self._thumbnail_progress_stream(response, thumbnail_size): + job.finish("failed") # an error event ended the stream + return response + + async def _thumbnail_progress_stream(self, response, thumbnail_size) -> bool: + """Stream one generation run; False when it ended with an error event.""" try: output_dirs = self._get_all_output_dirs() if not output_dirs: await self._emit_sse( response, "error", {"error": "ComfyUI output directory not found"} ) - return response + return False await self._emit_sse( response, @@ -1960,6 +1996,7 @@ class ImageRoutesMixin: remaining=plan["remaining"], ) self.invalidate_gallery_cache() + return True except Exception: self.logger.exception("Thumbnail generation failed") await self._emit_sse( @@ -1970,7 +2007,7 @@ class ImageRoutesMixin: "message": "Thumbnail generation failed. Check server logs.", }, ) - return response + return False async def _stream_thumbnails( self, response, targets, thumbnail_size, skipped=0, remaining=0 @@ -2179,8 +2216,17 @@ class ImageRoutesMixin: """Clear generated thumbnails across gallery roots, preserving originals. Invalid thumbnail directories return 400; unavailable gallery roots - return 404, and unexpected filesystem failures return 500. + return 404, and unexpected filesystem failures return 500. Holds the + registry as guarded work, like generation. """ + return await self._run_held( + THUMBNAILS_JOB_KIND, + "Thumbnail clear (gallery page)", + "guarded", + self._clear_thumbnails, + ) + + async def _clear_thumbnails(self) -> web.Response: try: output_dirs = self._get_all_output_dirs() if not output_dirs: diff --git a/py/api/jobs.py b/py/api/jobs.py index 27e4ecb..568a99e 100644 --- a/py/api/jobs.py +++ b/py/api/jobs.py @@ -9,9 +9,21 @@ as a snapshot and then every event published after it, until the job ends. Jobs live only in server memory and are gone after a restart. The registry keeps the running jobs and this session's last ``MAX_FINISHED_JOBS`` finished -ones. Record-changing jobs (the metadata scan, bulk image tagging, and later -maintenance, cleanup and restore) never run together, and one job of a kind -runs at a time. +ones. One job of a kind runs at a time, and each job has an admission class +(``ADMISSIONS``) that decides what may run beside it: + +- ``records`` changes library records or files (the metadata scan, bulk image + tagging, and the legacy restore, maintenance, cleanup, duplicate delete, + LoRA import and batch auto-tag routes). It is refused while any + ``records`` or ``guarded`` job runs. +- ``guarded`` must not overlap record changes but may overlap other guarded + work (thumbnail generation and clearing, later backup). It is refused while + a ``records`` job runs. +- ``read`` only reads; it is refused only by a running job of its own kind. + +A short legacy-page operation takes a ``hold``: a running, non-cancellable +job that lasts as long as the request, so the Activity page lists it and a +workspace job is refused while it runs (and the other way round). """ import asyncio @@ -60,6 +72,41 @@ SCAN_GROUPS = { IMAGE_TAG_JOB_KIND = "image-tags" IMAGE_TAG_GROUP_NAMES = ("tagged", "unchanged", "skipped", "errors") +ADMISSIONS = ("read", "guarded", "records") +# What each admission class is refused beside (besides its own kind). +_REFUSED_BESIDE = { + "read": frozenset(), + "guarded": frozenset({"records"}), + "records": frozenset({"records", "guarded"}), +} + +# Kinds of the legacy-page operations that hold the registry (D80; titles in +# their handlers end in "(admin page)", "(gallery page)" or "(legacy page)"). +RESTORE_JOB_KIND = "restore" +MAINTENANCE_JOB_KIND = "maintenance" +DUPLICATE_CLEANUP_JOB_KIND = "duplicate_cleanup" +THUMBNAILS_JOB_KIND = "thumbnails" +LORA_IMPORT_JOB_KIND = "lora_import" +AUTOTAG_BATCH_JOB_KIND = "autotag" + + +def resolve_admission(admission=None, record_changing=None) -> str: + """The admission class for a start call; ``record_changing`` is an alias. + + ``record_changing=True`` means ``records`` and ``False`` means ``read``; + with neither given the class is ``read``. Raises ValueError for an + unknown class or one that contradicts ``record_changing``. + """ + if admission is None: + return "records" if record_changing else "read" + if admission not in ADMISSIONS: + raise ValueError(f"Unknown job admission {admission!r}") + if record_changing is not None and record_changing != (admission == "records"): + raise ValueError( + f"record_changing={record_changing!r} contradicts admission {admission!r}" + ) + return admission + def _relative_path(path) -> str: """``path`` as a root-relative POSIX path; an absolute one keeps its name.""" @@ -85,13 +132,23 @@ class Job: """One tracked operation: state, counters, outcomes, log and subscribers.""" def __init__( - self, kind, title, *, record_changing=False, scope=None, groups=(), clock=None + self, + kind, + title, + *, + admission=None, + record_changing=None, + cancellable=True, + scope=None, + groups=(), + clock=None, ): self._clock = clock or time.time self.id = uuid.uuid4().hex self.kind = kind self.title = title - self.record_changing = record_changing + self.admission = resolve_admission(admission, record_changing) + self.cancellable = bool(cancellable) self.scope = dict(scope or {}) self.state = "running" self.started_at = self._clock() @@ -110,6 +167,10 @@ class Job: self.task = None self._subscribers = set() + @property + def record_changing(self): + return self.admission == "records" + @property def running(self): return self.state == "running" @@ -240,6 +301,9 @@ class Job: # What detail() lists, so a view re-reads it only when it changed. "listed": {group: len(rows) for group, rows in self.outcomes.items()}, "log_length": len(self.log), + # A view offers Cancel only when the server can stop the job. + "cancellable": self.cancellable, + "admission": self.admission, } def detail(self): @@ -291,33 +355,88 @@ class JobRegistry: self._logger = logger_ or logger self._jobs = {} - def start(self, kind, title, *, record_changing, scope, runner, groups=()) -> Job: + def start( + self, + kind, + title, + *, + admission=None, + record_changing=None, + cancellable=True, + scope=None, + runner, + groups=(), + ) -> Job: """Create a job and run ``runner(job)`` as a task. - Raises JobBusy, naming the job in the way, when a job of ``kind`` is - running or when ``record_changing`` and another record-changing job - runs. Must be called from the event loop. + ``admission`` is one of ``ADMISSIONS``; ``record_changing=True`` is + the older spelling of ``records`` and ``False`` of ``read``. One of + the two is required (TypeError otherwise), so no new job runs as + ``read`` beside a scan by omission. Raises JobBusy, naming the job in + the way, when the admission class refuses it (see the module + docstring). Must be called from the event loop. """ - blocker = self.blocker(kind, record_changing=record_changing) - if blocker is not None: - raise JobBusy(blocker) - self._prune() - job = Job( - kind, title, record_changing=record_changing, scope=scope, groups=groups + if admission is None and record_changing is None: + raise TypeError( + f"JobRegistry.start({kind!r}) needs an admission class " + "(admission= or record_changing=)" + ) + job = self._admit( + kind, + title, + admission=admission, + record_changing=record_changing, + cancellable=cancellable, + scope=scope, + groups=groups, ) - self._jobs[job.id] = job job.task = asyncio.ensure_future(self._run(job, runner)) return job - def blocker(self, kind, *, record_changing): - """The running job a new ``kind`` job would be refused for, or None.""" + def hold(self, kind, title, *, admission, scope=None): + """A running, non-cancellable job for a legacy-page operation. + + Returns an async context manager. Entering it registers the job and + yields it, or raises JobBusy at once, without waiting, when the + admission class refuses it; nothing is registered until then, so a + hold that is never entered leaves no job behind. The job ends when + the block exits: ``finished``, or ``failed`` when it raised (unless + the body already ended it). A caller that answers a refusal in its + own way checks ``blocker(kind, admission=...)`` first, with no await + before the ``async with``. + """ + resolve_admission(admission) # an unknown class fails at the call + return _Hold( + lambda: self._admit( + kind, title, admission=admission, cancellable=False, scope=scope + ) + ) + + def _admit(self, kind, title, *, admission, record_changing=None, **job_args): + resolved = resolve_admission(admission, record_changing) + blocker = self.blocker(kind, admission=resolved) + if blocker is not None: + raise JobBusy(blocker) + self._prune() + job = Job(kind, title, admission=resolved, **job_args) + self._jobs[job.id] = job + return job + + def blocker(self, kind, *, admission=None, record_changing=None): + """The running job a new ``kind`` job would be refused for, or None. + + The same kind refuses first; then the oldest running job whose class + the new job's admission class may not run beside. + """ same_kind = self.active(kind) if same_kind is not None: return same_kind - if record_changing: - for job in self._jobs.values(): - if job.running and job.record_changing: - return job + refused = _REFUSED_BESIDE[resolve_admission(admission, record_changing)] + running = [job for job in self._jobs.values() if job.running] + running.sort(key=lambda job: job._started_seq) + for job in running: + if job.admission in refused: + return job return None async def _run(self, job, runner): @@ -362,11 +481,28 @@ class JobRegistry: def cancel(self, job_id) -> bool: """Ask a running job to stop at its next safe boundary. - Returns whether a cancellation was requested: False for a finished or - unknown job, so a cancel after the end is a no-op. + Returns whether a cancellation was requested: False for a finished, + unknown or non-cancellable job, so a cancel after the end is a no-op. """ job = self._jobs.get(job_id) - if job is None or job.finished: + if job is None or job.finished or not job.cancellable: return False job.cancelled = True return True + + +class _Hold: + """The async context manager ``JobRegistry.hold`` returns.""" + + def __init__(self, admit): + self._admit = admit + self._job = None + + async def __aenter__(self): + self._job = self._admit() + return self._job + + async def __aexit__(self, exc_type, exc, tb): + if self._job.running: + self._job.finish("finished" if exc_type is None else "failed") + return False diff --git a/py/api/lora_integration.py b/py/api/lora_integration.py index 537b35c..7e66c2d 100644 --- a/py/api/lora_integration.py +++ b/py/api/lora_integration.py @@ -5,12 +5,47 @@ from pathlib import Path from aiohttp import web +from ..config_store import ConfigUnreadable, RevisionConflict, get_config_store +from .jobs import LORA_IMPORT_JOB_KIND + +# Flags of POST /lora/enable; each must be a JSON boolean when present. +_LORA_FLAGS = ("enabled", "trigger_words_enabled", "clear_civitai_api_key") + _LORA_PATH_ERROR = ( "LoraManager path must be a directory under ComfyUI's custom_nodes " "whose name contains 'lora'" ) +def _lora_settings_mutation(values): + """The config commit for a LoRA save: only ``integrations.lora_manager``. + + The submitted keys replace their values; other LoRA keys, other + integrations and every other section stay as they are on disk. + """ + + def mutate(config): + integrations = config.get("integrations") + integrations = dict(integrations) if isinstance(integrations, dict) else {} + lora = integrations.get("lora_manager") + lora = dict(lora) if isinstance(lora, dict) else {} + lora.update(values) + integrations["lora_manager"] = lora + config["integrations"] = integrations + return config + + return mutate + + +def _lora_error_frame(message): + """An import stream's error event, as the admin page's modal reads it. + + The modal shows ``status`` and ``progress`` from every frame and closes + on ``type: error``, naming ``message``. + """ + return {"type": "error", "status": message, "progress": 0, "message": message} + + class LoraIntegrationMixin: """Mixin providing LoraManager detection, scanning, and trigger word endpoints.""" @@ -131,14 +166,25 @@ class LoraIntegrationMixin: return allowed, None async def lora_enable(self, request): - """Enable or disable LoraManager integration and save to config.json.""" + """Enable or disable LoraManager integration and save it to config.json. + + The values are committed through the config store first (#220) and + published at runtime only once config.json holds them; a save that + cannot be written reports ``persisted: false`` and changes nothing. + The CivitAI key is never echoed back. + """ try: - data = await request.json() + data, error = await self._read_json_body(request) + if error is not None: + return error + for flag in _LORA_FLAGS: + if flag in data and not isinstance(data[flag], bool): + return self._bad_lora_request(f"{flag} must be true or false") enabled = data.get("enabled", False) path = data.get("path", "") trigger_words = data.get("trigger_words_enabled", False) - from ..config import IntegrationConfig, PromptManagerConfig + from ..config import IntegrationConfig from ..lora_utils import get_trigger_cache # The key is never echoed back to the UI, so a blank or absent @@ -161,20 +207,40 @@ class LoraIntegrationMixin: if error is not None: return error - # Update in-memory config - IntegrationConfig.LORA_MANAGER_ENABLED = enabled - IntegrationConfig.LORA_MANAGER_PATH = path - IntegrationConfig.LORA_TRIGGER_WORDS_ENABLED = trigger_words - IntegrationConfig.CIVITAI_API_KEY = civitai_key + values = { + "enabled": enabled, + "path": path, + "trigger_words_enabled": trigger_words, + "civitai_api_key": civitai_key, + } - # Persist to the configured config.json - PromptManagerConfig.save_to_file() + def publish(_config): + # Under the commit lock, so the last save written is the one + # the runtime keeps. + IntegrationConfig.LORA_MANAGER_ENABLED = enabled + IntegrationConfig.LORA_MANAGER_PATH = path + IntegrationConfig.LORA_TRIGGER_WORDS_ENABLED = trigger_words + IntegrationConfig.CIVITAI_API_KEY = civitai_key + + try: + _saved, revision = await self._run_in_executor( + get_config_store().commit, + _lora_settings_mutation(values), + expected_revision=None, + publish=publish, + ) + except (ConfigUnreadable, RevisionConflict, OSError) as exc: + return self._config_commit_failure(exc) - # Load trigger word cache if enabling cache = get_trigger_cache() if enabled and trigger_words and path: - count = await self._run_in_executor(cache.load, path) - self.logger.info(f"Trigger word cache loaded: {count} LoRAs") + try: + count = await self._run_in_executor(cache.load, path) + self.logger.info(f"Trigger word cache loaded: {count} LoRAs") + except Exception as e: + # Saved and enabled; only the trigger words are missing. + self.logger.warning(f"Trigger word cache could not be loaded: {e}") + cache.clear() elif not enabled: cache.clear() @@ -184,6 +250,10 @@ class LoraIntegrationMixin: "enabled": enabled, "path": self._public_path(path), "trigger_words_enabled": trigger_words, + "persisted": True, + "revision": revision, + "runtime_applied": True, + "cache_loaded": cache.is_loaded, } ) except Exception as e: @@ -218,19 +288,33 @@ class LoraIntegrationMixin: reason="OK", headers={"Content-Type": "text/event-stream"}, ) - await response.prepare(request) async def send_progress(data): line = f"data: {json.dumps(data)}\n\n" await response.write(line.encode("utf-8")) - try: - await self._lora_scan_stream(allowed, send_progress) - except Exception as e: - self.logger.exception("LoRA scan failed") - await send_progress({"type": "error", "message": self._public_error(e)}) + # The import replaces prompts and images: a records hold, so it + # never runs beside a scan, restore or tagging job. + busy = self._jobs.blocker(LORA_IMPORT_JOB_KIND, admission="records") + if busy is not None: + await response.prepare(request) + message = f"{busy.title} already running" + await send_progress(_lora_error_frame(message)) + await response.write_eof() + return response - await response.write_eof() + async with self._jobs.hold( + LORA_IMPORT_JOB_KIND, "LoRA import (admin page)", admission="records" + ) as job: + await response.prepare(request) + try: + await self._lora_scan_stream(allowed, send_progress) + except Exception as e: + self.logger.exception("LoRA scan failed") + job.finish("failed") + await send_progress(_lora_error_frame(self._public_error(e))) + + await response.write_eof() return response except Exception as e: diff --git a/py/api/settings_routes.py b/py/api/settings_routes.py new file mode 100644 index 0000000..258ad2a --- /dev/null +++ b/py/api/settings_routes.py @@ -0,0 +1,339 @@ +"""Settings API routes: GET and POST /prompt_manager/settings (#220). + +A save validates the payload, commits it to config.json through the config +store and publishes it at runtime inside that commit; a save that cannot be +written changes nothing at runtime and says so. ``_config_commit_failure`` +is shared with the LoRA integration's save. +""" + +import os + +from aiohttp import web + +from ..config_store import ( + ConfigUnreadable, + RevisionConflict, + get_config_store, +) +from .admin import _current_image_monitor + +try: + from ...utils.validators import validate_result_timeout, validate_worker_threads +except ImportError: + from utils.validators import validate_result_timeout, validate_worker_threads + + +# Settings owned by POST /settings: config attribute -> (section, key). +_SETTINGS_FILE_KEYS = { + "RESULT_TIMEOUT": ("web_ui", "result_timeout"), + "WEBUI_DISPLAY_MODE": ("web_ui", "webui_display_mode"), + "INFINITE_SCROLL": ("web_ui", "infinite_scroll"), + "WORKER_THREADS": ("performance", "worker_threads"), +} + + +def _section(config, name): + """``config[name]`` as a fresh dict (a non-object section starts empty).""" + value = config.get(name) + return dict(value) if isinstance(value, dict) else {} + + +def _settings_mutation(updates, new_roots): + """The config commit for a settings save: only the submitted keys change. + + Every other section and key (integrations and the CivitAI key, database, + unknown sections) is kept as it is on disk. + """ + + def mutate(config): + for name, value in updates.items(): + section, key = _SETTINGS_FILE_KEYS[name] + merged = _section(config, section) + merged[key] = value + config[section] = merged + if new_roots is not None: + gallery = _section(config, "gallery") + monitoring = _section(gallery, "monitoring") + monitoring["directories"] = list(new_roots) + gallery["monitoring"] = monitoring + config["gallery"] = gallery + return config + + return mutate + + +async def _read_json(request): + """Parse a JSON object body through the package's capped reader.""" + from . import _read_json_body + + return await _read_json_body(request) + + +class SettingsRoutesMixin: + """Mixin providing the settings read and save endpoints.""" + + def _register_settings_routes(self, routes): + @routes.get("/prompt_manager/settings") + async def get_settings_route(request): + return await self.get_settings(request) + + @routes.post("/prompt_manager/settings") + async def save_settings_route(request): + return await self.save_settings(request) + + async def get_settings(self, request): + """Get current settings.""" + try: + from ..config import PromptManagerConfig, GalleryConfig + + root_paths = [ + self._public_path(d) for d in GalleryConfig.MONITORING_DIRECTORIES + ] + revision = await self._run_in_executor(self._config_revision) + return web.json_response( + { + "success": True, + **revision, + "settings": { + "result_timeout": PromptManagerConfig.RESULT_TIMEOUT, + "webui_display_mode": PromptManagerConfig.WEBUI_DISPLAY_MODE, + "infinite_scroll": PromptManagerConfig.INFINITE_SCROLL, + "worker_threads": PromptManagerConfig.WORKER_THREADS, + "cpu_count": PromptManagerConfig.max_worker_threads(), + "gallery_root_paths": root_paths, + "gallery_root_path": root_paths[0] if root_paths else "", + "monitored_directories": [ + self._public_path(d) for d in self._monitored_directories() + ], + }, + } + ) + except Exception as e: + self.logger.error(f"Get settings error: {e}", exc_info=True) + return web.json_response( + {"success": False, "error": "Failed to get settings"}, + status=500, + ) + + def _monitored_directories(self): + """Directories the running image monitor watches, else the configured roots.""" + from ..config import GalleryConfig + + monitor = _current_image_monitor() + if monitor is not None: + return list(getattr(monitor, "monitored_directories", [])) + return list(GalleryConfig.MONITORING_DIRECTORIES) + + async def save_settings(self, request): + """Save settings: validate, commit to config.json, then publish (#220). + + Nothing changes at runtime until config.json holds the new values, so + a save that cannot be written reports ``persisted: false`` and leaves + the running settings as they were. ``expected_revision`` (the + ``revision`` from GET) refuses to overwrite a file changed meanwhile; + without it the payload merges into what is on disk (legacy page). + """ + try: + data, error_response = await _read_json(request) + if error_response is not None: + return error_response + + expected_revision = data.get("expected_revision") + if expected_revision is not None and not isinstance(expected_revision, str): + return web.json_response( + {"success": False, "error": "expected_revision must be a string"}, + status=400, + ) + try: + updates = self._validate_settings(data) + except ValueError as ve: + return web.json_response( + {"success": False, "error": str(ve)}, status=400 + ) + + new_roots, error = self._parse_gallery_roots(data) + if error: + return web.json_response({"success": False, "error": error}, status=400) + + published = {} + + def publish(_config): + # Under the commit lock: saves reach the runtime in the order + # they were written, so the runtime ends on the last one. + published.update(self._publish_settings(updates, new_roots)) + + try: + _saved, revision = await self._run_in_executor( + get_config_store().commit, + _settings_mutation(updates, new_roots), + expected_revision=expected_revision, + publish=publish, + ) + except (ConfigUnreadable, RevisionConflict, OSError) as exc: + return self._config_commit_failure(exc) + + return web.json_response( + { + "success": True, + "message": "Settings saved successfully", + "restart_required": bool(published["restart_required_fields"]), + "persisted": True, + "revision": revision, + **published, + } + ) + except Exception as e: + self.logger.error(f"Save settings error: {e}", exc_info=True) + return web.json_response( + {"success": False, "error": "Failed to save settings"}, + status=500, + ) + + def _publish_settings(self, updates, new_roots): + """Install committed settings at runtime (inside the config commit). + + The values are already durable. A publication that fails is reported + as ``runtime_applied: false`` with the fields that need a restart; + nothing is retried or rolled back. Never raises. + """ + from ..config import PromptManagerConfig + + for name, value in updates.items(): + setattr(PromptManagerConfig, name, value) + restart_fields = [] + runtime_applied = True + if new_roots is not None: + try: + if self._apply_gallery_roots(new_roots): + restart_fields.append("gallery_root_paths") + except Exception as e: + self.logger.error( + f"Saved gallery roots could not be applied: {e}", exc_info=True + ) + runtime_applied = False + restart_fields.append("gallery_root_paths") + return { + "runtime_applied": runtime_applied, + "restart_required_fields": restart_fields, + } + + def _config_commit_failure(self, exc): + """The response for a config commit that wrote nothing.""" + not_saved = {"success": False, "persisted": False, "runtime_applied": False} + if isinstance(exc, ConfigUnreadable): + self.logger.warning(f"Settings not saved: {exc}") + return web.json_response( + { + **not_saved, + "code": "config-unreadable", + "error": ( + "config.json cannot be read, so nothing was saved. " + "Fix or remove the file, then save again." + ), + }, + status=409, + ) + if isinstance(exc, RevisionConflict): + return web.json_response( + { + **not_saved, + "code": "settings-changed", + "error": "Settings changed since they were loaded. Reload them.", + "revision": exc.current, + }, + status=409, + ) + self.logger.error(f"Could not write config.json: {exc}") + return web.json_response( + { + **not_saved, + "code": "config-write-failed", + "error": "Settings could not be written to config.json.", + }, + status=500, + ) + + @staticmethod + def _config_revision(): + """``{"revision": ...}``, or a null revision with ``config_error``.""" + try: + _data, revision = get_config_store().read() + except ConfigUnreadable: + return {"revision": None, "config_error": "config-unreadable"} + return {"revision": revision} + + @staticmethod + def _validate_settings(data): + """Scalar settings of a payload as ``{CONFIG_ATTR: value}``. + + Raises ValueError with a user-facing message on the first bad value; + nothing is applied until every value passed. + """ + from ..config import PromptManagerConfig + + updates = {} + if "result_timeout" in data: + updates["RESULT_TIMEOUT"] = validate_result_timeout(data["result_timeout"]) + if "webui_display_mode" in data: + mode = data["webui_display_mode"] + if mode not in ("popup", "newtab"): + raise ValueError("webui_display_mode must be 'popup' or 'newtab'") + updates["WEBUI_DISPLAY_MODE"] = mode + if "infinite_scroll" in data: + if not isinstance(data["infinite_scroll"], bool): + raise ValueError("infinite_scroll must be true or false") + updates["INFINITE_SCROLL"] = data["infinite_scroll"] + if "worker_threads" in data: + updates["WORKER_THREADS"] = validate_worker_threads( + data["worker_threads"], PromptManagerConfig.max_worker_threads() + ) + return updates + + def _parse_gallery_roots(self, data): + """Extract and validate gallery roots from a settings payload. + + Accepts ``gallery_root_paths`` (list, preferred) or the legacy single + ``gallery_root_path`` string. Blank entries are dropped. + + Returns: + (roots, error): ``roots`` is the validated list, or ``None`` when + the payload carries no gallery root key; ``error`` is a message + when validation failed (and ``roots`` is then ``None``). + """ + if "gallery_root_paths" in data: + raw = data["gallery_root_paths"] + if not isinstance(raw, list): + return None, "gallery_root_paths must be a list" + elif "gallery_root_path" in data: + raw = [data["gallery_root_path"]] + else: + return None, None + + from ..config import GalleryConfig + + roots = [] + for entry in raw: + if not isinstance(entry, str): + return None, "Gallery root paths must be strings" + entry = entry.strip() + if not entry: + continue + ok, reason = GalleryConfig.validate_gallery_root(entry) + if not ok: + return ( + None, + f"Invalid gallery root '{os.path.basename(entry)}': {reason}", + ) + roots.append(GalleryConfig.resolve_gallery_root(entry)) + return roots, None + + def _apply_gallery_roots(self, roots): + """Install validated gallery roots; returns True when they changed.""" + from ..config import GalleryConfig + + if roots == list(GalleryConfig.MONITORING_DIRECTORIES): + return False + GalleryConfig.MONITORING_DIRECTORIES = roots + self._cached_output_dir = None + self.reset_gallery_roots_cache() + return True diff --git a/py/config_store.py b/py/config_store.py new file mode 100644 index 0000000..949423a --- /dev/null +++ b/py/config_store.py @@ -0,0 +1,197 @@ +"""The one serialized writer of config.json (#220). + +Every save that changes config.json goes through ``ConfigStore.commit``: the +settings page, the LoRA integration and, later, every System section. A +commit runs under one process-wide lock, re-reads the file inside it, applies +the caller's change to a copy and replaces the file atomically. So two saves +never overwrite each other's keys, sections this process does not know are +kept byte-for-byte in meaning, and a write that fails leaves the old file in +place and raises instead of being logged and reported as saved. + +A revision names the file content a client last saw: ``"sha256:"`` of +the file bytes, or ``"absent"`` when there is no file. A commit that names a +revision refuses to write over different content (``RevisionConflict``). + +Reads take the same lock, so a read handle is never open while the file is +replaced (Windows refuses that), and a caller's ``publish`` runs inside the +lock right after its write, so runtimes publish saves in the order they were +written. A symlinked config.json is written beside its real target. + +config.json can hold the CivitAI key, so the file is written owner-only. +""" + +import copy +import hashlib +import json +import os +import tempfile +import threading +import time +from typing import Callable, Optional, Tuple + +ABSENT_REVISION = "absent" + +# One lock for the process: every ConfigStore writes the same file. It is +# re-entrant so a commit's ``publish`` callback may read the config again. +_COMMIT_LOCK = threading.RLock() + +# Windows: an antivirus, indexer or backup handle on config.json makes +# os.replace fail with PermissionError for a moment, so it is retried briefly. +_REPLACE_ATTEMPTS = 5 if os.name == "nt" else 1 +_REPLACE_BACKOFF_SECONDS = 0.05 + + +class ConfigUnreadable(Exception): + """config.json exists but is not a readable JSON object; it is left alone.""" + + +class RevisionConflict(Exception): + """config.json changed since the revision the caller saw.""" + + def __init__(self, current: str): + super().__init__("config.json changed since it was read") + self.current = current + + +def revision_of(data: Optional[bytes]) -> str: + """The revision of a file's bytes; ``None`` means the file is absent.""" + if data is None: + return ABSENT_REVISION + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def _default_path() -> str: + from .config import PromptManagerConfig + + return PromptManagerConfig.get_config_path() + + +def _replace(source: str, target: str) -> None: + """``os.replace`` with a short retry on a transient Windows PermissionError.""" + for attempt in range(1, _REPLACE_ATTEMPTS + 1): + try: + os.replace(source, target) + return + except PermissionError: + if attempt == _REPLACE_ATTEMPTS: + raise + time.sleep(_REPLACE_BACKOFF_SECONDS) + + +def _write_atomically(path: str, payload: bytes) -> None: + """Replace ``path`` with ``payload``: temp file beside it, fsync, replace. + + The descriptor is closed before ``os.replace`` (Windows refuses to replace + over an open handle), and on any failure the temp file is closed and + removed, so a failed write leaves the old file and no stray temp file. + ``path`` is resolved first: a symlink keeps pointing at the updated + target. A single-file bind mount cannot be replaced (EBUSY) and fails as + an OSError, which callers report as not saved. + """ + path = os.path.realpath(path) + parent = os.path.dirname(path) or "." + os.makedirs(parent, exist_ok=True) + fd, temp_path = tempfile.mkstemp(dir=parent, prefix=".config-", suffix=".tmp") + try: + try: + handle = os.fdopen(fd, "wb") + except BaseException: + os.close(fd) + raise + with handle: # closed before the replace, also when a write fails + handle.write(payload) + handle.flush() + os.fsync(handle.fileno()) + if os.name == "posix": + os.chmod(temp_path, 0o600) + _replace(temp_path, path) + except BaseException: + try: + os.unlink(temp_path) + except OSError: + pass + raise + + +class ConfigStore: + """Serialized, atomic reads and commits of config.json.""" + + def __init__(self, path_getter: Optional[Callable[[], str]] = None): + self._path_getter = path_getter or _default_path + + @property + def path(self) -> str: + """The config file this store reads and writes (resolved per call).""" + return self._path_getter() + + def read(self) -> Tuple[dict, str]: + """``(config, revision)``; a missing file is ``({}, "absent")``. + + Raises ConfigUnreadable when the file cannot be read, is not JSON, or + is not a JSON object. Waits for a running commit (see the module + docstring). + """ + with _COMMIT_LOCK: + return self._read(self.path) + + @staticmethod + def _read(path: str) -> Tuple[dict, str]: + try: + with open(path, "rb") as handle: + raw = handle.read() + except (FileNotFoundError, NotADirectoryError): + # No file there (a parent that is a file cannot hold one either); + # a commit then fails on the write, as config-write-failed. + return {}, ABSENT_REVISION + except OSError as exc: + raise ConfigUnreadable(f"config.json cannot be read: {exc}") from exc + try: + data = json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, ValueError) as exc: + raise ConfigUnreadable("config.json is not valid JSON") from exc + if not isinstance(data, dict): + raise ConfigUnreadable("config.json is not a JSON object") + return data, revision_of(raw) + + def commit( + self, + mutate: Callable[[dict], dict], + *, + expected_revision: Optional[str], + publish: Optional[Callable[[dict], None]] = None, + ) -> Tuple[dict, str]: + """Apply ``mutate`` to the current config and write the result. + + ``mutate`` receives a deep copy of the file's content and returns the + whole new config (an object). ``expected_revision`` None merges into + whatever is on disk (legacy partial updates); a revision refuses to + write over different content. Returns ``(config, revision)`` as + written. Raises ConfigUnreadable, RevisionConflict, TypeError (a + non-object result) or OSError (the write failed; the old file stays). + + ``publish(config)``, when given, runs under the lock once the file + holds the new config, so concurrent saves reach the runtime in the + order they were written. It must handle its own errors: the values + are already durable when it runs. + """ + with _COMMIT_LOCK: + path = self.path + current, revision = self._read(path) + if expected_revision is not None and expected_revision != revision: + raise RevisionConflict(revision) + candidate = mutate(copy.deepcopy(current)) + if not isinstance(candidate, dict): + raise TypeError("A config commit must produce a JSON object") + payload = json.dumps(candidate, indent=2).encode("utf-8") + _write_atomically(path, payload) + if publish is not None: + publish(candidate) + return candidate, revision_of(payload) + + +_store = ConfigStore() + + +def get_config_store() -> ConfigStore: + """The process's config store (on ``PromptManagerConfig.get_config_path``).""" + return _store diff --git a/tests/browser/concept-inventory.mjs b/tests/browser/concept-inventory.mjs index 3912286..f7cba70 100644 --- a/tests/browser/concept-inventory.mjs +++ b/tests/browser/concept-inventory.mjs @@ -78,6 +78,17 @@ export async function seedConcept(request,data={}) { return response.json(); } +/** + * Seed the concept fixture and fix the browser's clock at the fixture's own instant (`manifest.now`), so the page and + * the fixture read one "now": a prompt used now reads "Used today" and the caption names the fixture's day even when + * the run crosses midnight between seeding and rendering. Timers keep running; only `Date` stands still. + */ +export async function seedConceptAt(page,request,data={}) { + const manifest=await seedConcept(request,data); + await page.clock.setFixedTime(new Date(manifest.now*1000)); + return manifest; +} + /** * Save `tags` as the current image tag set of the listed file at `path` (root 0) through the real observe and tag * routes (#249 PR C), releasing the capture afterwards; returns the receipt's saved names. diff --git a/tests/browser/full-inspector.spec.mjs b/tests/browser/full-inspector.spec.mjs index f1268b1..e4efc5a 100644 --- a/tests/browser/full-inspector.spec.mjs +++ b/tests/browser/full-inspector.spec.mjs @@ -237,9 +237,12 @@ const viewerStructure=details=>details.evaluate(node=>{ test('viewer details hold provenance, raw records and downloads',async({page,request})=>{ await page.setViewportSize({width:1600,height:1100});const reads=await patchInspection(page); await fullFixture(page,request);let details=await viewerDetails(page,'complete.png'); - // The panel content without the cover, then the two disclosures, then the pair send and history buttons (D20). + // The panel content without the cover, D117's Copy settings summary after the facts, then the two disclosures, + // then the pair send and history buttons (D20). expect(await viewerStructure(details)).toEqual({outer:['div','button.send','button'], - inner:['div.panel-heading','p.small','div.tags','section.prompt-section.positive','section.prompt-section.negative','div.tags','dl.meta-grid','details','details']}); + inner:['div.panel-heading','p.small','div.tags','section.prompt-section.positive','section.prompt-section.negative','div.tags','dl.meta-grid','button.settings-summary','details','details']}); + await expect(details.locator(':scope > div > button')).toHaveText('Copy settings summary'); + await expect(details.locator(':scope > div > button.settings-summary')).toHaveCSS('white-space','normal'); await expect(details.locator('.detail-cover,.detail-inner,.detail-footer')).toHaveCount(0); await expect(details.locator('.panel-heading h2')).toHaveText(POSITIVE_TITLE);await expect(details.locator('.panel-heading .chip')).toHaveText('PNG'); await expect(details.locator(':scope > div > p.small')).toHaveText('complete.png'); diff --git a/tests/browser/images-concept.spec.mjs b/tests/browser/images-concept.spec.mjs index 52e5d0b..08479b3 100644 --- a/tests/browser/images-concept.spec.mjs +++ b/tests/browser/images-concept.spec.mjs @@ -1,7 +1,7 @@ import {writeFile} from 'node:fs/promises'; import {test,expect} from './fixtures.mjs'; import {captureCanonicalState} from './history-canonical-comparison.mjs'; -import {SIZES,THEMES,MOTIONS,seedConcept,openConcept,openProduction,inventoryOf,diffInventory,mutate,stripExact,countCopy,reflow,withoutItems} from './concept-inventory.mjs'; +import {SIZES,THEMES,MOTIONS,seedConceptAt,openConcept,openProduction,inventoryOf,diffInventory,mutate,stripExact,countCopy,reflow,withoutItems} from './concept-inventory.mjs'; // Task C7 of #249: the production Images page against the immutable concept, on the concept-shaped real fixture (24 // images of the concept's six prompts, its seeds, sizes and favorites). Heading, toolbar, filter row, caption, grid, @@ -85,7 +85,7 @@ function listInfo(card,actionsWidth) { /** Recorded Images deviations only (spec §8); everything else in the concept inventory must equal production. */ const nocase=(a,b)=>{const x=a.toLowerCase(),y=b.toLowerCase();return xy?1:ab?1:0;}; -export function mapImagesConcept(reference,{manifest,now=new Date()}) { +export function mapImagesConcept(reference,{manifest,now=new Date(manifest.now*1000)}) { const paths=new Map(manifest.images.map(image=>[image.filename,image.path])); // D14: the sort appends production's two sorts. Save view is the concept's own (saved views, #249 D16 retired). const toolbar={...reference.toolbar}; @@ -318,7 +318,7 @@ async function behaviorsOf(page,scope) { return {hover,opened,viewerTitle,focusAfterViewer,selected,layouts,search,single}; } -test.beforeEach(async({request},testInfo)=>{testInfo.manifest=await seedConcept(request);}); +test.beforeEach(async({page,request},testInfo)=>{testInfo.manifest=await seedConceptAt(page,request);}); for(const [width,height] of SIZES)for(const theme of THEMES)test(`images page geometry, tokens, controls and behaviors match the concept ${width}x${height} ${theme}`,async({page,request},testInfo)=>{ test.setTimeout(240_000); @@ -348,7 +348,7 @@ for(const [width,height] of SIZES)for(const theme of THEMES)test(`images page ge importDiff:diffInventory(mapImportConcept(referenceImport),actualImport),referenceBehaviors,actualBehaviors}; } // Load more in production: a fixture past one listing page appends one batch and shows the concept's toast. - testInfo.manifest=await seedConcept(request,{extra_images:LOAD_MORE_EXTRA}); + testInfo.manifest=await seedConceptAt(page,request,{extra_images:LOAD_MORE_EXTRA}); for(const motion of MOTIONS) { const total=FIXTURE_IMAGES+LOAD_MORE_EXTRA; await openProductionLoaded(page,theme,motion,total); @@ -514,7 +514,7 @@ test('one appended image reads in the singular',async({page,request})=>{ test.setTimeout(120_000); await page.setViewportSize({width:1600,height:1100}); const total=PRODUCTION_BATCH+1; - await seedConcept(request,{extra_images:total-FIXTURE_IMAGES}); + await seedConceptAt(page,request,{extra_images:total-FIXTURE_IMAGES}); await openProductionLoaded(page,'dark','no-preference',total); await expect(page.locator('.infinite-continuation > p').first()).toHaveText(countCopy(`${PRODUCTION_BATCH} of ${total} images · more load as you scroll`)); // A dispatched press: scrolling the button into view lets the page's own observer load the one image first. @@ -530,7 +530,7 @@ test('one image appended by scrolling reads in the singular',async({page,request test.setTimeout(120_000); await page.setViewportSize({width:1600,height:1100}); const total=PRODUCTION_BATCH+1; - await seedConcept(request,{extra_images:total-FIXTURE_IMAGES}); + await seedConceptAt(page,request,{extra_images:total-FIXTURE_IMAGES}); await openProductionLoaded(page,'dark','no-preference',total); await expect(page.locator('#result-count')).toHaveText(new RegExp(`^${PRODUCTION_BATCH} of ${total} images`)); await page.locator('.infinite-continuation').scrollIntoViewIfNeeded(); @@ -551,7 +551,7 @@ test('one footer height for the collection (D3)',async({page,request})=>{ const reference=await inventoryOf(page,'#main'); expect(reference.grid.columns).toBe(2); expect(reference.cards.every(card=>card.info.p.height>parseFloat(card.info.p.lineHeight))).toBe(true); - const manifest=await seedConcept(request,{seedless_images:2,short_seedless:true}); + const manifest=await seedConceptAt(page,request,{seedless_images:2,short_seedless:true}); await openProductionLoaded(page,'dark','no-preference',FIXTURE_IMAGES+2); const actual=await inventoryOf(page,'#workspace-main'); const box=card=>({left:card.left,top:card.top,width:card.width,height:card.height}); diff --git a/tests/browser/inspect-concept.spec.mjs b/tests/browser/inspect-concept.spec.mjs index 509cc44..48be585 100644 --- a/tests/browser/inspect-concept.spec.mjs +++ b/tests/browser/inspect-concept.spec.mjs @@ -117,7 +117,7 @@ function measureViewer() { ['dl.meta-grid', node => node.closest('details') ? null : 'facts'], ['details > summary', node => text(node)], ['button.send', node => node.closest('.prompt-section') ? `send ${node.closest('.prompt-section').matches('.positive') ? 'positive' : 'negative'}` : 'send pair'], - ['button', node => /Prompt history/.test(node.textContent) ? 'Prompt history' : null], + ['button', node => /Prompt history/.test(node.textContent) ? 'Prompt history' : text(node) === 'Copy settings summary' ? 'settings summary' : null], ]; const sections = [...rail.querySelectorAll('*')].map(node => { for (const [selector, label] of SECTIONS) if (node.matches(selector)) return label(node); @@ -158,13 +158,18 @@ async function withProductionCopy(page) { }, { COPY, ACCEPT }); } -/** D66: the viewer of a local file is titled by its filename and has no tag chips and no Prompt history. */ +/** + * D66: the viewer of a local file is titled by its filename and has no tag chips and no Prompt history. D117 (LI-4): + * "Copy settings summary" follows the six facts. + */ function mapViewerConcept(reference) { + const sections = reference.sections.filter(section => section !== 'tag chips' && section !== 'Prompt history'); + const facts = sections.indexOf('facts'); return { ...reference, title: FILE, railTitle: FILE, - sections: reference.sections.filter(section => section !== 'tag chips' && section !== 'Prompt history'), + sections: [...sections.slice(0, facts + 1), 'settings summary', ...sections.slice(facts + 1)], }; } @@ -269,9 +274,9 @@ for (const [width, height] of SIZES) for (const theme of THEMES) { // The chooser names the chosen file after a real setInputFiles, as the concept's does. expect(production.initial.chosen).toEqual({ name: '', files: [] }); expect(production.selected.chosen).toEqual({ name: FILE, files: [FILE] }); - // D66 in the rail: no tag chips and no Prompt history; the title is the filename. + // D66 in the rail: no tag chips and no Prompt history; the title is the filename. D117 follows the facts. expect(production.viewer.sections).toEqual(['heading', 'filename', 'badges', 'positive', 'send positive', 'negative', - 'send negative', 'facts', 'File & provenance', 'Workflow & raw metadata', 'send pair']); + 'send negative', 'facts', 'settings summary', 'File & provenance', 'Workflow & raw metadata', 'send pair']); }); } diff --git a/tests/browser/inspector-concept.spec.mjs b/tests/browser/inspector-concept.spec.mjs index 1c8f77e..0a43d7f 100644 --- a/tests/browser/inspector-concept.spec.mjs +++ b/tests/browser/inspector-concept.spec.mjs @@ -45,10 +45,10 @@ function withCategoryScope(inventory) { } /** - * Recorded inspector deviations only (spec §8, TESTING.md D3, D20–D22, D34 and D38); everything else must equal - * production. `reference.viewerPlain` is the concept's own full details - * with its tag chips made plain (D20) and, under a coarse pointer, D34's targets, both at runtime: the browser lays out - * what follows them, as the Images spec's removed toolbar items do. + * Recorded inspector deviations only (spec §8, TESTING.md D3, D20–D22, D34, D38 and D117); everything else must + * equal production. `reference.viewerPlain` is the concept's own full details + * with its tag chips made plain (D20), D117's summary button added and, under a coarse pointer, D34's targets, all at + * runtime: the browser lays out what follows them, as the Images spec's removed toolbar items do. */ export function mapInspectorConcept(reference,{viewport}) { const {panel,viewerPlain,fileControls}=reference; @@ -105,7 +105,11 @@ async function openProductionPanel(page,theme) { await settle(page); } -/** The concept's tag chips in the open viewer as plain `span.chip`s (D20) and D34's coarse targets; returns an undo. */ +/** + * The concept's tag chips in the open viewer as plain `span.chip`s (D20), D34's coarse targets and D117's "Copy + * settings summary" after the six facts (LI-4), drawn as the concept's own plain button (its "Download sample JSON", + * without its action); returns an undo. + */ async function plainViewerChips(page) { const restoreTargets=await mutate(page,{css:D34_COARSE_TARGETS}); await page.evaluate(()=>{ @@ -113,10 +117,15 @@ async function plainViewerChips(page) { const span=document.createElement('span');span.className='chip';span.textContent=button.textContent; button.replaceWith(span);return [span,button]; }); + const summary=document.querySelector('#viewer-details [data-action="download-workflow"]').cloneNode(false); + for(const key of Object.keys(summary.dataset))delete summary.dataset[key]; + summary.textContent='Copy settings summary';summary.classList.add('settings-summary'); + document.querySelector('#viewer-details > div > dl.meta-grid').after(summary);window.__summaryButton=summary; }); await settle(page); return async()=>{ - await page.evaluate(()=>{for(const [span,button] of window.__plainChips)span.replaceWith(button);delete window.__plainChips;}); + await page.evaluate(()=>{for(const [span,button] of window.__plainChips)span.replaceWith(button);delete window.__plainChips; + window.__summaryButton.remove();delete window.__summaryButton;}); await restoreTargets(); }; } @@ -342,7 +351,7 @@ for(const [width,height] of SIZES)test(`image tags saved on the concept image ke // The comparison is only worth something if it notices a change (spec §7 mutation check): on the panel one CSS // mutation (the role label's letter spacing) and one DOM mutation (the footer removed); in the viewer (final review m5) -// its pair actions removed and D38's Show in folder removed; each applied after an exact match. +// its pair actions removed, D38's Show in folder removed and D117's summary removed; each applied after an exact match. test('inspector comparison detects mutations',async({page})=>{ test.setTimeout(120_000); const viewport={width:1600,height:1100}; @@ -366,6 +375,9 @@ test('inspector comparison detects mutations',async({page})=>{ expect(()=>assertInspectorMatch(reference,withoutActions,viewport)).toThrow(/viewerDetails\.actions/); const withoutReveal=await viewerMutated({remove:['#viewer-details button[aria-label$=" in folder"]']}); expect(()=>assertInspectorMatch(reference,withoutReveal,viewport)).toThrow(/fileControls/); + // D117: the summary action after the facts is part of the mapped concept, so losing it is a difference. + const withoutSummary=await viewerMutated({remove:['#viewer-details > div > button']}); + expect(()=>assertInspectorMatch(reference,withoutSummary,viewport)).toThrow(/viewerDetails\.structure\.inner/); assertInspectorMatch(reference,await viewerMutated(),viewport); await mutate(page,{remove:['#image-inspector > .detail-footer']}); diff --git a/tests/browser/library-concept.spec.mjs b/tests/browser/library-concept.spec.mjs index eeab9b9..8e85bae 100644 --- a/tests/browser/library-concept.spec.mjs +++ b/tests/browser/library-concept.spec.mjs @@ -1,7 +1,7 @@ import {writeFile} from 'node:fs/promises'; import {test,expect} from './fixtures.mjs'; import {captureCanonicalState} from './history-canonical-comparison.mjs'; -import {SIZES,THEMES,MOTIONS,seedConcept,openConcept,openProduction,inventoryOf,diffInventory,mutate,stripExact,countCopy,reflow} from './concept-inventory.mjs'; +import {SIZES,THEMES,MOTIONS,seedConceptAt,openConcept,openProduction,inventoryOf,diffInventory,mutate,stripExact,countCopy,reflow} from './concept-inventory.mjs'; // Task E3 of #249: the production Prompt library shelf against the immutable concept (app.js renderLibrary a572–586, // style.css s648–708), on the concept-shaped real fixture (the concept's six prompts with their negatives, categories, @@ -37,7 +37,8 @@ export function mapLibraryConcept(reference) { // D43: tag chips come in the store's alphabetical order; the row flows again from the concept's own chips. tags:sortedTags(card.tags), // D23: the stars are aria-hidden, with a visually hidden "Rated N out of 5" spoken in their place. The fixture's - // run_count (4) and last_used_at (now) give the concept's own "4 runs" and "Used today". The two-line clamp is + // run_count (4) and last_used_at (its own "now", where the page's clock stands) give the concept's own "4 runs" + // and "Used today". The two-line clamp is // applied below through `clampedCards`, measured on the concept with `CLAMP_CSS` (its samples wrap at 390px). footer:{...card.footer,facts:{...card.footer.facts,spoken:card.footer.facts.spoken.replace(/^(★+)/,stars=>`Rated ${stars.length} out of 5`)}}})); return {...reference,library:{...library,toolbar, @@ -238,7 +239,8 @@ async function openHost(context) { } test.beforeEach(async({page,request},testInfo)=>{ - testInfo.manifest=await seedConcept(request); + // The page's clock stands at the fixture's own "now", so its last use reads "Used today" on either side of midnight. + testInfo.manifest=await seedConceptAt(page,request); // Both documents copy through the clipboard; the stub records what they wrote. await page.addInitScript(()=>{window.copied=[];Object.defineProperty(navigator,'clipboard',{configurable:true,value:{writeText:async text=>{window.copied.push(text);}}});}); }); @@ -322,6 +324,27 @@ for(const theme of THEMES)test(`library grid and cards match the concept 1800x11 expect(compare(reference,actual,{sections:['library'],library:['grid','cards','trailing']})).toEqual([]); }); +// PR #284's CI run: the fixture recorded the last use at 23:59:5x UTC and the shelf rendered at 00:00:05, so production +// read "Used yesterday" against the concept's "Used today". Here the browser's wall clock starts at 23:59:58 UTC, the +// fixture records its use at that instant, and midnight passes before either document opens. +const BEFORE_MIDNIGHT=Date.UTC(2026,9,22,23,59,58); +const PAST_MIDNIGHT_MS=5_000; +test('a run that crosses midnight still reads the concept fixture\'s day',async({page,request})=>{ + await page.setViewportSize({width:1800,height:1100}); + await page.clock.install({time:BEFORE_MIDNIGHT}); + const manifest=await seedConceptAt(page,request,{now:BEFORE_MIDNIGHT/1000}); + expect(manifest.now*1000,'the fixture\'s now').toBe(BEFORE_MIDNIGHT); + const used=(await (await request.get('/prompt_manager/library')).json()).items.map(item=>Date.parse(item.last_used_at)); + expect(used,'every last use at the fixture\'s now').toEqual(Array(CONCEPT_PROMPTS).fill(BEFORE_MIDNIGHT)); + await page.clock.fastForward(PAST_MIDNIGHT_MS); + await openConceptLibrary(page,'dark','no-preference'); + const reference=await conceptInventory(page); + await openProductionLibrary(page,'dark','no-preference'); + const actual=await inventoryOf(page,'#workspace-main'); + expect(actual.library.cards.map(card=>card.footer.facts.text.replace(/^★+ · /,''))).toEqual(Array(CONCEPT_PROMPTS).fill('Used today')); + expect(compare(reference,actual,{sections:['library'],library:['grid','cards','trailing']})).toEqual([]); +}); + // The comparison is only worth something if it notices a change (spec §7 mutation check): one CSS mutation (the footer // padding) and one DOM mutation (the negative-variants chip removed), each applied after an exact match. test('library comparison detects mutations',async({page})=>{ diff --git a/tests/browser/server.py b/tests/browser/server.py index 9764aa7..212109e 100644 --- a/tests/browser/server.py +++ b/tests/browser/server.py @@ -23,6 +23,7 @@ from tests.browser.workspace_activity_fixture import register_activity_commands from tests.browser.workspace_concept_fixture import ( MAX_EXTRA_IMAGES as CONCEPT_MAX_EXTRA_IMAGES, MAX_SEEDLESS_IMAGES as CONCEPT_MAX_SEEDLESS_IMAGES, + MAX_EPOCH_SECONDS as CONCEPT_MAX_EPOCH_SECONDS, seed_workspace_concept, ) from tests.browser.workspace_visual_fixture import seed_workspace_visual @@ -489,8 +490,17 @@ def create_app(root: Path) -> web.Application: if ( request.query or not isinstance(data, dict) - or set(data) - set(limits) - {"short_seedless"} + or set(data) - set(limits) - {"short_seedless", "now"} or not isinstance(data.get("short_seedless", False), bool) + # A chosen "now" (epoch seconds) pins every time the fixture writes. + or ( + "now" in data + and ( + isinstance(data["now"], bool) + or not isinstance(data["now"], (int, float)) + or not 0 < data["now"] < CONCEPT_MAX_EPOCH_SECONDS + ) + ) or any( isinstance(data.get(key, 0), bool) or not isinstance(data.get(key, 0), int) @@ -510,6 +520,7 @@ def create_app(root: Path) -> web.Application: extra_images=data.get("extra_images", 0), seedless_images=data.get("seedless_images", 0), short_seedless=data.get("short_seedless", False), + now=data.get("now"), ) ) return web.json_response({"success": True, **manifest}) diff --git a/tests/browser/shell-concept.spec.mjs b/tests/browser/shell-concept.spec.mjs index a4076d7..1497631 100644 --- a/tests/browser/shell-concept.spec.mjs +++ b/tests/browser/shell-concept.spec.mjs @@ -178,27 +178,28 @@ async function captureFooter(page,testInfo,name) { test.beforeEach(async({request})=>{await seedConcept(request);}); -for(const [width,height] of SIZES)for(const theme of THEMES)test(`shell geometry, tokens and controls match the concept ${width}x${height} ${theme}`,async({page},testInfo)=>{ +// One test per route, size, theme and motion. All of them in one test per size and theme (12 page loads, 12 captures, +// 36 hovers) steadily took 22 s median and up to 33 s on CI WebKit, past the 30 s budget with no step stalled; one +// route in one motion is a sixth of that work under the same budget. +for(const [route,breadcrumb] of ROUTES)for(const [width,height] of SIZES)for(const theme of THEMES)for(const motion of MOTIONS)test(`shell geometry, tokens and controls match the concept on ${route} ${width}x${height} ${theme} ${motion}`,async({page},testInfo)=>{ await page.setViewportSize({width,height});const result={}; - for(const motion of MOTIONS)for(const [route,breadcrumb] of ROUTES) { - await openConcept(page,route,theme,motion);await page.mouse.move(0,0); - const reference=await inventoryOf(page,'#main'); - if(motion==='no-preference')await captureCanonicalState(page,testInfo,`concept-shell-${route}-${width}-${theme}`); - const referenceRings=FULL_HEADING.has(route)?await ringsOf(page,'#main'):null; - const referenceHovers=await hoversOf(page); - if(motion==='no-preference')await captureFooter(page,testInfo,`concept-footer-${route}-${width}-${theme}`); - await openProduction(page,route,theme,motion);await page.mouse.move(0,0); - const actual=await inventoryOf(page,'#workspace-main'); - if(motion==='no-preference')await captureCanonicalState(page,testInfo,`production-shell-${route}-${width}-${theme}`); - const actualRings=FULL_HEADING.has(route)?await ringsOf(page,'#workspace-main'):null; - const actualHovers=await hoversOf(page); - if(motion==='no-preference')await captureFooter(page,testInfo,`production-footer-${route}-${width}-${theme}`); - result[`${motion} ${route}`]={route,breadcrumb,reference,actual,diff:compare(route,reference,actual),referenceRings,actualRings, - referenceHovers,actualHovers}; - } - const path=testInfo.outputPath('shell-geometry.json');await writeFile(path,JSON.stringify(result,null,2)); - await testInfo.attach('shell-geometry',{path,contentType:'application/json'}); - for(const [key,{route,breadcrumb,reference,actual,diff,referenceRings,actualRings,referenceHovers,actualHovers}] of Object.entries(result)) { + await openConcept(page,route,theme,motion);await page.mouse.move(0,0); + const reference=await inventoryOf(page,'#main'); + if(motion==='no-preference')await captureCanonicalState(page,testInfo,`concept-shell-${route}-${width}-${theme}`); + const referenceRings=FULL_HEADING.has(route)?await ringsOf(page,'#main'):null; + const referenceHovers=await hoversOf(page); + if(motion==='no-preference')await captureFooter(page,testInfo,`concept-footer-${route}-${width}-${theme}`); + await openProduction(page,route,theme,motion);await page.mouse.move(0,0); + const actual=await inventoryOf(page,'#workspace-main'); + if(motion==='no-preference')await captureCanonicalState(page,testInfo,`production-shell-${route}-${width}-${theme}`); + const actualRings=FULL_HEADING.has(route)?await ringsOf(page,'#workspace-main'):null; + const actualHovers=await hoversOf(page); + if(motion==='no-preference')await captureFooter(page,testInfo,`production-footer-${route}-${width}-${theme}`); + result[`${motion} ${route}`]={reference,actual,diff:compare(route,reference,actual),referenceRings,actualRings, + referenceHovers,actualHovers}; + const path=testInfo.outputPath(`shell-geometry-${route}-${motion}.json`);await writeFile(path,JSON.stringify(result,null,2)); + await testInfo.attach(`shell-geometry-${route}-${motion}`,{path,contentType:'application/json'}); + for(const [key,{reference,actual,diff,referenceRings,actualRings,referenceHovers,actualHovers}] of Object.entries(result)) { // The concept itself is what this comparison assumes: its sample counts and its shell controls. expect.soft(reference.chrome.navs[0].rows.map(row=>[row.text,row.count?.text??null]),`${key} concept WORKSPACE rows`) .toEqual([['Images','24'],['Prompt library','6'],['Tags & categories','15'],['Inspect a file',null]]); diff --git a/tests/browser/workflows.spec.mjs b/tests/browser/workflows.spec.mjs index 2f31eda..0ee4620 100644 --- a/tests/browser/workflows.spec.mjs +++ b/tests/browser/workflows.spec.mjs @@ -74,6 +74,24 @@ test('a page whose leave was called off reads its list again on the next input', await expect(page.locator('#resultsTitle')).toHaveText('Recent Prompts'); }); +// The gallery's startup thumbnail check runs a second after load. When that second ends while the page is leaving, +// the check must start no read: WebKit fails it with "Fetch API cannot load … due to access control checks." and +// Playwright reports a page error (seen in CI on PR #289). The page clock makes the second end after the leave began. +for (const leave of ['beforeunload', 'pagehide']) { + test(`the gallery startup check starts no read after ${leave}`, async ({ page }) => { + await page.clock.install(); + const statusReads = []; + page.on('request', req => { if (req.url().includes('/images/thumbnails/status')) statusReads.push(req.url()); }); + await page.goto('/prompt_manager/gallery'); + await expect(page.locator('#galleryGrid .image-item')).toHaveCount(2); + await page.evaluate(type => window.dispatchEvent(new Event(type)), leave); + await page.clock.runFor(1500); + // A read made after the clock ran is listed, so the listener saw every status read. + await page.evaluate(() => fetch('/prompt_manager/images/thumbnails/status?probe=1')); + expect(statusReads.map(url => new URL(url).search)).toEqual(['?probe=1']); + }); +} + test('actual gallery review generates and persists tags for the selected root', async ({ page, request }) => { await page.goto('/prompt_manager/gallery'); await expect(page.locator('#galleryGrid .image-item')).toHaveCount(2); diff --git a/tests/browser/workspace-activity.spec.mjs b/tests/browser/workspace-activity.spec.mjs index 80f9fcd..cbd848c 100644 --- a/tests/browser/workspace-activity.spec.mjs +++ b/tests/browser/workspace-activity.spec.mjs @@ -2,6 +2,13 @@ import { startCoverage, saveCoverage } from '../../tools/vendor/browser-coverage import { test, expect } from './fixtures.mjs'; import { seedConcept, openConcept, openProduction } from './concept-inventory.mjs'; +// The Activity store's periodic list read (activity-store.mjs POLL_MS) and the detail view's read throttle +// (activity-job-view.mjs DETAIL_INTERVAL_MS). +const POLL_MS = 15_000; +const DETAIL_INTERVAL_MS = 1_000; +// A request is reported to the test a little after the browser sends it; that delay is not a shorter wait. +const REPORT_SLACK_MS = 100; + // Activity (#249 Activity-1): the concept's Activity page (app.js renderActivity 732–738), its top-bar button // (index.html 51–52) and sidebar row (app.js ~341), with the recorded D52 (counts of running jobs), D53 (the progress // pill) and D54 (real job items). Fake jobs come from tests/browser/workspace_activity_fixture.py. @@ -278,6 +285,8 @@ test('reload during a scan reattaches to the running job', async ({ page, reques }); test('lost server clears the pill and marks jobs ended', async ({ page, request }) => { + // A poll interval after the restart, then failed re-reads backing off for 1 + 2 + 4 s. + test.setTimeout(90_000); await page.setViewportSize({ width: 1600, height: 1100 }); const job = await startJob(request); await step(request, job.id, { processed: 4000 }); @@ -294,9 +303,12 @@ test('lost server clears the pill and marks jobs ended', async ({ page, request await expect(items(page).first().locator('p')).toHaveText(LOST); await expect(items(page).first().locator('progress, .row-actions')).toHaveCount(0); await expect(navRow(page).locator('.nav-count')).toHaveCount(0); - // One re-read found the restart; there is no retry storm and no finish toast for a job that only vanished. - await page.waitForTimeout(2500); - expect(reads.length).toBeLessThanOrEqual(2); + // One re-read found the restart; there is no retry storm and no finish toast for a job that only vanished. The + // read after it is the periodic one, a whole poll interval later: any extra read lands in that gap and fails it. + const found = reads.length; + expect(found, 'reads that found the restart').toBeGreaterThanOrEqual(1); + await expect.poll(() => reads.length, { timeout: POLL_MS + 10_000 }).toBeGreaterThan(found); + expect(reads[found] - reads[found - 1], 'the next read waits for the poll').toBeGreaterThanOrEqual(POLL_MS - REPORT_SLACK_MS); await expect(toast(page)).toHaveText(''); // A server that cannot be read at all also ends the running job, then is read again with a growing backoff. @@ -308,9 +320,12 @@ test('lost server clears the pill and marks jobs ended', async ({ page, request await expect(await request.post('/__test__/jobs/restart')).toBeOK(); await expect(pill(page)).toBeHidden(); await expect(items(page).filter({ hasText: LOST })).toHaveCount(1); - await page.waitForTimeout(3500); - // 1 s, then 2 s: at most two re-reads in 3.5 s. - expect(failed.length).toBeLessThanOrEqual(2); + // 1 s, then 2 s, then 4 s: each failed re-read waits twice as long as the one before. The gaps between the + // reads themselves are the evidence, so a slow runner noticing the ended job late cannot add a read to a window. + await expect.poll(() => failed.length, { timeout: 20_000 }).toBeGreaterThanOrEqual(3); + const gaps = failed.slice(1, 3).map((at, index) => at - failed[index]); + expect(gaps[0]).toBeGreaterThanOrEqual(2000 - REPORT_SLACK_MS); + expect(gaps[1]).toBeGreaterThanOrEqual(4000 - REPORT_SLACK_MS); await page.unroute('**/prompt_manager/jobs**'); expect(second.id).not.toBe(job.id); }); @@ -607,6 +622,8 @@ test('filter narrows the selected outcome', async ({ page, request }) => { test('job view renders 200-row cap note', async ({ page, request }) => { await page.setViewportSize({ width: 1600, height: 1100 }); + // The page's timers are Playwright's, running at real speed, so the M12 check below can run a read throttle out. + await page.clock.install(); const job = await startJob(request); await step(request, job.id, { processed: 260, bulk: { group: 'added', count: 250 }, records: [ ...Array.from({ length: 10 }, (_, index) => ({ group: 'no_metadata', path: `misc/plain_${index}.png`, detail: 'No prompt metadata in the file' })), @@ -646,11 +663,15 @@ test('job view renders 200-row cap note', async ({ page, request }) => { page.on('request', item => { if (new URL(item.url()).pathname === `/prompt_manager/jobs/${job.id}`) reads.push(Date.now()); }); await step(request, job.id, { processed: 300, bulk: { group: 'added', count: 40 } }); await expect(tiles(page).first().locator('dd')).toHaveText('290'); - await page.waitForTimeout(1500); + // Every read the counter update could have scheduled is due within one throttle interval: run it out, so any such + // read is sent before the log opens. Requests reach the test in the order they were sent, so once the log's read + // has answered, a read sent before it would already be listed. + await page.clock.runFor(DETAIL_INTERVAL_MS); expect(reads).toEqual([]); await scanLog(page).locator('summary').click(); await expect.poll(() => reads.length).toBe(1); await expect(scanLog(page).locator('pre')).toContainText('bulk/file_00039.png'); + expect(reads, 'only the log\'s own read').toHaveLength(1); }); test('cancel scan ends cancelled with partial counts', async ({ page, request }) => { @@ -678,16 +699,44 @@ test('cancel scan ends cancelled with partial counts', async ({ page, request }) await expect(heading(page).getByRole('button', { name: 'Cancel scan', exact: true })).not.toHaveAttribute('aria-disabled', 'true'); }); +// Truth rule: a held legacy-page job (a restore from the admin page) cannot be cancelled, so neither the Activity +// list nor the job's view offers Cancel; the cancellable scan beside it keeps its own (review I3). +test('a held legacy job shows no Cancel in the list or its view', async ({ page, request }) => { + await page.setViewportSize({ width: 1600, height: 1100 }); + const response = await request.post('/__test__/jobs/hold', { data: { kind: 'restore', title: 'Database restore (admin page)' } }); + await expect(response).toBeOK(); + const held = (await response.json()).job; + expect(held.cancellable).toBe(false); + await page.goto(`${workspace}#/activity`); + const item = items(page).filter({ has: page.getByRole('link', { name: /Database restore \(admin page\)$/ }) }); + await expect(item.locator('.chip')).toHaveText('Running'); + await expect(item.locator('progress')).toHaveCount(1); + await expect(main(page).getByRole('button', { name: 'Cancel job', exact: true })).toHaveCount(0); + + await item.getByRole('link').click(); + await expect(heading(page).locator('h1')).toHaveText('Database restore (admin page)'); + await expect(heading(page).locator('.heading-actions > button')).toHaveText(['Download report']); + await expect(heading(page).getByRole('button', { name: /^Cancel/ })).toHaveCount(0); + + // When the hold ends the view shows the end, still without a Cancel. + await finish(request, held.id); + await expect(heading(page).locator('p.muted')).toHaveText(/^Finished/); + await expect(heading(page).locator('.heading-actions > button')).toHaveText(['Download report']); +}); + test('busy start names the running job', async ({ page, request }) => { await page.setViewportSize({ width: 1600, height: 1100 }); + // The page's timers are Playwright's, so the toast's lifetime below is run out rather than waited for. + await page.clock.install(); const running = await startJob(request, { title: 'Database maintenance', kind: 'maintenance' }); await startFromImport(page); await expect(toast(page)).toHaveText('Another job is running: Database maintenance.'); await expect(toast(page).getByRole('link', { name: 'Database maintenance', exact: true })).toHaveAttribute('href', `#/activity/${running.id}`); await expect(page).toHaveURL(/#\/images$/); expect((await listed(request)).map(item => item.id)).toEqual([running.id]); - // Review M14: a toast with a link outlasts the plain 6.5 s, so the link stays reachable. - await page.waitForTimeout(7000); + // Review M14: a toast with a link outlasts the plain 6.5 s, so the link stays reachable. Running the page's timers + // 7 s on fires a 6.5 s clear for certain, however slowly this runner reached this line. + await page.clock.runFor(7000); await expect(toast(page).getByRole('link', { name: 'Database maintenance', exact: true })).toBeVisible(); await toast(page).getByRole('link', { name: 'Database maintenance', exact: true }).click(); await expect(heading(page).locator('h1')).toHaveText('Database maintenance'); diff --git a/tests/browser/workspace-history.spec.mjs b/tests/browser/workspace-history.spec.mjs index e08a1a9..48f8107 100644 --- a/tests/browser/workspace-history.spec.mjs +++ b/tests/browser/workspace-history.spec.mjs @@ -494,6 +494,23 @@ test('the inline current file names its prompts apart from the frozen run fields expect(await exact(dialog(page).getByRole('region',{name:'Positive prompt',exact:true}).locator('p.prompt-text'))).toBe(before); }); +// LI-4 (D117): the recorded history's current-file viewer is the shared full inspector, so it copies the legacy summary. +test('the current file viewer copies its settings summary',async({page})=>{ + await page.addInitScript(()=>{window.__copied=[];Object.defineProperty(navigator,'clipboard',{configurable:true,value:{writeText:async text=>{window.__copied.push(text);}}});}); + await page.route(url=>url.pathname.endsWith('/inspection'),async route=>{const response=await route.fetch();const body=await response.json(); + body.prompts={positive:{status:'resolved',text:'current file positive',source:'graph-inferred'},negative:{status:'resolved',text:'current file negative',source:'graph-inferred'}}; + await route.fulfill({response,json:body});}); + await open(page,page.fixture.detail_id);await marker(page,page.fixture.runs.shared).click();await expect(dialog(page).locator('[data-receipt]')).toHaveCount(5); + await dialog(page).locator('[data-receipt]').first().getByRole('button',{name:'View current file',exact:true}).click(); + const details=page.locator('dialog#viewer-dialog #viewer-details');await expect(details.locator('.panel-heading')).toBeVisible(); + const facts=await details.locator(':scope > div > dl.meta-grid dd').allTextContents(); + await details.getByRole('button',{name:'Copy settings summary',exact:true}).click(); + await expect(page.locator('#workspace-toast')).toHaveText('Settings summary copied.'); + const [seed,steps,cfg,sampler,,model]=facts; + expect(await page.evaluate(()=>window.__copied)).toEqual([[`Checkpoint: ${model}`,'Positive Prompt: current file positive','Negative Prompt: current file negative', + `Steps: ${steps}`,`CFG Scale: ${cfg}`,`Sampler: ${sampler}`,`Seed: ${seed}`].join('\n').replace(/: (Not recorded|Unresolved|Ambiguous|Unavailable within inspection limits)(?=\n|$)/g,': Unknown')]); +}); + test('the recorded run dialog with a resolved current file has no duplicate button or region names',async({page})=>{ await page.route(url=>url.pathname.endsWith('/inspection'),async route=>{const response=await route.fetch();const body=await response.json(); body.prompts={positive:{status:'resolved',text:'current file positive',source:'graph-inferred'},negative:{status:'resolved',text:'current file negative',source:'graph-inferred'}}; diff --git a/tests/browser/workspace-images.spec.mjs b/tests/browser/workspace-images.spec.mjs index ce27f85..4aec524 100644 --- a/tests/browser/workspace-images.spec.mjs +++ b/tests/browser/workspace-images.spec.mjs @@ -807,7 +807,12 @@ test('density changes and resize preserve identity focus and selection', async ( await page.getByRole('button',{name:'Grid',exact:true}).click(); for (const [width,columns] of [[951,3],[950,3],[700,2],[390,2]]) { await page.setViewportSize({width,height:844}); - await expect.poll(()=>cards(page).nth(columns).evaluate(node=>node.offsetLeft)).toBe(0); + // 700px and 390px both lay out two columns, so a card at the row start alone is also true of the previous + // width's cards before WebKit runs the first frame after the resize; wait for the row to fit the new grid. + await expect.poll(()=>page.locator('.image-grid').evaluate((grid,columns)=>{ + const row=[...grid.querySelectorAll(':scope > article[data-image-key]')]; + return row[columns].offsetLeft===0&&row[columns-1].getBoundingClientRect().right<=grid.getBoundingClientRect().right+1; + },columns)).toBe(true); await first.getByRole('button',{name:/^Open /}).focus();await page.keyboard.press('ArrowDown'); await expect(cards(page).nth(columns).getByRole('button',{name:/^Open /})).toBeFocused(); expect(await page.evaluate(()=>document.documentElement.scrollWidth-document.documentElement.clientWidth)).toBeLessThanOrEqual(1); @@ -922,6 +927,10 @@ test('browser Back closes the open Images dialog and leaves the next page usable test('count line shows images and positive prompts', async ({ page, request }) => { await page.addInitScript(() => { window.IntersectionObserver = undefined; }); + // D17's "Today" is the newest file's day: the page's clock stands at that file's mtime, so the fixture's day and + // the page's agree on either side of midnight. + const { images } = await (await request.get('/prompt_manager/images/output?mode=workspace&limit=40')).json(); + await page.clock.setFixedTime(new Date(Math.max(...images.map(image => image.modified_time)) * 1000)); await page.goto(workspace); await expect(cards(page)).toHaveCount(2); await expect(caption(page).locator('#result-count')).toHaveText('2 of 2 images · 2 positive prompts'); diff --git a/tests/browser/workspace-inspect.spec.mjs b/tests/browser/workspace-inspect.spec.mjs index 5923259..5d19793 100644 --- a/tests/browser/workspace-inspect.spec.mjs +++ b/tests/browser/workspace-inspect.spec.mjs @@ -207,6 +207,40 @@ test('chooser opens the shared full inspector without a second read', async ({ p expect(await page.evaluate(() => window.__slices)).toBe(0); }); +// PR L3 Task 10: local roles are the server inspector's (local-resolver.mjs), so for every parity PNG the rail shows the +// prompts, their status lines and the facts Images shows for the same bytes in the output folder. +const PARITY_PNGS = ['f01b', 'f02', 'f03', 'f06a', 'f06b', 'f06c', 'f06d', 'f06e', 'f07a', 'f07b', 'f08', 'f09', 'f10', 'f11', 'f12', + 'f14', 'f15', 'f16', 'f17', 'f19', 'f20', 'f21', 'f22', 'f23', 'f24']; +const roleSections = locator => locator.evaluate(node => [...node.querySelectorAll('.prompt-section')].map(item => item.textContent)); + +test('chooser shows the Images roles and resolution for every parity PNG', async ({ page, request }) => { + test.slow(); + for (const name of PARITY_PNGS) { + await expect(await request.post('/__test__/add-local-inspect-file', { data: { name: `${name}.png` } })).toBeOK(); + } + const served = {}; + await page.goto(`${workspace}#/images`); + for (const name of PARITY_PNGS) { + await page.locator(`article[data-image-key="0:local-inspect/${name}.png"] .image-open`).click(); + await expect(viewer(page).locator('#viewer-title')).toHaveText(`${name}.png`); + await expect(rail(page).locator('.panel-heading')).toBeVisible(); + await expect(rail(page).locator('.prompt-section')).not.toHaveCount(0); + served[name] = { content: await railContent(rail(page)), roles: await roleSections(rail(page)) }; + await page.keyboard.press('Escape'); + await expect(viewer(page)).toHaveCount(0); + } + await openInspect(page); + for (const name of PARITY_PNGS) { + await choose(page, parityPng(`${name}.png`)); + await openViewer(page); + await expect(viewer(page).locator('#viewer-title')).toHaveText(`${name}.png`); + await expect(rail(page).locator('.prompt-section')).not.toHaveCount(0); + expect({ content: await railContent(rail(page)), roles: await roleSections(rail(page)) }, name).toEqual(served[name]); + await page.keyboard.press('Escape'); + await expect(viewer(page)).toHaveCount(0); + } +}); + test('Import → Inspect a local image lands here', async ({ page }) => { await page.goto(`${workspace}#/images`); await page.getByRole('button', { name: 'Import', exact: true }).click(); @@ -481,15 +515,13 @@ test('replacing a video that just started loading never blocks the page', async }); test('a video replaced while still starting is reset once it settles', async ({ context, browserName }) => { - // A WebKit (GStreamer) video released exactly at its `loadstart` sometimes blocks the page, with or without any - // reset and also with the code before the deferred reset (2 of 24 runs); tracked in TESTING.md (D62). WebKit's - // no-hang guard is `replacing a video that just started loading never blocks the page`. - test.skip(browserName === 'webkit', 'WebKit release at loadstart is a tracked follow-up (D62)'); - test.setTimeout(120000); - // The video is replaced from its own `loadstart` (readyState HAVE_NOTHING). It leaves the page with no src before - // its URL is revoked, and is reset (load() with no src, which frees its decoder) only once it reports metadata or - // an error, so the page never blocks. Fresh pages share one browser process. - for (let attempt = 0; attempt < 5; attempt++) { + test.setTimeout(180000); + // The video is replaced from its own `loadstart` (readyState HAVE_NOTHING), or a few milliseconds after it. On + // WebKit (GStreamer) a video that left the page or the viewport before its pipeline had started deadlocked the + // page (TESTING.md D62), so it stays in place, inert and still playing its URL, with the new preview after it. + // Once it reports metadata or an error it is paused, loses its src, is reset (load() with no src, which frees its + // decoder) and leaves the page, and only then is its URL revoked. Fresh pages share one browser process. + for (let attempt = 0; attempt < 15; attempt++) { const page = await context.newPage(); try { if (attempt === 0) await startCoverage(page, browserName); @@ -510,10 +542,12 @@ test('a video replaced while still starting is reset once it settles', async ({ const video = slot.querySelector('video'); if (!video) return; observer.disconnect(); - video.addEventListener('loadstart', () => { + const replace = () => { window.__starting = { node: video, url: video.getAttribute('src'), readyState: video.readyState }; input.files = window.__pngFiles; input.dispatchEvent(new Event('change', { bubbles: true })); - }, { once: true }); + window.__starting.replaced = { connected: video.isConnected, src: video.hasAttribute('src'), inert: video.inert, resets: video.__resets ?? 0 }; + }; + video.addEventListener('loadstart', () => { if (window.__delay) setTimeout(replace, window.__delay); else replace(); }, { once: true }); }).observe(slot, { childList: true }); }); const png = parityPng('f02.png'); @@ -522,13 +556,17 @@ test('a video replaced while still starting is reset once it settles', async ({ transfer.items.add(new File([new Uint8Array(bytes)], 'f02.png', { type: 'image/png' })); window.__pngFiles = transfer.files; }, [...png.buffer]); + await page.evaluate(delay => { window.__delay = delay; }, attempt % 5); await choose(page, localFixture('core-tags.webm', 'video/webm')); await expect(preview(page).locator('img.local-file-preview')).toBeVisible({ timeout: 8000 }); await expect(preview(page).locator('video')).toHaveCount(0); - // Released while starting, and never reset synchronously: the URL went once the node had no src. - expect(await page.evaluate(() => ({ readyState: window.__starting.readyState, revoked: window.__starting.revoked }))) - .toEqual({ readyState: 0, revoked: { src: false, connected: false, resets: 0 } }); - await expect.poll(() => page.evaluate(() => window.__starting.node.__resets ?? 0), { timeout: 15000 }).toBe(1); + // Replaced while starting (always so at `loadstart` itself): kept in place and untouched. A video that had + // settled is released at once. Either way it is reset once, and its URL goes last. + await expect.poll(() => page.evaluate(() => window.__starting.revoked ?? null)).toEqual({ src: false, connected: false, resets: 1 }); + const { readyState, replaced } = await page.evaluate(() => ({ readyState: window.__starting.readyState, replaced: window.__starting.replaced })); + if (attempt % 5 === 0) expect(readyState).toBe(0); + expect(replaced).toEqual(readyState === 0 ? { connected: true, src: true, inert: true, resets: 0 } : { connected: false, src: false, inert: false, resets: 1 }); + expect(await page.evaluate(() => window.__starting.node.__resets)).toBe(1); // The page still answers. await chooser(page).setInputFiles(parityPng('f02.png', 'after.png'), { timeout: 8000 }); await expect(preview(page).locator('img.local-file-preview')).toBeVisible({ timeout: 8000 }); @@ -652,13 +690,19 @@ test('local roles send exactly', async ({ page, context }) => { test('local inspection makes no content request or storage write', async ({ page }) => { await instrument(page); - await openInspect(page); - // The shell's own reads (counts, Activity) settle before the baseline. - await expect(page.locator('aside.sidebar nav [data-route="images"] .nav-count')).toBeAttached(); - const baseline = (await urls(page)).writes.length; const requests = []; page.on('request', request => requests.push({ url: request.url(), method: request.method(), body: request.postData() })); page.on('websocket', socket => requests.push({ url: socket.url(), method: 'WS', body: null })); + const table = /\/prompt_manager\/js\/inspection-semantics\.json$/; + const tableRead = page.waitForResponse(response => table.test(response.url())); + await openInspect(page); + // The one static file Inspect reads, its node table, is fetched as the page mounts: before any selection. + expect((await tableRead).ok()).toBe(true); + // The shell's own reads (counts, Activity) settle before the baseline. + await expect(page.locator('aside.sidebar nav [data-route="images"] .nav-count')).toBeAttached(); + const baseline = (await urls(page)).writes.length; + const before = requests.length; + expect(requests.filter(entry => table.test(entry.url))).toHaveLength(1); for (const file of [sendablePng('private-name.png'), localFixture('core-tags.webm', 'video/webm', 'private-clip.webm')]) { await choose(page, file); await openViewer(page); @@ -666,7 +710,8 @@ test('local inspection makes no content request or storage write', async ({ page await page.keyboard.press('Escape'); await expect(viewer(page)).toHaveCount(0); } - const network = requests.filter(entry => !entry.url.startsWith('blob:') && !entry.url.startsWith('data:')); + const network = requests.slice(before).filter(entry => !entry.url.startsWith('blob:') && !entry.url.startsWith('data:')); + expect(requests.filter(entry => table.test(entry.url)), 'the table is read once per page').toHaveLength(1); expect(network.filter(entry => /private|lighthouse|fox/.test(`${entry.url} ${entry.body ?? ''}`))).toEqual([]); expect(network.filter(entry => entry.method !== 'GET')).toEqual([]); expect(network.filter(entry => !/\/prompt_manager\/(activity|images\/output|library|vocabulary)/.test(entry.url))).toEqual([]); @@ -685,3 +730,80 @@ test('touch layout at 390 has no horizontal overflow at 200% zoom', async ({ pag expect(area).toBe(true); } }); + +// LI-4 (D117): "Copy settings summary" after the six facts is the legacy metadata page's COPY ALL, in the shared full +// inspector, so a file served in Images and the same bytes chosen here copy the same summary of what the rail shows. +/** Records clipboard writes in the page, or (`clipboard: false`) leaves the page without a clipboard. */ +async function stubClipboard(page, { clipboard = true } = {}) { + await page.addInitScript(available => { + window.__copied = []; + Object.defineProperty(navigator, 'clipboard', { configurable: true, + value: available ? { writeText: async text => { window.__copied.push(text); } } : undefined }); + }, clipboard); +} +/** The summary the rail's own prompts and facts make, in the legacy COPY ALL lines and placeholders. */ +const expectedSummary = locator => locator.evaluate(node => { + const facts = Object.fromEntries([...node.querySelectorAll(':scope .meta-grid:not(details .meta-grid) > div')] + .map(item => [item.querySelector('dt').textContent, item.querySelector('dd').textContent])); + // A confirmed prompt has its Copy action; an explicitly empty one takes the legacy placeholder too. + const prompt = role => { + const text = node.querySelector(`.prompt-section.${role} [data-copy-role]`) ? node.querySelector(`.prompt-section.${role} p.prompt-text`).textContent : null; + return text === `No ${role} prompt (explicitly empty)` ? null : text; + }; + return [`Checkpoint: ${facts.Model}`, `Positive Prompt: ${prompt('positive') ?? 'No prompt found'}`, + `Negative Prompt: ${prompt('negative') ?? 'No negative prompt found'}`, `Steps: ${facts.Steps}`, `CFG Scale: ${facts.CFG}`, + `Sampler: ${facts.Sampler}`, `Seed: ${facts.Seed}`].join('\n'); +}); +async function copySummary(page) { + const button = rail(page).getByRole('button', { name: 'Copy settings summary', exact: true }); + // It follows the six facts, before "File & provenance". + expect(await button.evaluate(node => [node.previousElementSibling?.localName, node.nextElementSibling?.localName])).toEqual(['dl', 'details']); + await button.click(); +} + +test('Copy settings summary copies the same legacy summary in Images and Inspect', async ({ page, request }) => { + await stubClipboard(page); + await expect(await request.post('/__test__/add-local-inspect-file', { data: { name: 'f02.png' } })).toBeOK(); + await page.goto(`${workspace}#/images`); + await page.locator('article[data-image-key="0:local-inspect/f02.png"] .image-open').click(); + await expect(rail(page).locator('.prompt-section.positive p.prompt-text')).toBeVisible(); + const served = await expectedSummary(rail(page)); + expect(served).toMatch(/^Checkpoint: .+\nPositive Prompt: [\s\S]+\nNegative Prompt: [\s\S]+\nSteps: .+\nCFG Scale: .+\nSampler: .+\nSeed: .+$/); + expect(served).not.toContain('Unknown'); + await copySummary(page); + await expect(page.locator('#workspace-toast')).toHaveText('Settings summary copied.'); + expect(await page.evaluate(() => window.__copied)).toEqual([served]); + await page.keyboard.press('Escape'); + await expect(viewer(page)).toHaveCount(0); + + await openInspect(page); + await choose(page, parityPng('f02.png')); + await openViewer(page); + expect(await expectedSummary(rail(page))).toBe(served); + await copySummary(page); + await expect.poll(() => page.evaluate(() => window.__copied)).toEqual([served, served]); + // The panel variant (the Images side panel) keeps the concept's footer: no summary action there. + await page.keyboard.press('Escape'); + await page.goto(`${workspace}#/images`); + await expect(page.locator('#image-inspector .panel-heading')).toBeVisible(); + await expect(page.locator('#image-inspector').getByRole('button', { name: 'Copy settings summary', exact: true })).toHaveCount(0); +}); + +test('Copy settings summary without a clipboard opens the select-to-copy dialog', async ({ page }) => { + await stubClipboard(page, { clipboard: false }); + await openInspect(page); + await choose(page, sendablePng()); + await openViewer(page); + const summary = await expectedSummary(rail(page)); + expect(summary).toContain(`Positive Prompt: ${POSITIVE_LINES}`); + await copySummary(page); + const fallback = page.getByRole('dialog', { name: 'Copy settings summary', exact: true }); + await expect(fallback).toBeVisible(); + await expect(fallback.getByRole('textbox', { name: 'Select and copy this text', exact: true })).toHaveValue(summary); + await expect(fallback.getByRole('textbox', { name: 'Select and copy this text', exact: true })).toBeFocused(); + await page.keyboard.press('Escape'); + await expect(fallback).toHaveCount(0); + // Escape closes only the copy dialog; focus returns to the action in the still-open viewer. + await expect(viewer(page)).toBeVisible(); + await expect(rail(page).getByRole('button', { name: 'Copy settings summary', exact: true })).toBeFocused(); +}); diff --git a/tests/browser/workspace-library.spec.mjs b/tests/browser/workspace-library.spec.mjs index 7055766..5ef235f 100644 --- a/tests/browser/workspace-library.spec.mjs +++ b/tests/browser/workspace-library.spec.mjs @@ -610,7 +610,9 @@ test('card matches the concept and never wraps the category mid-word',async({pag await expect(await request.post('/prompt_manager/bulk/category',{data:{prompt_ids:[dual.id],category:''}})).toBeOK(); // A long saved title fills the title row, so only the chip's own min-content keeps "Uncategorized" and "asian" whole. const wide='A remarkably long saved positive prompt title that fills the whole card title row'; - await patchShelf(page,item=>item.id===cat.id?{run_count:4,last_used_at:new Date().toISOString(),rating:5}:item.id===dual.id||item.id===dog.id?{title:wide,run_count:1,last_used_at:null,rating:null}:{}); + // One "now" for the patched last use and the page's clock, so it reads "Used today" on either side of midnight. + const usedAt=new Date();await page.clock.setFixedTime(usedAt); + await patchShelf(page,item=>item.id===cat.id?{run_count:4,last_used_at:usedAt.toISOString(),rating:5}:item.id===dual.id||item.id===dog.id?{title:wide,run_count:1,last_used_at:null,rating:null}:{}); for(const [width,height] of [[1600,1100],[390,844]]) { await page.setViewportSize({width,height});await page.goto(route);await expect(cards(page)).toHaveCount(3); const card=page.locator(`article[data-prompt-id="${cat.id}"]`); diff --git a/tests/browser/workspace_activity_fixture.py b/tests/browser/workspace_activity_fixture.py index c2a3d4d..41dba88 100644 --- a/tests/browser/workspace_activity_fixture.py +++ b/tests/browser/workspace_activity_fixture.py @@ -14,6 +14,9 @@ reading files, so a test sets its progress and counters exactly: the progress to every subscriber; - ``POST /__test__/jobs/{id}/finish`` ends it as ``finished``, ``cancelled`` or ``failed``; a cancel through the real route ends it ``cancelled``; +- ``POST /__test__/jobs/hold`` opens a legacy-page hold (``kind``, ``title``): + a running, non-cancellable ``records`` job, as a legacy restore or + maintenance request takes, until ``finish`` ends it; - ``POST /__test__/jobs/restart`` acts out a ComfyUI restart: the open job streams drop without a final event and the registry starts empty. """ @@ -86,6 +89,7 @@ def register_activity_commands(app, routes, api): streams = set() endings = {} + holders = set() @web.middleware async def track_streams(request, handler): @@ -163,6 +167,36 @@ def register_activity_commands(app, routes, api): endings[job.id] = ending return web.json_response({"success": True, "job": job.summary()}) + @routes.post("/__test__/jobs/hold") + async def hold_job(request): + data = await body(request) + loop = asyncio.get_running_loop() + ending = loop.create_future() + entered = loop.create_future() + + async def hold(): + async with api._jobs.hold( + str(data.get("kind", "restore")), + str(data.get("title", "Database restore (admin page)")), + admission="records", + ) as job: + entered.set_result(job) + state = await ending + if state != "finished": + job.finish(state) + + holder = asyncio.ensure_future(hold()) + holders.add(holder) + holder.add_done_callback(holders.discard) + await asyncio.wait({entered, holder}, return_when=asyncio.FIRST_COMPLETED) + if not entered.done(): + if isinstance(holder.exception(), JobBusy): + raise web.HTTPConflict() + raise holder.exception() + job = entered.result() + endings[job.id] = ending + return web.json_response({"success": True, "job": job.summary()}) + @routes.post("/__test__/jobs/{job_id}/step") async def step_job(request): job = job_or_404(request) @@ -210,6 +244,7 @@ def register_activity_commands(app, routes, api): for transport in list(streams): transport.close() tasks = [job.task for job in old.list() if job.task and not job.task.done()] + tasks += [holder for holder in holders if not holder.done()] for task in tasks: task.cancel() await asyncio.gather(*tasks, return_exceptions=True) diff --git a/tests/browser/workspace_concept_fixture.py b/tests/browser/workspace_concept_fixture.py index b22c362..bcb306f 100644 --- a/tests/browser/workspace_concept_fixture.py +++ b/tests/browser/workspace_concept_fixture.py @@ -11,6 +11,7 @@ the same sidebar counts (24 images, 6 prompts, 15 tags). Nothing is mocked. from __future__ import annotations from dataclasses import dataclass +import datetime import json import math import os @@ -157,6 +158,8 @@ CONCEPT_MODEL = "illustrative-model.safetensors" MAX_EXTRA_IMAGES = 120 # Unlinked, seedless images (D11 "W × H · PNG") newer than the concept's, for D3 checks. MAX_SEEDLESS_IMAGES = 2 +# A test's chosen "now" (epoch seconds) stays before the year 3000. +MAX_EPOCH_SECONDS = 32_503_680_000 SEEDLESS_SIZE = (1536, 1152) # The same 4:3 shape with a short "96 × 72 · PNG" subtitle that stays on one line at # 390px beside the card's Info and Tag (the D3 footer check). @@ -342,6 +345,17 @@ def _bounded(name: str, value: int, maximum: int) -> None: raise ValueError(f"{name} is out of range") +def _instant(now: float | None) -> float: + """The fixture's one "now": ``now`` itself, or the wall clock when omitted.""" + if now is None: + return time.time() + if isinstance(now, bool) or not isinstance(now, (int, float)): + raise TypeError("now must be a number of epoch seconds") + if not 0 < now < MAX_EPOCH_SECONDS: + raise ValueError("now is out of range") + return float(now) + + def seed_workspace_concept( api: PromptManagerAPI, root: Path, @@ -349,9 +363,16 @@ def seed_workspace_concept( extra_images: int = 0, seedless_images: int = 0, short_seedless: bool = False, + now: float | None = None, ) -> dict: """Replace this server's disposable rows and files with the concept's data. + Every relative time the fixture writes (the prompts' last use, the files' + mtimes, the runs' observation times) is taken from one instant, ``now`` + (epoch seconds, the wall clock when omitted), returned as ``manifest["now"]``. + A browser whose clock is fixed at that instant reads the concept's "Used + today" and caption day however close to midnight the run starts. + ``extra_images`` continues the concept's rounds (same prompts, dimensions and seed rule) after its 24 images, for bounded-rendering and Load more checks. ``seedless_images`` adds unlinked PNGs without metadata, newest of all; @@ -361,6 +382,10 @@ def seed_workspace_concept( raise TypeError("short_seedless must be a boolean") _bounded("extra_images", extra_images, MAX_EXTRA_IMAGES) _bounded("seedless_images", seedless_images, MAX_SEEDLESS_IMAGES) + now = _instant(now) + used_at = datetime.datetime.fromtimestamp(now, datetime.timezone.utc).isoformat( + timespec="microseconds" + ) _clear(api, root) db = api.db epoch = db.get_execution_epoch() @@ -385,8 +410,15 @@ def seed_workspace_concept( db.update_prompt_pair_metadata(pair_ids[0], title=first_name) for name, text in prompt.negatives[1:]: pair_ids.append(db.save_prompt_pair(prompt.positive, text, title=name)) - # Real queue-hook counting (D23): four runs, the last one now. + # Real queue-hook counting (D23): four runs, the last one now, at the + # fixture's own instant rather than the database's later wall clock. db.record_prompt_use(positive_id, times=CONCEPT_RUN_COUNT) + with db.model.get_connection() as conn: + conn.execute( + "UPDATE prompts SET last_used_at = ? WHERE id = ?", + (used_at, positive_id), + ) + conn.commit() prompts.insert( 0, {"id": prompt.id, "positive_id": positive_id, "pair_ids": pair_ids} ) @@ -400,7 +432,6 @@ def seed_workspace_concept( favorite_root = _favorite_root_key(list(api._get_all_output_dirs())[0]) total = len(CONCEPT_PROMPTS) * CONCEPT_IMAGE_ROUNDS + extra_images rounds = math.ceil(total / len(CONCEPT_PROMPTS)) - now = time.time() images, runs = [], [] slots = [ (round_index, prompt_index, prompt) @@ -484,6 +515,8 @@ def seed_workspace_concept( api.invalidate_gallery_cache() return { "epoch": epoch, + # The fixture's "now" (epoch seconds), for the browser's fixed clock. + "now": now, # The newest file's mtime (epoch seconds), for the D17 caption date. "newest_modified": now - 10 if seedless_images else now - 60, "seedless": seedless, diff --git a/tests/fixtures/local-inspect/resolver-vectors.json b/tests/fixtures/local-inspect/resolver-vectors.json new file mode 100644 index 0000000..55e7a71 --- /dev/null +++ b/tests/fixtures/local-inspect/resolver-vectors.json @@ -0,0 +1 @@ +{"vectors": [{"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "encode", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "3", "state": "unresolved"}}, "name": "empty-positive", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" …\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": ""}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "feff-positive", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Expected a typed graph link or text input", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "no-negative", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": null, \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Negative input is absent", "reason_code": null, "sampler_id": "3", "state": "absent"}}, "name": "negative-missing-input", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Expected a typed graph link or text input", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "float-slot", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0.0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Expected a typed graph link or text input", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "bool-slot", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", true], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "int-id", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [6, 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "dangling", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"60\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (CLIPTextEncode).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (CLIPTextEncode).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "CLIPTextEncode", "id": "6"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "wrong-slot", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 3], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Cyclic prompt graph", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "cycle", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"8\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Reroute input is not uniquely identifiable", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "reroute-ambiguous", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"6\", 0], \"in\": [\"6\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "last wins"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "duplicate-key", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"last wins\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "nan-input", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": NaN, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": null, "id": "6"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "class-type-number", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": 1.5, \"inputs\": {\"text\": \"x\"}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": null, "id": "6"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "class-type-missing", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"inputs\": {\"text\": \"x\"}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "inputs-list", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": [\"x\"]}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "API graph is missing or malformed", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "prompt-list", "records": {"prompt": "[1, 2]"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "API graph is missing or malformed", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "prompt-empty", "records": {"prompt": "{}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Prompt evidence is malformed", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "prompt-bom", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Prompt evidence is malformed", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "huge-int", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Prompt evidence is malformed", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "only-execution", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"8\": {\"records\": [{\"text\": \"fox\", \"negative_text\": \"blur\", \"positive_text\": \"fox executed\", \"negative_effective_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "No supported sampler was found.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "No supported sampler was found.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "No supported sampler pair found (saw XSampler, YGuider)", "reason_code": "no-sampler", "sampler_id": null, "state": "unresolved"}}, "name": "no-sampler", "records": {"prompt": "{\"1\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"x\", \"clip\": [\"4\", 1]}}, \"2\": {\"class_type\": \"XSampler\", \"inputs\": {\"positive\": [\"1\", 0]}}, \"9\": {\"class_type\": \"YGuider\", \"inputs\": {\"guider\": [\"1\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "No supported sampler was found.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "No supported sampler was found.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "No supported sampler pair found", "reason_code": "no-sampler", "sampler_id": null, "state": "unresolved"}}, "name": "no-sampler-plain", "records": {"prompt": "{\"1\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"x\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "sdxl-equal", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"a\", \"text_l\": \"a\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "sdxl-one", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"a\", \"text_l\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (CLIPTextEncodeSDXL).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt combines several texts (CLIPTextEncodeSDXL).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "CLIPTextEncodeSDXL", "id": "8"}, "reason": "Encoder parts have different texts", "reason_code": "composite", "sampler_id": "3", "state": "unresolved"}}, "name": "sdxl-differ", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"a\", \"text_l\": \"b\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "t"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "smz-false", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"smZ CLIPTextEncode\", \"inputs\": {\"text\": \"t\", \"text_g\": \"g\", \"text_l\": \"g\", \"with_SDXL\": false}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (smZ CLIPTextEncode).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt is chosen at run time (smZ CLIPTextEncode).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "smZ CLIPTextEncode", "id": "8"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "3", "state": "unresolved"}}, "name": "smz-true", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"smZ CLIPTextEncode\", \"inputs\": {\"text\": \"t\", \"text_g\": \"g\", \"text_l\": \"g\", \"with_SDXL\": true}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Malformed string node setting", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "smz-1", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"smZ CLIPTextEncode\", \"inputs\": {\"text\": \"t\", \"text_g\": \"g\", \"text_l\": \"g\", \"with_SDXL\": 1}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (smZ CLIPTextEncode).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (smZ CLIPTextEncode).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "smZ CLIPTextEncode", "id": "8"}, "reason": "Linked string node setting", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "smz-[\"9\", 0]", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"smZ CLIPTextEncode\", \"inputs\": {\"text\": \"t\", \"text_g\": \"g\", \"text_l\": \"g\", \"with_SDXL\": [\"9\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "zeroed-conditioning", "status": "resolved", "text": ""}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "zeroed", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"8\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"6\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "passthrough", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"6\", 0], \"guidance\": 3.5}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "controlnet-slot1", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"ControlNetApplyAdvanced\", \"inputs\": {\"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "combine-equal", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"9\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"6\", 0], \"conditioning_2\": [\"9\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (ConditioningCombine).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt combines several texts (ConditioningCombine).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "ConditioningCombine", "id": "8"}, "reason": "Combined conditioning has different texts", "reason_code": "composite", "sampler_id": "3", "state": "unresolved"}}, "name": "combine-differ", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"6\", 0], \"conditioning_2\": [\"7\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "neg"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "select-true", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"10\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"6\", 0], \"on_false\": [\"7\", 0]}}, \"10\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"neg\", \"clip\": [\"4\", 1]}}, \"9\": {\"class_type\": \"PrimitiveBoolean\", \"inputs\": {\"value\": true}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "neg"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "select-false", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"10\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"6\", 0], \"on_false\": [\"7\", 0]}}, \"10\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"neg\", \"clip\": [\"4\", 1]}}, \"9\": {\"class_type\": \"PrimitiveBoolean\", \"inputs\": {\"value\": true}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Selector branches have different prompts", "reason_code": null, "sampler_id": "3", "state": "ambiguous"}}, "name": "select-[\"9\", 0]", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"10\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"9\", 0], \"on_true\": [\"6\", 0], \"on_false\": [\"7\", 0]}}, \"10\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"neg\", \"clip\": [\"4\", 1]}}, \"9\": {\"class_type\": \"PrimitiveBoolean\", \"inputs\": {\"value\": true}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Malformed selector condition", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "select-1", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"10\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": 1, \"on_true\": [\"6\", 0], \"on_false\": [\"7\", 0]}}, \"10\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"neg\", \"clip\": [\"4\", 1]}}, \"9\": {\"class_type\": \"PrimitiveBoolean\", \"inputs\": {\"value\": true}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Malformed selector condition", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "select-\"true\"", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"10\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": \"true\", \"on_true\": [\"6\", 0], \"on_false\": [\"7\", 0]}}, \"10\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"neg\", \"clip\": [\"4\", 1]}}, \"9\": {\"class_type\": \"PrimitiveBoolean\", \"inputs\": {\"value\": true}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "select-converge", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"9\", 0], \"on_true\": [\"6\", 0], \"on_false\": [\"11\", 0]}}, \"11\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Selector branches have different prompts", "reason_code": null, "sampler_id": "3", "state": "ambiguous"}}, "name": "select-shared", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"12\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"9\", 0], \"on_true\": [\"6\", 0], \"on_false\": [\"7\", 0]}}, \"12\": {\"class_type\": \"Switch conditioning [Crystools]\", \"inputs\": {\"boolean\": [\"9\", 0], \"on_true\": [\"7\", 0], \"on_false\": [\"6\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "PromptSearchList", "id": "8"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "3", "state": "unresolved"}}, "name": "runtime", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptSearchList\", \"inputs\": {}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoderxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (FancyEncoderxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "FancyEncoderxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "id": "8"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "unknown-encoder", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"FancyEncoderxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"inputs\": {\"text\": \"x\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": " multi\nline "}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "primitive", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \" multi\\nline \"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Output slot has an incompatible type", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "string-kind-mismatch", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"x\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": " a , "}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "string-concatenate", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \" a \", \"string_b\": \"\", \"delimiter\": \", \"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a b"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "text-concatenate-\"true\"", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": \" b \", \"text_a\": \" a\", \"text_aa\": \"x\", \"text_c\": \"\", \"clean_whitespace\": \"true\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": " a b "}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "text-concatenate-\"False\"", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": \" b \", \"text_a\": \" a\", \"text_aa\": \"x\", \"text_c\": \"\", \"clean_whitespace\": \"False\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a b"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "text-concatenate-true", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": \" b \", \"text_a\": \" a\", \"text_aa\": \"x\", \"text_c\": \"\", \"clean_whitespace\": true}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a b"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "text-concatenate-\"TRUE\"", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": \" b \", \"text_a\": \" a\", \"text_aa\": \"x\", \"text_c\": \"\", \"clean_whitespace\": \"TRUE\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": " a b "}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "text-concatenate-0", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": \" b \", \"text_a\": \" a\", \"text_aa\": \"x\", \"text_c\": \"\", \"clean_whitespace\": 0}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "ab"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "strip-newlines-true", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\n a\\nb \\n\", \"strip_newlines\": true}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "\n a\nb \n"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "strip-newlines-false", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\n a\\nb \\n\", \"strip_newlines\": false}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Malformed string node setting", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "strip-newlines-1", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\n a\\nb \\n\", \"strip_newlines\": 1}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": " | b"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "join-multi-3-false", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\", \"string_2\": \"b\", \"string_3\": \"\", \"string_4\": \"d\", \"inputcount\": 3, \"delimiter\": \" | \", \"return_list\": false}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "3", "state": "unresolved"}}, "name": "join-multi-1-false", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\", \"string_2\": \"b\", \"string_3\": \"\", \"string_4\": \"d\", \"inputcount\": 1, \"delimiter\": \" | \", \"return_list\": false}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Malformed string node setting", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "join-multi-0-false", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\", \"string_2\": \"b\", \"string_3\": \"\", \"string_4\": \"d\", \"inputcount\": 0, \"delimiter\": \" | \", \"return_list\": false}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Malformed string node setting", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "join-multi-1001-false", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\", \"string_2\": \"b\", \"string_3\": \"\", \"string_4\": \"d\", \"inputcount\": 1001, \"delimiter\": \" | \", \"return_list\": false}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (JoinStringMulti).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt is chosen at run time (JoinStringMulti).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "JoinStringMulti", "id": "8"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "3", "state": "unresolved"}}, "name": "join-multi-2-true", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\", \"string_2\": \"b\", \"string_3\": \"\", \"string_4\": \"d\", \"inputcount\": 2, \"delimiter\": \" | \", \"return_list\": true}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Malformed string node setting", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "join-multi-2.0-false", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\", \"string_2\": \"b\", \"string_3\": \"\", \"string_4\": \"d\", \"inputcount\": 2.0, \"delimiter\": \" | \", \"return_list\": false}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Malformed string node setting", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "join-multi-true-false", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"8\", 0], \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\", \"string_2\": \"b\", \"string_3\": \"\", \"string_4\": \"d\", \"inputcount\": true, \"delimiter\": \" | \", \"return_list\": false}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "before main"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "prompt-manager", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"main\", \"prepend_text\": \" before \", \"append_text\": \"\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Expected a typed graph link or text input", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "prompt-manager-null-prepend", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"main\", \"prepend_text\": null, \"append_text\": \"\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "lead,main"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "historical-PromptManagerV2-0", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \" main \", \"negative_text\": \" neg \", \"prepend_text\": \"lead,\", \"append_text\": null}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "neg"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "historical-PromptManagerV2-1", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 1], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \" main \", \"negative_text\": \" neg \", \"prepend_text\": \"lead,\", \"append_text\": null}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "lead,main"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "historical-PromptManagerPositive-0", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \" main \", \"negative_text\": \" neg \", \"prepend_text\": \"lead,\", \"append_text\": null}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "main"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "historical-PromptManagerNegative-0", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerNegative\", \"inputs\": {\"text\": \" main \", \"negative_text\": \" neg \", \"prepend_text\": \"lead,\", \"append_text\": null}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blur"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "pair-graph", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"fox\", \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "execution", "status": "resolved", "text": "blur"}, "positive": {"source": "execution", "status": "resolved", "text": "fox executed"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "pair-execution", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"8\": {\"records\": [{\"text\": \"fox\", \"negative_text\": \"blur\", \"positive_text\": \"fox executed\", \"negative_effective_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"fox\", \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blur"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "pair-execution-other-node", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"99\": {\"records\": [{\"text\": \"fox\", \"negative_text\": \"blur\", \"positive_text\": \"fox executed\", \"negative_effective_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"fox\", \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Execution evidence does not match recorded node inputs", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "pair-execution-mismatch", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"8\": {\"records\": [{\"text\": \"other\", \"negative_text\": \"blur\", \"positive_text\": \"fox executed\", \"negative_effective_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"fox\", \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Negative execution text must be unchanged", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "pair-execution-negative-changed", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"8\": {\"records\": [{\"text\": \"fox\", \"negative_text\": \"blur\", \"positive_text\": \"fox executed\", \"negative_effective_text\": \"x\", \"prepend_text\": \"\", \"append_text\": \"\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"fox\", \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "execution", "status": "resolved", "text": "blur"}, "positive": {"source": "execution", "status": "resolved", "text": "fox executed"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "pair-execution-version-true", "records": {"comfyassets_prompt_pairs": "{\"version\": true, \"nodes\": {\"8\": {\"records\": [{\"text\": \"fox\", \"negative_text\": \"blur\", \"positive_text\": \"fox executed\", \"negative_effective_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"fox\", \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "execution", "status": "resolved", "text": "blur"}, "positive": {"source": "execution", "status": "resolved", "text": "fox executed"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "pair-execution-version-float", "records": {"comfyassets_prompt_pairs": "{\"version\": 1.0, \"nodes\": {\"8\": {\"records\": [{\"text\": \"fox\", \"negative_text\": \"blur\", \"positive_text\": \"fox executed\", \"negative_effective_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"fox\", \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "execution", "status": "resolved", "text": "blur"}, "positive": {"source": "execution", "status": "resolved", "text": "fox executed"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "pair-execution-truncated", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"truncated\": [], \"nodes\": {\"8\": {\"records\": [{\"text\": \"fox\", \"negative_text\": \"blur\", \"positive_text\": \"fox executed\", \"negative_effective_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"fox\", \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Malformed execution evidence JSON", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "pair-execution-bad-json", "records": {"comfyassets_prompt_pairs": "{nope", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"fox\", \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Multiple distinct executions have no image anchor", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "pair-execution-two-records", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"8\": {\"records\": [{\"text\": \"fox\", \"negative_text\": \"blur\", \"positive_text\": \"fox executed\", \"negative_effective_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}, {\"text\": \"fox\", \"negative_text\": \"blur\", \"positive_text\": \"other\", \"negative_effective_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"fox\", \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "execution", "status": "resolved", "text": "blur"}, "positive": {"source": "execution", "status": "resolved", "text": "fox executed"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "pair-execution-runtime-input", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"8\": {\"records\": [{\"text\": \"fox\", \"negative_text\": \"blur\", \"positive_text\": \"fox executed\", \"negative_effective_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": [\"9\", 0], \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}, \"9\": {\"class_type\": \"PromptSearchList\", \"inputs\": {}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (PromptManagerText).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (PromptManagerText).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "PromptManagerText", "id": "9"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "pair-execution-runtime-wrong-slot", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"8\": {\"records\": [{\"text\": \"fox\", \"negative_text\": \"blur\", \"positive_text\": \"fox executed\", \"negative_effective_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"8\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": [\"9\", 2], \"negative_text\": \"blur\", \"prepend_text\": \"\", \"append_text\": \"\"}}, \"9\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"x\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "custom-cfg", "records": {"prompt": "{\"3\": {\"class_type\": \"SamplerCustomAdvanced\", \"inputs\": {\"noise\": [\"20\", 0], \"guider\": [\"21\", 0], \"sampler\": [\"22\", 0], \"sigmas\": [\"23\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"21\": {\"class_type\": \"CFGGuider\", \"inputs\": {\"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"model\": [\"4\", 0], \"cfg\": 5}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Guider has no negative input", "reason_code": null, "sampler_id": "3", "state": "absent"}}, "name": "custom-basic", "records": {"prompt": "{\"3\": {\"class_type\": \"SamplerCustomAdvanced\", \"inputs\": {\"noise\": [\"20\", 0], \"guider\": [\"21\", 0], \"sampler\": [\"22\", 0], \"sigmas\": [\"23\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"21\": {\"class_type\": \"BasicGuider\", \"inputs\": {\"conditioning\": [\"6\", 0], \"model\": [\"4\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Guider has no negative input", "reason_code": null, "sampler_id": "3", "state": "absent"}}, "name": "custom-reroute-guider", "records": {"prompt": "{\"3\": {\"class_type\": \"SamplerCustomAdvanced\", \"inputs\": {\"noise\": [\"20\", 0], \"guider\": [\"21\", 0], \"sampler\": [\"22\", 0], \"sigmas\": [\"23\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"21\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"24\", 0]}}, \"24\": {\"class_type\": \"BasicGuider\", \"inputs\": {\"conditioning\": [\"6\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (DualCFGGuider).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (DualCFGGuider).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "DualCFGGuider", "id": "21"}, "reason": "Unsupported guider", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "custom-unknown-guider", "records": {"prompt": "{\"3\": {\"class_type\": \"SamplerCustomAdvanced\", \"inputs\": {\"noise\": [\"20\", 0], \"guider\": [\"21\", 0], \"sampler\": [\"22\", 0], \"sigmas\": [\"23\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"21\": {\"class_type\": \"DualCFGGuider\", \"inputs\": {\"cond1\": [\"6\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (KSamplerPreview).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (KSamplerPreview).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "KSamplerPreview", "id": "30"}, "reason": "Unsupported latent ancestry", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "latent-stage", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"LatentUpscale\", \"inputs\": {\"samples\": [\"30\", 0]}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"30\": {\"class_type\": \"KSamplerPreview\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"6\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "latent-encode", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"VAEEncode\", \"inputs\": {\"pixels\": [\"31\", 0], \"vae\": [\"4\", 2]}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"31\": {\"class_type\": \"LoadImage\", \"inputs\": {\"image\": \"x.png\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Cyclic latent ancestry", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "latent-cycle", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"LatentUpscale\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "override-ignored-\"\"", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0], \"wildcard\": \"\"}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "override-ignored-\"wild\"", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0], \"wildcard\": \"wild\"}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "override-ignored-[\"6\", 0]", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0], \"wildcard\": [\"6\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt exceeds inspection limits.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt exceeds inspection limits.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Resolution depth limit reached", "reason_code": "depth-limit", "sampler_id": "3", "state": "unresolved"}}, "name": "depth-limit", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"r69\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"deep\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"r0\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"6\", 0]}}, \"r1\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r0\", 0]}}, \"r2\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r1\", 0]}}, \"r3\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r2\", 0]}}, \"r4\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r3\", 0]}}, \"r5\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r4\", 0]}}, \"r6\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r5\", 0]}}, \"r7\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r6\", 0]}}, \"r8\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r7\", 0]}}, \"r9\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r8\", 0]}}, \"r10\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r9\", 0]}}, \"r11\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r10\", 0]}}, \"r12\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r11\", 0]}}, \"r13\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r12\", 0]}}, \"r14\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r13\", 0]}}, \"r15\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r14\", 0]}}, \"r16\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r15\", 0]}}, \"r17\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r16\", 0]}}, \"r18\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r17\", 0]}}, \"r19\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r18\", 0]}}, \"r20\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r19\", 0]}}, \"r21\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r20\", 0]}}, \"r22\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r21\", 0]}}, \"r23\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r22\", 0]}}, \"r24\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r23\", 0]}}, \"r25\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r24\", 0]}}, \"r26\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r25\", 0]}}, \"r27\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r26\", 0]}}, \"r28\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r27\", 0]}}, \"r29\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r28\", 0]}}, \"r30\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r29\", 0]}}, \"r31\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r30\", 0]}}, \"r32\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r31\", 0]}}, \"r33\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r32\", 0]}}, \"r34\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r33\", 0]}}, \"r35\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r34\", 0]}}, \"r36\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r35\", 0]}}, \"r37\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r36\", 0]}}, \"r38\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r37\", 0]}}, \"r39\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r38\", 0]}}, \"r40\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r39\", 0]}}, \"r41\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r40\", 0]}}, \"r42\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r41\", 0]}}, \"r43\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r42\", 0]}}, \"r44\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r43\", 0]}}, \"r45\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r44\", 0]}}, \"r46\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r45\", 0]}}, \"r47\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r46\", 0]}}, \"r48\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r47\", 0]}}, \"r49\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r48\", 0]}}, \"r50\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r49\", 0]}}, \"r51\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r50\", 0]}}, \"r52\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r51\", 0]}}, \"r53\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r52\", 0]}}, \"r54\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r53\", 0]}}, \"r55\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r54\", 0]}}, \"r56\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r55\", 0]}}, \"r57\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r56\", 0]}}, \"r58\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r57\", 0]}}, \"r59\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r58\", 0]}}, \"r60\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r59\", 0]}}, \"r61\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r60\", 0]}}, \"r62\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r61\", 0]}}, \"r63\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r62\", 0]}}, \"r64\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r63\", 0]}}, \"r65\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r64\", 0]}}, \"r66\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r65\", 0]}}, \"r67\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r66\", 0]}}, \"r68\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r67\", 0]}}, \"r69\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r68\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "deep"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "depth-near", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"r60\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"deep\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"r0\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"6\", 0]}}, \"r1\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r0\", 0]}}, \"r2\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r1\", 0]}}, \"r3\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r2\", 0]}}, \"r4\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r3\", 0]}}, \"r5\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r4\", 0]}}, \"r6\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r5\", 0]}}, \"r7\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r6\", 0]}}, \"r8\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r7\", 0]}}, \"r9\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r8\", 0]}}, \"r10\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r9\", 0]}}, \"r11\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r10\", 0]}}, \"r12\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r11\", 0]}}, \"r13\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r12\", 0]}}, \"r14\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r13\", 0]}}, \"r15\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r14\", 0]}}, \"r16\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r15\", 0]}}, \"r17\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r16\", 0]}}, \"r18\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r17\", 0]}}, \"r19\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r18\", 0]}}, \"r20\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r19\", 0]}}, \"r21\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r20\", 0]}}, \"r22\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r21\", 0]}}, \"r23\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r22\", 0]}}, \"r24\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r23\", 0]}}, \"r25\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r24\", 0]}}, \"r26\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r25\", 0]}}, \"r27\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r26\", 0]}}, \"r28\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r27\", 0]}}, \"r29\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r28\", 0]}}, \"r30\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r29\", 0]}}, \"r31\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r30\", 0]}}, \"r32\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r31\", 0]}}, \"r33\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r32\", 0]}}, \"r34\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r33\", 0]}}, \"r35\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r34\", 0]}}, \"r36\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r35\", 0]}}, \"r37\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r36\", 0]}}, \"r38\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r37\", 0]}}, \"r39\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r38\", 0]}}, \"r40\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r39\", 0]}}, \"r41\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r40\", 0]}}, \"r42\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r41\", 0]}}, \"r43\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r42\", 0]}}, \"r44\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r43\", 0]}}, \"r45\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r44\", 0]}}, \"r46\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r45\", 0]}}, \"r47\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r46\", 0]}}, \"r48\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r47\", 0]}}, \"r49\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r48\", 0]}}, \"r50\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r49\", 0]}}, \"r51\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r50\", 0]}}, \"r52\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r51\", 0]}}, \"r53\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r52\", 0]}}, \"r54\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r53\", 0]}}, \"r55\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r54\", 0]}}, \"r56\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r55\", 0]}}, \"r57\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r56\", 0]}}, \"r58\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r57\", 0]}}, \"r59\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r58\", 0]}}, \"r60\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"r59\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Saved PromptManager text was overwritten by another node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "overwritten", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1], \"search_text\": \"\"}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1]}}}", "workflow": "{\"nodes\": [{\"id\": 6, \"type\": \"PromptManager\", \"widgets_values\": [\"own text\"]}, {\"id\": 7, \"type\": \"PromptManager\", \"widgets_values\": [\"other text\"]}]}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Saved PromptManager text was overwritten by another node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "overwritten-subgraph", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"1:6\", 0], \"negative\": [\"1:7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"1:6\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"same\", \"clip\": [\"4\", 1]}}, \"1:7\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"same\"}}}", "workflow": "{\"nodes\": [{\"id\": 1, \"type\": \"sub-a\", \"widgets_values\": []}], \"definitions\": {\"subgraphs\": [{\"id\": \"sub-a\", \"nodes\": [{\"id\": 6, \"type\": \"PromptManagerFocused\", \"widgets_values\": {\"text\": \"a\"}}, {\"id\": 7, \"type\": \"PromptManagerFocused\", \"widgets_values\": [\"b\"]}]}]}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "shared"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "shared"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "overwritten-bad-workflow", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1], \"search_text\": \"\"}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1]}}}", "workflow": "{bad"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "shared"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "shared"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "workflow-type-list", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1], \"search_text\": \"\"}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1]}}}", "workflow": "{\"nodes\": [{\"id\": 6, \"type\": \"PromptManager\"}, {\"id\": 8, \"type\": [\"PromptManager\"]}]}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "shared"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "shared"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "workflow-type-dict", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1], \"search_text\": \"\"}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1]}}}", "workflow": "{\"nodes\": [{\"id\": 6, \"type\": \"PromptManager\"}, {\"id\": 8, \"type\": {\"a\": 1}}]}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "same"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "same"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "workflow-type-in-subgraph", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"1:6\", 0], \"negative\": [\"1:7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"1:6\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"same\", \"clip\": [\"4\", 1]}}, \"1:7\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"same\"}}}", "workflow": "{\"nodes\": [{\"id\": 1, \"type\": \"sub-a\", \"widgets_values\": []}], \"definitions\": {\"subgraphs\": [{\"id\": \"sub-a\", \"nodes\": [{\"id\": 2, \"type\": [1]}]}]}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Saved PromptManager text was overwritten by another node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "workflow-type-skipped-keeps-overwritten", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1], \"search_text\": \"\"}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1]}}}", "workflow": "{\"nodes\": [{\"id\": 6, \"type\": \"PromptManager\", \"widgets_values\": [\"own text\"]}, {\"id\": 8, \"type\": [\"PromptManager\"]}, {\"id\": 7, \"type\": \"PromptManager\", \"widgets_values\": [\"other text\"]}]}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "shared"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "shared"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "workflow-type-without-id", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1], \"search_text\": \"\"}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"shared\", \"clip\": [\"4\", 1]}}}", "workflow": "{\"nodes\": [{\"id\": true, \"type\": [\"x\"]}, \"skipped\"]}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Every sampler branch uses the same prompt pair", "reason_code": null, "sampler_id": null, "state": "resolved"}}, "name": "several-samplers", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "detailer", "records": {"prompt": "{\"3\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"5\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"wildcard\": \"\"}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "embedded-parameters", "status": "resolved", "text": "blur"}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": null, "state": "resolved"}}, "name": "parameters-only", "records": {"parameters": "a fox\nNegative prompt: blur\nSteps: 20, Sampler: euler"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "prompt-and-parameters", "records": {"parameters": "ignored", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Every sampler branch uses the same prompt pair", "reason_code": null, "sampler_id": null, "state": "resolved"}}, "name": "select-two-identical", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-two-differ", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-two-negative-differs", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"8\", 0], \"latent_image\": [\"5\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-identical-without-negative", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"latent_image\": [\"5\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-one-unresolved", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"missing\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-both-unresolved", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"x\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"y\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Every sampler branch uses the same prompt pair", "reason_code": null, "sampler_id": null, "state": "resolved"}}, "name": "select-three-identical-one-advanced", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"10\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-same-text-other-recipe", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"11\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"11\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"fox\", \"prepend_text\": \"a\", \"append_text\": \"\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-multistage-latent", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"3\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-selector-and-plain", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"12\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"12\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"6\", 0], \"on_false\": [\"8\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "select-preview-dropped", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"13\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"14\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"9\", 0]}}, \"15\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"13\", 0]}}, \"16\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"14\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-all-previews-kept", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"13\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"14\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"9\", 0]}}, \"15\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"13\", 0]}}, \"16\": {\"class_type\": \"Image Comparer (rgthree)\", \"inputs\": {\"images\": [\"14\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-preview-and-file", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"13\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"14\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"9\", 0]}}, \"15\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"13\", 0]}}, \"16\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"14\", 0]}}, \"17\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"14\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "select-unconsumed-stage-kept", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"14\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"9\", 0]}}, \"16\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"14\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-preview-unhashable-class", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"14\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"9\", 0]}}, \"16\": {\"class_type\": [\"PreviewImage\"], \"inputs\": {\"images\": [\"14\", 0]}}, \"17\": {\"class_type\": {\"a\": 1}, \"inputs\": {\"images\": [\"3\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "select-preview-through-bad-node", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"13\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"15\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"13\", 0]}}, \"16\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"9\", 0]}}, \"18\": \"not a node\", \"19\": {\"class_type\": \"Note\", \"inputs\": {\"x\": [\"18\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "11", "state": "resolved"}}, "name": "select-detailer-same-prompt", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"10\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Sampler stages have different prompts", "reason_code": "multistage-differs", "sampler_id": "11", "state": "ambiguous"}}, "name": "select-detailer-differs", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"10\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-detailer-and-own-output", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"10\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}, \"13\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"10\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Expected an image link", "reason_code": "malformed", "sampler_id": "11", "state": "unresolved"}}, "name": "select-detailer-image-literal", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": \"not a link\", \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Cyclic sampler stage ancestry", "reason_code": "malformed", "sampler_id": "11", "state": "unresolved"}}, "name": "select-detailer-cycle", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"10\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"11\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Sampler stages have different prompts", "reason_code": "multistage-differs", "sampler_id": "21", "state": "ambiguous"}}, "name": "select-detailer-chain", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"10\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"21\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"11\", 0], \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"21\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "21", "state": "resolved"}}, "name": "select-detailer-chain-same", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"10\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"21\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"11\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"21\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Sampler stages have different prompts", "reason_code": "multistage-differs", "sampler_id": "11", "state": "ambiguous"}}, "name": "select-detailer-two-upstream", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"9\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"8\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"10\": {\"class_type\": \"ImageBlend\", \"inputs\": {\"image1\": [\"13\", 0], \"image2\": [\"14\", 0]}}, \"13\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"14\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"9\", 0]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (MysterySampler).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (MysterySampler).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "MysterySampler", "id": "10"}, "reason": "Unsupported sampler stage ancestry", "reason_code": "unsupported-node", "sampler_id": "11", "state": "unresolved"}}, "name": "select-detailer-unsupported-stage", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"10\": {\"class_type\": \"MysterySampler\", \"inputs\": {\"positive\": [\"6\", 0], \"model\": [\"4\", 0]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "11", "state": "unresolved"}}, "name": "select-detailer-upstream-unresolved", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"gone\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"10\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Selector branches have different prompts", "reason_code": null, "sampler_id": "11", "state": "ambiguous"}}, "name": "select-detailer-upstream-selector", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"15\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"15\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"6\", 0], \"on_false\": [\"8\", 0]}}, \"10\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (FaceDetailer).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt is chosen at run time (FaceDetailer).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "FaceDetailer", "id": "11"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "11", "state": "unresolved"}}, "name": "select-detailer-wildcard", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"10\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"wildcard\": \"[x|y]\"}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-detailer-no-upscaled-image", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"10\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"11\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "blurry"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "11", "state": "resolved"}}, "name": "select-detailer-no-sampler-upstream", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"10\": {\"class_type\": \"LoadImage\", \"inputs\": {\"image\": \"a.png\"}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-detailer-deep-ancestry", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"d0\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"d1\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d0\", 0]}}, \"d2\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d1\", 0]}}, \"d3\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d2\", 0]}}, \"d4\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d3\", 0]}}, \"d5\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d4\", 0]}}, \"d6\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d5\", 0]}}, \"d7\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d6\", 0]}}, \"d8\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d7\", 0]}}, \"d9\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d8\", 0]}}, \"d10\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d9\", 0]}}, \"d11\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d10\", 0]}}, \"d12\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d11\", 0]}}, \"d13\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d12\", 0]}}, \"d14\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d13\", 0]}}, \"d15\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d14\", 0]}}, \"d16\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d15\", 0]}}, \"d17\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d16\", 0]}}, \"d18\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d17\", 0]}}, \"d19\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d18\", 0]}}, \"d20\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d19\", 0]}}, \"d21\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d20\", 0]}}, \"d22\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d21\", 0]}}, \"d23\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d22\", 0]}}, \"d24\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d23\", 0]}}, \"d25\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d24\", 0]}}, \"d26\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d25\", 0]}}, \"d27\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d26\", 0]}}, \"d28\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d27\", 0]}}, \"d29\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d28\", 0]}}, \"d30\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d29\", 0]}}, \"d31\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d30\", 0]}}, \"d32\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d31\", 0]}}, \"d33\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d32\", 0]}}, \"d34\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d33\", 0]}}, \"d35\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d34\", 0]}}, \"d36\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d35\", 0]}}, \"d37\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d36\", 0]}}, \"d38\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d37\", 0]}}, \"d39\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d38\", 0]}}, \"d40\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d39\", 0]}}, \"d41\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d40\", 0]}}, \"d42\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d41\", 0]}}, \"d43\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d42\", 0]}}, \"d44\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d43\", 0]}}, \"d45\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d44\", 0]}}, \"d46\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d45\", 0]}}, \"d47\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d46\", 0]}}, \"d48\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d47\", 0]}}, \"d49\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d48\", 0]}}, \"d50\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d49\", 0]}}, \"d51\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d50\", 0]}}, \"d52\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d51\", 0]}}, \"d53\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d52\", 0]}}, \"d54\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d53\", 0]}}, \"d55\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d54\", 0]}}, \"d56\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d55\", 0]}}, \"d57\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d56\", 0]}}, \"d58\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d57\", 0]}}, \"d59\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d58\", 0]}}, \"d60\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d59\", 0]}}, \"d61\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d60\", 0]}}, \"d62\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d61\", 0]}}, \"d63\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d62\", 0]}}, \"d64\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d63\", 0]}}, \"d65\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d64\", 0]}}, \"d66\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d65\", 0]}}, \"d67\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d66\", 0]}}, \"d68\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d67\", 0]}}, \"d69\": {\"class_type\": \"ImageScale\", \"inputs\": {\"image\": [\"d68\", 0]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"d69\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"11\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt exceeds inspection limits.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt exceeds inspection limits.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Resolution depth limit reached", "reason_code": "depth-limit", "sampler_id": "s66", "state": "unresolved"}}, "name": "select-detailer-stacked-past-depth", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0], \"latent_image\": [\"5\", 0]}}, \"s0\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"3\", 0]}}, \"s1\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s0\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s2\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s1\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s3\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s2\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s4\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s3\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s5\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s4\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s6\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s5\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s7\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s6\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s8\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s7\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s9\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s8\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s10\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s9\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s10\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s12\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s11\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s13\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s12\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s14\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s13\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s15\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s14\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s16\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s15\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s17\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s16\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s18\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s17\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s19\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s18\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s20\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s19\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s21\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s20\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s22\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s21\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s23\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s22\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s24\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s23\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s25\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s24\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s26\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s25\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s27\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s26\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s28\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s27\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s29\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s28\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s30\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s29\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s31\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s30\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s32\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s31\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s33\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s32\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s34\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s33\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s35\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s34\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s36\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s35\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s37\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s36\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s38\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s37\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s39\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s38\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s40\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s39\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s41\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s40\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s42\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s41\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s43\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s42\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s44\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s43\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s45\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s44\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s46\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s45\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s47\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s46\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s48\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s47\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s49\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s48\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s50\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s49\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s51\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s50\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s52\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s51\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s53\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s52\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s54\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s53\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s55\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s54\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s56\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s55\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s57\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s56\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s58\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s57\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s59\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s58\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s60\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s59\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s61\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s60\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s62\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s61\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s63\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s62\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s64\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s63\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s65\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s64\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"s66\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"s65\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"s66\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Cyclic sampler stage ancestry", "reason_code": "malformed", "sampler_id": "21", "state": "unresolved"}}, "name": "select-detailer-mutual-cycle", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"6\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a fox\", \"clip\": [\"4\", 1]}}, \"7\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"blurry\", \"clip\": [\"4\", 1]}}, \"8\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a cat\", \"clip\": [\"4\", 1]}}, \"11\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"21\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"21\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"11\", 0], \"model\": [\"4\", 0], \"positive\": [\"6\", 0], \"negative\": [\"7\", 0]}}, \"12\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"21\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "embedded-parameters", "status": "resolved", "text": "blur"}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": null, "state": "resolved"}}, "name": "parameters-basic", "records": {"parameters": "a fox\nNegative prompt: blur\nSteps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Negative prompt delimiter is absent", "reason_code": null, "sampler_id": null, "state": "absent"}}, "name": "parameters-no-negative", "records": {"parameters": "a fox\nSteps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple negative prompt delimiters", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "parameters-two-delimiters", "records": {"parameters": "a\nNegative prompt: b\nNegative prompt: c\nSteps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": null, "state": "unresolved"}}, "name": "parameters-leading-delimiter", "records": {"parameters": "Negative prompt: blur\nSteps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": null, "state": "unresolved"}}, "name": "parameters-blank-positive", "records": {"parameters": " …\nNegative prompt: blur\nSteps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "parameters-footer-not-last", "records": {"parameters": "a fox\nSteps: 20, Sampler: euler, CFG scale: 7\nmore text"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "parameters-two-footers", "records": {"parameters": "a\nSteps: 20, Sampler: euler, CFG scale: 7\nSteps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"source": "embedded-parameters", "status": "resolved", "text": "blur"}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": null, "state": "resolved"}}, "name": "parameters-bare-footer", "records": {"parameters": "a fox\nNegative prompt: blur\nSteps: 20"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox\nSteps: 5"}}, "resolution": {"node": null, "reason": "Negative prompt delimiter is absent", "reason_code": null, "sampler_id": null, "state": "absent"}}, "name": "parameters-bare-and-full", "records": {"parameters": "a fox\nSteps: 5\nSteps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "parameters-two-bare", "records": {"parameters": "a\nSteps: 1\nSteps: 2"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Negative prompt delimiter is absent", "reason_code": null, "sampler_id": null, "state": "absent"}}, "name": "parameters-template", "records": {"parameters": "a fox\nSteps: 20, Sampler: euler, CFG scale: 7\nTemplate: {a|b}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a"}}, "resolution": {"node": null, "reason": "Negative prompt delimiter is absent", "reason_code": null, "sampler_id": null, "state": "absent"}}, "name": "parameters-both-templates", "records": {"parameters": "a\nSteps: 20, Sampler: euler, CFG scale: 7\nTemplate: x\nNegative Template: y"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "parameters-templates-reversed", "records": {"parameters": "a\nSteps: 20, Sampler: euler, CFG scale: 7\nNegative Template: y\nTemplate: x"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "parameters-two-templates", "records": {"parameters": "a\nSteps: 20, Sampler: euler, CFG scale: 7\nTemplate: x\nTemplate: y"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "parameters-template-multiline", "records": {"parameters": "a\nSteps: 20, Sampler: euler, CFG scale: 7\nTemplate: x\nmore"}}, {"evidence": {"prompts": {"negative": {"source": "embedded-parameters", "status": "resolved", "text": "blur"}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": null, "state": "resolved"}}, "name": "parameters-crlf", "records": {"parameters": "a fox\r\nNegative prompt: blur\r\nSteps: 20, Sampler: euler, CFG scale: 7\r\n"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "parameters-cr-only", "records": {"parameters": "a fox\rNegative prompt: blur\rSteps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a"}}, "resolution": {"node": null, "reason": "Negative prompt delimiter is absent", "reason_code": null, "sampler_id": null, "state": "absent"}}, "name": "parameters-cr-template", "records": {"parameters": "a\nSteps: 20, Sampler: euler, CFG scale: 7\rTemplate: x"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "parameters-line-separator", "records": {"parameters": "a fox
Steps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "parameters-next-line", "records": {"parameters": "a fox…Steps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Negative prompt delimiter is absent", "reason_code": null, "sampler_id": null, "state": "absent"}}, "name": "parameters-spaced-steps", "records": {"parameters": "a fox\nSteps:\n 20 \n, x"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Negative prompt delimiter is absent", "reason_code": null, "sampler_id": null, "state": "absent"}}, "name": "parameters-steps-newline-bare", "records": {"parameters": "a fox\nSteps:\n20\n"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Negative prompt delimiter is absent", "reason_code": null, "sampler_id": null, "state": "absent"}}, "name": "parameters-arabic-digits", "records": {"parameters": "a fox\nSteps: ٢٠, Sampler: euler"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "Negative prompt delimiter is absent", "reason_code": null, "sampler_id": null, "state": "absent"}}, "name": "parameters-fullwidth-digits", "records": {"parameters": "a fox\nSteps: 2, x"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "parameters-no-digits", "records": {"parameters": "a fox\nSteps: , x"}}, {"evidence": {"prompts": {"negative": {"source": "embedded-parameters", "status": "resolved", "text": "blur"}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": null, "state": "resolved"}}, "name": "parameters-trailing-spaces", "records": {"parameters": "a fox \nNegative prompt: blur \nSteps: 20, Sampler: euler, CFG scale: 7 \n \n"}}, {"evidence": {"prompts": {"negative": {"source": "embedded-parameters", "status": "resolved", "text": ""}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": null, "state": "resolved"}}, "name": "parameters-empty-negative", "records": {"parameters": "a fox\nNegative prompt:\nSteps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": null, "status": "absent", "text": null}}, "resolution": {"node": null, "reason": "No A1111 parameters", "reason_code": null, "sampler_id": null, "state": "absent"}}, "name": "parameters-empty", "records": {"parameters": ""}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": null, "state": "unresolved"}}, "name": "parameters-only-footer", "records": {"parameters": "Steps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"source": "embedded-parameters", "status": "resolved", "text": "blur"}, "positive": {"source": "embedded-parameters", "status": "resolved", "text": "a fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": null, "state": "resolved"}}, "name": "parameters-with-execution", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {}}", "parameters": "a fox\nNegative prompt: blur\nSteps: 20, Sampler: euler, CFG scale: 7"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (ConditioningCombine).", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "
   café\u001csnow"}}, "resolution": {"node": {"class_type": "ConditioningCombine", "id": "102"}, "reason": "Combined conditioning has different texts", "reason_code": "composite", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-0", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"103\": {\"records\": [{\"text\": \"\\u001csnow \", \"negative_text\": \"\\u0085\\u2028 detailed fur\", \"prepend_text\": \"\\u732b\", \"append_text\": \"\\ud83d\\ude00\", \"positive_text\": \"snow\\u3000\", \"negative_effective_text\": \"\\u0085\\u2028 detailed fur\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"
  \", \"prepend_text\": \"\\u001c…\", \"append_text\": \"\\ncafé\\u001csnow\", \"clip\": [\"4\", 1]}}, \"103\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001csnow \", \"negative_text\": \"…
 detailed fur\", \"prepend_text\": \"…猫
 \", \"append_text\": \"
 😀\"}}, \"105\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"…snow\\n\", \"prepend_text\": \"\\n猫\\t\", \"negative_text\": \"snow \"}}, \"104\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"105\", 0]}}, \"102\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"103\", 0], \"conditioning_2\": [\"104\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "zeroed-conditioning", "status": "resolved", "text": ""}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "猫, snow"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "fuzz-0-1", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"\\u001csnow
 \", \"prepend_text\": \"
 猫
 \", \"negative_text\": \"snow \"}}, \"105\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u001c café\", \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"105\", 0]}}, \"103\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"104\", 0]}}, \"102\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"103\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Expected a typed graph link or text input", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-2", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"105\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \" a, \"}}, \"104\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"105\", 0]}}, \"103\": {\"class_type\": \"TextInput\", \"inputs\": {\"text\": [\"104\", 0]}}, \"102\": {\"class_type\": \"TextInput\", \"inputs\": {\"string\": [\"103\", 0]}}, \"101\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"102\", 0], \"text_l\": \"\"}}, \"108\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\t猫\", \"clip\": [\"4\", 1]}}, \"109\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"snow\\n\"}}, \"107\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"108\", 0], \"on_false\": [\"109\", 0]}}, \"111\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"…snow\", \"text_l\": \"…snow\"}}, \"110\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"111\", 0]}}, \"106\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"107\", 0], \"on_false\": [\"110\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "café, 😀"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "café, snow\u001c \tfox
  猫"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "fuzz-0-3", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"\\nsnow\\u001c\", \"string_b\": \"\\tfox\\u2028 \", \"delimiter\": \" \"}}, \"103\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"104\", 0], \"string_b\": \"\\u3000\\u732b\\u001c\", \"delimiter\": \" \"}}, \"102\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": [\"103\", 0]}}, \"101\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"\\ufeffcaf\\u00e9\\n\", \"negative_text\": \"caf\\u00e9\\t\"}}, \"106\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"\\u3000\\ud83d\\ude00\\u0085\", \"prepend_text\": \"\\u001ccaf\\u00e9\\u2028 \", \"negative_text\": \"\\ufeffsnow\\u0085\"}}, \"105\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"106\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-4", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"107\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"StringConcatenate\"}, \"105\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_a\": \"\\t\\u2028 \", \"text_b\": \"\\u2028 fox\", \"clean_whitespace\": \"false\"}}, \"106\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \"\\nsnow\\t\"}}, \"103\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_c\": [\"104\", 0], \"text_b\": [\"105\", 0], \"text_d\": [\"106\", 0], \"clean_whitespace\": \"false\"}}, \"102\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"103\", 0]}}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"\\u3000a,fox\", \"negative_text\": \"\\u2028 snow\\n\\u732b\"}}, \"110\": {\"class_type\": \"StringConstant\", \"inputs\": {\"string\": \"\\u001c \"}}, \"109\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"110\", 0], \"text_l\": [\"110\", 0]}}, \"108\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"109\", 0]}}, \"111\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \" caf\\u00e9\\u001c\"}}, \"107\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"108\", 0], \"conditioning_2\": [\"111\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Selector branches have different prompts", "reason_code": null, "sampler_id": "3", "state": "ambiguous"}}, "name": "fuzz-0-5", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"112\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"\\ufeffdetailed fur\\u0085\", \"prepend_text\": \"\\t\\u732b\\u3000\", \"append_text\": \"caf\\u00e9\\ta,\"}}, \"103\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"104\", 0], \"clip\": [\"4\", 1]}}, \"102\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"103\", 0]}}, \"107\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u001ccaf\\u00e9\\t\", \"clip\": [\"4\", 1]}}, \"109\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u0085detailed fur\\u2028 snow\", \"clip\": [\"4\", 1]}}, \"108\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"109\", -1], \"conditioning_2\": [\"109\", -1]}}, \"106\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"107\", 0], \"on_false\": [\"108\", 0]}}, \"111\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u001ccaf\\u00e9\\u2028 \", \"clip\": [\"4\", 1]}}, \"110\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"111\", 0]}}, \"105\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"106\", 0], \"on_false\": [\"110\", 0]}}, \"101\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"102\", 0], \"on_false\": [\"105\", 0]}}, \"113\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\ufeff\\u0085\", \"prepend_text\": \"\\u0085snow\\ufeff\", \"append_text\": \"\\u3000fox\\u001c\", \"clip\": [\"4\", 1]}}, \"114\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\tdetailed fur snow\"}}, \"112\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"113\", 0], \"on_false\": [\"114\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "a, 猫\t"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "…猫\u001c"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "fuzz-0-6", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 1], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\ufeff\\n\", \"negative_text\": \"\\u0085\\u732b\\u001c\", \"prepend_text\": \"\\tdetailed fur\\n\", \"append_text\": \" fox\\u2028 \"}}, \"102\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"\\u732b\\t\", \"prepend_text\": \"\\u3000a,\\u2028 \", \"append_text\": \" \\t\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "😀"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "\n猫"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "fuzz-0-7", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 1], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"…\", \"negative_text\": \"\\n猫\", \"prepend_text\": \"…a,\", \"append_text\": \"\\u001ccafé \"}}, \"103\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"😀\"}}, \"102\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"103\", 0], \"text_l\": [\"103\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "101"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-8", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"…detailed fur
 \"}}, \"102\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \" 猫\\u001c\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "fox, snow  detailed fur café"}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-9", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"…snow\", \"string_3\": \" detailed fur café\", \"inputcount\": 3, \"delimiter\": \" \", \"return_list\": false}}, \"103\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": [\"104\", 0]}}, \"102\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": [\"103\", 0], \"strip_newlines\": false}}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"fox\", \"negative_text\": \"snow\\t\"}}, \"106\": {\"class_type\": \"TextInput\"}, \"105\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"106\", 0], \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "101"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-10", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\ndetailed fur\\ufefffox\"}}, \"105\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"detailed fur\\ufeff\", \"strip_newlines\": true}}, \"104\": {\"class_type\": \"TextInput\", \"inputs\": {\"string\": [\"105\", 0]}}, \"107\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \"\\u001cfox\\u2028 \"}}, \"108\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\nsnow\\u2028 \", \"strip_newlines\": false}}, \"106\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"107\", 0], \"string_b\": [\"108\", 0], \"delimiter\": \" \"}}, \"103\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": [\"104\", 0], \"string_2\": [\"106\", 0], \"inputcount\": 2, \"delimiter\": \",\", \"return_list\": false}}, \"102\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"103\", 0], \"prepend_text\": \"\\tdetailed fur\\u3000\", \"negative_text\": \" detailed fur caf\\u00e9\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Cyclic prompt graph", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-11", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"\\u0085snow \", \"string_b\": \"a,\\u0085\\u732b\", \"delimiter\": \", \"}}, \"103\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"104\", 0]}}, \"102\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": [\"102\", 0]}}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \" caf\\u00e9\\nsnow\", \"negative_text\": \"\\ud83d\\ude00\\u0085\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-12", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"105\": {\"class_type\": \"PrimitiveString\"}, \"104\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"105\", 0]}}, \"103\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"104\", 0], \"clip\": [\"4\", 1]}}, \"102\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"103\", 0]}}, \"101\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"102\", 0], \"conditioning_2\": [\"102\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-13", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"107\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\"}, \"105\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\ncaf\\u00e9\\u001c\", \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"caf\\u00e9\", \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"105\", 0], \"on_false\": [\"106\", 0]}}, \"103\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"104\", 0]}}, \"102\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"103\", 0]}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"110\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"\\tsnow\\u001ccaf\\u00e9\", \"string_b\": \"\\ufeffdetailed fur\\t\", \"delimiter\": \", \"}}, \"109\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_d\": \"\\ufeffa,\\u2028 detailed fur\", \"text_b\": \"\\u0085\\n\", \"text_c\": [\"110\", 0], \"clean_whitespace\": \"true\"}}, \"108\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"109\", 0], \"prepend_text\": \"\\n\\u0085\", \"append_text\": \"\\ta,\\ufeff\", \"clip\": [\"4\", 1]}}, \"107\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"108\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-14", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\"}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"\\u001csnow\\u001c\", \"prepend_text\": \"\\tdetailed fur\\ufeff\", \"negative_text\": \"\\ndetailed fur\\u001c\"}}, \"103\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\ud83d\\ude00\\u3000\"}}, \"104\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"\\u001ca,\\u001c\", \"prepend_text\": \"\\nsnow\\ud83d\\ude00\", \"append_text\": \"\\u001ca, \", \"clip\": [\"4\", 1]}}, \"102\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"103\", 0], \"conditioning_2\": [\"104\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (ConditioningCombine).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt combines several texts (ConditioningCombine).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "ConditioningCombine", "id": "101"}, "reason": "Combined conditioning has different texts", "reason_code": "composite", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-15", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"103\": {\"records\": [{\"text\": \"\\u2028 \\u732b\\u2028 detailed fur\", \"negative_text\": \"\\u001c\\ud83d\\ude00\\ufeff\", \"prepend_text\": \"caf\\u00e9\", \"append_text\": \"detailed fur\\ufeff\\ud83d\\ude00\", \"positive_text\": \"\\ufeffdetailed fur\\n\", \"negative_effective_text\": \"\\u001c\\ud83d\\ude00\\ufeff\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u2028 \\u732b\\u2028 detailed fur\", \"negative_text\": \"\\u001c\\ud83d\\ude00\\ufeff\", \"prepend_text\": \"\\tcaf\\u00e9 \", \"append_text\": \"\\u3000detailed fur\\ufeff\\ud83d\\ude00\"}}, \"102\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"103\", 0], \"conditioning_2\": [\"103\", 0]}}, \"104\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\t\", \"clip\": [\"4\", 1]}}, \"101\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"102\", 0], \"conditioning_2\": [\"104\", 0]}}, \"108\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"\\u3000fox\\u0085fox\", \"string_b\": \"\\u001c\\ud83d\\ude00\\n\\ud83d\\ude00\", \"delimiter\": \"\"}}, \"107\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"108\", 0], \"string_b\": \"\\ufeffa,\\u3000detailed fur\", \"delimiter\": \" \"}}, \"106\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"107\", 0], \"prepend_text\": \"\\u3000caf\\u00e9\\ufeff\", \"append_text\": \"\\ta,\\tcaf\\u00e9\", \"clip\": [\"4\", 1]}}, \"110\": {\"class_type\": \"TextInput\", \"inputs\": {\"text\": \"\\n\\u732b\\ufeff\"}}, \"109\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"110\", 0], \"prepend_text\": \"detailed fursnow\", \"negative_text\": \"\\ta,\"}}, \"105\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"106\", 0], \"on_false\": [\"109\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (Unknown).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (Unknown).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "Unknown", "id": "104"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-16", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"Unknown\", \"inputs\": {\"string_1\": \" \", \"string_2\": \"
 detailed fur\", \"string_3\": \"\\na,\\u001cfox\", \"inputcount\": 3, \"delimiter\": \" \", \"return_list\": false}}, \"103\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"104\", 0], \"text_l\": [\"104\", 0]}}, \"102\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"103\", 0]}}, \"101\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"102\", 0], \"conditioning_2\": [\"102\", 0]}}, \"106\": {\"class_type\": \"StringConstant\", \"inputs\": {\"text\": \" snow…\"}}, \"105\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"106\", 0], \"prepend_text\": \"a,\\u001cfox\", \"append_text\": \"\\na, a,\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (ConditioningCombine).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt combines several texts (ConditioningCombine).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "ConditioningCombine", "id": "102"}, "reason": "Combined conditioning has different texts", "reason_code": "composite", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-17", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"103\": {\"records\": [{\"text\": \"\\u001c\\u0085\", \"negative_text\": \"\\nfox\\u001c\", \"prepend_text\": \"fox\\ufeff\", \"append_text\": \"\\ufefffox\", \"positive_text\": \" snow caf\\u00e9\", \"negative_effective_text\": \"\\nfox\\u001c\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"107\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001c\\u0085\", \"negative_text\": \"\\nfox\\u001c\", \"prepend_text\": \" fox\\ufeff\", \"append_text\": \"\\ufefffox\\u3000\"}}, \"105\": {\"class_type\": \"StringConstant\", \"inputs\": {\"string\": \"\\n\\n\\ud83d\\ude00\"}}, \"104\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"105\", 0], \"prepend_text\": \"\\u0085a,\\u3000\\ud83d\\ude00\", \"negative_text\": \"\\u2028 \\u732b\\u001c\"}}, \"102\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"103\", 1], \"conditioning_2\": [\"104\", 0]}}, \"106\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"snow detailed fur\", \"prepend_text\": \"\\n\\u732b\\u001c\", \"negative_text\": \"detailed fur\\ud83d\\ude00\"}}, \"101\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"102\", 0]}}, \"109\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\n\\u732b \"}}, \"110\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\na,\\ufeffcaf\\u00e9\"}}, \"108\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"109\", 0], \"on_false\": [\"110\", 0]}}, \"107\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"108\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "PromptSearchList", "id": "101"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-18", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\tdetailed fur\\t\"}}, \"102\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\n\\ncaf\\u00e9\", \"text_l\": \"\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a, detailed fur, café"}}, "resolution": {"node": {"class_type": "PromptSearchList", "id": "105"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-19", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"\\ncafé\", \"prepend_text\": \"a, detailed fur\", \"negative_text\": \"\\na,\\u001c\"}}, \"104\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"…猫…\", \"clip\": [\"4\", 1]}}, \"102\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"103\", 0], \"on_false\": [\"104\", 0]}}, \"101\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"102\", 0]}}, \"105\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\u001ca,\\t\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "PromptSearchList", "id": "101"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-20", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\u0085fox\\ufeff\"}}, \"102\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u0085snow\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-21", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"\\u001ccaf\\u00e9\\u0085\", \"prepend_text\": \"\\u3000detailed fur\", \"negative_text\": \"\\u001c\"}}, \"101\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"103\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\tcaf\\u00e9 \", \"prepend_text\": \"\\t\\u732b\", \"append_text\": \"\\tcaf\\u00e9 \", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "101"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-22", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\tfox\\u001c\"}}, \"103\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \"\\u001ccaf\\u00e9\\ufeffa,\"}}, \"102\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"103\", 0], \"prepend_text\": \"\\tdetailed fur\\u0085\", \"append_text\": \"\\u3000fox\\u001c\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "\u001c😀…"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "foxfox, \u001c猫
 "}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "fuzz-0-23", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"104\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"\\ufefffoxfox\", \"string_b\": \"\\u001c\\u732b\\u2028 \", \"delimiter\": \", \"}}, \"102\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": [\"103\", 0], \"inputcount\": 1, \"delimiter\": \",\", \"return_list\": false}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"102\", 0], \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\tcaf\\u00e9\\u0085\"}}, \"105\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"106\", 0]}}, \"110\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"\\u001c\\ud83d\\ude00\\u0085\"}}, \"109\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"110\", 0]}}, \"108\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": [\"109\", 0]}}, \"107\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"108\", 0], \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"105\", 0], \"on_false\": [\"107\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-24", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"104\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \"\\u001c\"}}, \"102\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"103\", 0], \"clip\": [\"4\", 1]}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"104\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001ccafé
 café\", \"negative_text\": \" detailed fur 😀\", \"prepend_text\": \"detailed fur\\u001c\", \"append_text\": \"
 a,…detailed fur\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "102"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-25", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"…\"}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"105\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\t猫…\", \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"105\", 0]}}, \"103\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"104\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-26", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"105\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\tdetailed fur \", \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"105\", 0], \"conditioning_2\": [\"105\", 0]}}, \"103\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"104\", 0]}}, \"102\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"103\", 0]}}, \"101\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"106\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\ncaf\\u00e9\\ufeff\", \"negative_text\": \"\\u3000detailed fur\\u001ca,\", \"prepend_text\": \"\\u0085snow\\u001cfox\", \"append_text\": \"\\u001c\\u732b\\u3000\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-27", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"105\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"a, \"}}, \"104\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"105\", 0]}}, \"103\": {\"class_type\": \"Text Concatenate\", \"inputs\": []}, \"102\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"103\", 0]}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"102\", 0], \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\tdetailed fur\\u001c\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-28", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\"}, \"101\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"
 detailed fur\\t\", \"negative_text\": \" snow
 😀\", \"prepend_text\": \"\\n😀…\", \"append_text\": \"\\u001c😀
 \"}}, \"102\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"\\tfox…\", \"prepend_text\": \" 猫\", \"negative_text\": \"café…\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-29", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 1], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"snow \", \"negative_text\": \"\\u0085\\u0085\", \"prepend_text\": \"\\u2028 a, fox\", \"append_text\": \"\\tcaf\\u00e9\"}}, \"102\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\u001cdetailed fur\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "fox…detailed fur \u001c猫 snow a,"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a,\u001c"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "fuzz-0-30", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"a,\\u001c\", \"text_l\": \"a,\\u001c\"}}, \"102\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\u001c猫 snow\", \"prepend_text\": \" fox…detailed fur\", \"append_text\": \"a,\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Expected a typed graph link or text input", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-31", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"\\u0085a, \", \"string_b\": \"caf\\u00e9\\u2028 \", \"delimiter\": \", \"}}, \"105\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \"\\u0085caf\\u00e9\\u2028 \"}}, \"103\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"104\", 0], \"string_b\": [\"105\", 0], \"delimiter\": \"\"}}, \"102\": {\"class_type\": \"TextInput\", \"inputs\": {\"string\": [\"103\", 0]}}, \"101\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"\\tcaf\\u00e9\\n\", \"negative_text\": \"\\u0085fox\\ufeff\"}}, \"106\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\u001ca,\\t\", \"prepend_text\": \"\\u0085\\u732b\\t\", \"append_text\": \"\\ufeffa,\\t\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "foxsnow"}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "105"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-32", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"104\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_a\": \"…foxsnow\", \"clean_whitespace\": true}}, \"102\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"103\", 0], \"clip\": [\"4\", 1]}}, \"101\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"102\", 0]}}, \"105\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"detailed fur \"}}, \"104\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"105\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "fox fox 猫 café…fox …a,\t fox café\ta, snow"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "snow, fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "fuzz-0-33", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"103\": {\"records\": [{\"text\": \" caf\\u00e9\\u3000!\", \"negative_text\": \"\\u0085snow\\u3000\", \"prepend_text\": \"detailed fur\", \"append_text\": \"\", \"positive_text\": \"\\u001c\\ud83d\\ude00\\u2028 \", \"negative_effective_text\": \"\\u0085snow\\u3000\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \" café \", \"negative_text\": \"…snow \", \"prepend_text\": \"\\n detailed fur\", \"append_text\": \"\\n \"}}, \"105\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"\\u001cfox…\", \"prepend_text\": \" snow
 \", \"negative_text\": \"café detailed fur\"}}, \"104\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"105\", 0]}}, \"102\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"103\", 1], \"on_false\": [\"104\", 0]}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"109\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_c\": \"…a,\\t\", \"text_d\": \"fox\\n\", \"text_a\": \"\\u001ccafé…fox\", \"clean_whitespace\": \"false\"}}, \"108\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_c\": [\"109\", 0], \"clean_whitespace\": true}}, \"107\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": [\"108\", 0], \"prepend_text\": \" fox 猫\", \"append_text\": \"…café\\ta,\"}}, \"106\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"107\", 0], \"prepend_text\": \"\\tfox \", \"append_text\": \"snow \", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-34", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"111\": {\"records\": [{\"text\": \"\\nsnow\\u001c\", \"negative_text\": \"\\tdetailed fur \", \"prepend_text\": \"snow\", \"append_text\": \"\\ufeffa,\", \"positive_text\": \"\\u001c\\n\", \"negative_effective_text\": \"\\tdetailed fur \"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"105\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\nfox\\u2028 \", \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"105\", 0]}}, \"103\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"104\", 0]}}, \"102\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"103\", 0]}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"109\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"\\ta,\\u2028 \", \"prepend_text\": \"\\u2028 caf\\u00e9\\u3000\", \"append_text\": \"\\u2028 \\ud83d\\ude00\\n\", \"clip\": [\"4\", 1]}}, \"110\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"\\u3000\\u2028 \", \"prepend_text\": \"\\u3000detailed fur \", \"append_text\": \"\\tcaf\\u00e9\", \"clip\": [\"4\", 1]}}, \"108\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"109\", 0], \"on_false\": [\"110\", 0]}}, \"111\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\nsnow\\u001c\", \"negative_text\": \"\\tdetailed fur \", \"prepend_text\": \"snow\\u0085\", \"append_text\": \"\\ufeffa, \"}}, \"107\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"108\", 0], \"conditioning_2\": [\"111\", 0]}}, \"106\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"107\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "\t😀\t"}, "positive": {"source": "execution", "status": "resolved", "text": "fox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "fuzz-0-35", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"101\": {\"records\": [{\"text\": \"caf\\u00e9\\u2028 \", \"negative_text\": \"\\n\\u3000caf\\u00e9\", \"prepend_text\": \"caf\\u00e9\\ufeff\", \"append_text\": \"snow\", \"positive_text\": \"\\ufefffox\\ufeff\", \"negative_effective_text\": \"\\n\\u3000caf\\u00e9\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"caf\\u00e9\\u2028 \", \"negative_text\": \"\\n\\u3000caf\\u00e9\", \"prepend_text\": \"\\u3000caf\\u00e9\\ufeff\", \"append_text\": \"\\u3000snow\\u3000\"}}, \"102\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\ufeff\\ud83d\\ude00\\ta,\", \"negative_text\": \"\\t\\ud83d\\ude00\\t\", \"prepend_text\": \" \\u3000\", \"append_text\": \"\\u3000snow\\u001c\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (CLIPTextEncodeSDXL).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt combines several texts (CLIPTextEncodeSDXL).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "CLIPTextEncodeSDXL", "id": "101"}, "reason": "Encoder parts have different texts", "reason_code": "composite", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-36", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"café
 fox\", \"text_l\": \"
 \"}}, \"103\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\n猫\"}}, \"107\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"…\", \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"107\", 0]}}, \"109\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" 😀 a,\", \"clip\": [\"4\", 1]}}, \"110\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" café\\n猫\", \"clip\": [\"4\", 1]}}, \"108\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"109\", 0], \"on_false\": [\"110\", 0]}}, \"105\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"106\", 0], \"conditioning_2\": [\"108\", 0]}}, \"111\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"\\tsnow \", \"prepend_text\": \"猫…\", \"negative_text\": \"…detailed fur\\u001c\"}}, \"104\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"105\", 0], \"conditioning_2\": [\"111\", 0]}}, \"102\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"103\", 0], \"on_false\": [\"104\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "…猫\t"}}, "resolution": {"node": null, "reason": "Execution evidence does not match recorded node inputs", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-37", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"104\": {\"records\": [{\"text\": \"\\ndetailed fur\\u2028 !\", \"negative_text\": \"\\na,\\u2028 \", \"prepend_text\": \"\", \"append_text\": \"detailed fur\", \"positive_text\": \" \\ud83d\\ude00\\n\", \"negative_effective_text\": \"\\na,\\u2028 \"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"104\", 1], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": null, \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\\u0085\\u732b\\t\", \"inputcount\": 1, \"delimiter\": \",\", \"return_list\": false}}, \"102\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"103\", 0], \"text_l\": [\"103\", 0]}}, \"101\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"102\", 0], \"conditioning_2\": [\"102\", 0]}}, \"104\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\ndetailed fur\\u2028 \", \"negative_text\": \"\\na,\\u2028 \", \"prepend_text\": \" \\u0085\", \"append_text\": \"\\ndetailed fur\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "
 fox\t猫"}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-38", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u2028 fox\\t\\u732b\", \"clip\": [\"4\", 1]}}, \"105\": {\"class_type\": \"TextInput\", \"inputs\": {\"string\": \"\\u3000\\u732b\\u3000\"}}, \"104\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": [\"105\", 0], \"strip_newlines\": true}}, \"103\": {\"class_type\": \"StringConstant\", \"inputs\": {\"string\": [\"104\", 0]}}, \"102\": {\"class_type\": \"PromptManagerV2\"}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (Unknown).", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "
 snow
 \t"}}, "resolution": {"node": {"class_type": "Unknown", "id": "105"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-39", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"\\u2028 snow\\u2028 \"}}, \"103\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"104\", 0]}}, \"102\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": [\"103\", 0], \"text_c\": \"\\ufeff\\t\", \"clean_whitespace\": \"false\"}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"102\", 0], \"clip\": [\"4\", 1]}}, \"108\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\ufeff\\u732b\\u0085\", \"strip_newlines\": false}}, \"107\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"108\", 0]}}, \"106\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"107\", 0], \"prepend_text\": \"\", \"append_text\": \"\\ufeff\\u732b\\u3000\", \"clip\": [\"4\", 1]}}, \"105\": {\"class_type\": \"Unknown\", \"inputs\": {\"conditioning_1\": [\"106\", 0], \"conditioning_2\": [\"106\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Execution evidence does not match recorded node inputs", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-40", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"102\": {\"records\": [{\"text\": \"\\n\\ud83d\\ude00\\u2028 !\", \"negative_text\": \" fox\\t\", \"prepend_text\": \"caf\\u00e9\\ufeff\", \"append_text\": \"detailed fur\\ufeffdetailed fur\", \"positive_text\": \"\\u0085\\u732b\\u0085detailed fur\", \"negative_effective_text\": \" fox\\t\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\n\\ud83d\\ude00\\u2028 \", \"negative_text\": \" fox\\t\", \"prepend_text\": \"\\u0085caf\\u00e9\\ufeff\", \"append_text\": \"\\ndetailed fur\\ufeffdetailed fur\"}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"106\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"fox\", \"strip_newlines\": true}}, \"105\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"106\", 0]}}, \"104\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": [\"105\", 0]}}, \"103\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"104\", 0], \"prepend_text\": \" detailed fur\\u001c\", \"negative_text\": \"\\u0085\\na,\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "101"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-41", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"snow\\tsnow\"}}, \"104\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \" detailed fur \"}}, \"103\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": [\"104\", 0], \"strip_newlines\": true}}, \"102\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"103\", 0], \"prepend_text\": \"\\t\\u001c\", \"append_text\": \"
 snow \", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-42", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"猫\", \"prepend_text\": \"
 a,\\t\", \"append_text\": \" \\u001c\", \"clip\": [\"4\", 1]}}, \"101\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"102\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "café, detailed fur snow a, café \t😀"}, "positive": {"source": "execution", "status": "resolved", "text": "snow "}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "3", "state": "resolved"}}, "name": "fuzz-0-43", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"105\": {\"records\": [{\"text\": \"\\u3000a,\\u3000\", \"negative_text\": \"snow\\u3000\", \"prepend_text\": \"caf\\u00e9detailed fur\", \"append_text\": \"fox\\u001csnow\", \"positive_text\": \"\\u0085\\u732b\\u2028 \", \"negative_effective_text\": \"snow\\u3000\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"café…\", \"text_l\": \"café…\"}}, \"103\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"104\", 0], \"conditioning_2\": [\"104\", 0]}}, \"102\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"103\", 0]}}, \"105\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \" a, \", \"negative_text\": \"snow \", \"prepend_text\": \"\\tcafédetailed fur\", \"append_text\": \"\\nfox\\u001csnow\"}}, \"101\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"102\", 0], \"on_false\": [\"105\", 1]}}, \"109\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_a\": \"\\na, \", \"text_c\": \"\\ncafé\\n\", \"clean_whitespace\": true}}, \"108\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \" detailed fur snow\", \"string_2\": [\"109\", 0], \"string_3\": \"\\t😀…\", \"inputcount\": 3, \"delimiter\": \" \", \"return_list\": false}}, \"107\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"108\", 0], \"prepend_text\": \"…café\\u001c\", \"negative_text\": \"snow…\"}}, \"106\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"107\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "😀 😀"}}, "resolution": {"node": null, "reason": "Negative input is absent", "reason_code": null, "sampler_id": "3", "state": "absent"}}, "name": "fuzz-0-44", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\ud83d\\ude00 \\ud83d\\ude00\", \"clip\": [\"4\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "café\u001cdetailed fur, detailed fur"}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-45", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"106\": {\"records\": [{\"text\": \"\\u001cfox\\u3000!\", \"negative_text\": \"\\n\\ud83d\\ude00\\u0085\", \"prepend_text\": \"\\ufeff\", \"append_text\": \"a,\", \"positive_text\": \"\\ufeffdetailed fur\\ud83d\\ude00\", \"negative_effective_text\": \"\\n\\ud83d\\ude00\\u0085\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"
 detailed fur \", \"prepend_text\": \"…café\\u001cdetailed fur\", \"negative_text\": \"\\u001cfox\"}}, \"105\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"
 😀\"}}, \"104\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"105\", 0]}}, \"103\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": []}, \"106\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001cfox \", \"negative_text\": \"\\n😀…\", \"prepend_text\": \"
 \", \"append_text\": \" a,\\t\"}}, \"102\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"103\", 0], \"conditioning_2\": [\"106\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (CLIPTextEncodeSDXL).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt combines several texts (CLIPTextEncodeSDXL).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "CLIPTextEncodeSDXL", "id": "102"}, "reason": "Encoder parts have different texts", "reason_code": "composite", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-46", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"106\": {\"records\": [{\"text\": \"\\t\\u732b\\u2028 \", \"negative_text\": \"\\u001cfox\\ufeff\", \"prepend_text\": \"fox\\u3000detailed fur\", \"append_text\": \"detailed fur\", \"positive_text\": \" fox \\u732b\", \"negative_effective_text\": \"\\u001cfox\\ufeff\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"107\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \" detailed fur\", \"prepend_text\": \"café
 \", \"append_text\": \"\\ncafé\\n\"}}, \"102\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"103\", 0], \"text_l\": \"\\t😀
 猫\"}}, \"106\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\t猫
 \", \"negative_text\": \"\\u001cfox\", \"prepend_text\": \"\\nfox detailed fur\", \"append_text\": \"\\ndetailed fur
 \"}}, \"105\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"106\", 0]}}, \"104\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"105\", 0]}}, \"101\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"102\", 0], \"on_false\": [\"104\", 0]}}, \"110\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": \"fox
 \", \"text_c\": \"猫\", \"text_a\": \"\\t😀\", \"clean_whitespace\": \"false\"}}, \"109\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": [\"110\", 0], \"inputcount\": 1, \"delimiter\": \",\", \"return_list\": false}}, \"108\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"109\", 0], \"prepend_text\": \"\\u001c😀\\t猫\", \"append_text\": \" a,\", \"clip\": [\"4\", 1]}}, \"107\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"108\", 0], \"conditioning_2\": [\"108\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "102"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-47", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \" detailed fur
 猫\"}}, \"104\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\tcafé
 \", \"prepend_text\": \"
 …😀\", \"append_text\": \"fox\\n\", \"clip\": [\"4\", 1]}}, \"103\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"104\", 0]}}, \"101\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"102\", 0], \"on_false\": [\"103\", 0]}}, \"105\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \" 😀\", \"text_l\": \" 😀\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Missing or malformed graph node", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-48", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": []}, \"101\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\u001cfoxcafé\"}}, \"106\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \" detailed fur \"}}, \"105\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"106\", 0]}}, \"104\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"105\", 0], \"prepend_text\": \"\\tfox
 猫\", \"append_text\": \" 猫…café\", \"clip\": [\"4\", 1]}}, \"103\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"104\", 0]}}, \"102\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"103\", 0], \"conditioning_2\": [\"103\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "café
 😀"}}, "resolution": {"node": {"class_type": "PromptSearchList", "id": "102"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-49", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"café
 😀\", \"clip\": [\"4\", 1]}}, \"102\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"a,\\t\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "😀"}}, "resolution": {"node": {"class_type": "PromptSearchList", "id": "105"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-50", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"104\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \" \\t\"}}, \"102\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"103\", 0]}}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"\\u2028 \\ud83d\\ude00\\u2028 \", \"negative_text\": \"\\ufeffdetailed fur\\t\"}}, \"105\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \" caf\\u00e9\\ufeffsnow\"}}, \"104\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"105\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (ConditioningCombine).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt combines several texts (ConditioningCombine).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "ConditioningCombine", "id": "101"}, "reason": "Combined conditioning has different texts", "reason_code": "composite", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-51", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"105\": {\"records\": [{\"text\": \"\\ufeffcaf\\u00e9\\u2028 \", \"negative_text\": \"\\t\\u732b\\t\", \"prepend_text\": \"\\ufeff\\ud83d\\ude00\\u001cdetailed fur\", \"append_text\": \"fox\", \"positive_text\": \"\\ud83d\\ude00\\t\", \"negative_effective_text\": \"\\t\\u732b\\t\"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"a,\\tdetailed fur\", \"prepend_text\": \"\\nfox\\n\", \"append_text\": \"fox\\u3000\\ud83d\\ude00\"}}, \"103\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"caf\\u00e9 \", \"string_b\": [\"104\", 0], \"delimiter\": \", \"}}, \"102\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"103\", 0], \"text_l\": [\"103\", 0]}}, \"105\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\ufeffcaf\\u00e9\\u2028 \", \"negative_text\": \"\\t\\u732b\\t\", \"prepend_text\": \"\\ufeff\\ud83d\\ude00\\u001cdetailed fur\", \"append_text\": \"fox\\u2028 \"}}, \"101\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"102\", 0], \"conditioning_2\": [\"105\", 0]}}, \"109\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \" a,\\na,\", \"string_b\": \"\\ufeff\\u2028 \", \"delimiter\": \"\"}}, \"108\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"109\", 0]}}, \"107\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": [\"108\", 0]}}, \"106\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"107\", 0], \"text_l\": [\"107\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "fox, a,"}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "103"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-52", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\"}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \" a,\\t\", \"strip_newlines\": true}}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"\\tfox \", \"negative_text\": \" snow
 😀\"}}, \"103\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \" a,\"}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Selector branches have different prompts", "reason_code": null, "sampler_id": "3", "state": "ambiguous"}}, "name": "fuzz-0-53", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\nsnow\\ufeffdetailed fur\", \"negative_text\": \"\\nfox\\u2028 \", \"prepend_text\": \"a,\", \"append_text\": \"\\ta,\\u001c\"}}, \"105\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"\\u0085caf\\u00e9\", \"prepend_text\": \"\\ufeffcaf\\u00e9\\u3000\", \"append_text\": \"\\ufeff\\u732b\"}}, \"104\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": [\"105\", 0], \"strip_newlines\": true}}, \"103\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"104\", 0], \"prepend_text\": \"\\ufeffa,\\u0085\", \"append_text\": \"\\tdetailed fur\\u3000detailed fur\", \"clip\": [\"4\", 1]}}, \"101\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"102\", 0], \"on_false\": [\"103\", 0]}}, \"109\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\ufeff\\u732b\\u0085\", \"negative_text\": \"\\ta,\\n\\ud83d\\ude00\", \"prepend_text\": \"\\u2028 caf\\u00e9\\u2028 \", \"append_text\": \" \\ufeffcaf\\u00e9\"}}, \"108\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"109\", 1]}}, \"111\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\u001cdetailed fur\\u001c\", \"strip_newlines\": false}}, \"110\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"111\", 0], \"text_l\": \"\"}}, \"107\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"108\", 0], \"conditioning_2\": [\"110\", 0]}}, \"106\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"107\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Execution evidence does not match recorded node inputs", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-54", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"102\": {\"records\": [{\"text\": \"\\ufeffdetailed fur\\u0085!\", \"negative_text\": \"\\u3000fox \", \"prepend_text\": \"caf\\u00e9\", \"append_text\": \"\", \"positive_text\": \"\\ufeffa,\\n\", \"negative_effective_text\": \"\\u3000fox \"}]}}}", "prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"detailed fur…\", \"negative_text\": \" fox \", \"prepend_text\": \"…café\\u001c\", \"append_text\": \"
 \\u001c\"}}, \"101\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"102\", 1], \"conditioning_2\": [\"102\", 1]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "No supported sampler was found.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "No supported sampler was found.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "No supported sampler pair found", "reason_code": "no-sampler", "sampler_id": null, "state": "unresolved"}}, "name": "fuzz-0-55", "records": {"prompt": "{\"3\": {\"class_type\": null, \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"fox \", \"strip_newlines\": false}}, \"103\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"104\", 0], \"string_b\": \"\\u3000caf\\u00e9\\u2028 \", \"delimiter\": \"\"}}, \"102\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"103\", 0]}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"102\", 0], \"clip\": [\"4\", 1]}}, \"109\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\tcaf\\u00e9\\n\\ud83d\\ude00\", \"clip\": [\"4\", 1]}}, \"108\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"109\", 0]}}, \"107\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"108\", 0]}}, \"106\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"107\", 0]}}, \"105\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"106\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "101"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-56", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\t\\u732b\"}}, \"102\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\u001csnow\\u001c\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-57", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": []}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\u3000caf\\u00e9\\t\"}}, \"103\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"104\", 0]}}, \"102\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"103\", 0]}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"106\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\ufeffa,\\ufeff\\ud83d\\ude00\"}}, \"107\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \" a,\\ufeff\\ud83d\\ude00\"}}, \"105\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"106\", 0], \"on_false\": [\"107\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (CLIPTextEncodeSDXL).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt combines several texts (CLIPTextEncodeSDXL).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "CLIPTextEncodeSDXL", "id": "101"}, "reason": "Encoder parts have different texts", "reason_code": "composite", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-58", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"107\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"StringConstant\", \"inputs\": {\"string\": \"😀\"}}, \"103\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_d\": \"
 猫\\t\", \"text_a\": \"…detailed fur
 detailed fur\", \"text_b\": [\"104\", 0], \"clean_whitespace\": true}}, \"106\": {\"class_type\": \"StringConstant\", \"inputs\": {\"string\": \"
 detailed fur \"}}, \"105\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": [\"106\", 0], \"strip_newlines\": false}}, \"102\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"103\", 0], \"string_b\": [\"105\", 0], \"delimiter\": \" \"}}, \"101\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"102\", 0], \"text_l\": \"猫\\ndetailed fur\"}}, \"107\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"
 猫\\n\", \"negative_text\": \"\\nsnow
 猫\", \"prepend_text\": \"
 detailed fur \", \"append_text\": \"\\u001c\"}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Expected a typed graph link or text input", "reason_code": "malformed", "sampler_id": "3", "state": "unresolved"}}, "name": "fuzz-0-59", "records": {"prompt": "{\"3\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"\\nfox\\t\", \"prepend_text\": \" \\ud83d\\ude00\\u001c\", \"append_text\": \"\\u001csnow \"}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {}}, \"104\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\ncaf\\u00e9\\u3000\"}}, \"103\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"104\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (ConditioningCombine).", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "
   café\u001csnow"}}, "resolution": {"node": {"class_type": "ConditioningCombine", "id": "102"}, "reason": "Combined conditioning has different texts", "reason_code": "composite", "sampler_id": "106", "state": "unresolved"}}, "name": "select-fuzz-0-0", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"103\": {\"records\": [{\"text\": \"\\u001csnow \", \"negative_text\": \"\\u0085\\u2028 detailed fur\", \"prepend_text\": \"\\u732b\", \"append_text\": \"\\ud83d\\ude00\", \"positive_text\": \"snow\\u3000\", \"negative_effective_text\": \"\\u0085\\u2028 detailed fur\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\u2028 \\u3000\", \"prepend_text\": \"\\u001c\\u0085\", \"append_text\": \"\\ncaf\\u00e9\\u001csnow\", \"clip\": [\"4\", 1]}}, \"103\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001csnow \", \"negative_text\": \"\\u0085\\u2028 detailed fur\", \"prepend_text\": \"\\u0085\\u732b\\u2028 \", \"append_text\": \"\\u2028 \\ud83d\\ude00\"}}, \"105\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"\\u0085snow\\n\", \"prepend_text\": \"\\n\\u732b\\t\", \"negative_text\": \"\\ufeffsnow \"}}, \"104\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"105\", 0]}}, \"102\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"103\", 0], \"conditioning_2\": [\"104\", 0]}}, \"106\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"107\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"106\", 0]}}, \"108\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"107\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-1", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"snow \", \"negative_text\": \"\\n😀\\tcafé\", \"prepend_text\": \"😀café\", \"append_text\": \"\\tfox \"}}, \"106\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \" a, \"}}, \"105\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"106\", 0]}}, \"104\": {\"class_type\": \"TextInput\", \"inputs\": {\"text\": [\"105\", 0]}}, \"103\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": [\"104\", 0], \"strip_newlines\": false}}, \"102\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"103\", 0], \"prepend_text\": \"\\n猫\", \"append_text\": \" 猫…\", \"clip\": [\"4\", 1]}}, \"107\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 1], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"109\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"
 snow猫\", \"strip_newlines\": true}}, \"108\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"109\", 0], \"text_l\": [\"109\", 0]}}, \"110\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"猫\\t\", \"text_l\": \"猫\\t\"}}, \"111\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"108\", 0], \"negative\": [\"110\", 0], \"latent_image\": [\"5\", 0]}}, \"112\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"107\", 0]}}, \"115\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"\\u001ca, \", \"string_b\": \" detailed fur\\n\", \"delimiter\": \" \"}}, \"114\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"115\", 0], \"text_l\": [\"115\", 0]}}, \"118\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"café\", \"string_b\": \" 😀…\", \"delimiter\": \" \"}}, \"117\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_c\": [\"118\", 0], \"clean_whitespace\": \"false\"}}, \"116\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"117\", 0], \"clip\": [\"4\", 1]}}, \"113\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"112\", 0], \"model\": [\"4\", 0], \"positive\": [\"114\", 0], \"negative\": [\"116\", 0]}}, \"119\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"113\", 0]}}, \"120\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"113\", 0]}}, \"121\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"111\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "102"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "104", "state": "unresolved"}}, "name": "select-fuzz-0-2", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\t\\ud83d\\ude00\\u3000\"}}, \"103\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\na,\\t\", \"text_l\": \"\\na,\\t\"}}, \"101\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"102\", 0], \"conditioning_2\": [\"103\", 0]}}, \"104\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"105\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"104\", 0]}}, \"106\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"105\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "select-fuzz-0-3", "records": {"parameters": "Steps:٣, a\r\nfox\r"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "select-fuzz-0-4", "records": {"parameters": "Negative prompt: blur \tfox 
 a,
Negative prompt:\n"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "select-fuzz-0-5", "records": {"parameters": "\u001ccafé Negative Template: n\n"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "109"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "111", "state": "unresolved"}}, "name": "select-fuzz-0-6", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"105\": {\"records\": [{\"text\": \"\\u001csnow\\ufeff\", \"negative_text\": \"\\ud83d\\ude00\\t\", \"prepend_text\": \"\\ud83d\\ude00\", \"append_text\": \"detailed fur\", \"positive_text\": \"\\u0085snow\\ufeff\", \"negative_effective_text\": \"\\ud83d\\ude00\\t\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \"detailed fur…\"}}, \"103\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"104\", 0]}}, \"102\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_c\": \"fox😀\", \"text_a\": [\"103\", 0], \"text_d\": \" 😀😀\", \"clean_whitespace\": \"false\"}}, \"101\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"…a, \", \"negative_text\": \"fox\\u001c😀\"}}, \"105\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001csnow\", \"negative_text\": \"😀\\t\", \"prepend_text\": \" 😀 \", \"append_text\": \"\\u001cdetailed fur \"}}, \"106\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"109\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \" snow 猫\"}}, \"108\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"109\", 0]}}, \"107\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"108\", 0]}}, \"110\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"
 😀\", \"clip\": [\"4\", 1]}}, \"111\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"107\", 0], \"negative\": [\"110\", 0], \"latent_image\": [\"5\", 0]}}, \"112\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"106\", 0]}}, \"117\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" 猫…fox\", \"clip\": [\"4\", 1]}}, \"116\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"117\", 0]}}, \"115\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"116\", 0]}}, \"118\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \" 😀\\n\", \"prepend_text\": \"\\ncafé detailed fur\", \"negative_text\": \"…café \"}}, \"114\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"115\", 0], \"conditioning_2\": [\"118\", 0]}}, \"121\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"
 …\", \"prepend_text\": \"猫
 \", \"append_text\": \"\\nfox…\", \"clip\": [\"4\", 1]}}, \"120\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"121\", 0]}}, \"119\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"120\", 0]}}, \"113\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"114\", 0], \"negative\": [\"119\", 0], \"upscaled_image\": [\"112\", 0]}}, \"122\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"113\", 0]}}, \"123\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"111\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-7", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"110\": {\"records\": [{\"text\": \"\\u0085snow\\ufeff\", \"negative_text\": \"\\u001cdetailed fur\\u3000\", \"prepend_text\": \"caf\\u00e9\\ta,\", \"append_text\": \"fox\\ufeff\", \"positive_text\": \" caf\\u00e9\\u0085\", \"negative_effective_text\": \"\\u001cdetailed fur\\u3000\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"\\ncafé\"}}, \"103\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"104\", 0], \"prepend_text\": \"a,\\t\", \"negative_text\": \"a,
 \"}}, \"106\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \" 猫\\u001c\", \"string_b\": \"  \", \"delimiter\": \"\"}}, \"105\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"106\", 0], \"clip\": [\"4\", 1]}}, \"102\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"103\", 0], \"conditioning_2\": [\"105\", 0]}}, \"101\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"102\", 0]}}, \"107\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"110\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"…snow\", \"negative_text\": \"\\u001cdetailed fur \", \"prepend_text\": \"café\\ta,\", \"append_text\": \"fox\"}}, \"109\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"110\", 1], \"conditioning_2\": [\"110\", 1]}}, \"108\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"109\", 0]}}, \"111\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"\\ta, \", \"prepend_text\": \"…猫 \", \"negative_text\": \"snow\"}}, \"112\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"108\", 0], \"negative\": [\"111\", 0], \"latent_image\": [\"5\", 0]}}, \"114\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\tdetailed fur\\u001c\"}}, \"115\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"
 猫\"}}, \"113\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"114\", 0], \"on_false\": [\"115\", 0]}}, \"119\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \"\\u001cfox
 \"}}, \"120\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\nsnow
 \", \"strip_newlines\": false}}, \"118\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"119\", 0], \"string_b\": [\"120\", 0], \"delimiter\": \" \"}}, \"117\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"118\", 0], \"prepend_text\": \"\\u001csnow\\t\", \"negative_text\": \" snow \"}}, \"116\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"117\", 0]}}, \"121\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"113\", 0], \"negative\": [\"116\", 0], \"latent_image\": [\"5\", 0]}}, \"122\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"107\", 0]}}, \"123\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"122\", 0]}}, \"124\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"122\", 0]}}, \"125\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"112\", 0]}}, \"128\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \" a,\", \"text_l\": \" a,\"}}, \"131\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\t猫snow\", \"clip\": [\"4\", 1]}}, \"132\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"😀…\", \"clip\": [\"4\", 1]}}, \"130\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"131\", 0], \"on_false\": [\"132\", 0]}}, \"129\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"130\", 0]}}, \"127\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"128\", 0], \"conditioning_2\": [\"129\", 0]}}, \"126\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"125\", 0], \"model\": [\"4\", 0], \"positive\": [\"113\", 0], \"negative\": [\"127\", 0]}}, \"133\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"126\", 0]}}, \"134\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"121\", 0]}}, \"135\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"134\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": null, "state": "unresolved"}}, "name": "select-fuzz-0-8", "records": {"parameters": "Steps:٣, a
\u001ca,\t\n"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "PromptSearchList", "id": "119"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "117", "state": "unresolved"}}, "name": "select-fuzz-0-9", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"127\": {\"records\": [{\"text\": \"\\u0085\\u3000\", \"negative_text\": \"\\u2028 a,\\ufeff\", \"prepend_text\": \"\", \"append_text\": \"\\u732b\", \"positive_text\": \"snow \", \"negative_effective_text\": \"\\u2028 a,\\ufeff\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"…猫\\n\", \"strip_newlines\": false}}, \"103\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"104\", 0], \"prepend_text\": \"…detailed fur\", \"append_text\": \"detailed fur \", \"clip\": [\"4\", 1]}}, \"102\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"103\", 0]}}, \"101\": {\"class_type\": \"Unknown\", \"inputs\": {\"conditioning_1\": [\"102\", 0], \"conditioning_2\": [\"102\", 0]}}, \"108\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"…😀café\"}}, \"107\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"108\", 0]}}, \"106\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"a,
 detailed fur\", \"string_2\": [\"107\", 0], \"string_3\": \"
 café\", \"inputcount\": 3, \"delimiter\": \" \", \"return_list\": false}}, \"105\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"106\", 0], \"prepend_text\": \" café\\n\", \"append_text\": \"…café…\", \"clip\": [\"4\", 1]}}, \"109\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"113\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"\\u001csnow\\u001c\"}}, \"112\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": [\"113\", 0], \"prepend_text\": \"\\tdetailed fur\", \"append_text\": \"\\ndetailed fur\\u001c\"}}, \"111\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"112\", 0], \"clip\": [\"4\", 1]}}, \"110\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"111\", 0], \"conditioning_2\": [\"111\", 0]}}, \"114\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"😀 \"}}, \"115\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"110\", 0], \"negative\": [\"114\", 0], \"latent_image\": [\"109\", 0]}}, \"116\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"109\", 0]}}, \"119\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \" fox \"}}, \"118\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"119\", 0], \"conditioning_2\": [\"119\", 0]}}, \"122\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_d\": \"\\u001c😀
 猫\", \"clean_whitespace\": true}}, \"121\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": [\"122\", 0], \"strip_newlines\": true}}, \"120\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"121\", 0], \"text_l\": [\"121\", 0]}}, \"117\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"116\", 0], \"model\": [\"4\", 0], \"positive\": [\"118\", 0], \"negative\": [\"120\", 0]}}, \"123\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"115\", 0]}}, \"125\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\tsnow \"}}, \"127\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"… \", \"negative_text\": \"
 a,\", \"prepend_text\": \"\\u001c\", \"append_text\": \"猫\"}}, \"126\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"127\", 1]}}, \"124\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"125\", 0], \"negative\": [\"126\", 0], \"upscaled_image\": [\"123\", 0]}}, \"128\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"124\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "110", "state": "unresolved"}}, "name": "select-fuzz-0-10", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"113\": {\"records\": [{\"text\": \"\\u0085detailed fur\\ufeffdetailed fur\", \"negative_text\": \"\\ufeff\\u001c\", \"prepend_text\": \"a,\\u001cfox\", \"append_text\": \"a,fox\", \"positive_text\": \"\\u3000\\u732b\\u3000\", \"negative_effective_text\": \"\\ufeff\\u001c\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"105\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\nsnow\\u3000\", \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"105\", 0], \"conditioning_2\": [\"105\", 0]}}, \"103\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"104\", 0]}}, \"102\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"103\", 0]}}, \"101\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"106\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"\\ta,\\tcaf\\u00e9\", \"prepend_text\": \"\\ufeffa,\\u3000\", \"append_text\": \"\\ufeffdetailed fur\\n\", \"clip\": [\"4\", 1]}}, \"107\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"108\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"107\", 0]}}, \"109\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"108\", 0]}}, \"112\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\n\\ud83d\\ude00 \"}}, \"113\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u0085detailed fur\\ufeffdetailed fur\", \"negative_text\": \"\\ufeff\\u001c\", \"prepend_text\": \"\\na,\\u001cfox\", \"append_text\": \"\\ta,fox\"}}, \"111\": {\"class_type\": \"Unknown\", \"inputs\": {\"boolean\": true, \"on_true\": [\"112\", 0], \"on_false\": [\"113\", 1]}}, \"110\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"108\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"111\", 0]}}, \"114\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"110\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-11", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"105\": {\"records\": [{\"text\": \"\\u001c\\u0085\", \"negative_text\": \"\\nfox\\u001c\", \"prepend_text\": \"fox\\ufeff\", \"append_text\": \"\\ufefffox\", \"positive_text\": \" snow caf\\u00e9\", \"negative_effective_text\": \"\\nfox\\u001c\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\na,\\n\", \"strip_newlines\": false}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"102\", 0], \"clip\": [\"4\", 1]}}, \"105\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001c…\", \"negative_text\": \"\\nfox\\u001c\", \"prepend_text\": \" fox\", \"append_text\": \"fox \"}}, \"107\": {\"class_type\": \"StringConstant\", \"inputs\": {\"string\": \"\\n\\n😀\"}}, \"106\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"107\", 0], \"prepend_text\": \"…a, 😀\", \"negative_text\": \"
 猫\\u001c\"}}, \"104\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"105\", 1], \"conditioning_2\": [\"106\", 0]}}, \"108\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"snow detailed fur\", \"prepend_text\": \"\\n猫\\u001c\", \"negative_text\": \"detailed fur😀\"}}, \"103\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"104\", 0], \"conditioning_2\": [\"108\", 0]}}, \"109\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"111\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\n猫 \"}}, \"110\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"111\", 0]}}, \"116\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\tcafé\", \"clip\": [\"4\", 0.0]}}, \"115\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"116\", 0]}}, \"114\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"115\", 0], \"conditioning_2\": [\"115\", 0]}}, \"118\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\tdetailed fur\\t\"}}, \"117\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"118\", 0]}}, \"113\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"114\", 0], \"on_false\": [\"117\", 0]}}, \"112\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"113\", 0]}}, \"119\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"110\", 0], \"negative\": [\"112\", 0], \"latent_image\": [\"5\", 0]}}, \"120\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"121\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"109\", 0]}}, \"122\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"121\", 0]}}, \"123\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"121\", 0]}}, \"124\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"119\", 0]}}, \"125\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"124\", 0]}}, \"126\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"120\", 0]}}, \"130\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\\ncafé\", \"inputcount\": 1, \"delimiter\": \",\", \"return_list\": false}}, \"129\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": [\"130\", 0]}}, \"128\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"129\", 0], \"text_l\": \"\"}}, \"132\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"…猫…\", \"prepend_text\": \"\\n😀…\", \"append_text\": \"\\u001cfox…\", \"clip\": [\"4\", 1]}}, \"131\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"132\", 0], \"conditioning_2\": [\"132\", 0]}}, \"127\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"126\", 0], \"model\": [\"4\", 0], \"positive\": [\"128\", 0], \"negative\": [\"131\", 0]}}, \"133\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"127\", 0]}}, \"134\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"127\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt uses a node the inspector does not support (FancyEncoder).", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": " café\n"}}, "resolution": {"node": {"class_type": "FancyEncoder", "id": "104"}, "reason": "Unsupported node semantics or output slot", "reason_code": "unsupported-node", "sampler_id": "105", "state": "unresolved"}}, "name": "select-fuzz-0-12", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" café\\n\", \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \" detailed fur\"}}, \"103\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"104\", 0]}}, \"102\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"103\", 0], \"conditioning_2\": [\"103\", 0]}}, \"105\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"106\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"105\", 0]}}, \"107\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"106\", 0]}}, \"108\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"106\", 0]}}, \"109\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"106\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-13", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\t猫\", \"text_l\": \"\"}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"104\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \" fox\"}}, \"103\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"104\", 0], \"prepend_text\": \"…😀\\t\", \"negative_text\": \"
 a,\\t\"}}, \"105\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"106\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"107\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"105\", 0]}}, \"109\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"😀\", \"negative_text\": \" detailed fur…\", \"prepend_text\": \"😀😀\", \"append_text\": \"\\na, \"}}, \"108\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"107\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"109\", 1]}}, \"110\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"108\", 0]}}, \"111\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"106\", 0]}}, \"112\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"111\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "select-fuzz-0-14", "records": {"parameters": "Negative prompt:\r\ncafé…\r\n"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "café "}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "detailed fur 😀, snow \u001c café
 café"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "105", "state": "resolved"}}, "name": "select-fuzz-0-15", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"\\u001c\", \"prepend_text\": \"\\nsnow\\u2028 \", \"append_text\": \"\\u001ccaf\\u00e9\\u2028 caf\\u00e9\"}}, \"102\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": [\"103\", 0], \"clean_whitespace\": true}}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \" detailed fur\\u3000\\ud83d\\ude00\", \"negative_text\": \"detailed fur\\u001c\"}}, \"104\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u0085\\u732b\\u3000\", \"negative_text\": \"caf\\u00e9\\u3000\", \"prepend_text\": \"detailed fur\\n\", \"append_text\": \"\\u0085caf\\u00e9\\n\"}}, \"105\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"104\", 1], \"latent_image\": [\"5\", 0]}}, \"106\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"105\", 0]}}, \"107\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"106\", 0]}}, \"108\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"106\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "135", "state": "unresolved"}}, "name": "select-fuzz-0-16", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"107\": {\"records\": [{\"text\": \"\\tcaf\\u00e9\\t\", \"negative_text\": \"fox\\u2028 \", \"prepend_text\": \"caf\\u00e9\", \"append_text\": \"snow\\u2028 \\ud83d\\ude00\", \"positive_text\": \"\\u001ca,\\u001c\", \"negative_effective_text\": \"fox\\u2028 \"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\u3000detailed fur\\u3000\", \"strip_newlines\": false}}, \"105\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"\\u0085caf\\u00e9\\u001c\", \"prepend_text\": \"detailed fur\\ufeff\", \"append_text\": \"\\tfox \"}}, \"103\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\\u0085a, \", \"string_2\": [\"104\", 0], \"string_3\": [\"105\", 0], \"inputcount\": 3, \"delimiter\": \",\", \"return_list\": false}}, \"102\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_a\": [\"103\", 0], \"text_b\": \" \", \"clean_whitespace\": \"true\"}}, \"101\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"\\u0085detailed fur\\t\", \"negative_text\": \"\\u001cdetailed fur\\t\"}}, \"107\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\tcaf\\u00e9\\t\", \"negative_text\": \"fox\\u2028 \", \"prepend_text\": \"\\u3000caf\\u00e9\\u0085\", \"append_text\": \" snow\\u2028 \\ud83d\\ude00\"}}, \"106\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"107\", 0], \"conditioning_2\": [\"107\", 0]}}, \"108\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"108\", 0]}}, \"111\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_a\": \"\\u2028 a,\", \"text_c\": \"\\u3000a,\\ufeff\", \"clean_whitespace\": \"false\"}}, \"112\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"caf\\u00e9\\t\\u732b\", \"string_b\": \"\\ud83d\\ude00\\u3000\", \"delimiter\": \" \"}}, \"110\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_a\": \"\\ufeff\\u732b\\ufeff\", \"text_b\": [\"111\", 0], \"text_d\": [\"112\", 0], \"clean_whitespace\": true}}, \"109\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"110\", 0], \"clip\": [\"4\", 1]}}, \"113\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\ufeffa,\\u001c\", \"text_l\": \"\\ufeffa,\\u001c\"}}, \"114\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"109\", 0], \"negative\": [\"113\", 0], \"latent_image\": [\"5\", 0]}}, \"115\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"108\", 0]}}, \"119\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\u0085snow fox\", \"prepend_text\": \"\\tdetailed fur\", \"append_text\": \"\\u2028 \\ufefffox\", \"clip\": [\"4\", 1]}}, \"118\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"119\", 0]}}, \"120\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"\\u2028 snow \", \"prepend_text\": \" \\ud83d\\ude00\", \"append_text\": \"fox\\u3000\", \"clip\": [\"4\", 1]}}, \"117\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"118\", 0], \"conditioning_2\": [\"120\", 0]}}, \"124\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\na,\", \"clip\": [\"4\", 1]}}, \"123\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"124\", 0]}}, \"122\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"123\", 0]}}, \"121\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"122\", 0]}}, \"116\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"115\", 0], \"model\": [\"4\", 0], \"positive\": [\"117\", 0], \"negative\": [\"121\", 0]}}, \"125\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"116\", 0]}}, \"126\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"114\", 0]}}, \"131\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u3000\\u0085\", \"clip\": [\"4\", 1]}}, \"130\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"131\", 0]}}, \"133\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u3000fox\\t\", \"clip\": [\"4\", 1]}}, \"132\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"133\", 0]}}, \"129\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"130\", 0], \"on_false\": [\"132\", 0]}}, \"128\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"129\", 0]}}, \"134\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\tcaf\\u00e9a,\"}}, \"127\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"126\", 0], \"model\": [\"4\", 0], \"positive\": [\"128\", 0], \"negative\": [\"134\", 0]}}, \"139\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\tdetailed fur\", \"clip\": [\"4\", 1]}}, \"138\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"139\", 0]}}, \"137\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"138\", 0]}}, \"136\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"137\", 0]}}, \"135\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"127\", 0], \"model\": [\"4\", 0], \"positive\": [\"109\", 0], \"negative\": [\"136\", 0]}}, \"140\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"135\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "select-fuzz-0-17", "records": {"parameters": " detailed fur \r\nNegative Template: n\r\nNegative Template: n
Steps:٣, a\r\nNegative prompt: blur
"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Selector branches have different prompts", "reason_code": null, "sampler_id": "110", "state": "ambiguous"}}, "name": "select-fuzz-0-18", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"\\u0085detailed fur \", \"prepend_text\": \"caf\\u00e9\\u3000detailed fur\", \"negative_text\": \"\\u3000detailed fur \"}}, \"105\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\u001csnow\\u0085\", \"prepend_text\": \"\\u0085\\u2028 \", \"append_text\": \"\\n\\ud83d\\ude00\\t\", \"clip\": [\"4\", 1]}}, \"107\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u3000fox\\u001c\", \"clip\": [\"4\", 1]}}, \"108\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"detailed fur\\u001c\", \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"107\", 0], \"on_false\": [\"108\", 0]}}, \"104\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"105\", 0], \"on_false\": [\"106\", 0]}}, \"103\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"104\", 0]}}, \"102\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"103\", 0]}}, \"109\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"110\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"111\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"109\", 0]}}, \"114\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \" detailed fur\\u2028 \", \"prepend_text\": \"\\tcaf\\u00e9\\t\\u732b\", \"append_text\": \"caf\\u00e9\\ufeff\", \"clip\": [\"4\", 1]}}, \"113\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"114\", 0]}}, \"118\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"\\u2028 a,\\u2028 \"}}, \"117\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"118\", 0]}}, \"116\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"117\", 0], \"prepend_text\": \"\\u3000\\ud83d\\ude00\\n\", \"append_text\": \"\\u3000a,\\u3000\", \"clip\": [\"4\", 1]}}, \"119\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u3000detailed fur \", \"negative_text\": \"\\tcaf\\u00e9\", \"prepend_text\": \"\\u2028 snow\\n\", \"append_text\": \"\\u2028 snow\\u001c\"}}, \"115\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"116\", 0], \"on_false\": [\"119\", 1]}}, \"112\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"111\", 0], \"model\": [\"4\", 0], \"positive\": [\"113\", 0], \"negative\": [\"115\", 0]}}, \"122\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"\\ufeffa,\\u001c\", \"prepend_text\": \"\\ncaf\\u00e9\\u2028 \", \"append_text\": \" caf\\u00e9\"}}, \"121\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"122\", 0], \"text_l\": \"\\u2028 detailed fur\\n\"}}, \"120\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"112\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"121\", 0], \"wildcard\": \"\"}}, \"123\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"120\", 0]}}, \"124\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"110\", 0]}}, \"125\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"124\", 0]}}, \"126\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"124\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-19", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_c\": \"\\t \", \"clean_whitespace\": \"false\"}}, \"102\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"\\u2028 \\u2028 \", \"string_b\": [\"103\", 0], \"delimiter\": \", \"}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"102\", 0], \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\nsnow\\u001csnow\", \"prepend_text\": \"\\tfox\\t\", \"append_text\": \"\\nfox\", \"clip\": [\"4\", 1]}}, \"105\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"104\", 0], \"latent_image\": [\"5\", 0]}}, \"108\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"\\u001c\\ud83d\\ude00\\na,\"}}, \"107\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"108\", 0]}}, \"106\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"107\", 0], \"prepend_text\": \"\\u2028 a, detailed fur\", \"negative_text\": \"\\na,\\u3000\"}}, \"111\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"\\tsnow\\u3000\", \"prepend_text\": \"\\ufeff\\u732b\\u0085\", \"negative_text\": \"\\u0085detailed fur\\u001c\"}}, \"110\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"111\", 0]}}, \"109\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"110\", 0], \"conditioning_2\": [\"110\", 0]}}, \"112\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"106\", 0], \"negative\": [\"109\", 0], \"latent_image\": [\"5\", 0]}}, \"113\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"\\ta,\\n\", \"prepend_text\": \"\\u0085\\ndetailed fur\", \"negative_text\": \"\\u3000\\u2028 \"}}, \"114\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"113\", 0], \"latent_image\": [\"5\", 0]}}, \"115\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"105\", 0]}}, \"119\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\ufeffa,\\u3000\", \"text_l\": \"\\ufeffa,\\u3000\"}}, \"120\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"detailed fur a,\", \"prepend_text\": \"\\tsnow\\t\", \"append_text\": \"\\u0085fox\\n\", \"clip\": [\"4\", 1]}}, \"118\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"119\", 0], \"on_false\": [\"120\", 0]}}, \"117\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"118\", 0]}}, \"121\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\nsnow\\ufefffox\", \"prepend_text\": \"\\u2028 detailed fur \", \"append_text\": \"\\n\\u732b\\u3000\", \"clip\": [\"4\", 1]}}, \"116\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"115\", 0], \"model\": [\"4\", 0], \"positive\": [\"117\", 0], \"negative\": [\"121\", 0]}}, \"126\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"\\u0085\\t\"}}, \"125\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"126\", 0]}}, \"124\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": [\"125\", 0], \"inputcount\": 1, \"delimiter\": \" \", \"return_list\": false}}, \"123\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"124\", 0], \"clip\": [\"4\", 1]}}, \"127\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\tcaf\\u00e9\\u3000a,\", \"text_l\": \"\\tcaf\\u00e9\\u3000a,\"}}, \"122\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"116\", 0], \"model\": [\"4\", 0], \"positive\": [\"123\", 0], \"negative\": [\"127\", 0]}}, \"128\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"122\", 0]}}, \"129\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"112\", 0]}}, \"130\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"129\", 0]}}, \"131\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"129\", 0]}}, \"132\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"129\", 0]}}, \"133\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"114\", 0]}}, \"137\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\ufeff\\u732b\\u0085\", \"strip_newlines\": false}}, \"136\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"137\", 0]}}, \"135\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"136\", 0], \"prepend_text\": \"\", \"append_text\": \"\\ufeff\\u732b\\u3000\", \"clip\": [\"4\", 1]}}, \"140\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"\\u0085\\t\", \"prepend_text\": \"\\u2028 snow\\nsnow\", \"negative_text\": \"caf\\u00e9\\u0085caf\\u00e9\"}}, \"139\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"140\", 0]}}, \"138\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"139\", 0]}}, \"134\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"135\", 0], \"negative\": [\"138\", 0], \"upscaled_image\": [\"133\", 0]}}, \"141\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"134\", 0]}}, \"142\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"134\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Sampler stages have different prompts", "reason_code": "multistage-differs", "sampler_id": "111", "state": "ambiguous"}}, "name": "select-fuzz-0-20", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"103\": {\"records\": [{\"text\": \"\\t\\n\", \"negative_text\": \"\\u732b\\ufeff\", \"prepend_text\": \"\\u732bcaf\\u00e9\", \"append_text\": \"\", \"positive_text\": \"\\u2028 detailed fur\\u001c\", \"negative_effective_text\": \"\\u732b\\ufeff\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\t\\n\", \"negative_text\": \"猫\", \"prepend_text\": \"…猫café\", \"append_text\": \"\\n\\n\"}}, \"104\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\ta,
 \"}}, \"102\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"103\", 1], \"on_false\": [\"104\", 0]}}, \"105\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"… snow\", \"prepend_text\": \"\\tdetailed fur\", \"negative_text\": \"\\na,猫\"}}, \"101\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"102\", 0], \"on_false\": [\"105\", 0]}}, \"108\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"
 snow \", \"prepend_text\": \"fox\\n\", \"append_text\": \"…😀 \"}}, \"107\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": [\"108\", 0]}}, \"106\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"107\", 0], \"prepend_text\": \"\\nfox
 \", \"append_text\": \"😀 \", \"clip\": [\"4\", 1]}}, \"109\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"110\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"109\", 0]}}, \"115\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" fox\\t\", \"clip\": [\"4\", 1]}}, \"116\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"snow
 \", \"clip\": [\"4\", 1]}}, \"114\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_2\": [\"116\", 0]}}, \"113\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"114\", 0]}}, \"112\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"113\", 0]}}, \"111\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"110\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"112\", 0]}}, \"117\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"111\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Cyclic latent ancestry", "reason_code": "malformed", "sampler_id": "108", "state": "unresolved"}}, "name": "select-fuzz-0-21", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\u001c\\ud83d\\ude00\\u3000\\ud83d\\ude00\", \"strip_newlines\": false}}, \"101\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"102\", 0], \"text_l\": \"\"}}, \"106\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\u0085snow\\u2028 \", \"strip_newlines\": true}}, \"107\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\u3000\\u732b\\ufeff\", \"strip_newlines\": false}}, \"105\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_a\": [\"106\", 0], \"text_c\": [\"107\", 0], \"clean_whitespace\": true}}, \"104\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \" detailed fur snow\", \"string_2\": [\"105\", 0], \"inputcount\": 3, \"delimiter\": \",\", \"return_list\": false}}, \"103\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"104\", 0], \"prepend_text\": \"\\u2028 \\u732b\\n\", \"negative_text\": \"\\u0085caf\\u00e9\\u001c\"}}, \"108\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"108\", 0]}}, \"109\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"108\", 0]}}, \"110\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"109\", 0]}}, \"111\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"109\", 0]}}, \"112\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"109\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-22", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"110\": {\"records\": [{\"text\": \"\\n\\u3000snow\", \"negative_text\": \"\\u001csnow\\t\", \"prepend_text\": \"\\ufefffox\", \"append_text\": \"\\u732b\\ufeffdetailed fur\", \"positive_text\": \"caf\\u00e9\\u2028 \", \"negative_effective_text\": \"\\u001csnow\\t\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\ufeffsnow\\u3000\", \"negative_text\": \"\\u3000snow \", \"prepend_text\": \"\\ufeffa,\\tdetailed fur\", \"append_text\": \"\\u001cfox\"}}, \"101\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"102\", 1]}}, \"103\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\u0085\\ud83d\\ude00\"}}, \"104\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"108\": {\"class_type\": \"TextInput\", \"inputs\": {\"string\": \"\\u001ca,\\u2028 \"}}, \"107\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"108\", 0], \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"107\", 0]}}, \"105\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"106\", 0], \"conditioning_2\": [\"106\", 0]}}, \"110\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\n\\u3000snow\", \"negative_text\": \"\\u001csnow\\t\", \"prepend_text\": \"\\ufefffox\\u0085\", \"append_text\": \" \\u732b\\ufeffdetailed fur\"}}, \"114\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\t\\ud83d\\ude00\\u2028 \\u732b\", \"clip\": [\"4\", 1]}}, \"113\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"114\", 0]}}, \"115\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\ncaf\\u00e9\\t\", \"prepend_text\": \"\\u2028 \\ud83d\\ude00\", \"append_text\": \"\\nfox\\u3000detailed fur\", \"clip\": [\"4\", 1]}}, \"112\": {\"class_type\": \"ConditioningCombine\"}, \"111\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"112\", 0]}}, \"109\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"110\", 0], \"on_false\": [\"111\", 0]}}, \"116\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"105\", 0], \"negative\": [\"109\", 0], \"latent_image\": [\"5\", 0]}}, \"117\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"105\", 0], \"negative\": [\"109\", 0], \"latent_image\": [\"5\", 0]}}, \"118\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"104\", 0]}}, \"119\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"118\", 0]}}, \"120\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"116\", 0]}}, \"124\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"\\u001cdetailed fur\\u0085\", \"prepend_text\": \"\\u001csnow\\t\", \"append_text\": \"\\nfox\"}}, \"123\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": [\"124\", 0], \"strip_newlines\": true}}, \"122\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"123\", 0], \"clip\": [\"4\", 1]}}, \"121\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"120\", 0], \"model\": [\"4\", 0], \"positive\": [\"105\", 0], \"negative\": [\"122\", 0]}}, \"127\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"caf\\u00e9\\u0085\"}}, \"126\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"127\", 0]}}, \"125\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"121\", 0], \"model\": [\"4\", 0], \"positive\": [\"105\", 0], \"negative\": [\"126\", 0]}}, \"128\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"125\", 0]}}, \"129\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"117\", 0]}}, \"133\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"\\ufeff\\ud83d\\ude00\\ufeff\\ud83d\\ude00\", \"prepend_text\": \"\\u0085a,\\u3000\", \"append_text\": \"\\u3000\\ud83d\\ude00\\n\\u732b\", \"clip\": [\"4\", 1]}}, \"132\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"133\", 0]}}, \"131\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"132\", 0]}}, \"134\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u2028 caf\\u00e9\\u2028 \", \"negative_text\": \"\\u2028 \\u0085\\ud83d\\ude00\", \"prepend_text\": \"\\ufefffox\\n\", \"append_text\": \"\\ufeffsnowsnow\"}}, \"130\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"129\", 0], \"model\": [\"4\", 0], \"positive\": [\"131\", 0], \"negative\": [\"134\", 0]}}, \"135\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"130\", 0]}}, \"136\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"130\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "execution", "status": "resolved", "text": "😀\t"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "fox snowfox \u001csnow snow snow \tsnow  fox 😀"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "118", "state": "resolved"}}, "name": "select-fuzz-0-23", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"117\": {\"records\": [{\"text\": \"\\ufeffcaf\\u00e9\\u2028 \", \"negative_text\": \"\\t\\u732b\\t\", \"prepend_text\": \"\\ufeff\\ud83d\\ude00\\u001cdetailed fur\", \"append_text\": \"fox\", \"positive_text\": \"\\ud83d\\ude00\\t\", \"negative_effective_text\": \"\\t\\u732b\\t\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"105\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"\\u0085snow\\u3000\"}}, \"104\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"105\", 0]}}, \"106\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_c\": \"\\u0085\\u732b\\u3000caf\\u00e9\", \"clean_whitespace\": \"true\"}}, \"103\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": [\"104\", 0], \"text_d\": [\"106\", 0], \"clean_whitespace\": \"false\"}}, \"102\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": [\"103\", 0]}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"102\", 0], \"clip\": [\"4\", 1]}}, \"109\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\nfox\", \"negative_text\": \"caf\\u00e9\\u2028 \\ud83d\\ude00\", \"prepend_text\": \"\\u0085\\u732b\", \"append_text\": \"\\ta,\\u001c\"}}, \"108\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"109\", 1]}}, \"107\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"108\", 0]}}, \"110\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"107\", 0], \"latent_image\": [\"111\", 0]}}, \"111\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"107\", 0], \"latent_image\": [\"5\", 0]}}, \"115\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"\\u001csnow\\u3000snow\", \"prepend_text\": \"\\u0085snowfox\", \"append_text\": \"\\tsnow\\u3000\"}}, \"116\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \"\\tsnow\\u3000\"}}, \"114\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"115\", 0], \"string_b\": [\"116\", 0], \"delimiter\": \" \"}}, \"113\": {\"class_type\": \"StringConstant\", \"inputs\": {\"string\": [\"114\", 0]}}, \"112\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"113\", 0], \"prepend_text\": \"\\nfox\\n\", \"append_text\": \"fox\\u3000\\ud83d\\ude00\", \"clip\": [\"4\", 1]}}, \"117\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\ufeffcaf\\u00e9\\u2028 \", \"negative_text\": \"\\t\\u732b\\t\", \"prepend_text\": \"\\ufeff\\ud83d\\ude00\\u001cdetailed fur\", \"append_text\": \"fox\\u2028 \"}}, \"118\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"112\", 0], \"negative\": [\"117\", 0], \"latent_image\": [\"5\", 0]}}, \"119\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"110\", 0]}}, \"120\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"119\", 0]}}, \"121\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"111\", 0]}}, \"122\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"121\", 0]}}, \"123\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"118\", 0]}}, \"124\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"123\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt is chosen at run time (PromptSearchList).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "PromptSearchList", "id": "114"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "121", "state": "unresolved"}}, "name": "select-fuzz-0-24", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"101\": {\"records\": [{\"text\": \" a,\\ufeff\", \"negative_text\": \"\\u3000\\u2028 \", \"prepend_text\": \"\\u732b\", \"append_text\": \"a,\", \"positive_text\": \"detailed fur \", \"negative_effective_text\": \"\\u3000\\u2028 \"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \" a,\\ufeff\", \"negative_text\": \"\\u3000\\u2028 \", \"prepend_text\": \" \\u732b\\t\", \"append_text\": \"\\u2028 a,\\t\"}}, \"105\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\u2028 fox\\u3000\", \"strip_newlines\": false}}, \"104\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": [\"105\", 0], \"prepend_text\": \"\\u0085fox\\n\", \"append_text\": \"\\ufeffsnow\\u0085\"}}, \"103\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"104\", 0], \"prepend_text\": \"\\nfox\\u2028 \", \"append_text\": \"a,\", \"clip\": [\"4\", 1]}}, \"108\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"\\u3000fox\\n\", \"prepend_text\": \"\\ufeffa,\\t\", \"negative_text\": \"\\ufeffcaf\\u00e9\\u3000\"}}, \"109\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\nfox\\ufeff\"}}, \"107\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"108\", 0], \"on_false\": [\"109\", 0]}}, \"106\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"107\", 0]}}, \"102\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"103\", 0], \"on_false\": [\"106\", 0]}}, \"110\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"114\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\u2028 detailed fur\\u3000detailed fur\"}}, \"113\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"114\", 0]}}, \"117\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\n\\ud83d\\ude00\\n\", \"clip\": [\"4\", 1]}}, \"116\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"117\", 0]}}, \"119\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\na,\\t\", \"clip\": [\"4\", 1]}}, \"120\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\t\\ud83d\\ude00\\u2028 snow\", \"clip\": [\"4\", 1]}}, \"118\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"119\", 0], \"on_false\": [\"120\", 0]}}, \"115\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"116\", 0], \"conditioning_2\": [\"118\", 0]}}, \"112\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"113\", 0], \"on_false\": [\"115\", 0]}}, \"111\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"112\", 0]}}, \"121\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"111\", 0], \"latent_image\": [\"5\", 0]}}, \"125\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\u2028 \\n\\ud83d\\ude00\", \"text_l\": \"\\u2028 \\n\\ud83d\\ude00\"}}, \"127\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u0085fox\\u732b\", \"clip\": [\"4\", 1]}}, \"128\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\t \\ud83d\\ude00\", \"clip\": [\"4\", 1]}}, \"126\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"127\", 0], \"on_false\": [\"128\", 0]}}, \"124\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"125\", 0], \"on_false\": [\"126\", 0]}}, \"123\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"124\", 0]}}, \"122\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"123\", 0]}}, \"132\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\\u001cdetailed fur\\ufeff\", \"string_3\": \" snow\\u2028 \", \"inputcount\": 4, \"delimiter\": \",\", \"return_list\": false}}, \"131\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"132\", 0]}}, \"130\": {\"class_type\": \"StringConstant\", \"inputs\": {\"string\": [\"131\", 0]}}, \"129\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"130\", 0], \"prepend_text\": \" \\u732bsnow\", \"append_text\": \"\\u001csnow\\u3000\", \"clip\": [\"4\", 1]}}, \"133\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"122\", 0], \"negative\": [\"129\", 0], \"latent_image\": [\"5\", 0]}}, \"134\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"110\", 0]}}, \"139\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u001c\\u001c\\ud83d\\ude00\", \"clip\": [\"4\", 1]}}, \"138\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"139\", 0]}}, \"137\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"138\", 0]}}, \"136\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"137\", 0]}}, \"135\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"134\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"136\", 0], \"wildcard\": [\"4\", 0]}}, \"140\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"135\", 0]}}, \"141\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"121\", 0]}}, \"142\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"141\", 0]}}, \"143\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"133\", 0]}}, \"144\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"143\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (CLIPTextEncodeSDXL).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt combines several texts (CLIPTextEncodeSDXL).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "CLIPTextEncodeSDXL", "id": "102"}, "reason": "Encoder parts have different texts", "reason_code": "composite", "sampler_id": "112", "state": "unresolved"}}, "name": "select-fuzz-0-25", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"109\": {\"records\": [{\"text\": \" caf\\u00e9\\u3000snow\", \"negative_text\": \"\\u2028 caf\\u00e9\\ufeff\\ud83d\\ude00\", \"prepend_text\": \"\\u732b\", \"append_text\": \"\", \"positive_text\": \"\\u3000detailed fur\\u2028 caf\\u00e9\", \"negative_effective_text\": \"\\u2028 caf\\u00e9\\ufeff\\ud83d\\ude00\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"105\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"\\ncafé\"}}, \"104\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"105\", 0]}}, \"103\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\\t猫\", \"string_2\": [\"104\", 0], \"inputcount\": 2, \"delimiter\": \",\", \"return_list\": false}}, \"102\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"103\", 0], \"text_l\": \"\\u001c猫 \"}}, \"101\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"102\", 0], \"conditioning_2\": [\"102\", 0]}}, \"109\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \" café snow\", \"negative_text\": \"
 café😀\", \"prepend_text\": \"
 猫\\t\", \"append_text\": \"\\u001c…\"}}, \"108\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"109\", 1], \"conditioning_2\": [\"109\", 1]}}, \"107\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"108\", 0]}}, \"106\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"107\", 0], \"conditioning_2\": [\"107\", 0]}}, \"110\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"111\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"110\", 0]}}, \"113\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \" \\t猫\", \"negative_text\": \"\\nsnow \", \"prepend_text\": \"
 猫
 \", \"append_text\": \" 猫 \"}}, \"112\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"111\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"113\", 1]}}, \"114\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"112\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-26", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"112\": {\"records\": [{\"text\": \"\\ufeffa,\\t\", \"negative_text\": \"\\u3000\\ud83d\\ude00\\ncaf\\u00e9\", \"prepend_text\": \"\", \"append_text\": \"fox\", \"positive_text\": \"\\u0085snow \", \"negative_effective_text\": \"\\u3000\\ud83d\\ude00\\ncaf\\u00e9\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\n\\n\"}}, \"102\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"103\", 0]}}, \"105\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" \", \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"105\", 0]}}, \"101\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"102\", 0], \"on_false\": [\"104\", 0]}}, \"109\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"…snow\\n\", \"prepend_text\": \" 猫\", \"append_text\": \"\\ta,\\ta,\"}}, \"108\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": [\"109\", 0], \"inputcount\": 1, \"delimiter\": \",\", \"return_list\": false}}, \"107\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"108\", 0], \"prepend_text\": \" a,\\n😀\", \"append_text\": \"
 detailed fur \", \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"107\", 0], \"conditioning_2\": [\"107\", 0]}}, \"110\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"112\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"a,\\t\", \"negative_text\": \" 😀\\ncafé\", \"prepend_text\": \"\\t
 \", \"append_text\": \"\\tfox\\u001c\"}}, \"111\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"112\", 0]}}, \"113\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"😀\\u001c\"}}, \"114\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"111\", 0], \"negative\": [\"113\", 0], \"latent_image\": [\"5\", 0]}}, \"115\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"110\", 0]}}, \"116\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"115\", 0]}}, \"117\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"114\", 0]}}, \"119\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"猫\\t\"}}, \"120\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\tsnow 😀\"}}, \"118\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"117\", 0], \"model\": [\"4\", 0], \"positive\": [\"119\", 0], \"negative\": [\"120\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-27", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"127\": {\"records\": [{\"text\": \"\\u2028 \\ud83d\\ude00 \\ud83d\\ude00\", \"negative_text\": \"\\u732b \", \"prepend_text\": \"\\ufeffcaf\\u00e9\", \"append_text\": \"\", \"positive_text\": \" snow\\u001cdetailed fur\", \"negative_effective_text\": \"\\u732b \"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"\\u001cdetailed fur \", \"prepend_text\": \"\\u001c猫\\n\", \"negative_text\": \"\\t猫
 a,\"}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"103\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"104\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"…detailed fur\\n\", \"negative_text\": \"\\u001c猫\\u001c\", \"prepend_text\": \"\\u001csnow…fox\", \"append_text\": \" \"}}, \"106\": {\"class_type\": \"StringConstantMultiline\"}, \"105\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"106\", 0], \"prepend_text\": \"\\ndetailed fur\", \"append_text\": \"\\t\\u001c\", \"clip\": [\"4\", 1]}}, \"107\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"104\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"108\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"103\", 0]}}, \"112\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"…detailed fur\\t\", \"strip_newlines\": true}}, \"111\": {\"class_type\": \"TextInput\", \"inputs\": {\"text\": [\"112\", 0]}}, \"110\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"111\", 0], \"text_l\": [\"111\", 0]}}, \"115\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \" detailed fur…\"}}, \"114\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"115\", 0]}}, \"113\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"114\", 0], \"prepend_text\": \" \\tdetailed fur\", \"negative_text\": \"猫 \"}}, \"109\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"110\", 0], \"negative\": [\"113\", 0], \"upscaled_image\": [\"108\", 0]}}, \"120\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u001c😀
 a,\", \"clip\": [\"4\", 1]}}, \"119\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"120\", 0]}}, \"118\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"119\", 0]}}, \"117\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"118\", 0], \"conditioning_2\": [\"118\", 0]}}, \"121\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"…\\n\", \"clip\": [\"4\", 1]}}, \"116\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"109\", 0], \"model\": [\"4\", 0], \"positive\": [\"117\", 0], \"negative\": [\"121\", 0]}}, \"122\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"116\", 0]}}, \"123\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"107\", 0]}}, \"127\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"
 😀 😀\", \"negative_text\": \"猫 \", \"prepend_text\": \"café\\t\", \"append_text\": \"\\t\"}}, \"129\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a,\\t\", \"clip\": [\"4\", 1]}}, \"128\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"129\", 0]}}, \"126\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"127\", 0], \"conditioning_2\": [\"128\", 0]}}, \"131\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\u001ca,
 \", \"strip_newlines\": false}}, \"130\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"131\", 0], \"clip\": [\"4\", 1]}}, \"125\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"126\", 0], \"on_false\": [\"130\", 0]}}, \"135\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\n…\", \"clip\": [\"4\", 1]}}, \"134\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"135\", 0]}}, \"133\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"134\", 0]}}, \"138\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u001c猫\", \"clip\": [\"4\", 1]}}, \"137\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"138\", 0]}}, \"139\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"\\ndetailed fur…\", \"prepend_text\": \" \", \"negative_text\": \"a,猫\"}}, \"136\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"137\", 0], \"on_false\": [\"139\", 0]}}, \"132\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"133\", 0], \"on_false\": [\"136\", 0]}}, \"124\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"123\", 0], \"model\": [\"4\", 0], \"positive\": [\"125\", 0], \"negative\": [\"132\", 0]}}, \"140\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"124\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-28", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"101\": {\"records\": [{\"text\": \"\\u001c\\u732b \", \"negative_text\": \"\\u2028 \\u0085\", \"prepend_text\": \"\\u732b\", \"append_text\": \"\", \"positive_text\": \"snow\\u0085\", \"negative_effective_text\": \"\\u2028 \\u0085\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001c\\u732b \", \"negative_text\": \"\\u2028 \\u0085\", \"prepend_text\": \"\\u732b\\u001c\", \"append_text\": \"\\u3000\\u2028 \"}}, \"102\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\u001c\\u3000\"}}, \"103\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 1], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"104\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 1], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"105\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 1], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"106\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"103\", 0]}}, \"107\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"104\", 0]}}, \"110\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\u0085fox\\u3000\"}}, \"109\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"110\", 0]}}, \"114\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"\\ndetailed fur\"}}, \"113\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"114\", 0]}}, \"112\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"113\", 0], \"text_l\": [\"113\", 0]}}, \"116\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_a\": \"detailed fur\\u3000detailed fur\", \"text_d\": \"\\ufeff\\ud83d\\ude00\\u2028 \", \"text_c\": \"\\tsnow \", \"clean_whitespace\": true}}, \"115\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"116\", 0], \"prepend_text\": \"\\u001c\\ud83d\\ude00\\u001c\", \"negative_text\": \"\\u001ca,\\ufeff\\ud83d\\ude00\"}}, \"111\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"112\", 0], \"conditioning_2\": [\"115\", 0]}}, \"108\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"109\", 0], \"negative\": [\"111\", 0], \"upscaled_image\": [\"107\", 0]}}, \"118\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u3000snow\\u3000snow\", \"negative_text\": \"\\u0085fox\\u001c\", \"prepend_text\": \"foxcaf\\u00e9\", \"append_text\": \"\\ufeffsnow\\tcaf\\u00e9\"}}, \"117\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"108\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 1], \"negative\": [\"118\", 0]}}, \"119\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"117\", 0]}}, \"120\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"105\", 0]}}, \"122\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\ufeffcaf\\u00e9\\u2028 a,\", \"prepend_text\": \"\\u3000caf\\u00e9 \", \"append_text\": \"\\u732b \", \"clip\": [\"4\", 1]}}, \"121\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"101\", 1], \"negative\": [\"122\", 0], \"upscaled_image\": [\"120\", 0]}}, \"123\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"121\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-29", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\u3000a,\\u001c\", \"text_l\": \"\\u3000a,\\u001c\"}}, \"101\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"102\", 0]}}, \"103\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\ncaf\\u00e9\\t\", \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"105\", 0]}}, \"105\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"106\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u0085a,\\n\", \"clip\": [\"4\", 1]}}, \"109\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \" \\u732b\\u0085\"}}, \"108\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"109\", 0]}}, \"107\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"108\", 0], \"prepend_text\": \" \\ud83d\\ude00\\u3000\", \"negative_text\": \"a,\\n\"}}, \"110\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"106\", 0], \"negative\": [\"107\", 0], \"latent_image\": [\"104\", 0]}}, \"111\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"104\", 0]}}, \"115\": {\"class_type\": \"StringConstant\", \"inputs\": {\"text\": \"\\ncaf\\u00e9\\u001c\"}}, \"114\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": [\"115\", 0], \"inputcount\": 1, \"delimiter\": \" \", \"return_list\": false}}, \"113\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"114\", 0], \"clip\": [\"4\", 1]}}, \"112\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"111\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"113\", 0]}}, \"116\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"105\", 0]}}, \"117\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"116\", 0]}}, \"121\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\u732b\\ufeff\", \"strip_newlines\": false}}, \"120\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"121\", 0], \"prepend_text\": \"\\u0085a,\\u0085fox\", \"append_text\": \"\\u3000a,\\t\", \"clip\": [\"4\", 1]}}, \"123\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\u732b\\ufeff\"}}, \"122\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"123\", 0]}}, \"119\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"120\", 0], \"on_false\": [\"122\", 0]}}, \"118\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"116\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"119\", 0]}}, \"124\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"118\", 0]}}, \"125\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"110\", 0]}}, \"126\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"125\", 0]}}, \"127\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"125\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt is chosen at run time (JoinStringMulti).", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt is chosen at run time (JoinStringMulti).", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": {"class_type": "JoinStringMulti", "id": "104"}, "reason": "Text is decided at run time", "reason_code": "runtime-value", "sampler_id": "107", "state": "unresolved"}}, "name": "select-fuzz-0-30", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\\u001c😀\\u001c\", \"string_2\": \"猫 detailed fur\", \"string_3\": \"detailed fur
 😀\", \"inputcount\": 3, \"delimiter\": \" \", \"return_list\": true}}, \"103\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"
 detailed fur…\", \"string_2\": [\"104\", 0], \"inputcount\": 2, \"delimiter\": \",\", \"return_list\": false}}, \"102\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"103\", 0], \"text_l\": [\"103\", 0]}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"106\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \" snow\\n\"}}, \"105\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"106\", 0], \"prepend_text\": \"\\t…\", \"negative_text\": \"\\t\\u001c\"}}, \"107\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"108\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"107\", 0]}}, \"109\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"108\", 0]}}, \"110\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"108\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-31", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"102\": {\"records\": [{\"text\": \"\\tsnow\\u0085fox\", \"negative_text\": \"\\ufeff \", \"prepend_text\": \"caf\\u00e9\\ufeff\", \"append_text\": \"snow\", \"positive_text\": \"\\ufeffa,\", \"negative_effective_text\": \"\\ufeff \"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\tsnow…fox\", \"negative_text\": \" \", \"prepend_text\": \"café\", \"append_text\": \"\\u001csnow\\u001c\"}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"103\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\u001ca,\\t\", \"text_l\": \"\"}}, \"104\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"104\", 0]}}, \"107\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\t…\", \"strip_newlines\": false}}, \"106\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": [\"107\", 0], \"text_d\": \"\\t\\t\", \"clean_whitespace\": \"true\"}}, \"105\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"106\", 0], \"prepend_text\": \"café \", \"append_text\": \"a,\\ta,\", \"clip\": [\"4\", 1]}}, \"109\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"…😀…\", \"text_l\": \"\\n😀\\tcafé\"}}, \"112\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"detailed fur \", \"inputcount\": 1, \"delimiter\": \",\", \"return_list\": false}}, \"111\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"112\", 0], \"text_l\": \"snow\\t\"}}, \"110\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"111\", 0]}}, \"108\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"109\", 0], \"on_false\": [\"110\", 0]}}, \"113\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"105\", 0], \"negative\": [\"108\", 0], \"latent_image\": [\"5\", 0]}}, \"114\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"104\", 0]}}, \"118\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\\tdetailed fur\\u001c\", \"string_2\": \"\\tsnow\\n\", \"inputcount\": 3, \"delimiter\": \",\", \"return_list\": false}}, \"117\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"118\", 0], \"prepend_text\": \"snow…\", \"append_text\": \"\\u001ccafé\", \"clip\": [\"4\", 1]}}, \"116\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"117\", 0], \"conditioning_2\": [\"117\", 0]}}, \"115\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"114\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"116\", 0], \"wildcard\": \"x\"}}, \"119\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"115\", 0]}}, \"120\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"113\", 0]}}, \"121\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"120\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-32", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"102\": {\"records\": [{\"text\": \"\\u001cfox\\tsnow\", \"negative_text\": \"\\u001c\\t\", \"prepend_text\": \"detailed fur\", \"append_text\": \"\\u732b\", \"positive_text\": \"\\tfox\", \"negative_effective_text\": \"\\u001c\\t\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001cfox\\tsnow\", \"negative_text\": \"\\u001c\\t\", \"prepend_text\": \"
 detailed fur \", \"append_text\": \"…猫…\"}}, \"103\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\u001c猫 \", \"text_l\": \"\\ta, \"}}, \"101\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"102\", 1], \"on_false\": [\"103\", 0]}}, \"104\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"108\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"fox 猫\", \"clip\": [\"4\", 1]}}, \"107\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"108\", 0]}}, \"106\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"107\", 0]}}, \"105\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"106\", 0]}}, \"109\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"…😀\\u001c\"}}, \"110\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"105\", 0], \"negative\": [\"109\", 0], \"latent_image\": [\"5\", 0]}}, \"111\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"104\", 0]}}, \"112\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"111\", 0]}}, \"113\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"111\", 0]}}, \"114\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"110\", 0]}}, \"115\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"114\", 0]}}, \"116\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"114\", 0]}}, \"117\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"114\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-33", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"😀 \", \"clip\": [\"4\", 1]}}, \"102\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\t\\n\"}}, \"103\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"107\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_d\": \"\\ndetailed fur\\tfox\", \"text_a\": \"
 fox \", \"text_b\": \" café\\t\", \"clean_whitespace\": true}}, \"106\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": [\"107\", 0], \"string_2\": \" \\t\", \"inputcount\": 2, \"delimiter\": \" \", \"return_list\": false}}, \"105\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"106\", 0], \"text_l\": \"\"}}, \"104\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"105\", 0]}}, \"108\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"104\", 0], \"latent_image\": [\"5\", 0]}}, \"109\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"104\", 0], \"latent_image\": [\"5\", 0]}}, \"110\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"103\", 0]}}, \"111\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"110\", 0]}}, \"112\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"110\", 0]}}, \"113\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"108\", 0]}}, \"115\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"猫…\", \"text_l\": \"猫…\"}}, \"116\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\u001cfox\", \"prepend_text\": \"…a,\\t\", \"append_text\": \"
 😀café\", \"clip\": [\"4\", 1]}}, \"114\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"113\", 0], \"model\": [\"4\", 0], \"positive\": [\"115\", 0], \"negative\": [\"116\", 0]}}, \"117\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"109\", 0]}}, \"121\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \" café\\u001c\", \"prepend_text\": \"\\ta,\", \"append_text\": \"\\tdetailed furcafé\"}}, \"120\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": [\"121\", 0]}}, \"119\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"120\", 0], \"prepend_text\": \"\\tsnow \", \"append_text\": \"
 
 \", \"clip\": [\"4\", 1]}}, \"118\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"117\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"119\", 0]}}, \"122\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"118\", 0]}}, \"123\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"118\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "select-fuzz-0-34", "records": {"parameters": "Negative prompt: blur"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-35", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"102\": {\"records\": [{\"text\": \"\\u001c\\u3000\", \"negative_text\": \"\\n\", \"prepend_text\": \"a,\", \"append_text\": \"\\ud83d\\ude00\\t\\ud83d\\ude00\", \"positive_text\": \" a,\\u3000\\ud83d\\ude00\", \"negative_effective_text\": \"\\n\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"detailed fur\\u0085\\ud83d\\ude00\"}}, \"102\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001c\\u3000\", \"negative_text\": \"\\n\", \"prepend_text\": \"\\u3000a,\\n\", \"append_text\": \"\\u0085\\ud83d\\ude00\\t\\ud83d\\ude00\"}}, \"103\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"104\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"105\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"103\", 0]}}, \"106\": {\"class_type\": \"PreviewAny\"}, \"107\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"104\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-36", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": \"\\u001csnow\\t\", \"text_c\": \"
 fox…\", \"clean_whitespace\": true}}, \"105\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \" a, \", \"inputcount\": 1, \"delimiter\": \",\", \"return_list\": false}}, \"103\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_a\": \" fox\", \"text_b\": [\"104\", 0], \"text_c\": [\"105\", 0], \"clean_whitespace\": \"false\"}}, \"102\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"103\", 0], \"text_l\": \"\\u001ccafé…fox\"}}, \"101\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"102\", 0]}}, \"107\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"café\", \"prepend_text\": \"fox detailed fur\", \"append_text\": \"\\t猫
 fox\", \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"107\", 0]}}, \"108\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 0], \"latent_image\": [\"5\", 0]}}, \"112\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \"fox\\n\"}}, \"111\": {\"class_type\": \"StringConstant\", \"inputs\": {\"text\": [\"112\", 0]}}, \"110\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"111\", 0], \"prepend_text\": \"\\t猫\\nsnow\", \"append_text\": \"\\u001ca,…\", \"clip\": [\"4\", 1]}}, \"109\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"110\", 0]}}, \"115\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \" snowdetailed fur\", \"prepend_text\": \"\\ndetailed fur \", \"append_text\": \"…fox
 detailed fur\", \"clip\": [\"4\", 1]}}, \"114\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"115\", 0]}}, \"113\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"114\", 0]}}, \"116\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"109\", 0], \"negative\": [\"113\", 0], \"latent_image\": [\"108\", 0]}}, \"117\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"108\", 0]}}, \"118\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"116\", 0]}}, \"120\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\tcafé
 \", \"clip\": [\"4\", 1]}}, \"119\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"118\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"120\", 0]}}, \"121\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"119\", 0]}}, \"122\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"119\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-37", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"\\na,\\n\\u732b\", \"string_b\": \"\\u3000snow\\u2028 \", \"delimiter\": \" \"}}, \"102\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \" \\ud83d\\ude00\\ufeff\", \"string_2\": [\"103\", 0], \"inputcount\": 2, \"delimiter\": \" \", \"return_list\": false}}, \"101\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"\\u0085detailed fur\", \"append_text\": \"detailed fur \\u732b\", \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"TextInput\", \"inputs\": {\"string\": \" \\ud83d\\ude00\\u001c\\ud83d\\ude00\"}}, \"105\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"106\", 0], \"string_b\": \"\\u2028 \\u732b \", \"delimiter\": \"\"}}, \"104\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"105\", 0], \"text_l\": [\"105\", 0]}}, \"107\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"104\", 0], \"latent_image\": [\"5\", 0]}}, \"110\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"detailed fur \", \"prepend_text\": \"\\tsnow \", \"append_text\": \"\\n\\ud83d\\ude00 \"}}, \"109\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_c\": \"\\ufeffdetailed fur\\ufeff\", \"text_d\": [\"110\", 0], \"clean_whitespace\": true}}, \"108\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"109\", 0], \"text_l\": \"\\u001cfox\"}}, \"114\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \" caf\\u00e9\\u001c\"}}, \"113\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"114\", 0]}}, \"112\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"113\", 0]}}, \"111\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"112\", 0]}}, \"115\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"108\", 0], \"negative\": [\"111\", 0], \"latent_image\": [\"5\", 0]}}, \"116\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"107\", 0]}}, \"117\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"115\", 0]}}, \"121\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \" snow\\ufeff\", \"strip_newlines\": false}}, \"120\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"121\", 0], \"text_l\": [\"121\", 0]}}, \"123\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\nfox\\u3000a,\", \"text_l\": \"\\nfox\\u3000a,\"}}, \"122\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"123\", 0]}}, \"119\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"120\", 0], \"on_false\": [\"122\", 0]}}, \"118\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"117\", 0], \"model\": [\"4\", 0], \"positive\": [\"108\", 0], \"negative\": [\"119\", 0]}}, \"124\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"118\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "execution", "status": "resolved", "text": " a,"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "a,a,"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "105", "state": "resolved"}}, "name": "select-fuzz-0-38", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"104\": {\"records\": [{\"text\": \"\\u3000fox\\u2028 \", \"negative_text\": \" a,\", \"prepend_text\": \"caf\\u00e9\", \"append_text\": \"\\ud83d\\ude00\", \"positive_text\": \" \\u732b\\u3000\", \"negative_effective_text\": \" a,\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": \"a,\\u0085\"}}, \"102\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"103\", 0]}}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"\\u0085a,\\u0085\", \"negative_text\": \"\\u3000\\u732b\"}}, \"104\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u3000fox\\u2028 \", \"negative_text\": \" a,\", \"prepend_text\": \" caf\\u00e9\", \"append_text\": \"\\u2028 \\ud83d\\ude00\\n\"}}, \"105\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"104\", 1], \"latent_image\": [\"5\", 0]}}, \"106\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"105\", 0]}}, \"107\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"106\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "\n猫 fox"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "猫a,caféfox"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "103", "state": "resolved"}}, "name": "select-fuzz-0-39", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"caféfox\", \"prepend_text\": \"\\n猫a,\", \"negative_text\": \"
 a,\"}}, \"102\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\n猫 fox\", \"clip\": [\"4\", 1]}}, \"103\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"107\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\t😀
 detailed fur\", \"strip_newlines\": true}}, \"106\": {\"class_type\": \"StringConstant\", \"inputs\": {\"text\": [\"107\", 0]}}, \"105\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"106\", 0], \"prepend_text\": \"猫 😀\", \"append_text\": \" a,\\u001c\", \"clip\": [\"4\", 1]}}, \"104\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"105\", 0]}}, \"108\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"\\u001ca, \", \"prepend_text\": \"…fox \", \"negative_text\": \"…snow
 fox\"}}, \"109\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"104\", 0], \"negative\": [\"108\", 0], \"latent_image\": [\"5\", 0]}}, \"110\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"104\", 0], \"negative\": [\"108\", 0], \"latent_image\": [\"5\", 0]}}, \"111\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"103\", 0]}}, \"112\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"109\", 0]}}, \"113\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"112\", 0]}}, \"114\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"110\", 0]}}, \"115\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"114\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Cyclic latent ancestry", "reason_code": "malformed", "sampler_id": "103", "state": "unresolved"}}, "name": "select-fuzz-0-40", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"😀 \", \"prepend_text\": \" café \", \"negative_text\": \"…😀\\n\"}}, \"102\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \" snow\\t\", \"prepend_text\": \"\\u001ca,…\", \"negative_text\": \" a,\\t\"}}, \"103\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"103\", 0]}}, \"104\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"103\", 0]}}, \"105\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"104\", 0]}}, \"106\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"104\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-41", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"108\": {\"records\": [{\"text\": \"\\u001csnow\\t\\ud83d\\ude00!\", \"negative_text\": \"\\u3000\\ud83d\\ude00 \", \"prepend_text\": \"\\ud83d\\ude00\\u0085\\u732b\", \"append_text\": \"\\ufefffox\", \"positive_text\": \"\\nsnow\\u2028 a,\", \"negative_effective_text\": \"\\u3000\\ud83d\\ude00 \"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"café\\n\", \"prepend_text\": \"\\nsnow\", \"negative_text\": \" fox…fox\"}}, \"103\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"104\", 0], \"conditioning_2\": [\"104\", 0]}}, \"107\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\t \", \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"107\", 0]}}, \"108\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001csnow\\t😀\", \"negative_text\": \" 😀 \", \"prepend_text\": \"…😀…猫\", \"append_text\": \"fox \"}}, \"105\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"106\", 0], \"conditioning_2\": [\"108\", 0]}}, \"102\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"103\", 0], \"on_false\": [\"105\", 0]}}, \"101\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"110\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"😀\\t😀\", \"prepend_text\": \"…😀\\n\", \"negative_text\": \"snow\\t\"}}, \"109\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"110\", 0]}}, \"111\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"109\", 0], \"latent_image\": [\"5\", 0]}}, \"112\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"109\", 0], \"latent_image\": [\"5\", 0]}}, \"116\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\tcafé
 detailed fur\", \"text_l\": \"\\tcafé
 detailed fur\"}}, \"115\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"116\", 0]}}, \"114\": {\"class_type\": null, \"inputs\": {\"value\": [\"115\", 0]}}, \"113\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"114\", 0]}}, \"118\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a,…\", \"clip\": [\"4\", 1]}}, \"117\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"118\", 0]}}, \"119\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"113\", 0], \"negative\": [\"117\", 0], \"latent_image\": [\"5\", 0]}}, \"120\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"111\", 0]}}, \"125\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\ncafé\\t\", \"clip\": [\"4\", 1]}}, \"124\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"125\", 0]}}, \"126\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"…snow\\t\", \"text_l\": \"…snow\\t\"}}, \"123\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"124\", 0], \"conditioning_2\": [\"126\", 0]}}, \"122\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"123\", 0]}}, \"128\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"…café\\u001c\", \"prepend_text\": \"\\ta,\", \"append_text\": \"…a,\\u001c\", \"clip\": [\"4\", 1]}}, \"127\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"128\", 0]}}, \"121\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"122\", 0], \"negative\": [\"127\", 0], \"upscaled_image\": [\"120\", 0]}}, \"129\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"121\", 0]}}, \"130\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"112\", 0]}}, \"131\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"130\", 0]}}, \"132\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"130\", 0]}}, \"133\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"119\", 0]}}, \"135\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" \", \"clip\": [\"4\", 1]}}, \"134\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"133\", 0], \"model\": [\"4\", 0], \"positive\": [\"113\", 0], \"negative\": [\"135\", 0]}}, \"138\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\ndetailed fur\", \"strip_newlines\": false}}, \"137\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"138\", 0], \"prepend_text\": \" detailed fur café\", \"append_text\": \"\\ta, \", \"clip\": [\"4\", 1]}}, \"141\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\t😀\", \"prepend_text\": \"a,\", \"append_text\": \"
 猫\\u001c\", \"clip\": [\"4\", 1]}}, \"140\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"141\", 0]}}, \"139\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"140\", 0]}}, \"136\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"137\", 0], \"negative\": [\"139\", 0], \"upscaled_image\": [\"134\", 0]}}, \"142\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"136\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (ConditioningCombine).", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "snow\t"}}, "resolution": {"node": {"class_type": "ConditioningCombine", "id": "117"}, "reason": "Combined conditioning has different texts", "reason_code": "composite", "sampler_id": "110", "state": "unresolved"}}, "name": "select-fuzz-0-42", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u3000caf\\u00e9\\u001c\", \"clip\": [\"4\", 1]}}, \"101\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"102\", 0]}}, \"103\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\tdetailed fur\\ufeff\", \"negative_text\": \"\\ufeffcaf\\u00e9\", \"prepend_text\": \"\\u3000detailed fur\\u0085\", \"append_text\": \"\\t\\ud83d\\ude00\\ufeff\\u732b\"}}, \"104\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"104\", 0]}}, \"105\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"104\", 0]}}, \"109\": {\"class_type\": \"StringConstant\", \"inputs\": {\"text\": \"\\u001ccaf\\u00e9\\t\"}}, \"108\": {\"class_type\": \"StringConstant\", \"inputs\": {\"text\": [\"109\", 0]}}, \"107\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"108\", 0], \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"107\", 0], \"upscaled_image\": [\"105\", 0]}}, \"114\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"snow\\t\", \"clip\": [\"4\", 1]}}, \"113\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"114\", 0]}}, \"112\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"113\", 0]}}, \"111\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"112\", 0]}}, \"118\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\tfox\\u001c\", \"clip\": [\"4\", 1]}}, \"119\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" detailed fur\\u001c\", \"clip\": [\"4\", 1]}}, \"117\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"118\", 0], \"conditioning_2\": [\"119\", 0]}}, \"120\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \" fox\", \"negative_text\": \"\\u3000\\u0085\", \"prepend_text\": \"\\nsnow\\u3000\", \"append_text\": \" \\ud83d\\ude00\\u2028 \"}}, \"116\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"117\", 0], \"conditioning_2\": [\"120\", 1]}}, \"115\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"116\", 0]}}, \"110\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"106\", 0], \"model\": [\"4\", 0], \"positive\": [\"111\", 0], \"negative\": [\"115\", 0]}}, \"121\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"110\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "select-fuzz-0-43", "records": {"parameters": " snow \n\n猫…\n café "}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "absent", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "😀 …snow 猫"}}, "resolution": {"node": null, "reason": "Negative input is absent", "reason_code": null, "sampler_id": "103", "state": "absent"}}, "name": "select-fuzz-0-44", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\u0085snow \", \"prepend_text\": \"\\t\\ud83d\\ude00\\n\", \"append_text\": \"\\t\\u732b\\u3000\", \"clip\": [\"4\", 1]}}, \"102\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"103\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"107\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\ufeff\\u732b\", \"strip_newlines\": true}}, \"106\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"107\", 0]}}, \"105\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"106\", 0]}}, \"104\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"105\", 0], \"text_l\": [\"105\", 0]}}, \"108\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u0085snow\\u001c\", \"clip\": [\"4\", 1]}}, \"109\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"104\", 0], \"negative\": [\"108\", 0], \"latent_image\": [\"5\", 0]}}, \"110\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"102\", 0]}}, \"111\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"110\", 0]}}, \"112\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"103\", 0]}}, \"113\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"112\", 0]}}, \"114\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"109\", 0]}}, \"115\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"114\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": "zeroed-conditioning", "status": "resolved", "text": ""}, "positive": {"source": "graph-inferred", "status": "resolved", "text": ", 猫"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "108", "state": "resolved"}}, "name": "select-fuzz-0-45", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\n\\u732b \", \"strip_newlines\": false}}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \" \\ufeff\", \"negative_text\": \"\\u001c\\n\"}}, \"106\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \"\\u3000\\ud83d\\ude00\\u3000\", \"prepend_text\": \" \\u732b\\u0085\", \"append_text\": \"\\u001cfox\\u001c\", \"clip\": [\"4\", 1]}}, \"105\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"106\", 0]}}, \"104\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"105\", 0]}}, \"103\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"104\", 0]}}, \"107\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"108\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"109\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"107\", 0]}}, \"110\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"109\", 0]}}, \"111\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"108\", 0]}}, \"112\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"111\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "select-fuzz-0-46", "records": {"parameters": "\u001c… 
 detailed fur Negative prompt: fox…\n"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Expected a typed graph link or text input", "reason_code": "malformed", "sampler_id": "109", "state": "unresolved"}}, "name": "select-fuzz-0-47", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"108\": {\"records\": [{\"text\": \"\\u2028 detailed fur\\t\", \"negative_text\": \"\\tfox\\u001c\", \"prepend_text\": \"\\ufeff\", \"append_text\": \"\\u732b \\u732b\", \"positive_text\": \"\\u001c\\u732b\\u2028 \", \"negative_effective_text\": \"\\tfox\\u001c\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"TextInput\", \"inputs\": {\"string\": \" café \"}}, \"103\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": [\"104\", 0]}}, \"106\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\\nfox
 \", \"string_4\": \"snow\\nsnow\", \"inputcount\": 4, \"delimiter\": \" \", \"return_list\": false}}, \"105\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": [\"106\", 0], \"strip_newlines\": false}}, \"102\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"103\", 0], \"string_b\": [\"105\", 0], \"delimiter\": \", \"}}, \"101\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"102\", 0], \"clip\": [\"4\", 1]}}, \"108\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"
 detailed fur\\t\", \"negative_text\": \"\\tfox\\u001c\", \"prepend_text\": \" \", \"append_text\": \"
 猫 猫\"}}, \"107\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"108\", 0]}}, \"109\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"107\", 0], \"latent_image\": [\"5\", 0]}}, \"111\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\nsnow\\n\", \"text_l\": \"\\nsnow\\n\"}}, \"110\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"111\", 0]}}, \"113\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"…detailed fur \", \"strip_newlines\": false}}, \"112\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"113\", 0], \"prepend_text\": \"snow\\t\", \"append_text\": \"
 😀\\u001cdetailed fur\", \"clip\": [\"4\", 1]}}, \"114\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"110\", 0], \"negative\": [\"112\", 0], \"latent_image\": [\"5\", 0]}}, \"115\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"109\", 0]}}, \"116\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"114\", 0]}}, \"117\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"116\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-48", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"105\": {\"records\": [{\"text\": \"\\u3000\\tfox\", \"negative_text\": \"\\u2028 detailed fur\\u0085a,\", \"prepend_text\": \"detailed fur\", \"append_text\": \"detailed fur\\ufeff\", \"positive_text\": \"\\nsnow\\u2028 \", \"negative_effective_text\": \"\\u2028 detailed fur\\u0085a,\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\ta, 😀\", \"strip_newlines\": false}}, \"103\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": [\"104\", 0]}}, \"102\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"103\", 0], \"prepend_text\": \"\\nsnow\\u001c猫\", \"negative_text\": \"\\ncafé\"}}, \"101\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"102\", 0], \"conditioning_2\": [\"102\", 0]}}, \"105\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \" \\tfox\", \"negative_text\": \"
 detailed fur…a,\", \"prepend_text\": \"
 detailed fur\", \"append_text\": \"detailed fur\"}}, \"106\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"107\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"107\", 0]}}, \"108\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"106\", 0]}}, \"109\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"108\", 0]}}, \"110\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"108\", 0]}}, \"111\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"107\", 0]}}, \"112\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"111\", 0]}}, \"115\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"
 café\\t\", \"prepend_text\": \"\\n😀
 \", \"append_text\": \"猫\", \"clip\": [\"4\", 1]}}, \"116\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"\\u001c\\u001ca,\"}}, \"114\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 0], \"on_true\": [\"115\", 0], \"on_false\": [\"116\", 0]}}, \"113\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"114\", 0], \"upscaled_image\": [\"111\", 0]}}, \"117\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"113\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "select-fuzz-0-49", "records": {"parameters": "Negative Template: n Steps: x\r\n"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-50", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"101\": {\"records\": [{\"text\": \"\\u2028 snow\\u3000\", \"negative_text\": \" detailed fur\\ufeff\", \"prepend_text\": \"\", \"append_text\": \"detailed fur\", \"positive_text\": \"\\u001c\\ud83d\\ude00\\t\", \"negative_effective_text\": \" detailed fur\\ufeff\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"
 snow \", \"negative_text\": \" detailed fur\", \"prepend_text\": \" \\t\", \"append_text\": \"\\tdetailed fur
 \"}}, \"104\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\n
 \", \"negative_text\": \"
 fox\\n\", \"prepend_text\": \"
 猫\\u001ca,\", \"append_text\": \"\\u001cdetailed fur\\t😀\"}}, \"103\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"104\", 0]}}, \"102\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"103\", 0]}}, \"105\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"106\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"107\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"105\", 0]}}, \"108\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"107\", 0]}}, \"109\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"107\", 0]}}, \"110\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"106\", 0]}}, \"114\": {\"class_type\": \"StringConstantMultiline\", \"inputs\": {\"string\": \"\\ncafé\", \"strip_newlines\": true}}, \"113\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"detailed fur 猫\", \"string_b\": [\"114\", 0], \"delimiter\": \", \"}}, \"112\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"113\", 0], \"prepend_text\": \"\\nfox \", \"append_text\": \" detailed fur \", \"clip\": [\"4\", 1]}}, \"111\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"110\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"112\", 0]}}, \"115\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"111\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "107", "state": "unresolved"}}, "name": "select-fuzz-0-51", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \" a, \", \"prepend_text\": \"…猫\", \"negative_text\": \"…\\n\"}}, \"105\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" detailed fur\\n\", \"clip\": [\"4\", 1]}}, \"103\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"104\", 0], \"conditioning_2\": [\"105\", 0]}}, \"102\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"103\", 0]}}, \"101\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"102\", 0]}}, \"106\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"
 \\tfox\", \"negative_text\": \"
 \", \"prepend_text\": \"…café \", \"append_text\": \"…\\n\"}}, \"107\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"106\", 1], \"latent_image\": [\"5\", 0]}}, \"108\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"107\", 0]}}, \"109\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"108\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Sampler stages have different prompts", "reason_code": "multistage-differs", "sampler_id": "108", "state": "ambiguous"}}, "name": "select-fuzz-0-52", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"\\u001c
 \", \"prepend_text\": \"snow\\u001cdetailed fur\", \"append_text\": \"a,\"}}, \"102\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"103\", 0], \"clip\": [\"4\", 1]}}, \"101\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"102\", 0]}}, \"104\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": \"\\ncafé
 snow\", \"prepend_text\": \"\\tfox
 \", \"negative_text\": \"
 
 \"}}, \"105\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"104\", 0], \"latent_image\": [\"5\", 0]}}, \"106\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"105\", 0]}}, \"107\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"106\", 0]}}, \"109\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" snow\", \"clip\": [\"4\", 1]}}, \"112\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\u001cfox\\t\", \"text_l\": \"\\u001cfox\\t\"}}, \"111\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"112\", 0]}}, \"110\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"111\", 0]}}, \"108\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"109\", 0], \"negative\": [\"110\", 0], \"upscaled_image\": [\"106\", 0]}}, \"113\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"missing\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-53", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"124\": {\"records\": [{\"text\": \"a,\\n\", \"negative_text\": \"\\n\\u2028 \", \"prepend_text\": \"a,\", \"append_text\": \"detailed fur\", \"positive_text\": \"\\ncaf\\u00e9\\u0085\", \"negative_effective_text\": \"\\n\\u2028 \"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\ufeff\"}}, \"105\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"\\u2028 caf\\u00e9 \", \"string_b\": \"\\u0085\\u732b\\u2028 \", \"delimiter\": \" \"}}, \"104\": {\"class_type\": \"TextInput\", \"inputs\": {\"string\": [\"105\", 0]}}, \"103\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": [\"104\", 0]}}, \"102\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": [\"103\", 0], \"clip\": [\"4\", 1]}}, \"106\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"108\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\tcaf\\u00e9\\u0085\\ud83d\\ude00\", \"negative_text\": \"\\u3000detailed fur\\u2028 caf\\u00e9\", \"prepend_text\": \" a,\\u3000caf\\u00e9\", \"append_text\": \"detailed fur\\u2028 \\ud83d\\ude00\"}}, \"110\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"fox\\n\", \"clip\": [\"4\", 1]}}, \"112\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\u001c\\ud83d\\ude00\\ufeffsnow\"}}, \"113\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\u001c\\ud83d\\ude00 \", \"prepend_text\": \"\\u2028 snow\\u0085\", \"append_text\": \"\\u001ca,\\ud83d\\ude00\", \"clip\": [\"4\", 1]}}, \"111\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"112\", 0], \"on_false\": [\"113\", 0]}}, \"109\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": false, \"on_true\": [\"110\", 0], \"on_false\": [\"111\", 0]}}, \"107\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"108\", 0], \"conditioning_2\": [\"109\", 0]}}, \"114\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"107\", 0], \"latent_image\": [\"5\", 0]}}, \"115\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"116\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"106\", 0]}}, \"117\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"116\", 0]}}, \"118\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"114\", 0]}}, \"119\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"118\", 0]}}, \"120\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"115\", 0]}}, \"121\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"120\", 0]}}, \"124\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"a,\\n\", \"negative_text\": \"\\n\\u2028 \", \"prepend_text\": \"\\na,\\u0085\", \"append_text\": \"detailed fur \"}}, \"126\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"\\nsnow\\u3000\"}}, \"125\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"126\", 0], \"conditioning_2\": [\"126\", 0]}}, \"123\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"124\", 1], \"conditioning_2\": [\"125\", 0]}}, \"122\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"120\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"123\", 0]}}, \"127\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"122\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Multiple sampler branches", "reason_code": null, "sampler_id": null, "state": "ambiguous"}}, "name": "select-fuzz-0-54", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"122\": {\"records\": [{\"text\": \"\\u0085\\u732b\\t\", \"negative_text\": \"\\u2028 caf\\u00e9\\u3000\", \"prepend_text\": \"\\ud83d\\ude00\\u3000a,\", \"append_text\": \"caf\\u00e9\", \"positive_text\": \" \\ud83d\\ude00\\u3000\", \"negative_effective_text\": \"\\u2028 caf\\u00e9\\u3000\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"102\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\ta,\", \"clip\": [\"4\", 1]}}, \"101\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"102\", 0]}}, \"106\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"café \", \"prepend_text\": \"…a, café\", \"negative_text\": \"…a,\\n😀\"}}, \"105\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"106\", 0]}}, \"108\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"猫\", \"text_l\": \"猫\"}}, \"109\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\n猫\\n\", \"text_l\": \"\\n猫\\n\"}}, \"107\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"108\", 0], \"on_false\": [\"109\", 0]}}, \"104\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"105\", 0], \"on_false\": [\"107\", 0]}}, \"110\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"…fox…snow\"}}, \"103\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"104\", 0], \"conditioning_2\": [\"110\", 0]}}, \"111\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"103\", 0], \"latent_image\": [\"5\", 0]}}, \"113\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \"a,\"}}, \"112\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"113\", 0]}}, \"117\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_b\": \"猫\", \"text_d\": \"\", \"text_a\": \"a,
 \", \"clean_whitespace\": \"true\"}}, \"116\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"😀 fox\", \"string_b\": [\"117\", 0], \"delimiter\": \"\"}}, \"115\": {\"class_type\": \"PrimitiveString\", \"inputs\": {\"value\": [\"116\", 0]}}, \"114\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"115\", 0], \"prepend_text\": \"\", \"negative_text\": \"\\u001cdetailed fur\\n\"}}, \"118\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"112\", 0], \"negative\": [\"114\", 0], \"latent_image\": [\"5\", 0]}}, \"119\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"112\", 0], \"negative\": [\"114\", 0], \"latent_image\": [\"118\", 0]}}, \"120\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"111\", 0]}}, \"122\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"…猫\\t\", \"negative_text\": \"
 café \", \"prepend_text\": \" 😀 a,\", \"append_text\": \" café \"}}, \"121\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"120\", 0], \"model\": [\"4\", 0], \"positive\": [\"112\", 0], \"negative\": [\"122\", 0]}}, \"123\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"121\", 0]}}, \"124\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"118\", 0]}}, \"125\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"124\", 0]}}, \"126\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"119\", 0]}}, \"128\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": \" 猫 \", \"prepend_text\": \"猫\\u001c😀\", \"append_text\": \" 猫\\tdetailed fur\", \"clip\": [\"4\", 1]}}, \"132\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"
 \", \"clip\": [\"4\", 1]}}, \"131\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"132\", 0]}}, \"130\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"131\", 0]}}, \"129\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"130\", 0], \"conditioning_2\": [\"130\", 0]}}, \"127\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"126\", 0], \"model\": [\"4\", 0], \"positive\": [\"128\", 0], \"negative\": [\"129\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Prompt metadata in this file is malformed.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "A1111 settings footer is not unique", "reason_code": "malformed", "sampler_id": null, "state": "unresolved"}}, "name": "select-fuzz-0-55", "records": {"parameters": "…😀\t\r\nTemplate: t\rSteps: x Steps:٣, a\r\t\n\r"}}, {"evidence": {"prompts": {"negative": {"source": "graph-inferred", "status": "resolved", "text": "fox 😀 detailed fur 猫\u001c猫"}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "detailed fur, fox 
 😀\u001c, … a,"}}, "resolution": {"node": null, "reason": "", "reason_code": null, "sampler_id": "106", "state": "resolved"}}, "name": "select-fuzz-0-56", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"104\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": \"
 😀\\u001c\", \"string_b\": \"… \", \"delimiter\": \", \"}}, \"103\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": [\"104\", 0], \"prepend_text\": \" fox \", \"append_text\": \"a,\"}}, \"102\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"103\", 0]}}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"detailed fur\\n\", \"negative_text\": \"😀
 \"}}, \"105\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \" detailed fur \", \"negative_text\": \"\\u001ca,…café\", \"prepend_text\": \"
 fox 😀\", \"append_text\": \"
 猫\\u001c猫\"}}, \"106\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"105\", 0], \"latent_image\": [\"5\", 0]}}, \"107\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"missing\", 0]}}, \"108\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"107\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Prompt combines several texts (CLIPTextEncodeSDXL).", "source": null, "status": "unresolved", "text": null}, "positive": {"source": "graph-inferred", "status": "resolved", "text": "detailed fur, snow 
 a,
  a,"}}, "resolution": {"node": {"class_type": "CLIPTextEncodeSDXL", "id": "104"}, "reason": "Encoder parts have different texts", "reason_code": "composite", "sampler_id": "105", "state": "unresolved"}}, "name": "select-fuzz-0-57", "records": {"comfyassets_prompt_pairs": "{\"version\": 1, \"nodes\": {\"107\": {\"records\": [{\"text\": \" snow\\u0085\", \"negative_text\": \"\\u001c\\u732b\\ufeff\", \"prepend_text\": \"fox\", \"append_text\": \"a,\\ufeff\\ud83d\\ude00\", \"positive_text\": \"\\ufeffcaf\\u00e9\\ufeff\", \"negative_effective_text\": \"\\u001c\\u732b\\ufeff\"}]}}}", "prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\\u0085snow\\ufeff\", \"string_2\": \"\\u2028 a,\\u2028 \", \"inputcount\": 2, \"delimiter\": \" \", \"return_list\": false}}, \"102\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"103\", 0], \"string_b\": \"\\u3000a, \", \"delimiter\": \"\"}}, \"101\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"102\", 0], \"prepend_text\": \"\\ufeffdetailed fur\\u2028 \", \"negative_text\": \"\\u001ccaf\\u00e9\\t\"}}, \"104\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\na,\\u3000\", \"text_l\": \" \"}}, \"105\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"104\", 0], \"latent_image\": [\"5\", 0]}}, \"107\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \" snow\\u0085\", \"negative_text\": \"\\u001c\\u732b\\ufeff\", \"prepend_text\": \"\\u001c\\u0085fox\", \"append_text\": \"\\u0085a,\\ufeff\\ud83d\\ude00\"}}, \"106\": {\"class_type\": \"Reroute\", \"inputs\": {}}, \"111\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\ncaf\\u00e9\\ufeffa,\", \"text_l\": \"\\u3000a,\\n\"}}, \"110\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"111\", 0]}}, \"109\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"110\", 0]}}, \"108\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"109\", 0]}}, \"112\": {\"class_type\": \"SamplerCustom\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"106\", 0], \"negative\": [\"108\", 0], \"latent_image\": [\"5\", 0]}}, \"113\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"105\", 0]}}, \"114\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"113\", 0]}}, \"115\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"112\", 0]}}, \"119\": {\"class_type\": \"JoinStringMulti\", \"inputs\": {\"string_1\": \"\\u0085fox\\t\", \"inputcount\": 1, \"delimiter\": \" \", \"return_list\": false}}, \"118\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"119\", 0], \"text_l\": [\"119\", 0]}}, \"121\": {\"class_type\": \"PrimitiveStringMultiline\", \"inputs\": {\"value\": \"\\u0085\\n\"}}, \"120\": {\"class_type\": \"PromptManagerFocused\", \"inputs\": {\"text\": [\"121\", 0], \"prepend_text\": \"\\u0085detailed fur\\u3000\", \"append_text\": \"\\t\", \"clip\": [\"4\", 1]}}, \"117\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": [\"4\", 1], \"on_true\": [\"118\", 0], \"on_false\": [\"120\", 0]}}, \"122\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"\\t\\u732b \", \"prepend_text\": \"\\u3000\\u732b\\u2028 \", \"negative_text\": \"\\tdetailed fur\\ta,\"}}, \"116\": {\"class_type\": \"FaceDetailer\", \"inputs\": {\"image\": [\"115\", 0], \"model\": [\"4\", 0], \"positive\": [\"117\", 0], \"negative\": [\"122\", 0]}}, \"124\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\u2028 snow \", \"text_l\": \"\"}}, \"125\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": \"\\u001c\\u732b\\u0085caf\\u00e9\", \"prepend_text\": \"\\ufeffcaf\\u00e9\\u0085\", \"negative_text\": \"\\n\\u732b\\t\"}}, \"123\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"116\", 0], \"model\": [\"4\", 0], \"positive\": [\"124\", 0], \"negative\": [\"125\", 0]}}, \"126\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"123\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}, "positive": {"message": "Positive prompt is empty.", "source": null, "status": "unresolved", "text": null}}, "resolution": {"node": null, "reason": "Positive prompt is empty", "reason_code": null, "sampler_id": "108", "state": "unresolved"}}, "name": "select-fuzz-0-58", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"101\": {\"class_type\": \"PromptManager\", \"inputs\": {\"text\": \"\\u001csnow\\n\", \"prepend_text\": \"\\u3000detailed fur\\ufeffsnow\", \"append_text\": \"\\u0085\\u3000\", \"clip\": [\"4\", 1]}}, \"102\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \" \\ufeff\"}}, \"103\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"104\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"102\", 0], \"latent_image\": [\"5\", 0]}}, \"105\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"103\", 0]}}, \"106\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"105\", 0]}}, \"107\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"104\", 0]}}, \"112\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" fox a,\", \"clip\": [\"4\", 1]}}, \"111\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"112\", 0]}}, \"114\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \" detailed fur\\u001c\", \"clip\": [\"4\", 1]}}, \"113\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"114\", 0]}}, \"110\": {\"class_type\": \"easy ifElse\", \"inputs\": {\"boolean\": true, \"on_true\": [\"111\", 0], \"on_false\": [\"113\", 0]}}, \"109\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"110\", 0]}}, \"117\": {\"class_type\": \"Text Concatenate\", \"inputs\": {\"text_a\": \" \\ud83d\\ude00\\u3000\", \"text_b\": \"\\ncaf\\u00e9 \", \"text_d\": \"\\n\\ud83d\\ude00\\u001c\", \"clean_whitespace\": \"true\"}}, \"118\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"\\u001ccaf\\u00e9 \\u732b\", \"prepend_text\": \"\\u2028 \\u732b\\n\\u732b\", \"append_text\": \"\\ufeff \"}}, \"116\": {\"class_type\": \"StringConcatenate\", \"inputs\": {\"string_a\": [\"117\", 0], \"string_b\": [\"118\", 0], \"delimiter\": \", \"}}, \"115\": {\"class_type\": \"PromptManagerPositive\", \"inputs\": {\"text\": [\"116\", 0], \"prepend_text\": \"\\u2028 caf\\u00e9\", \"negative_text\": \"\\tcaf\\u00e9\\u3000\"}}, \"108\": {\"class_type\": \"DetailerForEach\", \"inputs\": {\"image\": [\"107\", 0], \"model\": [\"4\", 0], \"positive\": [\"109\", 0], \"negative\": [\"115\", 0]}}}"}}, {"evidence": {"prompts": {"negative": {"source": null, "status": "ambiguous", "text": null}, "positive": {"source": null, "status": "ambiguous", "text": null}}, "resolution": {"node": null, "reason": "Sampler stages have different prompts", "reason_code": "multistage-differs", "sampler_id": "113", "state": "ambiguous"}}, "name": "select-fuzz-0-59", "records": {"prompt": "{\"4\": {\"class_type\": \"CheckpointLoaderSimple\", \"inputs\": {\"ckpt_name\": \"m\"}}, \"5\": {\"class_type\": \"EmptyLatentImage\", \"inputs\": {\"width\": 64}}, \"103\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \"…猫…\", \"prepend_text\": \"a,\", \"append_text\": \"
 detailed fur\\u001c\"}}, \"102\": {\"class_type\": \"PromptManagerV2\", \"inputs\": {\"text\": [\"103\", 0], \"prepend_text\": \"\\t猫\", \"negative_text\": \"\\n\\u001c\"}}, \"101\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"102\", 0]}}, \"104\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"106\": {\"class_type\": \"PromptSearchList\", \"inputs\": {\"text\": \"fox\"}}, \"108\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": \"\\u001c😀 \", \"text_l\": \"\\nfox…\"}}, \"107\": {\"class_type\": \"FluxGuidance\", \"inputs\": {\"conditioning\": [\"108\", 0]}}, \"105\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"106\", 0], \"conditioning_2\": [\"107\", 0]}}, \"109\": {\"class_type\": \"FancyEncoder\", \"inputs\": {\"text\": \" 😀a,\"}}, \"110\": {\"class_type\": \"KSamplerAdvanced\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"105\", 0], \"negative\": [\"109\", 0], \"latent_image\": [\"5\", 0]}}, \"111\": {\"class_type\": \"KSampler\", \"inputs\": {\"seed\": 1, \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"latent_image\": [\"5\", 0]}}, \"112\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"104\", 0]}}, \"115\": {\"class_type\": \"PromptManagerPair\", \"inputs\": {\"text\": \"\\u001ccafé\", \"negative_text\": \"\\u001c猫café\", \"prepend_text\": \"café\\n\", \"append_text\": \"…😀 \"}}, \"114\": {\"class_type\": \"Reroute\", \"inputs\": {\"value\": [\"115\", 0]}}, \"119\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"a,\\u001c\", \"clip\": [\"4\", 1]}}, \"120\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u001c猫\\u001c\", \"clip\": [\"4\", 1]}}, \"118\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"119\", 0], \"conditioning_2\": [\"120\", 0]}}, \"122\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"\\u001cdetailed fursnow\", \"clip\": [\"4\", 1]}}, \"121\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"122\", 0], \"conditioning_2\": [\"122\", 0]}}, \"117\": {\"class_type\": \"ConditioningCombine\", \"inputs\": {\"conditioning_1\": [\"118\", 0], \"conditioning_2\": [\"121\", 0]}}, \"116\": {\"class_type\": \"ConditioningZeroOut\", \"inputs\": {\"conditioning\": [\"117\", 0]}}, \"113\": {\"class_type\": \"UltimateSDUpscaleNoUpscale\", \"inputs\": {\"model\": [\"4\", 0], \"positive\": [\"114\", 0], \"negative\": [\"116\", 0], \"upscaled_image\": [\"112\", 0]}}, \"123\": {\"class_type\": \"SaveImage\", \"inputs\": {\"images\": [\"113\", 0]}}, \"124\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"110\", 0]}}, \"125\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"124\", 0]}}, \"126\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"124\", 0]}}, \"127\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"111\", 0]}}, \"128\": {\"class_type\": \"PreviewAny\", \"inputs\": {\"images\": [\"127\", 0]}}, \"131\": {\"class_type\": \"PromptManagerText\", \"inputs\": {\"text\": \" fox\", \"prepend_text\": \"\\ndetailed fur…\", \"append_text\": \" \\u001c\"}}, \"130\": {\"class_type\": \"CLIPTextEncodeSDXL\", \"inputs\": {\"text_g\": [\"131\", 0], \"text_l\": [\"131\", 0]}}, \"129\": {\"class_type\": \"UltimateSDUpscale\", \"inputs\": {\"image\": [\"127\", 0], \"model\": [\"4\", 0], \"positive\": [\"101\", 0], \"negative\": [\"130\", 0]}}, \"132\": {\"class_type\": \"PreviewImage\", \"inputs\": {\"images\": [\"129\", 0]}}}"}}], "version": 2} diff --git a/tests/js/gallery-thumbnails.test.js b/tests/js/gallery-thumbnails.test.js index 431beea..5b085f2 100644 --- a/tests/js/gallery-thumbnails.test.js +++ b/tests/js/gallery-thumbnails.test.js @@ -73,6 +73,28 @@ test('Not Now persists the collection state and suppresses the unchanged prompt' assert.equal(f.prompts.length, 1); }); +// WebKit fails a fetch a leaving page starts ("Fetch API cannot load … due to access control checks.") and logs it +// as a page error: the delayed startup check starts no read once the page has begun to leave (as admin.js, #290). +test('the startup check starts no read once the page is leaving', async () => { + const f = fixture({ success: true, total_images: 100, pending: 30, collection_id: 'a' }); + f.manager.pageLifetime = new AbortController(); + f.manager.pageLifetime.abort(); + await f.check(); + assert.equal(f.requests.length, 0); + assert.equal(f.prompts.length, 0); +}); + +test('the startup check passes the page lifetime to its read', async () => { + const f = fixture({ success: true, total_images: 100, pending: 30, collection_id: 'a' }); + const signals = []; + f.manager.pageLifetime = new AbortController(); + const get = f.manager.api.get; + f.manager.api.get = async (url, options) => { signals.push(options?.signal); return get(url); }; + await f.check(); + assert.deepEqual(signals, [f.manager.pageLifetime.signal]); + assert.equal(f.prompts.length, 1); +}); + test('disabled checks do not request collection status', async () => { const f = fixture({}, { checkThumbnailsAtStartup: false }); await f.check(); diff --git a/tests/js/legacy-refusals.test.js b/tests/js/legacy-refusals.test.js new file mode 100644 index 0000000..6daec1c --- /dev/null +++ b/tests/js/legacy-refusals.test.js @@ -0,0 +1,150 @@ +// The legacy admin and gallery pages show a server refusal's reason (#220, PR #289 review I1 and M8): a job that +// runs in the workspace refuses their record-changing requests, and a corrupt config.json refuses a settings save. +// Each page must name the reason, not report a lost connection, a bare HTTP status or a success. +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); + +function elements() { + const nodes = {}; + return { + nodes, + getElementById: id => nodes[id] ||= { id, textContent: '', innerHTML: '', disabled: false, style: {}, + value: '', checked: false, addEventListener() {}, querySelectorAll: () => [] }, + }; +} + +function jsonResponse(status, body) { + return { ok: status >= 200 && status < 300, status, statusText: status === 409 ? 'Conflict' : 'OK', + json: async () => body }; +} + +function sseResponse(frames) { + const bytes = new TextEncoder().encode(frames.map(frame => `data: ${JSON.stringify(frame)}\n\n`).join('')); + let sent = false; + return { ok: true, status: 200, body: { getReader: () => ({ + read: async () => (sent ? { done: true } : (sent = true, { done: false, value: bytes })), + }) } }; +} + +function load(file, className, fetchUrl, extra = {}, dom = elements()) { + const timers = []; + const notifications = []; + const storage = new Map([['pm_cached', '1'], ['userData', 'keep']]); + const localStorage = { + getItem: key => storage.get(key) ?? null, + setItem: (key, value) => storage.set(key, String(value)), + removeItem: key => storage.delete(key), + }; + const context = vm.createContext({ + document: { readyState: 'loading', addEventListener() {}, getElementById: dom.getElementById, + querySelectorAll: () => [] }, + window: {}, console: { error() {}, warn() {}, debug() {} }, + setTimeout: callback => timers.push(callback), + confirm: () => true, + localStorage, + Object: { ...Object, keys: target => (target === localStorage ? [...storage.keys()] : Object.keys(target)) }, + TextDecoder, TextEncoder, JSON, + ApiClient: { fetchUrl }, + ...extra, + }); + const source = fs.readFileSync(path.join(__dirname, `../../web/js/${file}`), 'utf8'); + const Page = vm.runInContext(`${source}\n${className};`, context, { filename: path.join(__dirname, `../../web/js/${file}`) }); + const page = Object.create(Page.prototype); + page.showNotification = (message, type) => notifications.push([type, message]); + page.showModal = id => notifications.push(['show', id]); + page.hideModal = id => notifications.push(['hide', id]); + return { page, dom, timers, notifications, storage }; +} + +const BUSY = 'Scan for metadata already running'; + +test('gallery cache clear reports a refused server clear and never claims success', async () => { + const calls = []; + const { page, dom, notifications, storage } = load('gallery.js', 'GalleryManager', async url => { + calls.push(url); + return jsonResponse(409, { success: false, error: BUSY }); + }); + page.getSettings = () => ({ thumbnailsGenerated: true }); + dom.getElementById('thumbnailStatus').textContent = 'Generated'; + + await page.clearCache(); + + assert.deepEqual(calls, ['/prompt_manager/images/clear-thumbnails']); + assert.deepEqual(notifications, [['error', `Thumbnails were not cleared: ${BUSY}`]]); + assert.equal(dom.nodes.thumbnailStatus.textContent, 'Generated'); + assert.equal(storage.get('gallerySettings'), undefined); // thumbnailsGenerated is not reset +}); + +test('gallery cache clear still says so when the server clears', async () => { + const { page, dom, notifications, storage } = load('gallery.js', 'GalleryManager', + async () => jsonResponse(200, { success: true, cleared_count: 3 })); + page.getSettings = () => ({ thumbnailsGenerated: true }); + + await page.clearCache(); + + assert.deepEqual(notifications, [['success', 'PromptManager cache cleared successfully! Original images untouched.']]); + assert.equal(dom.nodes.thumbnailStatus.textContent, 'Not generated'); + assert.equal(JSON.parse(storage.get('gallerySettings')).thumbnailsGenerated, false); + assert.equal(storage.get('userData'), 'keep'); + assert.equal(storage.has('pm_cached'), false); +}); + +test('admin LoRA import shows a refusal frame and closes its modal', async () => { + const { page, dom, timers, notifications } = load('admin.js', 'PromptAdmin', + async () => sseResponse([{ type: 'error', status: BUSY, progress: 0, message: BUSY }])); + page.saveLoraSettings = async () => true; + page.loadStatistics = () => assert.fail('a refused import must not reload statistics'); + + await page.runLoraImport(); + + assert.equal(dom.nodes.loraImportStatus.textContent, `Error: ${BUSY}`); + assert.deepEqual(notifications.filter(([type]) => type === 'error'), [['error', `LoRA import failed: ${BUSY}`]]); + assert.equal(timers.length, 1); + timers[0](); + assert.deepEqual(notifications.at(-1), ['hide', 'loraImportModal']); +}); + +test('admin settings save shows why config.json refused it', async () => { + const message = 'config.json cannot be read, so nothing was saved. Fix or remove the file, then save again.'; + const { page, notifications } = load('admin.js', 'PromptAdmin', + async () => jsonResponse(409, { success: false, persisted: false, code: 'config-unreadable', error: message })); + page.settings = {}; + page._collectScanPaths = () => []; + page.saveLoraSettings = async () => assert.fail('nothing else is saved after a refusal'); + + await page.saveSettings(); + + assert.deepEqual(notifications, [['error', `Failed to save settings: ${message}`]]); +}); + +test('admin settings save keeps its plain message when the reply has no reason', async () => { + const { page, notifications } = load('admin.js', 'PromptAdmin', async () => ({ ok: false, status: 500, + json: async () => { throw new SyntaxError('not json'); } })); + page.settings = {}; + page._collectScanPaths = () => []; + + await page.saveSettings(); + + assert.deepEqual(notifications, [['error', 'Failed to save settings']]); +}); + +test('admin maintenance names the job that refused it', async () => { + const { page, dom, notifications } = load('admin.js', 'PromptAdmin', + async () => jsonResponse(409, { success: false, error: BUSY }), { + escapeHtml: text => String(text), + document: { readyState: 'loading', addEventListener() {}, getElementById: id => dom.getElementById(id), + querySelectorAll: selector => (selector === '.maintenance-option:checked' ? [{ id: 'vacuum' }] : []) }, + }); + + await page.runMaintenance(); + + const results = dom.nodes.maintenanceResults.innerHTML; + assert.match(results, /Maintenance Failed/); + assert.match(results, new RegExp(BUSY)); + assert.doesNotMatch(results, /HTTP 409/); + assert.deepEqual(notifications, [['error', '❌ Maintenance failed']]); + assert.equal(dom.nodes.runMaintenanceBtn.disabled, false); +}); diff --git a/tests/js/support/local-inspect-oracle.mjs b/tests/js/support/local-inspect-oracle.mjs index 61711ce..c85d1d1 100644 --- a/tests/js/support/local-inspect-oracle.mjs +++ b/tests/js/support/local-inspect-oracle.mjs @@ -14,6 +14,11 @@ // the carrier rule alone, as the server selects from ffprobe's tag map. // A {records} entry (records null = an unreadable file) answers // {classified: null, result: null, metadata} for the projection alone. +// A {mode: 'resolve'} entry (PR L3) answers {evidence}: resolveLocalEvidence of its {records} texts +// with the repository semantics table. With a {path} it reads the file locally and also answers +// {facts} (and {unreadable: true} with the server's unreadable evidence when nothing could be read): +// extractGenerationFacts on the local payload through projectInspectionPayload, exactly as +// tests/js/support/inspection-parity-facts.mjs reads the server's response for vectors.json. import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; @@ -28,6 +33,22 @@ const web = path.join(here, '..', '..', '..', 'web', 'js', 'workspace'); const { selectVideoCarrier } = await import(moduleUrl(path.join(web, 'local-video.mjs'))); const { projectMetadata } = await import(moduleUrl(path.join(web, 'local-projection.mjs'))); const { readLocalFile, toInspectionRead } = await import(moduleUrl(path.join(web, 'local-inspection.mjs'))); +const { resolveLocalEvidence, compileSemantics, unreadableEvidence } = await import(moduleUrl(path.join(web, 'local-resolver.mjs'))); +const { buildLocalPayload } = await import(moduleUrl(path.join(web, 'local-inspection.mjs'))); +const { projectInspectionPayload } = await import(moduleUrl(path.join(web, 'inspection-state.mjs'))); +const semantics = compileSemantics(JSON.parse(fs.readFileSync(path.join(here, '..', '..', '..', 'web', 'js', 'inspection-semantics.json'), 'utf8'))); +const { extractGenerationFacts } = require(path.join(here, '..', '..', '..', 'web', 'js', 'comfy-metadata.js')); + +/** The local evidence for one set of record texts. */ +const resolved = records => ({ evidence: resolveLocalEvidence(records, semantics) }); + +/** The local wire's evidence and six facts for one read (no legacy fill, as in the vectors' own harness). */ +function localPayload(read, file, classification) { + const payload = buildLocalPayload({ read, file, classification, semantics }); + const record = projectInspectionPayload(payload, { kind: 'local-file', key: 'local:1', path: file.name }, null, extractGenerationFacts); + const facts = Object.fromEntries(Object.entries(record.generation).map(([field, fact]) => [field, { status: fact.status, value: fact.value }])); + return { evidence: { prompts: payload.prompts, resolution: payload.resolution }, facts }; +} const selectionRead = (selection) => selection.status === 'complete' ? { records: selection.records, record_formats: selection.record_formats, carrierText: selection.carrierText, limited: selection.limited } : null; @@ -37,6 +58,19 @@ for await (const chunk of process.stdin) chunks.push(chunk); const output = {}; for (const entry of JSON.parse(Buffer.concat(chunks).toString('utf8'))) { const { name } = entry; + if (entry.mode === 'resolve') { + if (entry.records !== undefined) { output[name] = resolved(entry.records); continue; } + const blob = await fs.openAsBlob(entry.path); + const { classification, result } = await readLocalFile(blob, { pngMetadata }); + const read = classification === null ? null : toInspectionRead(classification, result).read; + if (classification === null) { output[name] = { unreadable: true }; continue; } + // The evidence is the local wire's own (buildLocalPayload), and it equals resolveLocalEvidence of the records. + const local = localPayload(result, { name: path.basename(entry.path), size: blob.size }, classification); + const direct = read === null ? unreadableEvidence() : resolved(read.records).evidence; + if (JSON.stringify(direct) !== JSON.stringify(local.evidence)) throw new Error(`${name}: the wire's roles are not the resolver's`); + output[name] = read === null ? { unreadable: true, ...local } : local; + continue; + } if (entry.records !== undefined) { const read = entry.records === null ? null : { records: entry.records, diff --git a/tests/js/workspace-activity-store.test.js b/tests/js/workspace-activity-store.test.js index a562fec..5fce2cd 100644 --- a/tests/js/workspace-activity-store.test.js +++ b/tests/js/workspace-activity-store.test.js @@ -261,8 +261,13 @@ test('summaries are validated and the copy follows the spec', async () => { const loose = parseJob({ id: ID_A, state: 'running', progress: 140.4, processed: -1, total: 1.5, counters: { added: 'x', errors: 2 }, title: ' ', started_at: 'x', scope: [] }); assert.deepEqual(loose, { id: ID_A, kind: '', title: 'Background job', state: 'running', started_at: null, finished_at: null, - progress: 100, status_line: '', processed: 0, total: 0, counters: { added: 0, errors: 2 }, scope: {}, listed: {}, log_length: 0 }); + progress: 100, status_line: '', processed: 0, total: 0, counters: { added: 0, errors: 2 }, scope: {}, listed: {}, log_length: 0, + cancellable: true }); assert.equal(parseJob(summary({ progress: -3 })).progress, 0); + // A held legacy job cannot be cancelled; an older server sends no key, which keeps Cancel. + assert.equal(parseJob(summary({ cancellable: false })).cancellable, false); + assert.equal(parseJob(summary({ cancellable: true })).cancellable, true); + assert.equal(parseJob(summary()).cancellable, true); const scan = parseJob(summary({ progress: 42, processed: 8240, total: 19622, counters: { added: 1, errors: 1 } })); assert.deepEqual(pillParts(scan), { label: 'Scanning · 42%', files: ' · 8,240 / 19,622', text: 'Scanning · 42% · 8,240 / 19,622' }); diff --git a/tests/js/workspace-file-inspector.test.js b/tests/js/workspace-file-inspector.test.js index cb722ff..6371f59 100644 --- a/tests/js/workspace-file-inspector.test.js +++ b/tests/js/workspace-file-inspector.test.js @@ -215,3 +215,151 @@ test('none and unavailable render no image chips', async () => { assert.deepEqual(inspectorTagChips(tagged({state, tags:[]}, {status:'none', tags:[], tags_scope:null}), {filterable:true}), []); } }); + +// LI-4 (D117): "Copy settings summary" after the six facts of the full inspector consolidates the legacy metadata +// page's COPY ALL (web/metadata.html copyAllData). The legacy template is read from that page itself and run on the +// legacy parse, so the summary's text is the legacy page's, line for line, wherever the two agree on the values. +const METADATA_PAGE = fs.readFileSync(new URL('../../web/metadata.html', import.meta.url), 'utf8'); +function legacyCopyAll(params) { + const template = METADATA_PAGE.match(/const allData = `([\s\S]*?)`;/)[1]; + return new Function('currentWorkflowData', `return \`${template}\`;`)(params); +} +const legacyParams = record => ComfyMetadata.extractGenerationParams({prompt:record.metadata.prompt_graph.status === 'parsed' ? record.metadata.prompt_graph.value : null, + workflow:record.metadata.workflow_graph.status === 'parsed' ? record.metadata.workflow_graph.value : null}); + +/** + * Each summary line against the legacy page's, allowing only the recorded differences (D117): a number keeps the + * inspector's (the server's) spelling ("8.0" where the legacy page wrote 8); a fact the legacy parse left "Unknown" may + * be known; and a negative the #249 ZeroOut guard zeroed reads "No negative prompt found" where the legacy parse read + * the positive's text through the zero-out. + */ +function assertLegacySummary(summary, legacy, record) { + const lines = summary.split('\n'), expected = legacy.split('\n'); + assert.deepEqual(lines.map(line => line.split(':')[0]), expected.map(line => line.split(':')[0]), 'the legacy labels, in order'); + lines.forEach((line, index) => { + if (line === expected[index]) return; + const [label, value] = [line.slice(0, line.indexOf(': ')), line.slice(line.indexOf(': ') + 2)]; + const old = expected[index].slice(label.length + 2); + if (label === 'Negative Prompt' && record.prompts.negative?.source === 'zeroed-conditioning' && value === 'No negative prompt found') return; + if (old === 'Unknown' && !/Prompt$/.test(label)) return; + assert.ok(value.trim() !== '' && Number(value) === Number(old), `${label}: "${value}" differs from the legacy "${old}"`); + }); +} + +test('settings summary is the legacy COPY ALL text for a local file', async () => { + const {settingsSummary} = await load(); + assert.equal(typeof settingsSummary, 'function', 'settingsSummary is exported'); + const record = await localRecord(localPng('fox.png')); + assertLegacySummary(settingsSummary(record), legacyCopyAll(legacyParams(record)), record); + assert.equal(settingsSummary(record), ['Checkpoint: synthetic-v1.safetensors', 'Positive Prompt: a red fox in snow, detailed fur', + 'Negative Prompt: blurry, lowres', 'Steps: 20', 'CFG Scale: 8.0', 'Sampler: euler', 'Seed: 7'].join('\n')); + // Every parity PNG read in this browser: the legacy page's text but for the recorded differences, and exactly the + // legacy text for most of them (f09 among them: the server is ambiguous, so the record shows the legacy fill). + const exact = []; + for (const name of fs.readdirSync(PARITY).filter(item => item.endsWith('.png')).sort()) { + const parity = await localRecord(new File([fs.readFileSync(new URL(name, PARITY))], name)); + const summary = settingsSummary(parity), legacy = legacyCopyAll(legacyParams(parity)); + assertLegacySummary(summary, legacy, parity); + if (summary === legacy) exact.push(name); + } + assert.ok(exact.includes('f09.png')); + assert.deepEqual(fs.readdirSync(PARITY).filter(item => item.endsWith('.png') && !exact.includes(item)).sort(), ['f01b.png', 'f17.png', 'f23.png']); +}); + +test('settings summary uses the legacy placeholders for what the record does not know', async () => { + const {settingsSummary} = await load(); + const unknown = {...record(), prompts:{positive:{status:'unresolved'}, negative:{status:'ambiguous'}}, generation:{}}; + assert.equal(settingsSummary(unknown), legacyCopyAll({}).replace('Positive Prompt: undefined', 'Positive Prompt: No prompt found') + .replace('Negative Prompt: undefined', 'Negative Prompt: No negative prompt found')); + assert.equal(settingsSummary(unknown), ['Checkpoint: Unknown', 'Positive Prompt: No prompt found', 'Negative Prompt: No negative prompt found', + 'Steps: Unknown', 'CFG Scale: Unknown', 'Sampler: Unknown', 'Seed: Unknown'].join('\n')); + // A server answer is shown as the inspector shows it: known facts verbatim, every other state the legacy "Unknown". + const server = {...record(), prompts:{positive:{status:'resolved', text:'a\n猫'}, negative:{status:'resolved', text:''}}, generation:{ + model:{status:'known', value:'m.safetensors'}, steps:{status:'not-recorded', value:null}, cfg:{status:'known', value:'6.50'}, + sampler:{status:'ambiguous', value:null}, seed:{status:'known', value:'9007199254740993'}, scheduler:{status:'known', value:'karras'}}}; + assert.equal(settingsSummary(server), ['Checkpoint: m.safetensors', 'Positive Prompt: a\n猫', 'Negative Prompt: No negative prompt found', + 'Steps: Unknown', 'CFG Scale: 6.50', 'Sampler: Unknown', 'Seed: 9007199254740993'].join('\n')); +}); + +/** The full inspector with a toast node and `clipboard` as `navigator.clipboard`; returns the summary button. */ +function renderSummary(record, {variant = 'full', clipboard} = {}) { + const f = mountedDom(), scope = createScope(), fallbacks = [], toast = new f.Node('div'); + f.doc.getElementById = id => id === 'workspace-toast' ? toast : null; + const previous = Object.getOwnPropertyDescriptor(globalThis, 'navigator'); + Object.defineProperty(globalThis, 'navigator', {configurable:true, value:clipboard === undefined ? {} : {clipboard}}); + const view = mountFileInspector(f.container, {sender:{open() {}}, parentScope:scope, variant, copyFallback:(text, options) => fallbacks.push({text, ...options})}); + view.setState({loading:false, error:false, record}); + const button = f.walk(f.container, n => n.tagName === 'button' && n.textContent === 'Copy settings summary')[0] ?? null; + return {f, button, toast, fallbacks, scope, done() { + view.dispose(); scope.dispose(); f.restore(); + if (previous) Object.defineProperty(globalThis, 'navigator', previous); else delete globalThis.navigator; + }}; +} + +test('Copy settings summary follows the facts in the full inspector only', async () => { + const full = renderSummary(record()); + try { + assert.ok(full.button, 'the full inspector offers Copy settings summary'); + const inner = full.button.parent, index = inner.children.indexOf(full.button); + assert.equal(inner.children[index - 1].tagName, 'dl', 'it follows the six facts'); + assert.equal(inner.children[index + 1].tagName, 'details', 'File & provenance follows it'); + // M7: the D117 wrap rule targets this button by its class, never any button of a details block. + assert.equal(full.button.className, 'settings-summary'); + } finally { full.done(); } + const panel = renderSummary(record(), {variant:'panel'}); + try { assert.equal(panel.button, null, 'the Images side panel keeps the concept footer'); } finally { panel.done(); } +}); + +test('Copy settings summary copies the summary and shows the shared toast', async () => { + const {settingsSummary} = await load(), copied = []; + const view = renderSummary(record(), {clipboard:{writeText:async text => {copied.push(text);}}}); + try { + await view.button.listeners.get('click')[0].callback({target:view.button}); + assert.deepEqual(copied, [settingsSummary(record())]); + assert.equal(view.toast.textContent, 'Settings summary copied.'); + assert.equal(view.fallbacks.length, 0); + } finally { view.done(); } +}); + +test('Copy settings summary without a clipboard opens the select-to-copy fallback', async () => { + const {settingsSummary} = await load(); + for (const clipboard of [undefined, {writeText:async () => {throw new Error('Denied');}}]) { + const view = renderSummary(record(), {clipboard}); + try { + await view.button.listeners.get('click')[0].callback({target:view.button}); + assert.equal(view.fallbacks.length, 1); + assert.equal(view.fallbacks[0].text, settingsSummary(record())); + assert.equal(view.fallbacks[0].title, 'Copy settings summary'); + assert.equal(view.fallbacks[0].trigger, view.button); + assert.notEqual(view.toast.textContent, 'Settings summary copied.'); + } finally { view.done(); } + } +}); + +test('the inspector is busy while a read is loading, then not', async () => { + const f = mountedDom(), scope = createScope(); + const view = mountFileInspector(f.container, {sender:{open() {}}, parentScope:scope, variant:'full'}); + try { + view.setState({loading:true, error:false, record:null}); + assert.equal(f.container.getAttribute('aria-busy'), 'true'); + assert.ok(f.walk(f.container, n => n.textContent === 'Loading file details…').length); + view.setState({loading:false, error:false, record:record()}); + assert.equal(f.container.getAttribute('aria-busy'), null); + view.setState({loading:true, error:false, record:null}); + view.setState({loading:false, error:true, record:null}); + assert.equal(f.container.getAttribute('aria-busy'), null); + view.setState({loading:true, error:false, record:null}); + } finally { + view.dispose(); + assert.equal(f.container.getAttribute('aria-busy'), null, 'dispose clears the busy state'); + scope.dispose(); f.restore(); + } +}); + +test('the D117 wrap rule targets only the settings summary button', async () => { + const fs = await import('node:fs'); + const css = fs.readFileSync(new URL('../../web/js/workspace/workspace.css', import.meta.url), 'utf8'); + const rules = css.split('\n').filter(line => line.includes('D117')); + assert.deepEqual(rules.map(line => line.trim()), ['.ca-tools #viewer-details .settings-summary { white-space: normal; /* D117 */ }']); + assert.doesNotMatch(css, /#viewer-details > div > button/); +}); diff --git a/tests/js/workspace-local-inspection.test.js b/tests/js/workspace-local-inspection.test.js index 0769055..2994f9e 100644 --- a/tests/js/workspace-local-inspection.test.js +++ b/tests/js/workspace-local-inspection.test.js @@ -20,6 +20,16 @@ const { extractGenerationFacts } = ComfyMetadata; // The page loads both as scripts; the legacy fill and the PNG reader find them on the global. globalThis.PngMetadata = PngMetadata; globalThis.ComfyMetadata = ComfyMetadata; +// The page's API client and the node table it serves (local-semantics.mjs fetches it once). +const SEMANTICS = fs.readFileSync(path.join(here, "..", "..", "web", "js", "inspection-semantics.json"), "utf8"); +globalThis.window = { ApiClient: { resolveUrl: (value) => value } }; +// A test can hold the table request (`tableGate`) to stand for a slow or stalled proxy. +let tableGate = null; +globalThis.fetch = async (url) => { + if (url !== "/prompt_manager/js/inspection-semantics.json") throw new Error(`unexpected request ${url}`); + if (tableGate) await tableGate; + return new Response(SEMANTICS, { status: 200 }); +}; const m = await import("../../web/js/workspace/local-inspection.mjs").catch((error) => { if (error.code !== "ERR_MODULE_NOT_FOUND") throw error; return {}; @@ -59,6 +69,37 @@ test("contract", () => { assert.ok(Object.isFrozen(m.localFileAdapter)); }); +// M3: the first read waits for the page's one table request; an aborted, replaced or cleared selection must not. +test("a read waiting for the node table ends with its selection", async () => { + let release; + tableGate = new Promise((resolve) => { release = resolve; }); + try { + const file = pngFile("slow.png", [tEXt("prompt", PROMPT)]); + const aborted = new AbortController(); + const reading = m.localFileAdapter.read(await handleFor(file), { signal: aborted.signal }); + const settled = (promise) => Promise.race([promise.then(() => "resolved", (error) => error.name), new Promise((resolve) => setTimeout(() => resolve("pending"), 200))]); + await new Promise((resolve) => setTimeout(resolve, 20)); + aborted.abort(); + assert.equal(await settled(reading), "AbortError"); + // Through the controller: clearing the selection ends its read; nothing is published for it. + const controller = createInspectionController({ api: null, adapter: m.localFileAdapter, extractFacts: extractGenerationFacts }); + const states = []; + controller.subscribe((state) => states.push(state)); + const selected = controller.select(await handleFor(file)); + await new Promise((resolve) => setTimeout(resolve, 20)); + controller.clear(); + assert.equal(await settled(selected), "resolved"); + assert.ok(states.every((state) => state.record === null && state.error === false), "no record or error for a cleared selection"); + controller.dispose(); + } finally { + tableGate = null; + release(); + } + // The shared request still lands for the next selection. + const { wire } = await recordFor(pngFile("next.png", [tEXt("prompt", PROMPT)])); + assert.equal(wire.resolution.state, "resolved"); +}); + test("a PNG File becomes the server-shaped wire with local provenance", async () => { const file = pngFile("Fox Render.PNG", [tEXt("prompt", PROMPT), tEXt("workflow", WORKFLOW)]); const { wire, record, handle } = await recordFor(file); @@ -66,8 +107,12 @@ test("a PNG File becomes the server-shaped wire with local provenance", async () size: file.size, width: 640, height: 480, modified_time: 1_700_000_000.123 }); assert.equal(wire.success, true); assert.equal(wire.inspection_version, 1); - assert.deepEqual(wire.prompts, { positive: { status: "unresolved" }, negative: { status: "unresolved" } }); - assert.deepEqual(wire.resolution, { state: "unresolved", sampler_id: null, reason: "Resolved in this browser from the workflow.", reason_code: null, node: null }); + // The roles the server's inspector resolves for the same records (local-resolver.mjs). + assert.deepEqual(wire.prompts, { + positive: { status: "resolved", text: "a red fox in snow, detailed fur", source: "graph-inferred" }, + negative: { status: "resolved", text: "blurry, lowres", source: "graph-inferred" }, + }); + assert.deepEqual(wire.resolution, { state: "resolved", sampler_id: "3", reason: "", reason_code: null, node: null }); assert.deepEqual(wire.library, { status: "unavailable", scope: "saved-library", tags: [], category_scope: null, tags_scope: null }); assert.equal(wire.metadata.status, "available"); assert.deepEqual(wire.metadata.raw, { prompt: PROMPT, workflow: WORKFLOW }); @@ -96,6 +141,33 @@ test("a PNG File becomes the server-shaped wire with local provenance", async () assert.equal(exported.file.source_label, "Selected local file"); }); +test("roles answer as the server's inspector: unreadable, unavailable table, ambiguous", async () => { + const png = { media: "image", kind: "png" }; + const unreadable = m.buildLocalPayload({ read: null, file: { name: "a.png" }, classification: png }); + assert.deepEqual(unreadable.resolution, { state: "unresolved", sampler_id: null, reason: "Media metadata could not be read", reason_code: null, node: null }); + assert.equal(unreadable.prompts.positive.message, "Metadata in this file could not be read."); + const records = { prompt: PROMPT }; + const read = { status: "complete", records, limited: [], width: 2, height: 3 }; + // Without a table (it could not be fetched) the roles stay open, with the server's own line. + const missing = m.buildLocalPayload({ read, file: { name: "a.png" }, classification: png }); + assert.equal(missing.resolution.reason, "Inspection node semantics are unavailable"); + assert.equal(missing.prompts.positive.message, "The inspector's node table is unavailable."); + const semantics = await (await import("../../web/js/workspace/local-semantics.mjs")).loadInspectionSemantics(); + const graph = JSON.parse(PROMPT); + graph["9"] = { ...graph["3"], inputs: { ...graph["3"].inputs, positive: ["8", 0] } }; + graph["8"] = { class_type: "CLIPTextEncode", inputs: { text: "a cat", clip: ["4", 1] } }; + const several = m.buildLocalPayload({ read: { ...read, records: { prompt: JSON.stringify(graph) } }, file: { name: "a.png" }, classification: png, semantics }); + assert.equal(several.resolution.reason, "Multiple sampler branches"); + assert.deepEqual(several.prompts.positive, { status: "ambiguous", text: null, source: null }); + // The legacy fill then applies exactly as it does to the server's answer for the same bytes in Images. + const handle = await handleFor(pngFile("grid.png", [tEXt("prompt", JSON.stringify(graph))])); + const local = m.localFileAdapter.toRecord(several, handle, extractGenerationFacts); + const asServed = { ...several, file: { ...several.file, observation: "current-file", source_label: "output" } }; + const served = projectInspectionPayload(asServed, { kind: "rooted-file", key: "0:grid.png", root_index: 0, path: "grid.png", root_scope: null }, null, extractGenerationFacts); + assert.deepEqual(local.prompts, served.prompts); + assert.deepEqual(local.generation, served.generation); +}); + test("file facts follow the server: extension like Path.suffix, null when unknown", async () => { const cases = [["a.b.PnG", ".png"], [".hidden", ""], ["noext", ""], ["dot.", ""], ["x.tar.gz", ".gz"]]; for (const [name, extension] of cases) { @@ -307,3 +379,89 @@ test("the parity corpus PNGs read into renderable records", async () => { assert.ok(["parsed", "limited", "malformed"].includes(wire.metadata.prompt_graph.status)); } }); + +// M4: resolution runs on the page's main thread, so a read yields between its bounded steps (the busy state paints +// and input is handled) and no step may exceed a budget on the largest record the readers keep (2 MiB of roles). +const ROLE_BYTES = 2 * 1024 * 1024; +// Generous for a shared CI runner: a desktop resolves these in 130-455 ms. +const STEP_BUDGET_MS = 2500; +const ENCODE = (text) => ({ class_type: "CLIPTextEncode", inputs: { text, clip: ["4", 1] } }); +const KSAMPLER = (positive, negative, latent) => ({ class_type: "KSampler", inputs: { seed: 1, steps: 20, cfg: 8.0, sampler_name: "euler", scheduler: "normal", + denoise: 1.0, model: ["4", 0], positive, negative, latent_image: latent } }); +const LOADERS = { "4": { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "m.safetensors" } }, "5": { class_type: "EmptyLatentImage", inputs: { width: 64, height: 64, batch_size: 1 } } }; +/** The largest graph of this shape whose JSON fits the role budget. */ +function maximal(build, high) { + let low = 1; + while (low < high) { + const middle = (low + high + 1) >> 1; + if (Buffer.byteLength(JSON.stringify(build(middle)), "utf8") <= ROLE_BYTES) low = middle; + else high = middle - 1; + } + return JSON.stringify(build(low)); +} +const MAXIMAL_GRAPHS = { + // Many candidates: every sampler is a final stage with its own pair. + independent: maximal((count) => { + const graph = { ...LOADERS }; + for (let index = 0; index < count; index++) { + graph[`s${index}`] = KSAMPLER([`p${index}`, 0], [`n${index}`, 0], ["5", 0]); + graph[`p${index}`] = ENCODE(`fox ${index}`); + graph[`n${index}`] = ENCODE(`blur ${index}`); + } + return graph; + }, 6666), + // One long stage chain through the latent: the deepest stage walk. + chain: maximal((count) => { + const graph = { ...LOADERS, "6": ENCODE("fox"), "7": ENCODE("blur") }; + for (let index = 0; index < count; index++) graph[`s${index}`] = KSAMPLER(["6", 0], ["7", 0], index ? [`s${index - 1}`, 0] : ["5", 0]); + return graph; + }, 19996), +}; + +test("resolution of a maximal 2 MiB graph stays within the step budget", async () => { + const { resolveLocalEvidence } = await import("../../web/js/workspace/local-resolver.mjs"); + const semantics = await (await import("../../web/js/workspace/local-semantics.mjs")).loadInspectionSemantics(); + for (const [name, prompt] of Object.entries(MAXIMAL_GRAPHS)) { + assert.ok(Buffer.byteLength(prompt, "utf8") > ROLE_BYTES - 1024, `${name} fills the role budget`); + const started = performance.now(); + const evidence = resolveLocalEvidence({ prompt }, semantics); + const elapsed = performance.now() - started; + assert.ok(elapsed < STEP_BUDGET_MS, `${name}: ${Math.round(elapsed)} ms`); + assert.notEqual(evidence.resolution.reason_code, "malformed", name); + } +}); + +test("a read yields between bounded steps and answers byte for byte as one synchronous build", async () => { + const semantics = await (await import("../../web/js/workspace/local-semantics.mjs")).loadInspectionSemantics(); + const file = pngFile("maximal.png", [tEXt("prompt", MAXIMAL_GRAPHS.independent)]); + const handle = await handleFor(file); + const marks = []; + const yieldControl = async (signal) => { + signal?.throwIfAborted(); + marks.push(performance.now()); + await new Promise((resolve) => setTimeout(resolve, 0)); + }; + const started = performance.now(); + const wire = await m.localFileAdapter.read(handle, { signal: new AbortController().signal, yieldControl }); + marks.push(performance.now()); + // Resolution and the projection each run in a turn of their own after the table is in. + assert.ok(marks.length >= 3, `yields: ${marks.length - 1}`); + const gaps = marks.map((mark, index) => mark - (index ? marks[index - 1] : started)); + assert.ok(Math.max(...gaps) < STEP_BUDGET_MS, `longest step ${Math.round(Math.max(...gaps))} ms`); + const { result } = await m.readLocalFile(file, { classification: handle.classification }); + const direct = await m.fitLocalPayload(m.buildLocalPayload({ read: result, file: { name: handle.filename, size: file.size, lastModified: file.lastModified }, + classification: handle.classification, semantics })); + assert.equal(JSON.stringify(wire), JSON.stringify(direct)); + assert.equal(wire.resolution.state, "ambiguous"); + // An abort at any yield ends the read there. + for (let stop = 0; stop < marks.length - 1; stop++) { + const controller = new AbortController(); + let seen = 0; + const stopping = async (signal) => { + if (seen++ === stop) controller.abort(); + signal.throwIfAborted(); + await new Promise((resolve) => setTimeout(resolve, 0)); + }; + await assert.rejects(m.localFileAdapter.read(handle, { signal: controller.signal, yieldControl: stopping }), { name: "AbortError" }, `yield ${stop}`); + } +}); diff --git a/tests/js/workspace-local-resolver.test.js b/tests/js/workspace-local-resolver.test.js new file mode 100644 index 0000000..cc9e160 --- /dev/null +++ b/tests/js/workspace-local-resolver.test.js @@ -0,0 +1,161 @@ +// PR L3 Tasks 8-9: the server's inspection resolver in this browser. The values below are what CPython answers +// (json.loads, len, str.strip, repr, utils/prompt_inspector.inspect_prompt_evidence); the full comparison with the +// server runs in tests/test_local_inspect_parity.py through tests/js/support/local-inspect-oracle.mjs. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; + +const py = await import('../../web/js/workspace/py-json.mjs'); +const resolver = await import('../../web/js/workspace/local-resolver.mjs'); +const semantics = resolver.compileSemantics(JSON.parse(fs.readFileSync(new URL('../../web/js/inspection-semantics.json', import.meta.url), 'utf8'))); + +test('json.loads keeps Python value kinds and dict order', () => { + const value = py.pyJsonLoads('{"9": 1, "10": 1.0, "a": NaN, "9": [true, -0, 9007199254740993, -Infinity]}'); + assert.ok(value instanceof Map); + // A repeated key keeps its first position and takes its last value; integer-like keys keep text order. + assert.deepEqual([...value.keys()], ['9', '10', 'a']); + const [flag, zero, big, negative] = value.get('9'); + assert.equal(flag, true); assert.equal(zero, 0); assert.equal(big, 9007199254740993n); + assert.ok(negative instanceof py.PyFloat); assert.equal(negative.value, -Infinity); + assert.ok(value.get('10') instanceof py.PyFloat, '1.0 is a float, not an int'); + assert.ok(Number.isNaN(value.get('a').value)); + assert.equal(py.isInt(value.get('10')), false); + assert.equal(py.pyJsonLoads('"\\ud83d\\ude00\\ud800"'), '\u{1f600}\ud800'); + for (const bad of ['{}', '{"a":1,}', '[01]', '{"a" 1}', '"\u0001"', '"\\x"', '1 2', '-', `${'9'.repeat(4301)}`, '"\\ud800\\uzzzz"']) { + assert.throws(() => py.pyJsonLoads(bad), py.PyValueError, bad); + } + assert.equal(py.pyJsonLoads(` ${'9'.repeat(4300)}\n`), BigInt('9'.repeat(4300))); +}); + +// One depth cap for both browser CPython-JSON parsers (py-json.mjs here, local-video.mjs for video envelopes): every +// array or object counts, an empty one too, as CPython's scanner enters recursion for each; past the cap it refuses. +test('json.loads refuses nesting past the shared depth cap, counting every container', () => { + const video = import('../../web/js/workspace/local-video.mjs'); + assert.equal(py.JSON_MAX_DEPTH, 1000); + const nest = (depth, inner = '') => '['.repeat(depth) + inner + ']'.repeat(depth); + const objects = (depth, inner) => '{"a":'.repeat(depth) + inner + '}'.repeat(depth); + for (const text of [nest(1000), nest(1000, '1'), nest(999, '{}'), objects(1000, '1'), objects(999, '[]')]) assert.doesNotThrow(() => py.pyJsonLoads(text)); + for (const text of [nest(1001), nest(1001, '1'), nest(1000, '{}'), objects(1001, '1'), objects(1000, '[]')]) { + assert.throws(() => py.pyJsonLoads(text), py.PyValueError); + } + return video.then(({ pythonJsonLoads, PythonJsonError }) => { + for (const text of [nest(1000), nest(1000, '1'), objects(999, '[]')]) assert.doesNotThrow(() => pythonJsonLoads(text)); + for (const text of [nest(1001), nest(1001, '1'), objects(1000, '[]')]) { + assert.throws(() => pythonJsonLoads(text), error => error instanceof PythonJsonError && error.recursion === true); + } + }); +}); + +test('str helpers measure, slice and strip as CPython', () => { + assert.equal(py.pyLen('a\u{1f600}\ud800'), 3); + assert.equal(py.pySlice('\u{1f600}\u{1f600}x', 2), '\u{1f600}\u{1f600}'); + // str.isspace() includes \x1c-\x1f and \x85 and excludes , unlike String.prototype.trim. + assert.equal(py.pyStrip('\x1c\x85 a b \x1f'), 'a b'); + assert.equal(py.pyStrip('a'), 'a'); + assert.deepEqual([1.0, 0.0001, 1e-5, 1e16, 123456789012345.6, -2.5, 1e22].map(py.pyFloatRepr), + ['1.0', '0.0001', '1e-05', '1e+16', '123456789012345.6', '-2.5', '1e+22']); + assert.deepEqual([null, true, 0, 1n, new py.PyFloat(2), '', [], new Map()].map(py.pyTruthy), [false, true, false, true, true, false, false, false]); + assert.deepEqual([1, 1n, true, new py.PyFloat(1), '1', 2].map(py.pyEqualsOne), [true, true, true, true, false, false]); +}); + +const encode = text => ({class_type: 'CLIPTextEncode', inputs: {text, clip: ['4', 1]}}); +const sampler = (extra = {}) => ({class_type: 'KSampler', inputs: {seed: 1, positive: ['6', 0], negative: ['7', 0], latent_image: ['5', 0], model: ['4', 0], ...extra}}); +const base = (nodes = {}) => ({'3': sampler(), '4': {class_type: 'CheckpointLoaderSimple', inputs: {ckpt_name: 'm.safetensors'}}, + '5': {class_type: 'EmptyLatentImage', inputs: {width: 64, height: 64, batch_size: 1}}, '6': encode('a fox'), '7': encode('blurry'), ...nodes}); + +test('a single chain resolves both roles with their sources', () => { + const evidence = resolver.resolveLocalEvidence({prompt: JSON.stringify(base())}, semantics); + assert.deepEqual(evidence, { + prompts: {positive: {status: 'resolved', text: 'a fox', source: 'graph-inferred'}, negative: {status: 'resolved', text: 'blurry', source: 'graph-inferred'}}, + resolution: {state: 'resolved', sampler_id: '3', reason: '', reason_code: null, node: null}, + }); +}); + +test('records without roles, malformed text and a missing sampler answer as the server', () => { + assert.deepEqual(resolver.resolveLocalEvidence({workflow: '{}'}, semantics).resolution, + {state: 'absent', sampler_id: null, reason: 'No parsed prompt metadata', reason_code: null, node: null}); + const malformed = resolver.resolveLocalEvidence({prompt: '{broken'}, semantics); + assert.equal(malformed.resolution.reason, 'Prompt evidence is malformed'); + assert.equal(malformed.prompts.positive.message, 'Prompt metadata in this file is malformed.'); + const none = resolver.resolveLocalEvidence({prompt: JSON.stringify({'1': encode('x'), '2': {class_type: 'MySampler', inputs: {positive: ['1', 0]}}})}, semantics); + assert.deepEqual(none.resolution, {state: 'unresolved', sampler_id: null, reason: 'No supported sampler pair found (saw MySampler)', reason_code: 'no-sampler', node: null}); + assert.equal(none.prompts.negative.message, 'No supported sampler was found.'); + assert.equal(resolver.resolveLocalEvidence({prompt: JSON.stringify(base()), parameters: 'x'}, null).resolution.reason, 'Inspection node semantics are unavailable'); +}); + +test('a float output slot is not a link, and a runtime node is named', () => { + const floatSlot = resolver.resolveLocalEvidence({prompt: JSON.stringify(base()).replace('"positive":["6",0]', '"positive":["6",0.0]')}, semantics); + assert.deepEqual(floatSlot.resolution, {state: 'unresolved', sampler_id: '3', reason: 'Expected a typed graph link or text input', reason_code: 'malformed', node: null}); + const runtime = resolver.resolveLocalEvidence({prompt: JSON.stringify(base({'6': {class_type: 'PromptSearchList', inputs: {}}}))}, semantics); + assert.deepEqual(runtime.resolution.node, {id: '6', class_type: 'PromptSearchList'}); + assert.equal(runtime.prompts.positive.message, 'Prompt is chosen at run time (PromptSearchList).'); +}); + +test('execution evidence replaces a PromptManagerPair text when it matches the node inputs', () => { + const pair = {class_type: 'PromptManagerPair', inputs: {text: 'fox', negative_text: 'blur', prepend_text: '', append_text: ''}}; + const record = {text: 'fox', negative_text: 'blur', positive_text: 'fox, executed', negative_effective_text: 'blur', prepend_text: '', append_text: ''}; + const graph = base({'3': sampler({positive: ['8', 0], negative: ['8', 1]}), '8': pair}); + const evidence = resolver.resolveLocalEvidence({prompt: JSON.stringify(graph), comfyassets_prompt_pairs: JSON.stringify({version: 1, nodes: {'8': {records: [record]}}})}, semantics); + assert.deepEqual(evidence.prompts.positive, {status: 'resolved', text: 'fox, executed', source: 'execution'}); + const stale = resolver.resolveLocalEvidence({prompt: JSON.stringify(graph), comfyassets_prompt_pairs: JSON.stringify({version: 1, nodes: {'8': {records: [{...record, text: 'other'}]}}})}, semantics); + assert.equal(stale.resolution.reason, 'Execution evidence does not match recorded node inputs'); +}); + +test('identical sampler branches resolve to their shared pair, differing ones stay ambiguous (#270)', () => { + const shared = resolver.resolveLocalEvidence({prompt: JSON.stringify(base({'9': sampler()}))}, semantics); + assert.deepEqual(shared.resolution, {state: 'resolved', sampler_id: null, reason: 'Every sampler branch uses the same prompt pair', reason_code: null, node: null}); + assert.equal(shared.prompts.positive.text, 'a fox'); + const differ = resolver.resolveLocalEvidence({prompt: JSON.stringify(base({'9': sampler({positive: ['8', 0]}), '8': encode('a cat')}))}, semantics); + assert.deepEqual(differ.resolution, {state: 'ambiguous', sampler_id: null, reason: 'Multiple sampler branches', reason_code: null, node: null}); + assert.equal(differ.prompts.positive.status, 'ambiguous'); +}); + +test('a detailer stage is compared with the sampler in its image ancestry', () => { + const graph = base({'10': {class_type: 'VAEDecode', inputs: {samples: ['3', 0]}}, '8': encode('a cat'), '12': {class_type: 'SaveImage', inputs: {images: ['11', 0]}}, + '11': {class_type: 'FaceDetailer', inputs: {image: ['10', 0], model: ['4', 0], positive: ['8', 0], negative: ['7', 0]}}}); + const evidence = resolver.resolveLocalEvidence({prompt: JSON.stringify(graph)}, semantics); + assert.deepEqual(evidence.resolution, {state: 'ambiguous', sampler_id: '11', reason: 'Sampler stages have different prompts', reason_code: 'multistage-differs', node: null}); + assert.equal(evidence.prompts.positive.message, undefined); +}); + +test('A1111 parameters split on the unique settings footer as Python does', () => { + const read = text => resolver.resolveLocalEvidence({parameters: text}, semantics); + assert.deepEqual(read('a fox\nNegative prompt: blur\nSteps: 20, Sampler: euler').prompts, { + positive: {status: 'resolved', text: 'a fox', source: 'embedded-parameters'}, negative: {status: 'resolved', text: 'blur', source: 'embedded-parameters'}, + }); + // Python's multiline ^ and $ break only at \n: a U+2028 or a lone \r never starts the footer line. + for (const text of ['a fox\u2028Steps: 20, x', 'a fox\rSteps: 20, x']) assert.equal(read(text).resolution.reason, 'A1111 settings footer is not unique'); + // \d is any Unicode decimal digit and \s spans line breaks, as in Python's re. + assert.equal(read('a fox\nSteps: \u0662\u0660, x').resolution.reason, 'Negative prompt delimiter is absent'); + assert.equal(read('a fox\nSteps:\n 20 \n, x').resolution.state, 'absent'); + assert.equal(read('a fox\nSteps: 1, x\nNegative Template: n\nTemplate: t').resolution.reason_code, 'malformed'); + assert.equal(read('\u{1f600}'.repeat(60000) + '\nSteps: 1, x').resolution.state, 'absent', 'the length limit counts code points'); +}); + +test('an unusable semantics table is refused', () => { + for (const doc of [null, {schema: 2}, {schema: 1, samplers: []}]) assert.throws(() => resolver.compileSemantics(doc), resolver.SemanticsError); +}); + +// The committed server answers (tests/fixtures/local-inspect/resolver-vectors.json, written by +// `python3 -m tests.local_resolver_cases --write` and checked current by the Python suite). +test('committed server answers equal the local evidence', () => { + const {vectors} = JSON.parse(fs.readFileSync(new URL('../fixtures/local-inspect/resolver-vectors.json', import.meta.url), 'utf8')); + const canonical = value => Array.isArray(value) ? value.map(canonical) : + value !== null && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) : value; + let compared = 0; + for (const {name, records, evidence} of vectors) { + assert.deepEqual(canonical(resolver.resolveLocalEvidence(records, semantics)), canonical(evidence), name); + compared++; + } + assert.ok(compared > 280, `compared ${compared}`); +}); + +test('a list or object workflow node type is skipped, as on the server (#286)', () => { + const graph = base({'6': {class_type: 'PromptManager', inputs: {text: 'a fox', clip: ['4', 1]}}}); + for (const type of [['PromptManager'], {name: 'PromptManager'}]) { + const evidence = resolver.resolveLocalEvidence({prompt: JSON.stringify(graph), workflow: JSON.stringify({nodes: [{id: 6, type, widgets_values: ['x']}]})}, semantics); + assert.equal(evidence.resolution.state, 'resolved'); + assert.equal(evidence.prompts.positive.status, 'resolved'); + assert.equal(evidence.prompts.positive.text, 'a fox'); + } +}); diff --git a/tests/js/workspace-local-semantics.test.js b/tests/js/workspace-local-semantics.test.js new file mode 100644 index 0000000..f0e657d --- /dev/null +++ b/tests/js/workspace-local-semantics.test.js @@ -0,0 +1,43 @@ +// web/js/workspace/local-semantics.mjs (#249 local Inspect, PR L3 Task 10): the node table local Inspect resolves +// with is the server's own web/js/inspection-semantics.json, fetched once through the API client before a file is +// chosen. A table that cannot be fetched or used answers as the server does when its table is unusable. +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; + +const TABLE = fs.readFileSync(new URL("../../web/js/inspection-semantics.json", import.meta.url), "utf8"); +let instance = 0; +/** A fresh module instance (its cache is per page) with `fetch` and the API client stubbed. */ +async function load(respond) { + const calls = []; + globalThis.window = { ApiClient: { resolveUrl: (path) => `/proxy${path}` } }; + globalThis.fetch = async (url, init) => { calls.push([url, init]); return respond(); }; + const module = await import(`../../web/js/workspace/local-semantics.mjs?instance=${++instance}`); + return { module, calls }; +} +test.after(() => { delete globalThis.window; delete globalThis.fetch; }); + +test("the table is fetched once through the API client and compiled", async () => { + const { module, calls } = await load(() => new Response(TABLE, { status: 200 })); + const [first, second] = await Promise.all([module.loadInspectionSemantics(), module.loadInspectionSemantics()]); + assert.equal(first, second); + assert.ok(first.samplers.has("KSampler")); + assert.equal(await module.loadInspectionSemantics(), first); + assert.deepEqual(calls, [["/proxy/prompt_manager/js/inspection-semantics.json", { credentials: "same-origin" }]]); +}); + +test("a failed or unusable table is null, and a later selection asks again", async () => { + for (const respond of [() => new Response("nope", { status: 404 }), () => new Response("{\"schema\": 2}", { status: 200 }), () => { throw new TypeError("offline"); }]) { + const { module, calls } = await load(respond); + assert.equal(await module.loadInspectionSemantics(), null); + assert.equal(await module.loadInspectionSemantics(), null); + assert.equal(calls.length, 2); + } +}); + +test("without the API client nothing is fetched", async () => { + const { module, calls } = await load(() => new Response(TABLE)); + delete globalThis.window; + assert.equal(await module.loadInspectionSemantics(), null); + assert.deepEqual(calls, []); +}); diff --git a/tests/js/workspace-local-session.test.js b/tests/js/workspace-local-session.test.js index 7749876..36bd64d 100644 --- a/tests/js/workspace-local-session.test.js +++ b/tests/js/workspace-local-session.test.js @@ -193,8 +193,8 @@ test("unclassified input creates no object URL", async () => { // declares a supported type it opens the inspector's unreadable state, with no preview and no URL (ledger ruling). const gone = pngFile("gone.png", "fox", { gate: () => Promise.reject(new DOMException("The file was removed", "NotReadableError")) }); assert.equal(value.select([gone]), "accepted"); - await settle(); - const unreadable = value.controller.state; + // The read yields between its steps (local-inspection.mjs): wait for the answer, not a fixed number of turns. + const unreadable = await recorded(value.controller); assert.ok(unreadable.record, "the unreadable record is published"); assert.deepEqual([value.state.phase, value.state.message, value.state.preview], ["ready", m.LOCAL_SESSION_MESSAGES.unreadable, null]); assert.equal(m.LOCAL_SESSION_MESSAGES.unreadable, "Embedded metadata could not be read."); diff --git a/tests/js/workspace-toolbar.test.js b/tests/js/workspace-toolbar.test.js index d825d6a..91e1c3b 100644 --- a/tests/js/workspace-toolbar.test.js +++ b/tests/js/workspace-toolbar.test.js @@ -521,6 +521,10 @@ test('copy fallback selects the text', async () => { assert.equal(note.className, 'small'); assert.equal(note.textContent, 'Automatic clipboard access requires a secure browser context.'); assert.throws(() => openCopyFallback(42, { scope }), TypeError); + // LI-4 (D117): the settings summary names its own dialog; the select-to-copy body is the same. + openCopyFallback('Seed: 7', { scope, title: 'Copy settings summary' }); + assert.equal(byId(dom, 'modal-title').textContent, 'Copy settings summary'); + assert.equal(byId(dom, 'copy-text').value, 'Seed: 7'); } finally { closeWorkspaceDialog(); scope.dispose(); diff --git a/tests/local_resolver_cases.py b/tests/local_resolver_cases.py new file mode 100644 index 0000000..3880ef4 --- /dev/null +++ b/tests/local_resolver_cases.py @@ -0,0 +1,1405 @@ +"""Record sets for the local resolver parity test (#249 local Inspect, PR L3 Task 8). + +Every case is a dict of record texts as ``read_inspection_file`` returns them +(``prompt``, ``workflow``, ``comfyassets_prompt_pairs``), so the server's +``inspect_prompt_evidence`` and the browser's ``resolveLocalEvidence`` read the +same bytes. ``golden_cases`` names one case per traced strategy, bound and +malformed shape; ``fuzz_cases`` builds coherent single-sampler graphs from the +same node vocabulary, then corrupts some of them. All content is synthetic. +``single_chain`` says, from the server's own sampler table, whether a record +set resolves without candidate selection (the Task 8 subset); +``local_resolver_selection_cases`` holds the candidate-selection cases. +""" + +import argparse +import json +import random +from pathlib import Path + +from utils.inspection_semantics import get_semantics +from utils.prompt_inspector import inspect_prompt_evidence +from utils.prompt_pair_resolver import INSPECTION_PROFILE + +VECTORS_PATH = ( + Path(__file__).resolve().parent + / "fixtures" + / "local-inspect" + / "resolver-vectors.json" +) +# Committed record sets stay small; the large bound cases are built at test time. +MAX_VECTOR_CHARS = 20_000 +COMMITTED_FUZZ = (0, 60) + +SPACES = ["", " ", "\t", "\n", "\x1c", "\x85", " ", "", "
 "] +WORDS = ["fox", "snow", "café", "猫", "\U0001f600", "a,", "detailed fur", ""] + + +def _dump(graph, ensure_ascii=False): + return json.dumps(graph, ensure_ascii=ensure_ascii) + + +def _encode(text, name="CLIPTextEncode"): + return {"class_type": name, "inputs": {"text": text, "clip": ["4", 1]}} + + +def _graph(positive, negative, extra=None, sampler=None): + graph = { + "3": sampler + or { + "class_type": "KSampler", + "inputs": { + "seed": 1, + "model": ["4", 0], + "positive": positive, + "negative": negative, + "latent_image": ["5", 0], + }, + }, + "4": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "m"}}, + "5": {"class_type": "EmptyLatentImage", "inputs": {"width": 64}}, + } + graph.update(extra or {}) + return graph + + +def _pair(text="fox", negative="blur", prepend="", append=""): + return { + "class_type": "PromptManagerPair", + "inputs": { + "text": text, + "negative_text": negative, + "prepend_text": prepend, + "append_text": append, + }, + } + + +def _execution(node_id, **changes): + record = { + "text": "fox", + "negative_text": "blur", + "positive_text": "fox executed", + "negative_effective_text": "blur", + "prepend_text": "", + "append_text": "", + } + record.update(changes) + return json.dumps({"version": 1, "nodes": {node_id: {"records": [record]}}}) + + +def _chain(length, start="6"): + nodes = {start: _encode("deep")} + previous = start + for index in range(length): + node_id = f"r{index}" + nodes[node_id] = {"class_type": "Reroute", "inputs": {"value": [previous, 0]}} + previous = node_id + return nodes, previous + + +def _combine_tree(levels): + """A full ConditioningCombine tree over equal encoders: many distinct visits.""" + nodes, layer = {}, [] + for index in range(2**levels): + nodes[f"e{index}"] = _encode("same") + layer.append(f"e{index}") + level = 0 + while len(layer) > 1: + nxt = [] + for index in range(0, len(layer), 2): + node_id = f"c{level}_{index}" + nodes[node_id] = { + "class_type": "ConditioningCombine", + "inputs": { + "conditioning_1": [layer[index], 0], + "conditioning_2": [layer[index + 1], 0], + }, + } + nxt.append(node_id) + layer, level = nxt, level + 1 + return nodes, layer[0] + + +def golden_cases(): + """Named record sets, one per strategy, bound and malformed shape.""" + cases = {} + + def add(name, graph=None, prompt=None, **records): + cases[name] = {"prompt": prompt if prompt is not None else _dump(graph)} + cases[name].update(records) + + neg = ["7", 0] + base = {"6": _encode("a fox"), "7": _encode("blurry")} + add("encode", _graph(["6", 0], neg, base)) + add("empty-positive", _graph(["6", 0], neg, {**base, "6": _encode(" \x85")})) + add("feff-positive", _graph(["6", 0], neg, {**base, "6": _encode("")})) + add("no-negative", _graph(["6", 0], None, base)) + add( + "negative-missing-input", + None, + prompt=_dump(_graph(["6", 0], neg, base)).replace('"negative": ["7", 0], ', ""), + ) + add( + "float-slot", + None, + prompt=_dump(_graph(["6", 0], neg, base)).replace('["6", 0]', '["6", 0.0]'), + ) + add( + "bool-slot", + None, + prompt=_dump(_graph(["6", 0], neg, base)).replace('["6", 0]', '["6", true]'), + ) + add("int-id", None, prompt=_dump(_graph([6, 0], neg, base))) + add("dangling", _graph(["60", 0], neg, base)) + add("wrong-slot", _graph(["6", 3], neg, base)) + add( + "cycle", + _graph( + ["8", 0], + neg, + {**base, "8": {"class_type": "Reroute", "inputs": {"value": ["8", 0]}}}, + ), + ) + add( + "reroute-ambiguous", + _graph( + ["8", 0], + neg, + { + **base, + "8": { + "class_type": "Reroute", + "inputs": {"value": ["6", 0], "in": ["6", 0]}, + }, + }, + ), + ) + add( + "duplicate-key", + None, + prompt=_dump(_graph(["6", 0], neg, base))[:-1] + + ', "6": ' + + _dump(_encode("last wins")) + + "}", + ) + add( + "nan-input", + None, + prompt=_dump(_graph(["6", 0], neg, base)).replace('"seed": 1', '"seed": NaN'), + ) + add( + "class-type-number", + _graph( + ["6", 0], neg, {**base, "6": {"class_type": 1.5, "inputs": {"text": "x"}}} + ), + ) + add( + "class-type-missing", + _graph(["6", 0], neg, {**base, "6": {"inputs": {"text": "x"}}}), + ) + add( + "inputs-list", + _graph( + ["6", 0], + neg, + {**base, "6": {"class_type": "CLIPTextEncode", "inputs": ["x"]}}, + ), + ) + add("prompt-list", None, prompt="[1, 2]") + add("prompt-empty", None, prompt="{}") + add("prompt-bom", None, prompt="" + _dump(_graph(["6", 0], neg, base))) + add( + "huge-int", + None, + prompt=_dump(_graph(["6", 0], neg, base)).replace( + '"seed": 1', '"seed": ' + "9" * 4301 + ), + ) + add("only-execution", None, prompt=None) + cases["only-execution"] = {"comfyassets_prompt_pairs": _execution("8")} + add( + "no-sampler", + { + "1": _encode("x"), + "2": {"class_type": "XSampler", "inputs": {"positive": ["1", 0]}}, + "9": {"class_type": "YGuider", "inputs": {"guider": ["1", 0]}}, + }, + ) + add("no-sampler-plain", {"1": _encode("x")}) + sdxl = { + "class_type": "CLIPTextEncodeSDXL", + "inputs": {"text_g": "a", "text_l": "a"}, + } + add("sdxl-equal", _graph(["8", 0], neg, {**base, "8": sdxl})) + add( + "sdxl-one", + _graph( + ["8", 0], + neg, + { + **base, + "8": { + "class_type": "CLIPTextEncodeSDXL", + "inputs": {"text_g": "a", "text_l": ""}, + }, + }, + ), + ) + add( + "sdxl-differ", + _graph( + ["8", 0], + neg, + { + **base, + "8": { + "class_type": "CLIPTextEncodeSDXL", + "inputs": {"text_g": "a", "text_l": "b"}, + }, + }, + ), + ) + for flag in (False, True, 1, ["9", 0]): + smz = { + "class_type": "smZ CLIPTextEncode", + "inputs": {"text": "t", "text_g": "g", "text_l": "g", "with_SDXL": flag}, + } + add(f"smz-{json.dumps(flag)}", _graph(["8", 0], neg, {**base, "8": smz})) + add( + "zeroed", + _graph( + ["6", 0], + ["8", 0], + { + **base, + "8": { + "class_type": "ConditioningZeroOut", + "inputs": {"conditioning": ["6", 0]}, + }, + }, + ), + ) + add( + "passthrough", + _graph( + ["8", 0], + neg, + { + **base, + "8": { + "class_type": "FluxGuidance", + "inputs": {"conditioning": ["6", 0], "guidance": 3.5}, + }, + }, + ), + ) + add( + "controlnet-slot1", + _graph( + ["8", 0], + ["8", 1], + { + **base, + "8": { + "class_type": "ControlNetApplyAdvanced", + "inputs": {"positive": ["6", 0], "negative": ["7", 0]}, + }, + }, + ), + ) + add( + "combine-equal", + _graph( + ["8", 0], + neg, + { + **base, + "9": _encode("a fox"), + "8": { + "class_type": "ConditioningCombine", + "inputs": {"conditioning_1": ["6", 0], "conditioning_2": ["9", 0]}, + }, + }, + ), + ) + add( + "combine-differ", + _graph( + ["8", 0], + neg, + { + **base, + "8": { + "class_type": "ConditioningCombine", + "inputs": {"conditioning_1": ["6", 0], "conditioning_2": ["7", 0]}, + }, + }, + ), + ) + for condition in (True, False, ["9", 0], 1, "true"): + select = { + "class_type": "easy ifElse", + "inputs": {"boolean": condition, "on_true": ["6", 0], "on_false": ["7", 0]}, + } + add( + f"select-{json.dumps(condition)}", + _graph( + ["8", 0], + ["10", 0], + { + **base, + "8": select, + "10": _encode("neg"), + "9": {"class_type": "PrimitiveBoolean", "inputs": {"value": True}}, + }, + ), + ) + same = { + "class_type": "easy ifElse", + "inputs": {"boolean": ["9", 0], "on_true": ["6", 0], "on_false": ["11", 0]}, + } + add( + "select-converge", + _graph(["8", 0], neg, {**base, "8": same, "11": _encode("a fox")}), + ) + # Two selectors on one condition: forced together, so two candidates, not four. + add( + "select-shared", + _graph( + ["8", 0], + ["12", 0], + { + **base, + "8": { + "class_type": "easy ifElse", + "inputs": { + "boolean": ["9", 0], + "on_true": ["6", 0], + "on_false": ["7", 0], + }, + }, + "12": { + "class_type": "Switch conditioning [Crystools]", + "inputs": { + "boolean": ["9", 0], + "on_true": ["7", 0], + "on_false": ["6", 0], + }, + }, + }, + ), + ) + add( + "runtime", + _graph( + ["8", 0], + neg, + {**base, "8": {"class_type": "PromptSearchList", "inputs": {}}}, + ), + ) + add( + "unknown-encoder", + _graph( + ["8", 0], + neg, + { + **base, + "8": {"class_type": "FancyEncoder" + "x" * 80, "inputs": {"text": "x"}}, + }, + ), + ) + add( + "primitive", + _graph( + ["6", 0], + neg, + { + **base, + "6": _encode(["8", 0]), + "8": { + "class_type": "PrimitiveStringMultiline", + "inputs": {"value": " multi\nline "}, + }, + }, + ), + ) + add( + "string-kind-mismatch", + _graph( + ["8", 0], + neg, + {**base, "8": {"class_type": "PrimitiveString", "inputs": {"value": "x"}}}, + ), + ) + concat = { + "class_type": "StringConcatenate", + "inputs": {"string_a": " a ", "string_b": "", "delimiter": ", "}, + } + add( + "string-concatenate", + _graph(["6", 0], neg, {**base, "6": _encode(["8", 0]), "8": concat}), + ) + for clean in ("true", "False", True, "TRUE", 0): + text_concat = { + "class_type": "Text Concatenate", + "inputs": { + "text_b": " b ", + "text_a": " a", + "text_aa": "x", + "text_c": "", + "clean_whitespace": clean, + }, + } + add( + f"text-concatenate-{json.dumps(clean)}", + _graph(["6", 0], neg, {**base, "6": _encode(["8", 0]), "8": text_concat}), + ) + for strip in (True, False, 1): + multiline = { + "class_type": "StringConstantMultiline", + "inputs": {"string": "\n a\nb \n", "strip_newlines": strip}, + } + add( + f"strip-newlines-{json.dumps(strip)}", + _graph(["6", 0], neg, {**base, "6": _encode(["8", 0]), "8": multiline}), + ) + for count, as_list in ( + (3, False), + (1, False), + (0, False), + (1001, False), + (2, True), + (2.0, False), + (True, False), + ): + join = { + "class_type": "JoinStringMulti", + "inputs": { + "string_1": "", + "string_2": "b", + "string_3": "", + "string_4": "d", + "inputcount": count, + "delimiter": " | ", + "return_list": as_list, + }, + } + add( + f"join-multi-{json.dumps(count)}-{json.dumps(as_list)}", + _graph(["6", 0], neg, {**base, "6": _encode(["8", 0]), "8": join}), + ) + manager = { + "class_type": "PromptManager", + "inputs": { + "text": "main", + "prepend_text": " before ", + "append_text": "", + "clip": ["4", 1], + }, + } + add("prompt-manager", _graph(["8", 0], neg, {**base, "8": manager})) + add( + "prompt-manager-null-prepend", + _graph( + ["8", 0], + neg, + { + **base, + "8": {**manager, "inputs": {**manager["inputs"], "prepend_text": None}}, + }, + ), + ) + for name, slot in ( + ("PromptManagerV2", 0), + ("PromptManagerV2", 1), + ("PromptManagerPositive", 0), + ("PromptManagerNegative", 0), + ): + historical = { + "class_type": name, + "inputs": { + "text": " main ", + "negative_text": " neg ", + "prepend_text": "lead,", + "append_text": None, + }, + } + add( + f"historical-{name}-{slot}", + _graph(["8", slot], neg, {**base, "8": historical}), + ) + pair_graph = _graph(["8", 0], ["8", 1], {**base, "8": _pair()}) + add("pair-graph", pair_graph) + add("pair-execution", pair_graph, comfyassets_prompt_pairs=_execution("8")) + add( + "pair-execution-other-node", + pair_graph, + comfyassets_prompt_pairs=_execution("99"), + ) + add( + "pair-execution-mismatch", + pair_graph, + comfyassets_prompt_pairs=_execution("8", text="other"), + ) + add( + "pair-execution-negative-changed", + pair_graph, + comfyassets_prompt_pairs=_execution("8", negative_effective_text="x"), + ) + add( + "pair-execution-version-true", + pair_graph, + comfyassets_prompt_pairs=_execution("8").replace( + '"version": 1', '"version": true' + ), + ) + add( + "pair-execution-version-float", + pair_graph, + comfyassets_prompt_pairs=_execution("8").replace( + '"version": 1', '"version": 1.0' + ), + ) + add( + "pair-execution-truncated", + pair_graph, + comfyassets_prompt_pairs=_execution("8").replace( + '"version": 1', '"version": 1, "truncated": []' + ), + ) + add("pair-execution-bad-json", pair_graph, comfyassets_prompt_pairs="{nope") + two = json.loads(_execution("8")) + two["nodes"]["8"]["records"].append( + dict(two["nodes"]["8"]["records"][0], positive_text="other") + ) + add( + "pair-execution-two-records", + pair_graph, + comfyassets_prompt_pairs=json.dumps(two), + ) + linked_pair = _graph( + ["8", 0], + ["8", 1], + { + **base, + "8": {**_pair(), "inputs": {**_pair()["inputs"], "text": ["9", 0]}}, + "9": {"class_type": "PromptSearchList", "inputs": {}}, + }, + ) + add( + "pair-execution-runtime-input", + linked_pair, + comfyassets_prompt_pairs=_execution("8"), + ) + add( + "pair-execution-runtime-wrong-slot", + _graph( + ["8", 0], + ["8", 1], + { + **base, + "8": {**_pair(), "inputs": {**_pair()["inputs"], "text": ["9", 2]}}, + "9": {"class_type": "PromptManagerText", "inputs": {"text": "x"}}, + }, + ), + comfyassets_prompt_pairs=_execution("8"), + ) + guided = { + "class_type": "SamplerCustomAdvanced", + "inputs": { + "noise": ["20", 0], + "guider": ["21", 0], + "sampler": ["22", 0], + "sigmas": ["23", 0], + "latent_image": ["5", 0], + }, + } + add( + "custom-cfg", + _graph( + None, + None, + { + **base, + "21": { + "class_type": "CFGGuider", + "inputs": { + "positive": ["6", 0], + "negative": ["7", 0], + "model": ["4", 0], + "cfg": 5, + }, + }, + }, + sampler=guided, + ), + ) + add( + "custom-basic", + _graph( + None, + None, + { + **base, + "21": { + "class_type": "BasicGuider", + "inputs": {"conditioning": ["6", 0], "model": ["4", 0]}, + }, + }, + sampler=guided, + ), + ) + add( + "custom-reroute-guider", + _graph( + None, + None, + { + **base, + "21": {"class_type": "Reroute", "inputs": {"value": ["24", 0]}}, + "24": { + "class_type": "BasicGuider", + "inputs": {"conditioning": ["6", 0]}, + }, + }, + sampler=guided, + ), + ) + add( + "custom-unknown-guider", + _graph( + None, + None, + { + **base, + "21": {"class_type": "DualCFGGuider", "inputs": {"cond1": ["6", 0]}}, + }, + sampler=guided, + ), + ) + add( + "latent-stage", + _graph( + ["6", 0], + neg, + { + **base, + "5": {"class_type": "LatentUpscale", "inputs": {"samples": ["30", 0]}}, + "30": { + "class_type": "KSamplerPreview", + "inputs": {"model": ["4", 0], "positive": ["6", 0]}, + }, + }, + ), + ) + add( + "latent-encode", + _graph( + ["6", 0], + neg, + { + **base, + "5": { + "class_type": "VAEEncode", + "inputs": {"pixels": ["31", 0], "vae": ["4", 2]}, + }, + "31": {"class_type": "LoadImage", "inputs": {"image": "x.png"}}, + }, + ), + ) + add( + "latent-cycle", + _graph( + ["6", 0], + neg, + { + **base, + "5": {"class_type": "LatentUpscale", "inputs": {"samples": ["3", 0]}}, + }, + ), + ) + for override in ("", "wild", ["6", 0]): + detail = { + "class_type": "KSampler", + "inputs": { + "positive": ["6", 0], + "negative": neg, + "latent_image": ["5", 0], + "wildcard": override, + }, + } + add( + f"override-ignored-{json.dumps(override)}", + _graph(None, None, base, sampler=detail), + ) + chain, end = _chain(70) + add("depth-limit", _graph([end, 0], neg, {**base, **chain})) + chain, end = _chain(61) + add("depth-near", _graph([end, 0], neg, {**base, **chain})) + tree, top = _combine_tree(11) + add("work-limit-visits", _graph([top, 0], neg, {**base, **tree})) + tree, top = _combine_tree(9) + add("diamond-memo", _graph([top, 0], [top, 0], {**base, **tree})) + add("text-limit", _graph(["6", 0], neg, {**base, "6": _encode("x" * 100_001)})) + add( + "text-at-limit", + _graph(["6", 0], neg, {**base, "6": _encode("\U0001f600" * 100_000)}), + ) + big = { + f"s{index}": { + "class_type": "PrimitiveString", + "inputs": {"value": "y" * 99_000}, + } + for index in range(20) + } + joined = { + "class_type": "JoinStringMulti", + "inputs": { + "string_1": ["s0", 0], + "inputcount": 20, + "delimiter": "", + "return_list": False, + }, + } + joined["inputs"].update( + {f"string_{index + 1}": [f"s{index}", 0] for index in range(1, 20)} + ) + add( + "join-over-length", + _graph(["6", 0], neg, {**base, **big, "6": _encode(["8", 0]), "8": joined}), + ) + concats = { + f"k{index}": { + "class_type": "StringConcatenate", + "inputs": {"string_a": "z" * 90_000, "string_b": "", "delimiter": ""}, + } + for index in range(20) + } + combine = {f"q{index}": _encode([f"k{index}", 0]) for index in range(20)} + add("built-chars", _graph(["q0", 0], ["q1", 0], {**base, **concats, **combine})) + ceiling = { + str(index): {"class_type": "Note", "inputs": {}} for index in range(100, 20_102) + } + add("graph-ceiling", _graph(["6", 0], neg, {**base, **ceiling})) + workflow = { + "nodes": [ + {"id": 6, "type": "PromptManager", "widgets_values": ["own text"]}, + {"id": 7, "type": "PromptManager", "widgets_values": ["other text"]}, + ] + } + overwritten = {**base, "6": _encode("shared"), "7": _encode("shared")} + overwritten["6"]["inputs"]["search_text"] = "" + add( + "overwritten", + _graph(["6", 0], ["7", 0], overwritten), + workflow=json.dumps(workflow), + ) + nested = { + "nodes": [{"id": 1, "type": "sub-a", "widgets_values": []}], + "definitions": { + "subgraphs": [ + { + "id": "sub-a", + "nodes": [ + { + "id": 6, + "type": "PromptManagerFocused", + "widgets_values": {"text": "a"}, + }, + { + "id": 7, + "type": "PromptManagerFocused", + "widgets_values": ["b"], + }, + ], + } + ] + }, + } + nested_graph = { + **base, + "1:6": { + "class_type": "PromptManager", + "inputs": {"text": "same", "clip": ["4", 1]}, + }, + "1:7": {"class_type": "PromptManager", "inputs": {"text": "same"}}, + } + add( + "overwritten-subgraph", + _graph(["1:6", 0], ["1:7", 0], nested_graph), + workflow=json.dumps(nested), + ) + add( + "overwritten-bad-workflow", + _graph(["6", 0], ["7", 0], overwritten), + workflow="{bad", + ) + # A list or object node ``type`` is skipped like a non-dict node (#286), also + # inside a subgraph; a skipped node never hides the rest of the walk. + for label, kind in (("list", ["PromptManager"]), ("dict", {"a": 1})): + add( + f"workflow-type-{label}", + _graph(["6", 0], ["7", 0], overwritten), + workflow=json.dumps( + {"nodes": [{"id": 6, "type": "PromptManager"}, {"id": 8, "type": kind}]} + ), + ) + add( + "workflow-type-in-subgraph", + _graph(["1:6", 0], ["1:7", 0], nested_graph), + workflow=json.dumps( + { + **nested, + "definitions": { + "subgraphs": [{"id": "sub-a", "nodes": [{"id": 2, "type": [1]}]}] + }, + } + ), + ) + add( + "workflow-type-skipped-keeps-overwritten", + _graph(["6", 0], ["7", 0], overwritten), + workflow=json.dumps( + { + "nodes": [ + workflow["nodes"][0], + {"id": 8, "type": ["PromptManager"]}, + workflow["nodes"][1], + ] + } + ), + ) + add( + "workflow-type-without-id", + _graph(["6", 0], ["7", 0], overwritten), + workflow=json.dumps({"nodes": [{"id": True, "type": ["x"]}, "skipped"]}), + ) + add( + "several-samplers", + _graph( + ["6", 0], + neg, + { + **base, + "9": { + "class_type": "KSampler", + "inputs": {"positive": ["6", 0], "negative": neg}, + }, + }, + ), + ) + add( + "detailer", + _graph( + None, + None, + base, + sampler={ + "class_type": "FaceDetailer", + "inputs": { + "image": ["5", 0], + "positive": ["6", 0], + "negative": neg, + "wildcard": "", + }, + }, + ), + ) + cases["parameters-only"] = { + "parameters": "a fox\nNegative prompt: blur\nSteps: 20, Sampler: euler" + } + cases["prompt-and-parameters"] = { + "prompt": _dump(_graph(["6", 0], neg, base)), + "parameters": "ignored", + } + return cases + + +def _visits_graph(count): + """``count`` PrimitiveString links joined under one encoder (``count`` visits).""" + first, second = count // 2, count - count // 2 + nodes = { + f"p{index}": {"class_type": "PrimitiveString", "inputs": {"value": "w"}} + for index in range(count) + } + for name, start, size in (("j1", 0, first), ("j2", first, second)): + inputs = {"inputcount": max(size, 1), "delimiter": "", "return_list": False} + inputs.update( + {f"string_{index + 1}": [f"p{start + index}", 0] for index in range(size)} + ) + inputs.setdefault("string_1", "") + nodes[name] = {"class_type": "JoinStringMulti", "inputs": inputs} + nodes["k"] = { + "class_type": "StringConcatenate", + "inputs": {"string_a": ["j1", 0], "string_b": ["j2", 0], "delimiter": ""}, + } + nodes["6"] = _encode(["k", 0]) + nodes["7"] = _encode("blurry") + return {"prompt": _dump(_graph(["6", 0], ["7", 0], nodes))} + + +def _depth_graph(length): + chain, end = _chain(length) + return { + "prompt": _dump(_graph([end, 0], ["7", 0], {**chain, "7": _encode("blurry")})) + } + + +def _scan_graph(tail): + """Whitespace-only Text Concatenate parts: scanned before stripping, never built.""" + nodes, parts = {}, [] + for index in range(4): + if index < 3: + inputs = { + f"text_{letter}": " " * 99_000 for letter in "abcdefghijklmnopqrstu" + } + else: + inputs, left = {}, tail + for letter in "abcdefghijklmnopqrstu": + inputs[f"text_{letter}"] = " " * min(99_000, left) + left -= min(99_000, left) + inputs["text_z"] = "kept" if index == 0 else "" + nodes[f"t{index}"] = {"class_type": "Text Concatenate", "inputs": inputs} + parts.append([f"t{index}", 0]) + nodes["k"] = { + "class_type": "JoinStringMulti", + "inputs": { + "string_1": parts[0], + "string_2": parts[1], + "string_3": parts[2], + "string_4": parts[3], + "inputcount": 4, + "delimiter": "", + "return_list": False, + }, + } + nodes["6"] = _encode(["k", 0]) + nodes["7"] = _encode("blurry") + return {"prompt": _dump(_graph(["6", 0], ["7", 0], nodes))} + + +def _threshold(build, low, high): + """The largest parameter in [low, high) the server still resolves.""" + + def resolved(value): + evidence = inspect_prompt_evidence( + build(value), None, profile=INSPECTION_PROFILE + ) + return evidence["resolution"]["state"] == "resolved" + + assert resolved(low) and not resolved(high) + while high - low > 1: + middle = (low + high) // 2 + if resolved(middle): + low = middle + else: + high = middle + return low + + +# The nesting the browser's CPython-JSON parsers accept (py-json.mjs JSON_MAX_DEPTH). +JSON_MAX_DEPTH = 1000 +JSON_DEPTHS = (JSON_MAX_DEPTH - 1, JSON_MAX_DEPTH, JSON_MAX_DEPTH + 1) + + +def _nested(depth): + """``depth`` nested arrays.""" + return "[" * depth + "]" * depth + + +def depth_cases(): + """``(name, kind, depth, records)``: a deep record at the JSON depth cap. + + A ``prompt`` holds a junk input nested to ``depth`` containers in all (the + graph, its node and the inputs are three); parsed, both roles resolve. + A ``workflow`` nested to ``depth`` makes two PromptManager nodes + overwritten (R1) when it is read, and the prompt resolves when it is not. + """ + cases = [] + base = {"6": _encode("a fox"), "7": _encode("blurry")} + for depth in JSON_DEPTHS: + graph = _dump(_graph(["6", 0], ["7", 0], {**base, "9": {"class_type": "Note"}})) + junk = '"inputs": {"junk": ' + _nested(depth - 3) + "}" + prompt = graph.replace('"class_type": "Note"', '"class_type": "Note", ' + junk) + cases.append((f"prompt-depth-{depth}", "prompt", depth, {"prompt": prompt})) + overwritten = {**base, "6": _encode("shared"), "7": _encode("shared")} + overwritten["6"]["inputs"]["search_text"] = "" + nodes = [ + {"id": 6, "type": "PromptManager", "widgets_values": ["own text"]}, + {"id": 7, "type": "PromptManager", "widgets_values": ["other text"]}, + ] + workflow = json.dumps({"nodes": nodes, "extra": 0}).replace( + '"extra": 0', '"extra": ' + _nested(depth - 1) + ) + records = { + "prompt": _dump(_graph(["6", 0], ["7", 0], overwritten)), + "workflow": workflow, + } + cases.append((f"workflow-depth-{depth}", "workflow", depth, records)) + return cases + + +def boundary_cases(): + """Each work bound at its edge: the last value that resolves, then the next.""" + cases = {} + for name, build, low, high in ( + ("visits", _visits_graph, 10, 2000), + ("depth", _depth_graph, 1, 80), + ("scanned", _scan_graph, 0, 2_000_000), + ): + edge = _threshold(build, low, high) + cases[f"{name}-edge"] = build(edge) + cases[f"{name}-over"] = build(edge + 1) + long_name = "\U0001f600" * 70 + cases["named-astral"] = { + "prompt": _dump( + _graph( + ["\U0001f600" * 70, 0], + ["7", 0], + { + "\U0001f600" + * 70: {"class_type": long_name, "inputs": {"text": "x"}}, + "7": _encode("b"), + }, + ) + ) + } + return cases + + +class _Builder: + """Coherent random graphs over the traced vocabulary, sometimes corrupted.""" + + def __init__(self, rng): + self.rng = rng + self.nodes = {} + self.records = {} + self.counter = 100 + + def new_id(self): + self.counter += 1 + return str(self.counter) + + def word(self): + rng = self.rng + parts = [rng.choice(SPACES), rng.choice(WORDS), rng.choice(SPACES)] + if rng.random() < 0.3: + parts.append(rng.choice(WORDS)) + return "".join(parts) + + def string(self, depth): + """A string input: a literal, or a link to a string node.""" + rng = self.rng + if depth > 3 or rng.random() < 0.45: + return self.word() + node_id, choice = self.new_id(), rng.randrange(8) + if choice == 0: + node = { + "class_type": rng.choice( + ["PrimitiveString", "PrimitiveStringMultiline"] + ), + "inputs": {"value": self.string(depth + 1)}, + } + elif choice == 1: + node = { + "class_type": "StringConcatenate", + "inputs": { + "string_a": self.string(depth + 1), + "string_b": self.string(depth + 1), + "delimiter": rng.choice(["", ", ", " "]), + }, + } + elif choice == 2: + inputs = { + f"text_{letter}": self.string(depth + 1) + for letter in rng.sample("abcd", rng.randint(1, 3)) + } + inputs["clean_whitespace"] = rng.choice(["true", "false", True]) + node = {"class_type": "Text Concatenate", "inputs": inputs} + elif choice == 3: + node = { + "class_type": "StringConstantMultiline", + "inputs": { + "string": self.string(depth + 1), + "strip_newlines": rng.choice([True, False]), + }, + } + elif choice == 4: + count = rng.randint(1, 4) + inputs = { + f"string_{index}": self.string(depth + 1) + for index in range(1, count + 1) + if index == 1 or rng.random() < 0.8 + } + inputs.update( + { + "inputcount": count, + "delimiter": rng.choice([" ", ","]), + "return_list": rng.random() < 0.1, + } + ) + node = {"class_type": "JoinStringMulti", "inputs": inputs} + elif choice == 5: + node = { + "class_type": "Reroute", + "inputs": {"value": self.string_link(depth + 1)}, + } + elif choice == 6: + node = { + "class_type": rng.choice(["StringConstant", "TextInput"]), + "inputs": {rng.choice(["string", "text"]): self.string(depth + 1)}, + } + else: + node = { + "class_type": "PromptManagerText", + "inputs": { + "text": self.string(depth + 1), + "prepend_text": self.word(), + "append_text": self.word(), + }, + } + self.nodes[node_id] = node + return [node_id, 0] + + def string_link(self, depth): + value = self.string(depth) + if isinstance(value, list): + return value + node_id = self.new_id() + self.nodes[node_id] = { + "class_type": "PrimitiveString", + "inputs": {"value": value}, + } + return [node_id, 0] + + def conditioning(self, depth): + rng = self.rng + node_id = self.new_id() + choice = rng.randrange(11) if depth < 4 else 0 + slot = 0 + if choice == 0: + node = { + "class_type": "CLIPTextEncode", + "inputs": {"text": self.string(depth + 1), "clip": ["4", 1]}, + } + elif choice == 1: + text = self.string(depth + 1) + node = { + "class_type": "CLIPTextEncodeSDXL", + "inputs": { + "text_g": text, + "text_l": ( + text if rng.random() < 0.6 else rng.choice(["", self.word()]) + ), + }, + } + elif choice == 2: + node = { + "class_type": "FluxGuidance", + "inputs": {"conditioning": self.conditioning(depth + 1)}, + } + elif choice == 3: + node = { + "class_type": "ConditioningZeroOut", + "inputs": {"conditioning": self.conditioning(depth + 1)}, + } + elif choice == 4: + first = self.conditioning(depth + 1) + node = { + "class_type": "ConditioningCombine", + "inputs": { + "conditioning_1": first, + "conditioning_2": ( + first if rng.random() < 0.5 else self.conditioning(depth + 1) + ), + }, + } + elif choice == 5: + condition = rng.choice([True, False, ["4", 0], ["4", 1]]) + node = { + "class_type": "easy ifElse", + "inputs": { + "boolean": condition, + "on_true": self.conditioning(depth + 1), + "on_false": self.conditioning(depth + 1), + }, + } + elif choice == 6: + node = { + "class_type": rng.choice(["PromptManager", "PromptManagerFocused"]), + "inputs": { + "text": self.string(depth + 1), + "prepend_text": self.word(), + "append_text": self.word(), + "clip": ["4", 1], + }, + } + elif choice == 7: + node = { + "class_type": rng.choice(["PromptManagerV2", "PromptManagerPositive"]), + "inputs": { + "text": self.string(depth + 1), + "prepend_text": self.word(), + "negative_text": self.word(), + }, + } + elif choice == 8: + slot = rng.choice([0, 1]) + texts = { + "text": self.word(), + "negative_text": self.word(), + "prepend_text": self.word(), + "append_text": self.word(), + } + node = {"class_type": "PromptManagerPair", "inputs": dict(texts)} + if rng.random() < 0.6: + record = { + **texts, + "positive_text": self.word(), + "negative_effective_text": texts["negative_text"], + } + record["prepend_text"], record["append_text"] = ( + record["prepend_text"].strip(), + record["append_text"].strip(), + ) + if rng.random() < 0.2: + record["text"] = record["text"] + "!" + self.records[EXECUTION_KEY] = json.dumps( + {"version": 1, "nodes": {node_id: {"records": [record]}}} + ) + elif choice == 9: + node = { + "class_type": "Reroute", + "inputs": {"value": self.conditioning(depth + 1)}, + } + else: + node = { + "class_type": rng.choice(["PromptSearchList", "FancyEncoder"]), + "inputs": {"text": self.word()}, + } + self.nodes[node_id] = node + return [node_id, slot] + + def corrupt(self): + rng = self.rng + node_id = rng.choice(sorted(self.nodes)) + node = self.nodes[node_id] + links = [ + key + for key, value in node.get("inputs", {}).items() + if isinstance(value, list) + ] + choice = rng.randrange(6) + if choice == 0 and links: + node["inputs"][rng.choice(links)][1] = rng.choice([0.0, True, 7, -1]) + elif choice == 1 and links: + node["inputs"][rng.choice(links)][0] = rng.choice(["missing", node_id]) + elif choice == 2: + node["class_type"] = rng.choice([None, 3, "Unknown"]) + elif choice == 3: + node["inputs"] = rng.choice([[], {}]) + elif choice == 4 and links: + del node["inputs"][rng.choice(links)] + else: + node.pop("inputs", None) + + +EXECUTION_KEY = "comfyassets_prompt_pairs" + + +def fuzz_cases(seed, count): + rng = random.Random(seed) + cases = {} + for index in range(count): + builder = _Builder(rng) + positive = builder.conditioning(0) + negative = builder.conditioning(0) if rng.random() < 0.9 else None + graph = _graph(positive, negative, builder.nodes) + if negative is None: + del graph["3"]["inputs"]["negative"] + if rng.random() < 0.25: + builder.nodes = graph + builder.corrupt() + records = {"prompt": json.dumps(graph, ensure_ascii=rng.random() < 0.5)} + records.update(builder.records) + cases[f"fuzz-{seed}-{index}"] = records + return cases + + +def single_chain(records): + """Whether the server answers ``records`` without choosing among samplers.""" + roles = {key for key in records if key in ("prompt", "parameters", EXECUTION_KEY)} + if "prompt" not in roles: + return "parameters" not in roles + try: + graph = json.loads(records["prompt"]) + except (ValueError, RecursionError): + return True + if ( + not isinstance(graph, dict) + or not graph + or len(graph) > INSPECTION_PROFILE.node_ceiling + ): + return True + samplers = get_semantics().samplers + found = [ + node["class_type"] + for node in graph.values() + if isinstance(node, dict) + and isinstance(node.get("class_type"), str) + and node["class_type"] in samplers + and samplers[node["class_type"]]["strategy"] + in ("ksampler", "custom-advanced", "detailer-stage") + ] + return len(found) <= 1 and not any( + samplers[name]["strategy"] == "detailer-stage" for name in found + ) + + +def _utf8(records): + """Whether the record texts can be written as UTF-8 (no lone surrogate).""" + try: + json.dumps(records, ensure_ascii=False).encode("utf-8") + except UnicodeEncodeError: + return False + return True + + +def committed_vectors(): + """The committed record sets with the server's evidence, in a stable order.""" + try: + from tests import local_resolver_selection_cases as selection + except ImportError: # pragma: no cover - discover -s tests puts tests/ first + import local_resolver_selection_cases as selection + + named = {**golden_cases(), **selection.selection_cases()} + cases = { + name: records + for name, records in named.items() + if len(json.dumps(records)) <= MAX_VECTOR_CHARS and _utf8(records) + } + cases.update(fuzz_cases(*COMMITTED_FUZZ)) + cases.update(selection.selection_fuzz_cases(*COMMITTED_FUZZ)) + vectors = [ + { + "name": name, + "records": records, + "evidence": inspect_prompt_evidence( + records, None, profile=INSPECTION_PROFILE + ), + } + for name, records in cases.items() + ] + return {"version": 2, "vectors": vectors} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--write", action="store_true", help="rewrite the vectors") + if parser.parse_args().write: + text = json.dumps(committed_vectors(), ensure_ascii=False, sort_keys=True) + VECTORS_PATH.write_text(text + "\n", encoding="utf-8", newline="\n") + + +if __name__ == "__main__": + main() diff --git a/tests/local_resolver_selection_cases.py b/tests/local_resolver_selection_cases.py new file mode 100644 index 0000000..9591e97 --- /dev/null +++ b/tests/local_resolver_selection_cases.py @@ -0,0 +1,574 @@ +"""Candidate-selection record sets for the local resolver parity test (PR L3 Task 9). + +``local_resolver_cases`` covers one sampler's tracing. These cases reach the +rest of ``_resolve``: several sampler candidates and the identical-branch rule +(#270), stages that only reach previews, detailer stages and their upstream +samplers (multistage), and A1111 ``parameters`` with every footer shape. The +server's ``inspect_prompt_evidence`` is the oracle; nothing here states an +expected answer. All content is synthetic. +""" + +import json +import random + +try: + from tests.local_resolver_cases import EXECUTION_KEY, SPACES, WORDS, _Builder +except ImportError: # pragma: no cover - discover -s tests puts tests/ first + from local_resolver_cases import EXECUTION_KEY, SPACES, WORDS, _Builder + + +def _encode(text): + return {"class_type": "CLIPTextEncode", "inputs": {"text": text, "clip": ["4", 1]}} + + +def _sampler(positive, negative, latent=None, name="KSampler"): + inputs = {"seed": 1, "model": ["4", 0], "positive": positive} + if negative is not None: + inputs["negative"] = negative + inputs["latent_image"] = latent or ["5", 0] + return {"class_type": name, "inputs": inputs} + + +def _decode(sampler_id): + return {"class_type": "VAEDecode", "inputs": {"samples": [sampler_id, 0]}} + + +def _save(image, name="SaveImage"): + return {"class_type": name, "inputs": {"images": image}} + + +def _detailer(image, positive, negative, name="FaceDetailer", **extra): + inputs = {"image": image, "model": ["4", 0], "positive": positive} + inputs["negative"] = negative + inputs.update(extra) + return {"class_type": name, "inputs": inputs} + + +def _base(**nodes): + graph = { + "4": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "m"}}, + "5": {"class_type": "EmptyLatentImage", "inputs": {"width": 64}}, + "6": _encode("a fox"), + "7": _encode("blurry"), + "8": _encode("a cat"), + } + graph.update(nodes) + return graph + + +def _samplers(): + """Several unanchored samplers: identical, differing and mixed branches.""" + same = _sampler(["6", 0], ["7", 0]) + cases = { + "two-identical": _base(**{"3": same, "9": same}), + "two-differ": _base(**{"3": same, "9": _sampler(["8", 0], ["7", 0])}), + "two-negative-differs": _base(**{"3": same, "9": _sampler(["6", 0], ["8", 0])}), + "identical-without-negative": _base( + **{"3": _sampler(["6", 0], None), "9": _sampler(["6", 0], None)} + ), + "one-unresolved": _base(**{"3": same, "9": _sampler(["missing", 0], ["7", 0])}), + "both-unresolved": _base( + **{"3": _sampler(["x", 0], ["7", 0]), "9": _sampler(["y", 0], ["7", 0])} + ), + "three-identical-one-advanced": _base( + **{ + "3": same, + "9": same, + "10": _sampler(["6", 0], ["7", 0], name="KSamplerAdvanced"), + } + ), + # Equal texts, different recipes: a PromptManager pair against an encoder. + "same-text-other-recipe": _base( + **{ + "3": same, + "9": _sampler(["11", 0], ["7", 0]), + "11": { + "class_type": "PromptManager", + "inputs": { + "text": "fox", + "prepend_text": "a", + "append_text": "", + "clip": ["4", 1], + }, + }, + } + ), + "multistage-latent": _base( + **{"3": same, "9": _sampler(["6", 0], ["7", 0], latent=["3", 0])} + ), + } + selector = { + "class_type": "easy ifElse", + "inputs": {"boolean": ["4", 0], "on_true": ["6", 0], "on_false": ["8", 0]}, + } + cases["selector-and-plain"] = _base( + **{"3": same, "9": _sampler(["12", 0], ["7", 0]), "12": selector} + ) + return cases + + +def _previews(): + """Stages whose outputs reach only previews are not the saved file's.""" + same, other = _sampler(["6", 0], ["7", 0]), _sampler(["8", 0], ["7", 0]) + return { + "preview-dropped": _base( + **{ + "3": same, + "9": other, + "13": _decode("3"), + "14": _decode("9"), + "15": _save(["13", 0]), + "16": _save(["14", 0], "PreviewImage"), + } + ), + "all-previews-kept": _base( + **{ + "3": same, + "9": other, + "13": _decode("3"), + "14": _decode("9"), + "15": _save(["13", 0], "PreviewImage"), + "16": _save(["14", 0], "Image Comparer (rgthree)"), + } + ), + "preview-and-file": _base( + **{ + "3": same, + "9": other, + "13": _decode("3"), + "14": _decode("9"), + "15": _save(["13", 0]), + "16": _save(["14", 0], "PreviewImage"), + "17": _save(["14", 0]), + } + ), + "unconsumed-stage-kept": _base( + **{ + "3": same, + "9": other, + "14": _decode("9"), + "16": _save(["14", 0], "PreviewImage"), + } + ), + # A list or object class_type is never a preview class (#286). + "preview-unhashable-class": _base( + **{ + "3": same, + "9": other, + "14": _decode("9"), + "16": {"class_type": ["PreviewImage"], "inputs": {"images": ["14", 0]}}, + "17": {"class_type": {"a": 1}, "inputs": {"images": ["3", 0]}}, + } + ), + "preview-through-bad-node": _base( + **{ + "3": same, + "9": other, + "13": _decode("3"), + "15": _save(["13", 0]), + "16": _save(["9", 0], "PreviewImage"), + "18": "not a node", + "19": {"class_type": "Note", "inputs": {"x": ["18", 0]}}, + } + ), + } + + +def _detailers(): + """Detailer stages judged with the samplers in their image ancestry.""" + same = _sampler(["6", 0], ["7", 0]) + cases = { + "detailer-same-prompt": _base( + **{ + "3": same, + "10": _decode("3"), + "11": _detailer(["10", 0], ["6", 0], ["7", 0]), + "12": _save(["11", 0]), + } + ), + "detailer-differs": _base( + **{ + "3": same, + "10": _decode("3"), + "11": _detailer(["10", 0], ["8", 0], ["7", 0]), + "12": _save(["11", 0]), + } + ), + "detailer-and-own-output": _base( + **{ + "3": same, + "10": _decode("3"), + "11": _detailer(["10", 0], ["8", 0], ["7", 0]), + "12": _save(["11", 0]), + "13": _save(["10", 0]), + } + ), + "detailer-image-literal": _base( + **{ + "11": _detailer("not a link", ["6", 0], ["7", 0]), + "12": _save(["11", 0]), + } + ), + "detailer-cycle": _base( + **{ + "11": _detailer(["10", 0], ["6", 0], ["7", 0]), + "10": {"class_type": "ImageScale", "inputs": {"image": ["11", 0]}}, + "12": _save(["11", 0]), + } + ), + "detailer-chain": _base( + **{ + "3": same, + "10": _decode("3"), + "11": _detailer(["10", 0], ["6", 0], ["7", 0]), + "21": _detailer( + ["11", 0], ["8", 0], ["7", 0], name="UltimateSDUpscale" + ), + "12": _save(["21", 0]), + } + ), + "detailer-chain-same": _base( + **{ + "3": same, + "10": _decode("3"), + "11": _detailer(["10", 0], ["6", 0], ["7", 0]), + "21": _detailer(["11", 0], ["6", 0], ["7", 0], name="DetailerForEach"), + "12": _save(["21", 0]), + } + ), + "detailer-two-upstream": _base( + **{ + "3": same, + "9": _sampler(["8", 0], ["7", 0]), + "10": { + "class_type": "ImageBlend", + "inputs": {"image1": ["13", 0], "image2": ["14", 0]}, + }, + "13": _decode("3"), + "14": _decode("9"), + "11": _detailer(["10", 0], ["6", 0], ["7", 0]), + "12": _save(["11", 0]), + } + ), + "detailer-unsupported-stage": _base( + **{ + "10": { + "class_type": "MysterySampler", + "inputs": {"positive": ["6", 0], "model": ["4", 0]}, + }, + "11": _detailer(["10", 0], ["6", 0], ["7", 0]), + "12": _save(["11", 0]), + } + ), + "detailer-upstream-unresolved": _base( + **{ + "3": _sampler(["gone", 0], ["7", 0]), + "10": _decode("3"), + "11": _detailer(["10", 0], ["6", 0], ["7", 0]), + "12": _save(["11", 0]), + } + ), + "detailer-upstream-selector": _base( + **{ + "3": _sampler(["15", 0], ["7", 0]), + "15": { + "class_type": "easy ifElse", + "inputs": { + "boolean": ["4", 0], + "on_true": ["6", 0], + "on_false": ["8", 0], + }, + }, + "10": _decode("3"), + "11": _detailer(["10", 0], ["6", 0], ["7", 0]), + "12": _save(["11", 0]), + } + ), + "detailer-wildcard": _base( + **{ + "3": same, + "10": _decode("3"), + "11": _detailer(["10", 0], ["6", 0], ["7", 0], wildcard="[x|y]"), + "12": _save(["11", 0]), + } + ), + "detailer-no-upscaled-image": _base( + **{ + "3": same, + "10": _decode("3"), + "11": _detailer( + ["10", 0], ["6", 0], ["7", 0], name="UltimateSDUpscaleNoUpscale" + ), + "12": _save(["11", 0]), + } + ), + "detailer-no-sampler-upstream": _base( + **{ + "10": {"class_type": "LoadImage", "inputs": {"image": "a.png"}}, + "11": _detailer(["10", 0], ["6", 0], ["7", 0]), + "12": _save(["11", 0]), + } + ), + } + deep = {"3": _sampler(["6", 0], ["7", 0]), "d0": _decode("3")} + for index in range(1, 70): + deep[f"d{index}"] = { + "class_type": "ImageScale", + "inputs": {"image": [f"d{index - 1}", 0]}, + } + deep["11"] = _detailer(["d69", 0], ["6", 0], ["7", 0]) + deep["12"] = _save(["11", 0]) + cases["detailer-deep-ancestry"] = _base(**deep) + # More nested detailer stages than the depth bound. + stacked = {"3": _sampler(["6", 0], ["7", 0]), "s0": _decode("3")} + previous = "s0" + for index in range(1, 67): + stacked[f"s{index}"] = _detailer([previous, 0], ["6", 0], ["7", 0]) + previous = f"s{index}" + stacked["12"] = _save([previous, 0]) + cases["detailer-stacked-past-depth"] = _base(**stacked) + # Each detailer walks a wide shared image ancestry: the total visit budget + # runs out across stages before any candidate is read. + wide = { + f"l{index}": {"class_type": "LoadImage", "inputs": {}} for index in range(1500) + } + wide["hub"] = { + "class_type": "ImageBatchMany", + "inputs": {f"i{index}": [f"l{index}", 0] for index in range(1500)}, + } + for index in range(15): + wide[f"w{index}"] = _detailer(["hub", 0], ["6", 0], ["7", 0]) + wide[f"o{index}"] = _save([f"w{index}", 0]) + cases["detailers-exhaust-total-visits"] = _base(**wide) + cases["detailer-mutual-cycle"] = _base( + **{ + "11": _detailer(["21", 0], ["6", 0], ["7", 0]), + "21": _detailer(["11", 0], ["6", 0], ["7", 0]), + "12": _save(["21", 0]), + } + ) + # The sampler's output reaches a preview through more nodes than one + # detailer's allowance: the walk runs out, so the sampler is not absorbed. + long = {"3": _sampler(["6", 0], ["7", 0]), "10": _decode("3")} + previous = "10" + for index in range(2100): + long[f"c{index}"] = { + "class_type": "ImageScale", + "inputs": {"image": [previous, 0]}, + } + previous = f"c{index}" + long["p"] = _save([previous, 0], "PreviewImage") + long["11"] = _detailer(["10", 0], ["6", 0], ["7", 0]) + long["12"] = _save(["11", 0]) + cases["detailer-output-walk-exhausted"] = _base(**long) + # Every detailer's ancestry walk has its own allowance: the first one is + # wide, and the second still absorbs the first. + chained = {"3": _sampler(["6", 0], ["7", 0]), "10": _decode("3")} + chained["h1"] = { + "class_type": "ImageBatchMany", + "inputs": { + "i": ["10", 0], + **{f"i{index}": [f"a{index}", 0] for index in range(1500)}, + }, + } + chained["h2"] = { + "class_type": "ImageBatchMany", + "inputs": { + "i": ["11", 0], + **{f"i{index}": [f"b{index}", 0] for index in range(600)}, + }, + } + for index in range(1500): + chained[f"a{index}"] = {"class_type": "LoadImage", "inputs": {}} + for index in range(600): + chained[f"b{index}"] = {"class_type": "LoadImage", "inputs": {}} + chained["11"] = _detailer(["h1", 0], ["6", 0], ["7", 0]) + chained["21"] = _detailer(["h2", 0], ["6", 0], ["7", 0]) + chained["12"] = _save(["21", 0]) + cases["detailers-each-reset"] = _base(**chained) + return cases + + +_FOOTER = "Steps: 20, Sampler: euler, CFG scale: 7" + + +def _parameters(): + """A1111 ``parameters`` texts: every footer shape and delimiter rule.""" + texts = { + "basic": f"a fox\nNegative prompt: blur\n{_FOOTER}", + "no-negative": f"a fox\n{_FOOTER}", + "two-delimiters": f"a\nNegative prompt: b\nNegative prompt: c\n{_FOOTER}", + "leading-delimiter": f"Negative prompt: blur\n{_FOOTER}", + "blank-positive": f" \x85\nNegative prompt: blur\n{_FOOTER}", + "footer-not-last": f"a fox\n{_FOOTER}\nmore text", + "two-footers": f"a\n{_FOOTER}\n{_FOOTER}", + "bare-footer": "a fox\nNegative prompt: blur\nSteps: 20", + "bare-and-full": f"a fox\nSteps: 5\n{_FOOTER}", + "two-bare": "a\nSteps: 1\nSteps: 2", + "template": f"a fox\n{_FOOTER}\nTemplate: {{a|b}}", + "both-templates": f"a\n{_FOOTER}\nTemplate: x\nNegative Template: y", + "templates-reversed": f"a\n{_FOOTER}\nNegative Template: y\nTemplate: x", + "two-templates": f"a\n{_FOOTER}\nTemplate: x\nTemplate: y", + "template-multiline": f"a\n{_FOOTER}\nTemplate: x\nmore", + "crlf": f"a fox\r\nNegative prompt: blur\r\n{_FOOTER}\r\n", + "cr-only": f"a fox\rNegative prompt: blur\r{_FOOTER}", + "cr-template": f"a\n{_FOOTER}\rTemplate: x", + "line-separator": f"a fox
{_FOOTER}", + "next-line": f"a fox\x85{_FOOTER}", + "spaced-steps": "a fox\nSteps:\n 20 \n, x", + "steps-newline-bare": "a fox\nSteps:\n20\n", + "arabic-digits": "a fox\nSteps: ٢٠, Sampler: euler", + "fullwidth-digits": "a fox\nSteps: 2, x", + "no-digits": "a fox\nSteps: , x", + "trailing-spaces": f"a fox \nNegative prompt: blur \n{_FOOTER} \n \n", + "empty-negative": f"a fox\nNegative prompt:\n{_FOOTER}", + "astral-within-limit": "\U0001f600" * 60_000 + f"\n{_FOOTER}", + "over-limit": "x" * 100_001, + "at-limit": "x" * (100_000 - len(_FOOTER) - 1) + "\n" + _FOOTER, + "empty": "", + "only-footer": _FOOTER, + "lone-surrogate": f"\ud800 fox\nNegative prompt: \udfff\n{_FOOTER}", + } + cases = {f"parameters-{name}": {"parameters": text} for name, text in texts.items()} + cases["parameters-with-execution"] = { + "parameters": texts["basic"], + EXECUTION_KEY: json.dumps({"version": 1, "nodes": {}}), + } + return cases + + +def selection_cases(): + """Named record sets for every Task 9 path, each a ``read_inspection_file`` dict.""" + cases = {} + for group in (_samplers(), _previews(), _detailers()): + for name, graph in group.items(): + cases[f"select-{name}"] = {"prompt": json.dumps(graph, ensure_ascii=False)} + cases.update(_parameters()) + return cases + + +class _Selection(_Builder): + """Random graphs with several samplers, outputs, previews and detailers.""" + + def sampler(self): + rng = self.rng + positive = self.conditioning(0) + negative = self.conditioning(0) if rng.random() < 0.85 else None + name = rng.choice(["KSampler", "KSampler", "KSamplerAdvanced", "SamplerCustom"]) + node_id = self.new_id() + self.nodes[node_id] = _sampler(positive, negative, name=name) + return node_id + + def shared(self, ids): + """A copy of an earlier sampler: an identical branch.""" + node_id = self.new_id() + self.nodes[node_id] = json.loads(json.dumps(self.nodes[self.rng.choice(ids)])) + return node_id + + def output(self, image): + rng = self.rng + choice = rng.randrange(5) + if choice == 4: + return + name = ["SaveImage", "SaveImage", "PreviewImage", "PreviewAny"][choice] + self.nodes[self.new_id()] = _save(image, name) + + def detailer(self, image): + rng = self.rng + node_id = self.new_id() + positive = self.conditioning(1) if rng.random() < 0.5 else None + name = rng.choice(list(_DETAILER_NAMES)) + node = _detailer(image, positive, self.conditioning(1), name=name) + if positive is None: + node["inputs"]["positive"] = self.reuse_positive() + if name == "UltimateSDUpscaleNoUpscale": + node["inputs"]["upscaled_image"] = node["inputs"].pop("image") + if name in ("FaceDetailer", "DetailerForEach") and rng.random() < 0.15: + node["inputs"]["wildcard"] = rng.choice(["", "x", ["4", 0]]) + self.nodes[node_id] = node + return node_id + + def reuse_positive(self): + samplers = [ + node + for node in self.nodes.values() + if node.get("class_type") + in ("KSampler", "KSamplerAdvanced", "SamplerCustom") + ] + if samplers: + return self.rng.choice(samplers)["inputs"]["positive"] + return self.conditioning(1) + + def build(self): + rng = self.rng + ids = [self.sampler()] + for _ in range(rng.randrange(3)): + ids.append(self.shared(ids) if rng.random() < 0.4 else self.sampler()) + for sampler_id in list(ids): + decoded = self.new_id() + self.nodes[decoded] = _decode(sampler_id) + image = [decoded, 0] + for _ in range(rng.randrange(3)): + if rng.random() < 0.5: + image = [self.detailer(image), 0] + else: + self.output(image) + self.output(image) + if rng.random() < 0.1: + self.nodes[sampler_id]["inputs"]["latent_image"] = [rng.choice(ids), 0] + graph = { + "4": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "m"}}, + "5": {"class_type": "EmptyLatentImage", "inputs": {"width": 64}}, + } + graph.update(self.nodes) + return graph + + +_DETAILER_NAMES = ( + "FaceDetailer", + "DetailerForEach", + "UltimateSDUpscale", + "UltimateSDUpscaleNoUpscale", +) +_PARAMETER_LINES = [ + "Negative prompt:", + "Negative prompt: blur", + "Steps: 20, Sampler: euler", + "Steps: 7", + "Steps: x", + "Template: t", + "Negative Template: n", + "Steps:٣, a", +] + + +def _parameter_text(rng): + parts = [] + for _ in range(rng.randint(1, 5)): + if rng.random() < 0.5: + parts.append(rng.choice(_PARAMETER_LINES)) + else: + parts.append(rng.choice(SPACES) + rng.choice(WORDS) + rng.choice(SPACES)) + joints = ["\n", "\n", "\r\n", "\r", "
", " "] + return "".join(part + rng.choice(joints) for part in parts).rstrip( + rng.choice(["", "\n", " "]) + ) + + +def selection_fuzz_cases(seed, count): + """Seeded several-sampler graphs (sometimes corrupted) and parameters texts.""" + rng = random.Random(seed) + cases = {} + for index in range(count): + if rng.random() < 0.2: + cases[f"select-fuzz-{seed}-{index}"] = {"parameters": _parameter_text(rng)} + continue + builder = _Selection(rng) + graph = builder.build() + if rng.random() < 0.2: + builder.nodes = graph + builder.corrupt() + records = {"prompt": json.dumps(graph, ensure_ascii=rng.random() < 0.5)} + records.update(builder.records) + cases[f"select-fuzz-{seed}-{index}"] = records + return cases diff --git a/tests/lock_probe.py b/tests/lock_probe.py new file mode 100644 index 0000000..88146f2 --- /dev/null +++ b/tests/lock_probe.py @@ -0,0 +1,35 @@ +"""A lock stand-in that reports when a second caller has to wait for it. + +Concurrency tests patch it over a module's lock so they can wait until the +second caller is really blocked on the lock, instead of hoping a thread +start or one event-loop tick got it there. +""" + +import threading + + +class ProbeLock: + """A re-entrant lock whose ``contended`` event is set when a caller blocks.""" + + def __init__(self): + self._lock = threading.RLock() + self.contended = threading.Event() + + def acquire(self, blocking=True, timeout=-1): + if self._lock.acquire(blocking=False): + return True + self.contended.set() + if not blocking: + return False + return self._lock.acquire(timeout=timeout) + + def release(self): + self._lock.release() + + def __enter__(self): + self.acquire() + return self + + def __exit__(self, *exc): + self.release() + return False diff --git a/tests/test_api_admin.py b/tests/test_api_admin.py index bddcc39..61d703b 100644 --- a/tests/test_api_admin.py +++ b/tests/test_api_admin.py @@ -5,6 +5,7 @@ database and a stub ``folder_paths`` module, so the tests run without ComfyUI. """ import asyncio +import hashlib import json import os import shutil @@ -734,9 +735,9 @@ class TestSettingsReadMonitorThroughModule(AdminAPITestCase): def test_no_sys_modules_scan(self): import inspect - from py.api.admin import AdminRoutesMixin + from py.api.settings_routes import SettingsRoutesMixin - source = inspect.getsource(AdminRoutesMixin.get_settings) + source = inspect.getsource(SettingsRoutesMixin.get_settings) self.assertNotIn("sys.modules", source) async def test_reports_directories_of_running_monitor(self): @@ -1059,14 +1060,29 @@ class TestSettingsMisc(AdminAPITestCase): saved["gallery"]["monitoring"]["directories"], [self._root(self.output_dir)] ) - async def test_save_replaces_a_corrupt_config_file(self): - with open(self.config_path, "w") as f: - f.write("{not json") + async def test_save_refuses_a_corrupt_config_file(self): + # #220: a config.json that cannot be parsed is the user's file, never + # silently replaced; the save is refused and nothing is published. + Path(self.config_path).write_bytes(b"{not json") + before_timeout = PromptManagerConfig.RESULT_TIMEOUT resp = await self.client.request( "POST", "/prompt_manager/settings", json={"result_timeout": 4} ) - self.assertEqual(resp.status, 200) - self.assertEqual(self._read_config()["web_ui"]["result_timeout"], 4) + body = await resp.json() + self.assertEqual((resp.status, body["code"]), (409, "config-unreadable")) + self.assertIs(body["success"], False) + self.assertIs(body["persisted"], False) + self.assertEqual(PromptManagerConfig.RESULT_TIMEOUT, before_timeout) + self.assertEqual(Path(self.config_path).read_bytes(), b"{not json") + + async def test_get_settings_reports_an_unreadable_config(self): + Path(self.config_path).write_bytes(b"{not json") + resp = await self.client.request("GET", "/prompt_manager/settings") + body = await resp.json() + self.assertEqual(resp.status, 200) # the legacy page still loads + self.assertIsNone(body["revision"]) + self.assertEqual(body["config_error"], "config-unreadable") + self.assertIn("result_timeout", body["settings"]) async def test_saved_config_file_is_private_to_the_user(self): import stat @@ -1107,12 +1123,156 @@ class TestSettingsMisc(AdminAPITestCase): self.assertEqual(resp.status, 200) self.assertFalse(data["restart_required"]) - async def test_unwritable_config_path_still_succeeds(self): - os.environ[CONFIG_PATH_ENV] = os.path.join(self.tmpdir, "missing", "c.json") + async def test_unwritable_config_path_reports_failure_without_runtime_change(self): + # #220: the parent is a file, so config.json cannot be written. The + # save reports the failure and publishes nothing. + blocker = Path(self.tmpdir) / "not-a-dir" + blocker.write_text("x") + os.environ[CONFIG_PATH_ENV] = str(blocker / "c.json") + GalleryConfig.MONITORING_DIRECTORIES = [] + before_timeout = PromptManagerConfig.RESULT_TIMEOUT + with self.assertLogs("prompt_manager.api", level="ERROR"): + resp = await self.client.request( + "POST", + "/prompt_manager/settings", + json={ + "result_timeout": before_timeout + 1, + "gallery_root_paths": [self._root(self.output_dir)], + }, + ) + body = await resp.json() + self.assertEqual( + (resp.status, body["code"], body["persisted"]), + (500, "config-write-failed", False), + ) + self.assertIs(body["success"], False) + self.assertIs(body["runtime_applied"], False) + self.assertEqual(PromptManagerConfig.RESULT_TIMEOUT, before_timeout) + self.assertEqual(GalleryConfig.MONITORING_DIRECTORIES, []) + self.assertEqual(blocker.read_text(), "x") + + async def test_save_reports_the_commit(self): resp = await self.client.request( "POST", "/prompt_manager/settings", json={"result_timeout": 3} ) + body = await resp.json() self.assertEqual(resp.status, 200) + self.assertEqual(body["message"], "Settings saved successfully") + self.assertIs(body["persisted"], True) + self.assertIs(body["runtime_applied"], True) + self.assertIs(body["restart_required"], False) + self.assertEqual(body["restart_required_fields"], []) + digest = hashlib.sha256(Path(self.config_path).read_bytes()).hexdigest() + self.assertEqual(body["revision"], "sha256:" + digest) + + async def test_changed_roots_name_the_restart_field(self): + resp = await self.client.request( + "POST", + "/prompt_manager/settings", + json={"gallery_root_paths": [self._root(self.output_dir)]}, + ) + body = await resp.json() + self.assertEqual(resp.status, 200) + self.assertIs(body["restart_required"], True) + self.assertEqual(body["restart_required_fields"], ["gallery_root_paths"]) + + async def test_runtime_publication_failure_after_commit_is_reported(self): + # The values are durable; the response must not claim they are live. + self.api.reset_gallery_roots_cache = _raise + with self.assertLogs("prompt_manager.api", level="ERROR"): + resp = await self.client.request( + "POST", + "/prompt_manager/settings", + json={"gallery_root_paths": [self._root(self.output_dir)]}, + ) + body = await resp.json() + self.assertEqual(resp.status, 200) + self.assertIs(body["persisted"], True) + self.assertIs(body["runtime_applied"], False) + self.assertIs(body["restart_required"], True) + self.assertEqual(body["restart_required_fields"], ["gallery_root_paths"]) + self.assertEqual( + self._read_config()["gallery"]["monitoring"]["directories"], + [self._root(self.output_dir)], + ) + + async def test_get_settings_reports_revision(self): + resp = await self.client.request("GET", "/prompt_manager/settings") + body = await resp.json() + self.assertEqual(body["revision"], "absent") + self.assertNotIn("config_error", body) + + self._write_config({"web_ui": {"result_timeout": 2}}) + body = await ( + await self.client.request("GET", "/prompt_manager/settings") + ).json() + digest = hashlib.sha256(Path(self.config_path).read_bytes()).hexdigest() + self.assertEqual(body["revision"], "sha256:" + digest) + + async def test_native_save_with_stale_revision_is_409(self): + self._write_config({"web_ui": {"result_timeout": 2}}) + body = await ( + await self.client.request("GET", "/prompt_manager/settings") + ).json() + seen = body["revision"] + self._write_config({"web_ui": {"result_timeout": 6}}) # another writer + before = Path(self.config_path).read_bytes() + before_timeout = PromptManagerConfig.RESULT_TIMEOUT + + resp = await self.client.request( + "POST", + "/prompt_manager/settings", + json={"result_timeout": 9, "expected_revision": seen}, + ) + body = await resp.json() + self.assertEqual((resp.status, body["code"]), (409, "settings-changed")) + self.assertIs(body["persisted"], False) + digest = hashlib.sha256(before).hexdigest() + self.assertEqual(body["revision"], "sha256:" + digest) + self.assertEqual(Path(self.config_path).read_bytes(), before) + self.assertEqual(PromptManagerConfig.RESULT_TIMEOUT, before_timeout) + + async def test_native_save_with_current_revision_commits(self): + body = await ( + await self.client.request("GET", "/prompt_manager/settings") + ).json() + resp = await self.client.request( + "POST", + "/prompt_manager/settings", + json={"result_timeout": 9, "expected_revision": body["revision"]}, + ) + self.assertEqual(resp.status, 200) + self.assertEqual(self._read_config()["web_ui"]["result_timeout"], 9) + + async def test_expected_revision_must_be_a_string(self): + resp = await self.client.request( + "POST", + "/prompt_manager/settings", + json={"result_timeout": 9, "expected_revision": 5}, + ) + self.assertEqual(resp.status, 400) + self.assertFalse(os.path.exists(self.config_path)) + + async def test_legacy_partial_update_without_revision_still_merges(self): + self._write_config( + { + "integrations": {"lora_manager": {"civitai_api_key": "secret"}}, + "web_ui": {"result_timeout": 1, "webui_display_mode": "popup"}, + "custom_section": {"keep": True}, + } + ) + resp = await self.client.request( + "POST", "/prompt_manager/settings", json={"result_timeout": 8} + ) + self.assertEqual(resp.status, 200) + self.assertNotIn("secret", await resp.text()) + saved = self._read_config() + self.assertEqual( + saved["integrations"]["lora_manager"]["civitai_api_key"], "secret" + ) + self.assertEqual(saved["custom_section"], {"keep": True}) + self.assertEqual(saved["web_ui"]["result_timeout"], 8) + self.assertEqual(saved["web_ui"]["webui_display_mode"], "popup") # not sent async def test_save_unexpected_error_is_500(self): self.api._parse_gallery_roots = _raise diff --git a/tests/test_api_backup_restore.py b/tests/test_api_backup_restore.py index d91b302..aa211ff 100644 --- a/tests/test_api_backup_restore.py +++ b/tests/test_api_backup_restore.py @@ -5,12 +5,16 @@ stream the upload to disk, reject anything that is not a healthy prompts database and leave the live database untouched when it does. """ +import asyncio import glob +import hashlib import os import sqlite3 import sys import tempfile +import threading import unittest +from pathlib import Path from unittest.mock import MagicMock, patch _mock_server = MagicMock() @@ -142,6 +146,85 @@ class TestBackupRoute(BackupRestoreTestCase): self.assertIn("backup", data["error"].lower()) +class TestRestoreHoldsTheRegistry(BackupRestoreTestCase): + """A legacy restore and a workspace job refuse each other (#269 gap).""" + + def _digest(self): + return { + suffix: hashlib.sha256(Path(self.db_path + suffix).read_bytes()).hexdigest() + for suffix in ("", "-wal") + if os.path.exists(self.db_path + suffix) + } + + async def test_legacy_restore_refused_while_scan_job_runs(self): + self._save("keep me") + upload = self._make_db_file(["restored"]) + gate = asyncio.Event() + + async def runner(job): + await gate.wait() + + scan = self.api._jobs.start( + "scan", "Scan for metadata", record_changing=True, scope={}, runner=runner + ) + before = self._digest() + try: + resp = await self._restore(upload) + self.assertEqual(resp.status, 409) + self.assertEqual( + await resp.json(), + {"success": False, "error": "Scan for metadata already running"}, + ) + self.assertEqual(self._digest(), before) + self.assertEqual( + [p["text"] for p in self.api.db.search_prompts()], ["keep me"] + ) + self.assertEqual(glob.glob(self.db_path + ".backup_*"), []) + finally: + gate.set() + await asyncio.wait_for(scan.task, 5) + + async def test_scan_refused_while_legacy_restore_holds(self): + upload = self._make_db_file(["restored"]) + entered = threading.Event() + release = threading.Event() + model = self.api.db.model + real_verify = model.verify_database_file + + def gated(path, **kwargs): + entered.set() + release.wait(10) + return real_verify(path, **kwargs) + + loop = asyncio.get_running_loop() + with patch.object(model, "verify_database_file", gated): + restore = asyncio.ensure_future(self._restore(upload)) + self.assertTrue(await loop.run_in_executor(None, entered.wait, 10)) + resp = await self.client.post("/prompt_manager/jobs/scan") + body = await resp.json() + listed = (await (await self.client.get("/prompt_manager/jobs")).json())[ + "jobs" + ] + release.set() + restored = await restore + + self.assertEqual((resp.status, body["code"]), (409, "busy")) + self.assertEqual(body["running"]["title"], "Database restore (admin page)") + self.assertEqual( + [(j["kind"], j["state"], j["cancellable"]) for j in listed], + [("restore", "running", False)], + ) + self.assertEqual(restored.status, 200) + self.assertEqual(self.api._jobs.list()[0].state, "finished") + self.assertIsNone(self.api._jobs.active("restore")) + + async def test_rejected_upload_ends_its_hold_failed(self): + resp = await self._restore(self._write("notes.txt", b"not a database")) + self.assertEqual(resp.status, 400) + job = self.api._jobs.list()[0] + self.assertEqual((job.kind, job.state), ("restore", "failed")) + + class TestRestoreRoute(BackupRestoreTestCase): async def test_restore_replaces_database_and_reports_backup(self): self._save("old prompt") diff --git a/tests/test_api_core.py b/tests/test_api_core.py index 8971810..4af56a9 100644 --- a/tests/test_api_core.py +++ b/tests/test_api_core.py @@ -486,6 +486,30 @@ class TestStaticRoutes(CoreRoutesTestCase): self.assertEqual(resp.status, 200) self.assertIn("application/javascript", resp.headers["Content-Type"]) + async def test_inspection_semantics_table_is_the_one_json_served(self): + """Local Inspect reads the node table the server resolves with (#249 PR L3). + + Only that exact file is served; every other JSON stays forbidden. + """ + resp = await self.client.request( + "GET", "/prompt_manager/js/inspection-semantics.json" + ) + self.assertEqual(resp.status, 200) + self.assertIn("application/json", resp.headers["Content-Type"]) + table = Path(__file__).resolve().parents[1] / "web" / "js" + self.assertEqual( + await resp.read(), (table / "inspection-semantics.json").read_bytes() + ) + for path in ( + "js/shared/toolkit/provenance.json", + "lib/viewerjs/provenance.json", + "lib/inspection-semantics.json", + "js/workspace/..%2Finspection-semantics.json", + "js/INSPECTION-SEMANTICS.json", + ): + resp = await self.client.request("GET", f"/prompt_manager/{path}") + self.assertEqual(resp.status, 403, path) + async def test_lib_served(self): web_dir = self._fake_root() (web_dir / "lib" / "x.css").write_text("body{}") @@ -578,6 +602,28 @@ class TestStaticRoutes(CoreRoutesTestCase): resp = await self.client.request("GET", "/prompt_manager/lib/link.js") self.assertEqual(resp.status, 403) + async def test_inspection_semantics_symlink_escaping_web_dir_is_403(self): + """The exact-path JSON gets the same realpath containment as any file.""" + web_dir = self._fake_root() + secret = Path(self.tmpdir) / "secret.json" + secret.write_text('{"secret": true}') + try: + os.symlink(secret, web_dir / "js" / "inspection-semantics.json") + except (OSError, NotImplementedError): + self.skipTest("symlinks not available") + resp = await self.client.request( + "GET", "/prompt_manager/js/inspection-semantics.json" + ) + self.assertEqual(resp.status, 403) + self.assertNotIn("secret", await resp.text()) + + async def test_inspection_semantics_missing_is_404(self): + self._fake_root() + resp = await self.client.request( + "GET", "/prompt_manager/js/inspection-semantics.json" + ) + self.assertEqual(resp.status, 404) + async def test_empty_path_is_not_served(self): for prefix in ("lib", "js"): resp = await self.client.request("GET", f"/prompt_manager/{prefix}/") diff --git a/tests/test_api_images.py b/tests/test_api_images.py index 18062bb..8c0361c 100644 --- a/tests/test_api_images.py +++ b/tests/test_api_images.py @@ -5,11 +5,13 @@ Uses aiohttp's test client against a real PromptManagerAPI wired to a temporary SQLite database and a temporary output directory. """ +import asyncio import json import os import pathlib import sys import tempfile +import threading import unittest from contextlib import ExitStack from pathlib import Path @@ -638,6 +640,93 @@ class TestGenerateThumbnailsPost(ThumbnailTestCase): self.assertEqual(data["total_images"], 0) +class TestThumbnailRoutesHoldTheRegistry(ThumbnailTestCase): + """Thumbnail work is ``guarded``: never beside a record-changing job.""" + + def _run_scan(self): + gate = asyncio.Event() + + async def runner(job): + await gate.wait() + + job = self.api._jobs.start( + "scan", "Scan for metadata", admission="records", scope={}, runner=runner + ) + + async def release(): + gate.set() + await asyncio.wait_for(job.task, 5) + + self.addAsyncCleanup(release) + return job + + async def test_thumbnail_generate_refused_while_scan_runs(self): + make_png(self.output_dir / "a.png") + self._run_scan() + + resp = await self.client.request( + "POST", "/prompt_manager/images/generate-thumbnails", json={} + ) + self.assertEqual(resp.status, 409) + self.assertEqual( + await resp.json(), + {"success": False, "error": "Scan for metadata already running"}, + ) + + resp = await self.client.request("POST", self.PROGRESS_URL) + text = await resp.text() + self.assertEqual(resp.status, 200) # the gallery reads an SSE stream + self.assertEqual( + text, + "event: error\ndata: " + + json.dumps( + { + "error": "Scan for metadata already running", + "message": "Scan for metadata already running", + } + ) + + "\n\n", + ) + + thumbs = self.output_dir / "thumbnails" + make_png(thumbs / "b_thumb.png") + resp = await self.client.request( + "POST", "/prompt_manager/images/clear-thumbnails" + ) + self.assertEqual(resp.status, 409) + self.assertFalse(self._thumb_path("a").exists()) + self.assertTrue((thumbs / "b_thumb.png").exists()) + + async def test_scan_refused_while_thumbnails_generate(self): + make_png(self.output_dir / "a.png") + entered = threading.Event() + release = threading.Event() + real = self.api._generate_gallery_thumbnails_sync + + def gated(*args): + entered.set() + release.wait(10) + return real(*args) + + self.api._generate_gallery_thumbnails_sync = gated + loop = asyncio.get_running_loop() + generate = asyncio.ensure_future( + self.client.request( + "POST", "/prompt_manager/images/generate-thumbnails", json={} + ) + ) + self.assertTrue(await loop.run_in_executor(None, entered.wait, 10)) + resp = await self.client.request("POST", "/prompt_manager/jobs/scan") + body = await resp.json() + self.assertEqual((resp.status, body["code"]), (409, "busy")) + self.assertEqual( + body["running"]["title"], "Thumbnail generation (gallery page)" + ) + release.set() + self.assertEqual((await generate).status, 200) + self.assertTrue(self._thumb_path("a").exists()) + + class TestRecentImagesCaps(ImageAPITestCase): """Every path through /images/recent hands the DB a bounded window.""" diff --git a/tests/test_api_lora.py b/tests/test_api_lora.py index 36b9955..810671b 100644 --- a/tests/test_api_lora.py +++ b/tests/test_api_lora.py @@ -4,11 +4,13 @@ Filesystem detection and network downloads are patched out so the tests run anywhere without a LoraManager install or a CivitAI connection. """ +import asyncio import json import os import shutil import sys import tempfile +import threading import types import unittest from pathlib import Path @@ -34,11 +36,20 @@ from aiohttp.test_utils import AioHTTPTestCase from database.operations import PromptDatabase from py.api import PromptManagerAPI -from py import lora_utils +from py import config_store, lora_utils from py.config import IntegrationConfig, PromptManagerConfig +from py.api import settings_routes as settings_module +from py.api import lora_integration as lora_module from py.lora_utils import get_trigger_cache from utils.hashing import generate_prompt_hash +try: + from tests.lock_probe import ProbeLock + from tests.open_handles import assert_closed +except ImportError: # discovered with tests/ as the top-level directory + from lock_probe import ProbeLock + from open_handles import assert_closed + SECRET_KEY = "sk-civitai-super-secret-0123456789" @@ -56,7 +67,13 @@ class LoraAPITestCase(AioHTTPTestCase): return_value=[self.custom_nodes], ) self._custom_nodes_patch.start() - self._real_save = PromptManagerConfig.save_to_file + # Never the repository's config.json (live user data): every save in + # these tests goes to a file in this test's temp directory. + self.config_file = self.tmp_root / "config.json" + self._env_patch = patch.dict( + os.environ, {"PROMPT_MANAGER_CONFIG_PATH": str(self.config_file)} + ) + self._env_patch.start() self._config_patch = patch.multiple( IntegrationConfig, @@ -70,8 +87,6 @@ class LoraAPITestCase(AioHTTPTestCase): "py.lora_utils.detect_lora_manager", return_value=None ) self.detect_mock = self._detect_patch.start() - self._save_patch = patch.object(PromptManagerConfig, "save_to_file") - self.save_mock = self._save_patch.start() get_trigger_cache().clear() app = web.Application() @@ -83,7 +98,7 @@ class LoraAPITestCase(AioHTTPTestCase): async def tearDownAsync(self): await super().tearDownAsync() # closes the aiohttp test client - self._save_patch.stop() + self._env_patch.stop() self._detect_patch.stop() self._config_patch.stop() self._custom_nodes_patch.stop() @@ -96,8 +111,19 @@ class LoraAPITestCase(AioHTTPTestCase): ): if os.path.exists(path): os.unlink(path) + assert_closed(self, self.tmp_root) shutil.rmtree(self.tmp_root, ignore_errors=True) + def _saved(self): + return json.loads(self.config_file.read_text()) + + def _make_unwritable(self): + """Point the config path under a regular file, so no write can succeed.""" + blocker = self.tmp_root / "not-a-dir" + blocker.write_text("x") + self.config_file = blocker / "config.json" + os.environ["PROMPT_MANAGER_CONFIG_PATH"] = str(self.config_file) + async def _post_json(self, url, body): return await self.client.request( "POST", @@ -250,7 +276,7 @@ class TestLoraEnableRoute(LoraAPITestCase): ) self.assertEqual(resp.status, 400) self.assertFalse(IntegrationConfig.LORA_MANAGER_ENABLED) - self.save_mock.assert_not_called() + self.assertFalse(self.config_file.exists()) async def test_enable_persists_and_loads_trigger_cache(self): lm = _make_lora_manager(self.custom_nodes) @@ -285,7 +311,15 @@ class TestLoraEnableRoute(LoraAPITestCase): os.path.normcase(os.path.realpath(str(lm))), ) self.assertEqual(IntegrationConfig.CIVITAI_API_KEY, SECRET_KEY) - self.save_mock.assert_called_once_with() + saved = self._saved()["integrations"]["lora_manager"] + self.assertEqual(saved["civitai_api_key"], SECRET_KEY) + self.assertEqual(saved["path"], IntegrationConfig.LORA_MANAGER_PATH) + self.assertIs(saved["enabled"], True) + self.assertIs(saved["trigger_words_enabled"], True) + self.assertEqual( + (data["persisted"], data["runtime_applied"], data["cache_loaded"]), + (True, True, True), + ) self.assertTrue(get_trigger_cache().is_loaded) self.assertEqual(get_trigger_cache().get_trigger_words("neon"), ["glow"]) @@ -319,7 +353,7 @@ class TestLoraEnableRoute(LoraAPITestCase): self.assertFalse(json.loads(body)["success"]) self.assertNotIn(str(self.tmp_root), body) self.assertFalse(IntegrationConfig.LORA_MANAGER_ENABLED) - self.save_mock.assert_not_called() + self.assertFalse(self.config_file.exists()) async def test_enable_with_custom_node_lacking_lora_in_its_name_is_400(self): lm = _make_lora_manager(self.custom_nodes).parent / "ComfyUI-Other" @@ -344,7 +378,6 @@ class TestLoraEnableRoute(LoraAPITestCase): async def test_enable_writes_the_configured_config_file(self): config_file = self.tmp_root / "cfg" / "config.json" - self.save_mock.side_effect = self._real_save with patch.dict(os.environ, {"PROMPT_MANAGER_CONFIG_PATH": str(config_file)}): resp = await self._post_json( @@ -353,7 +386,6 @@ class TestLoraEnableRoute(LoraAPITestCase): ) self.assertEqual(resp.status, 200) - self.save_mock.assert_called_once_with() self.assertTrue(config_file.is_file()) saved = json.loads(config_file.read_text())["integrations"]["lora_manager"] self.assertFalse(saved["enabled"]) @@ -414,9 +446,299 @@ class TestLoraEnableRoute(LoraAPITestCase): self.assertEqual(resp.status, 400) async def test_persist_failure_is_500(self): - self.save_mock.side_effect = OSError("read-only") - resp = await self._post_json("/prompt_manager/lora/enable", {"enabled": False}) + self._make_unwritable() + with self.assertLogs("prompt_manager.api", level="ERROR"): + resp = await self._post_json( + "/prompt_manager/lora/enable", {"enabled": False} + ) self.assertEqual(resp.status, 500) + self.assertEqual((await resp.json())["code"], "config-write-failed") + + async def test_enable_write_failure_reports_and_keeps_runtime(self): + # #220: a save that cannot be written publishes nothing and says so. + lm = _make_lora_manager(self.custom_nodes) + self.detect_mock.return_value = str(lm) + IntegrationConfig.CIVITAI_API_KEY = SECRET_KEY + self._make_unwritable() + + with self.assertLogs("prompt_manager.api", level="ERROR"): + resp = await self._post_json( + "/prompt_manager/lora/enable", + { + "enabled": True, + "path": str(lm), + "trigger_words_enabled": True, + "clear_civitai_api_key": True, + }, + ) + + text = await resp.text() + body = json.loads(text) + self.assertEqual(resp.status, 500) + self.assertEqual(body["code"], "config-write-failed") + self.assertEqual((body["persisted"], body["runtime_applied"]), (False, False)) + self.assertFalse(IntegrationConfig.LORA_MANAGER_ENABLED) + self.assertEqual(IntegrationConfig.LORA_MANAGER_PATH, "") + self.assertFalse(IntegrationConfig.LORA_TRIGGER_WORDS_ENABLED) + self.assertEqual(IntegrationConfig.CIVITAI_API_KEY, SECRET_KEY) + self.assertFalse(get_trigger_cache().is_loaded) + self.assertNotIn(SECRET_KEY, text) + self.assertNotIn(str(self.tmp_root), text) + + async def test_enable_preserves_unknown_sections(self): + stored = "stored-value" # a plain marker: what is on disk must stay + self.config_file.write_text( + json.dumps( + { + "custom_section": {"keep": True}, + "web_ui": {"result_timeout": 11}, + "integrations": { + "lora_manager": {"civitai_api_key": stored, "extra": 1}, + "other_tool": {"on": True}, + }, + } + ) + ) + IntegrationConfig.CIVITAI_API_KEY = stored + + resp = await self._post_json( + "/prompt_manager/lora/enable", + {"enabled": False, "trigger_words_enabled": True}, + ) + + self.assertEqual(resp.status, 200) + saved = self._saved() + self.assertEqual(saved["custom_section"], {"keep": True}) + self.assertEqual(saved["web_ui"], {"result_timeout": 11}) + self.assertEqual(saved["integrations"]["other_tool"], {"on": True}) + lora = saved["integrations"]["lora_manager"] + self.assertEqual(lora["extra"], 1) + self.assertEqual(lora["civitai_api_key"], stored) + self.assertIs(lora["trigger_words_enabled"], True) + self.assertNotIn("database", saved) # no runtime defaults written + + async def test_enable_refuses_a_corrupt_config_file(self): + self.config_file.write_bytes(b"{not json") + resp = await self._post_json( + "/prompt_manager/lora/enable", + {"enabled": False, "trigger_words_enabled": True}, + ) + body = await resp.json() + self.assertEqual((resp.status, body["code"]), (409, "config-unreadable")) + self.assertEqual(self.config_file.read_bytes(), b"{not json") + self.assertFalse(IntegrationConfig.LORA_TRIGGER_WORDS_ENABLED) + + async def test_enable_requires_boolean_flags_and_body_cap(self): + for body in ( + {"enabled": "yes"}, + {"enabled": False, "trigger_words_enabled": 1}, + {"enabled": False, "clear_civitai_api_key": "true"}, + ): + with self.subTest(body=body): + resp = await self._post_json("/prompt_manager/lora/enable", body) + self.assertEqual(resp.status, 400) + resp = await self._post_json("/prompt_manager/lora/enable", [True]) + self.assertEqual(resp.status, 400) + resp = await self._post_json( + "/prompt_manager/lora/enable", + {"enabled": False, "civitai_api_key": "k" * (2 * 1024 * 1024)}, + ) + self.assertEqual(resp.status, 413) + self.assertFalse(self.config_file.exists()) + self.assertFalse(IntegrationConfig.LORA_MANAGER_ENABLED) + + async def test_blank_key_unchanged_and_explicit_clear(self): + self.config_file.write_text( + json.dumps({"integrations": {"lora_manager": {"civitai_api_key": "old"}}}) + ) + IntegrationConfig.CIVITAI_API_KEY = "old" + + resp = await self._post_json( + "/prompt_manager/lora/enable", {"enabled": False, "civitai_api_key": " "} + ) + self.assertEqual(resp.status, 200) + self.assertEqual( + self._saved()["integrations"]["lora_manager"]["civitai_api_key"], "old" + ) + self.assertEqual(IntegrationConfig.CIVITAI_API_KEY, "old") + + resp = await self._post_json( + "/prompt_manager/lora/enable", + {"enabled": False, "clear_civitai_api_key": True}, + ) + self.assertEqual(resp.status, 200) + self.assertEqual( + self._saved()["integrations"]["lora_manager"]["civitai_api_key"], "" + ) + self.assertEqual(IntegrationConfig.CIVITAI_API_KEY, "") + + async def test_cache_load_failure_reports_persisted_but_unloaded(self): + lm = _make_lora_manager(self.custom_nodes) + self.detect_mock.return_value = str(lm) + + with ( + patch.object( + type(get_trigger_cache()), "load", side_effect=OSError("unreadable") + ), + self.assertLogs("prompt_manager.api", level="WARNING"), + ): + resp = await self._post_json( + "/prompt_manager/lora/enable", + {"enabled": True, "path": str(lm), "trigger_words_enabled": True}, + ) + + text = await resp.text() + body = json.loads(text) + self.assertEqual(resp.status, 200) + self.assertIs(body["success"], True) + self.assertEqual((body["persisted"], body["cache_loaded"]), (True, False)) + self.assertTrue(IntegrationConfig.LORA_MANAGER_ENABLED) + self.assertIs(self._saved()["integrations"]["lora_manager"]["enabled"], True) + self.assertNotIn("unreadable", text) + + +class TestConfigWritersShareOneStore(LoraAPITestCase): + + async def asyncSetUp(self): + await super().asyncSetUp() + self._orig_timeout = PromptManagerConfig.RESULT_TIMEOUT + + async def asyncTearDown(self): + PromptManagerConfig.RESULT_TIMEOUT = self._orig_timeout + await super().asyncTearDown() + + async def test_settings_and_lora_saves_do_not_overwrite_each_other(self): + # #220: the LoRA save is sent while the settings save is inside its + # commit (lock held, file already read). The LoRA commit re-reads the + # file under the same lock, so both edits survive and nothing else + # is lost (before, the LoRA save rewrote the whole runtime config). + self.config_file.write_text(json.dumps({"custom_section": {"keep": True}})) + inside = threading.Event() + release = threading.Event() + real_mutation = settings_module._settings_mutation + + def gated_mutation(updates, new_roots): + mutate = real_mutation(updates, new_roots) + + def gated(config): + inside.set() + release.wait(10) + return mutate(config) + + return gated + + lora_calls = [] + real_lora_mutation = lora_module._lora_settings_mutation + + def recorded_lora_mutation(values): + mutate = real_lora_mutation(values) + + def recorded(config): + lora_calls.append(dict(config)) + return mutate(config) + + return recorded + + probe = ProbeLock() + loop = asyncio.get_running_loop() + with ( + patch.object(settings_module, "_settings_mutation", gated_mutation), + patch.object( + lora_module, "_lora_settings_mutation", recorded_lora_mutation + ), + patch.object(config_store, "_COMMIT_LOCK", probe), + ): + settings = asyncio.ensure_future( + self._post_json("/prompt_manager/settings", {"result_timeout": 13}) + ) + self.assertTrue(await loop.run_in_executor(None, inside.wait, 10)) + lora = asyncio.ensure_future( + self._post_json( + "/prompt_manager/lora/enable", + {"enabled": False, "trigger_words_enabled": True}, + ) + ) + # Wait until the LoRA commit is blocked on the lock the settings + # commit holds; its mutation must not have run yet. + self.assertTrue(await loop.run_in_executor(None, probe.contended.wait, 10)) + self.assertEqual(lora_calls, []) + release.set() + settings_resp, lora_resp = await asyncio.gather(settings, lora) + self.assertEqual(len(lora_calls), 1) + self.assertEqual(lora_calls[0]["web_ui"]["result_timeout"], 13) + + self.assertEqual((settings_resp.status, lora_resp.status), (200, 200)) + saved = self._saved() + self.assertEqual(saved["custom_section"], {"keep": True}) + self.assertEqual(saved["web_ui"]["result_timeout"], 13) + self.assertIs( + saved["integrations"]["lora_manager"]["trigger_words_enabled"], True + ) + settings_body = await settings_resp.json() + lora_body = await lora_resp.json() + self.assertNotEqual(settings_body["revision"], lora_body["revision"]) + + def _delay_first_commit_result(self): + """Hold back the first config commit's result from its handler. + + The commit itself has run (the file is written); only the handler's + continuation waits until the returned event is set. Before #220's fix + round the handler published after this point, so a later save could + publish first and the earlier one overwrite it at runtime. + """ + original = self.api._run_in_executor + written = asyncio.Event() + resume = asyncio.Event() + delayed = [] + + async def run(func, *args, **kwargs): + result = await original(func, *args, **kwargs) + if getattr(func, "__func__", None) is config_store.ConfigStore.commit: + if not delayed: + delayed.append(func) + written.set() + await resume.wait() + return result + + patcher = patch.object(self.api, "_run_in_executor", run) + patcher.start() + self.addCleanup(patcher.stop) + return written, resume + + async def test_concurrent_settings_saves_end_on_the_last_written(self): + written, resume = self._delay_first_commit_result() + first = asyncio.ensure_future( + self._post_json("/prompt_manager/settings", {"result_timeout": 11}) + ) + await asyncio.wait_for(written.wait(), 10) + second = await self._post_json( + "/prompt_manager/settings", {"result_timeout": 22} + ) + resume.set() + first = await first + self.assertEqual((first.status, second.status), (200, 200)) + self.assertEqual(self._saved()["web_ui"]["result_timeout"], 22) + self.assertEqual(PromptManagerConfig.RESULT_TIMEOUT, 22) + + async def test_concurrent_lora_saves_end_on_the_last_written(self): + written, resume = self._delay_first_commit_result() + first = asyncio.ensure_future( + self._post_json( + "/prompt_manager/lora/enable", + {"enabled": False, "trigger_words_enabled": True}, + ) + ) + await asyncio.wait_for(written.wait(), 10) + second = await self._post_json( + "/prompt_manager/lora/enable", + {"enabled": False, "trigger_words_enabled": False}, + ) + resume.set() + first = await first + self.assertEqual((first.status, second.status), (200, 200)) + lora = self._saved()["integrations"]["lora_manager"] + self.assertIs(lora["trigger_words_enabled"], False) + self.assertIs(IntegrationConfig.LORA_TRIGGER_WORDS_ENABLED, False) class TestLoraScanRoute(LoraAPITestCase): @@ -445,6 +767,12 @@ class TestLoraScanRoute(LoraAPITestCase): body = await resp.text() self.assertNotIn(str(self.tmp_root), body) self.assertIn("x.db", body) + # The admin page's modal shows the failure as its status and closes. + error = self._sse_events(body)[-1] + self.assertEqual( + (error["type"], error["status"], error["progress"]), + ("error", error["message"], 0), + ) @staticmethod def _sse_events(text): diff --git a/tests/test_api_prompt_inspector.py b/tests/test_api_prompt_inspector.py index 4163b43..56c0523 100644 --- a/tests/test_api_prompt_inspector.py +++ b/tests/test_api_prompt_inspector.py @@ -1643,6 +1643,62 @@ class TestPromptInspector(ImageAPITestCase): str(path), False, report_ambiguity=True ) + async def test_unhashable_workflow_node_type_is_skipped(self): + """A UI workflow node whose ``type`` is a list or dict is skipped (#286). + + The R1 check looks each node's ``type`` up among subgraph IDs; an + unhashable ``type`` used to raise there and the route answered 500. + Such a node can be neither a subgraph instance nor a PromptManager + node, so the walk skips it and a prompt that resolves still resolves. + """ + source = graph() + source["1"]["class_type"] = "PromptManager" + for kind in (["PromptManager"], {"name": "PromptManager"}): + with self.subTest(kind=type(kind).__name__): + workflow = json.dumps( + {"nodes": [{"id": 1, "type": kind, "widgets_values": ["x"]}]} + ) + png( + self.output_dir / "image.png", + prompt=json.dumps(source), + workflow=workflow, + ) + response, result = await self.inspect() + self.assertEqual(response.status, 200) + self.assertEqual(result["resolution"]["state"], "resolved") + self.assertEqual(result["resolution"]["sampler_id"], "3") + self.assertEqual(result["prompts"]["positive"]["status"], "resolved") + self.assertEqual(result["prompts"]["positive"]["text"], "portrait") + self.assertEqual(result["prompts"]["negative"]["text"], "blurry") + self.assertNotIn("unhashable", json.dumps(result)) + + async def test_unhashable_graph_class_type_is_not_a_preview_output(self): + """A list or object ``class_type`` never names a preview output (#286). + + With several samplers the inspector first drops stages that reach only + previews; looking such a ``class_type`` up among the preview classes + used to raise, and the route answered 500. + """ + for kind in (["PreviewImage"], {"name": "PreviewImage"}): + with self.subTest(kind=type(kind).__name__): + source = graph() + source["9"] = { + "class_type": "KSampler", + "inputs": {"positive": ["8", 0], "negative": ["2", 0]}, + } + source["8"] = { + "class_type": "CLIPTextEncode", + "inputs": {"text": "cat"}, + } + source["10"] = {"class_type": kind, "inputs": {"images": ["9", 0]}} + png(self.output_dir / "image.png", prompt=json.dumps(source)) + response, result = await self.inspect() + self.assertEqual(response.status, 200) + self.assertEqual( + result["resolution"]["reason"], "Multiple sampler branches" + ) + self.assertEqual(result["resolution"]["state"], "ambiguous") + class TestInspectorImageTags(ImageAPITestCase): """B3: the inspector envelope's ``image_tags`` facet (spec amendment S1).""" diff --git a/tests/test_config_store.py b/tests/test_config_store.py new file mode 100644 index 0000000..5099dbb --- /dev/null +++ b/tests/test_config_store.py @@ -0,0 +1,419 @@ +"""Tests for the serialized config store (py/config_store.py, #220). + +Every test works on a config.json inside its own temporary directory; the +repository's config.json (live user data) is never read or written. +""" + +import hashlib +import json +import os +import shutil +import sys +import tempfile +import threading +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +_mock_server = MagicMock() +_mock_server.PromptServer.instance.routes = MagicMock() +sys.modules.setdefault("server", _mock_server) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from py import config_store # noqa: E402 +from py.config_store import ( # noqa: E402 + ABSENT_REVISION, + ConfigStore, + ConfigUnreadable, + RevisionConflict, + get_config_store, + revision_of, +) + +try: + from tests.lock_probe import ProbeLock # noqa: E402 + from tests.open_handles import assert_closed # noqa: E402 +except ImportError: # discovered with tests/ as the top-level directory + from lock_probe import ProbeLock # noqa: E402 + from open_handles import assert_closed # noqa: E402 + + +class ConfigStoreTests(unittest.TestCase): + def setUp(self): + self.tmpdir = os.path.realpath(tempfile.mkdtemp()) + self.path = os.path.join(self.tmpdir, "config.json") + self.store = ConfigStore(lambda: self.path) + + def tearDown(self): + try: + assert_closed(self, self.tmpdir) + finally: + shutil.rmtree(self.tmpdir, ignore_errors=True) + + def _write(self, data): + Path(self.path).write_text(json.dumps(data), encoding="utf-8") + + def _bytes(self): + return Path(self.path).read_bytes() + + def _leftovers(self): + return sorted(name for name in os.listdir(self.tmpdir) if name != "config.json") + + # ── read ────────────────────────────────────────────────────────── + + def test_read_of_a_missing_file_is_empty_and_absent(self): + self.assertEqual(self.store.read(), ({}, ABSENT_REVISION)) + + def test_read_reports_the_sha256_of_the_file_bytes(self): + self._write({"web_ui": {"result_timeout": 3}}) + data, revision = self.store.read() + self.assertEqual(data, {"web_ui": {"result_timeout": 3}}) + expected = "sha256:" + hashlib.sha256(self._bytes()).hexdigest() + self.assertEqual(revision, expected) + self.assertEqual(revision_of(self._bytes()), expected) + + def test_non_object_config_is_unreadable(self): + self._write([1, 2]) + with self.assertRaises(ConfigUnreadable): + self.store.read() + + def test_unreadable_path_is_unreadable(self): + os.mkdir(self.path) # a directory where the file should be + with self.assertRaises(ConfigUnreadable): + self.store.read() + + # ── commit ──────────────────────────────────────────────────────── + + def test_commit_preserves_unknown_sections_and_secret(self): + self._write( + { + "custom_section": {"keep": True}, + "integrations": {"lora_manager": {"civitai_api_key": "secret"}}, + "web_ui": {"result_timeout": 1, "show_test_button": True}, + } + ) + + def mutate(config): + config.setdefault("web_ui", {})["result_timeout"] = 7 + return config + + saved, revision = self.store.commit(mutate, expected_revision=None) + + on_disk = json.loads(self._bytes()) + self.assertEqual(saved, on_disk) + self.assertEqual(on_disk["custom_section"], {"keep": True}) + self.assertEqual( + on_disk["integrations"]["lora_manager"]["civitai_api_key"], "secret" + ) + self.assertEqual( + on_disk["web_ui"], {"result_timeout": 7, "show_test_button": True} + ) + self.assertEqual(revision, revision_of(self._bytes())) + self.assertEqual(self.store.read(), (on_disk, revision)) + + def test_commit_creates_a_missing_file_and_its_parent(self): + self.path = os.path.join(self.tmpdir, "nested", "config.json") + saved, revision = self.store.commit( + lambda config: {**config, "a": 1}, expected_revision=ABSENT_REVISION + ) + self.assertEqual(saved, {"a": 1}) + self.assertEqual(json.loads(Path(self.path).read_text()), {"a": 1}) + self.assertNotEqual(revision, ABSENT_REVISION) + + def test_mutate_works_on_a_copy(self): + self._write({"web_ui": {"result_timeout": 1}}) + seen = {} + + def mutate(config): + seen["config"] = config + config["web_ui"]["result_timeout"] = 2 + raise RuntimeError("validation failed late") + + before = self._bytes() + with self.assertRaises(RuntimeError): + self.store.commit(mutate, expected_revision=None) + self.assertEqual(self._bytes(), before) + self.assertEqual(self.store.read()[0], {"web_ui": {"result_timeout": 1}}) + + def test_mutate_must_return_an_object(self): + self._write({"a": 1}) + before = self._bytes() + with self.assertRaises(TypeError): + self.store.commit(lambda config: None, expected_revision=None) + self.assertEqual(self._bytes(), before) + + def test_malformed_existing_config_is_refused_and_untouched(self): + Path(self.path).write_bytes(b"{not json") + calls = [] + with self.assertRaises(ConfigUnreadable): + self.store.commit( + lambda config: calls.append(config) or config, expected_revision=None + ) + self.assertEqual(calls, []) + self.assertEqual(self._bytes(), b"{not json") + self.assertEqual(self._leftovers(), []) + + def test_write_failure_leaves_bytes_and_raises(self): + self._write({"a": 1}) + before = self._bytes() + with patch.object(config_store.os, "fsync", side_effect=OSError("disk full")): + with self.assertRaises(OSError): + self.store.commit(lambda c: {**c, "b": 2}, expected_revision=None) + self.assertEqual(self._bytes(), before) + self.assertEqual(self._leftovers(), []) + + def test_unwritable_directory_raises_oserror(self): + blocker = os.path.join(self.tmpdir, "afile") + Path(blocker).write_text("x") + self.path = os.path.join(blocker, "config.json") # parent is a file + with self.assertRaises(OSError): + self.store.commit(lambda c: {"a": 1}, expected_revision=None) + + def test_stale_revision_conflicts_without_write(self): + self._write({"a": 1}) + _data, revision = self.store.read() + self._write({"a": 2}) # another writer got there first + before = self._bytes() + calls = [] + with self.assertRaises(RevisionConflict) as caught: + self.store.commit( + lambda c: calls.append(c) or c, expected_revision=revision + ) + self.assertEqual(caught.exception.current, revision_of(before)) + self.assertEqual(calls, []) + self.assertEqual(self._bytes(), before) + + def test_matching_revision_commits(self): + self._write({"a": 1}) + _data, revision = self.store.read() + saved, _ = self.store.commit( + lambda c: {**c, "b": 2}, expected_revision=revision + ) + self.assertEqual(saved, {"a": 1, "b": 2}) + + def test_temp_file_removed_after_replace_failure(self): + self._write({"a": 1}) + before = self._bytes() + with patch.object( + config_store.os, "replace", side_effect=PermissionError("locked") + ): + with self.assertRaises(OSError): + self.store.commit(lambda c: {**c, "b": 2}, expected_revision=None) + self.assertEqual(self._bytes(), before) + self.assertEqual(self._leftovers(), []) + + @unittest.skipUnless(os.name == "posix", "POSIX file modes") + def test_committed_file_is_private_to_the_user(self): + import stat + + self._write({"a": 1}) + os.chmod(self.path, 0o644) + self.store.commit(lambda c: c, expected_revision=None) + self.assertEqual(stat.S_IMODE(os.stat(self.path).st_mode), 0o600) + + def _probe_lock(self): + probe = ProbeLock() + patcher = patch.object(config_store, "_COMMIT_LOCK", probe) + patcher.start() + self.addCleanup(patcher.stop) + return probe + + def _hold_lock_in_a_commit(self, value="first"): + """Start a commit that holds the lock until the returned event is set.""" + inside = threading.Event() + release = threading.Event() + errors = [] + + def slow(config): + inside.set() + release.wait(10) + config[value] = 1 + return config + + def run(): + try: + self.store.commit(slow, expected_revision=None) + except Exception as exc: # pragma: no cover - reported by the test + errors.append(exc) + + thread = threading.Thread(target=run) + thread.start() + self.assertTrue(inside.wait(10)) + return thread, release, errors + + def test_concurrent_commits_serialize(self): + # The second commit is observed blocked on the lock before the first + # one may finish, so this fails whenever commit stops taking the lock. + self._write({"keep": True}) + probe = self._probe_lock() + first, release, errors = self._hold_lock_in_a_commit() + calls = [] + + def fast(config): + calls.append(dict(config)) + config["second"] = 2 + return config + + second = threading.Thread( + target=lambda: self.store.commit(fast, expected_revision=None) + ) + second.start() + self.assertTrue(probe.contended.wait(10), "second commit never waited") + self.assertEqual(calls, []) # not called while the first holds the lock + release.set() + first.join(10) + second.join(10) + + self.assertEqual(errors, []) + self.assertEqual(calls, [{"keep": True, "first": 1}]) # re-read inside + self.assertEqual( + json.loads(self._bytes()), {"keep": True, "first": 1, "second": 2} + ) + + def test_read_waits_for_a_running_commit(self): + # Windows cannot replace a file another handle has open, so a read + # runs under the commit lock and never overlaps a replace. + self._write({"keep": True}) + probe = self._probe_lock() + first, release, errors = self._hold_lock_in_a_commit() + seen = [] + reader = threading.Thread(target=lambda: seen.append(self.store.read())) + reader.start() + self.assertTrue(probe.contended.wait(10), "read did not take the lock") + self.assertEqual(seen, []) + release.set() + first.join(10) + reader.join(10) + self.assertEqual(errors, []) + self.assertEqual(seen[0][0], {"keep": True, "first": 1}) + self.assertEqual(seen[0][1], revision_of(self._bytes())) + + def test_replace_is_retried_after_a_transient_permission_error(self): + # Windows: an antivirus or indexer handle makes os.replace fail with + # PermissionError for a moment; the commit retries before failing. + self._write({"a": 1}) + real_replace = os.replace + attempts = [] + + def flaky(src, dst): + attempts.append(dst) + if len(attempts) == 1: + raise PermissionError("in use by another process") + return real_replace(src, dst) + + with ( + patch.object(config_store, "_REPLACE_ATTEMPTS", 5), + patch.object(config_store.os, "replace", side_effect=flaky), + patch.object(config_store.time, "sleep") as sleep, + ): + saved, _ = self.store.commit( + lambda c: {**c, "b": 2}, expected_revision=None + ) + self.assertEqual(saved, {"a": 1, "b": 2}) + self.assertEqual(json.loads(self._bytes()), {"a": 1, "b": 2}) + self.assertEqual(len(attempts), 2) + sleep.assert_called_once_with(config_store._REPLACE_BACKOFF_SECONDS) + self.assertEqual(self._leftovers(), []) + + def test_replace_gives_up_after_the_last_attempt(self): + self._write({"a": 1}) + before = self._bytes() + with ( + patch.object(config_store, "_REPLACE_ATTEMPTS", 3), + patch.object( + config_store.os, "replace", side_effect=PermissionError("locked") + ) as replace, + patch.object(config_store.time, "sleep") as sleep, + ): + with self.assertRaises(PermissionError): + self.store.commit(lambda c: {**c, "b": 2}, expected_revision=None) + self.assertEqual((replace.call_count, sleep.call_count), (3, 2)) + self.assertEqual(self._bytes(), before) + self.assertEqual(self._leftovers(), []) + + def test_replace_is_tried_once_outside_windows(self): + expected = 5 if os.name == "nt" else 1 + self.assertEqual(config_store._REPLACE_ATTEMPTS, expected) + + @unittest.skipUnless(hasattr(os, "symlink"), "needs symlinks") + def test_symlinked_config_is_written_through_the_link(self): + # A config.json that is a symlink (or a single-file bind mount) is + # written beside its real target, so the link stays a link. + real_dir = os.path.join(self.tmpdir, "real") + os.mkdir(real_dir) + target = os.path.join(real_dir, "settings.json") + Path(target).write_text(json.dumps({"a": 1}), encoding="utf-8") + try: + os.symlink(target, self.path) + except (OSError, NotImplementedError): # pragma: no cover - Windows + self.skipTest("symlinks are not permitted here") + + saved, revision = self.store.commit( + lambda c: {**c, "b": 2}, expected_revision=None + ) + + self.assertTrue(os.path.islink(self.path)) + self.assertEqual(json.loads(Path(target).read_text()), {"a": 1, "b": 2}) + self.assertEqual(self.store.read(), (saved, revision)) + self.assertEqual(os.listdir(real_dir), ["settings.json"]) + + def test_publish_runs_under_the_lock_after_the_write(self): + # Runtimes publish inside the commit, so two saves publish in the + # order they were written and the runtime ends on the last one. + self._write({"a": 1}) + seen = [] + + # The lock is re-entrant, so another thread probes whether it is held. + def probe_publish(config): + result = [] + thread = threading.Thread( + target=lambda: result.append( + config_store._COMMIT_LOCK.acquire(blocking=False) + ) + ) + thread.start() + thread.join(10) + seen.append((config, json.loads(self._bytes()), result[0])) + + saved, _ = self.store.commit( + lambda c: {**c, "b": 2}, expected_revision=None, publish=probe_publish + ) + self.assertEqual(seen, [(saved, {"a": 1, "b": 2}, False)]) + + def test_publish_is_skipped_when_the_write_fails(self): + self._write({"a": 1}) + published = [] + with patch.object(config_store.os, "fsync", side_effect=OSError("disk full")): + with self.assertRaises(OSError): + self.store.commit( + lambda c: {**c, "b": 2}, + expected_revision=None, + publish=published.append, + ) + self.assertEqual(published, []) + + def test_handles_closed_after_commit(self): + self._write({"a": 1}) + self.store.read() + self.store.commit(lambda c: {**c, "b": 2}, expected_revision=None) + assert_closed(self, self.tmpdir) + os.unlink(self.path) # Windows refuses this while a handle is open + self.assertEqual(self._leftovers(), []) + + +class SingletonTests(unittest.TestCase): + def test_get_config_store_is_one_process_store_on_the_configured_path(self): + store = get_config_store() + self.assertIs(store, get_config_store()) + tmpdir = tempfile.mkdtemp() + try: + custom = os.path.join(tmpdir, "c.json") + with patch.dict(os.environ, {"PROMPT_MANAGER_CONFIG_PATH": custom}): + self.assertEqual(store.path, custom) + finally: + shutil.rmtree(tmpdir, ignore_errors=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_image_tag_jobs.py b/tests/test_image_tag_jobs.py index 3332422..8964b07 100644 --- a/tests/test_image_tag_jobs.py +++ b/tests/test_image_tag_jobs.py @@ -420,6 +420,23 @@ class TestStartAndExclusion(ImageTagJobTestCase): # A refused start never leaves the gate taken. self.assertIsNone(model_gate.holder()) + async def test_image_tag_job_refused_while_legacy_restore_holds(self): + self.png("a.png") + async with self.api._jobs.hold( + "restore", "Database restore (admin page)", admission="records" + ): + status, body = await self.start(self.add_body(["a.png"], ["x"])) + self.assertEqual((status, body["code"]), (409, "busy")) + self.assertEqual(body["running"]["title"], "Database restore (admin page)") + status, body = await self.start(self.auto_body(["a.png"])) + self.assertEqual((status, body["code"]), (409, "busy")) + self.assertIsNone(model_gate.holder()) + async with self.api._jobs.hold( + "thumbnails", "Thumbnail generation (gallery page)", admission="guarded" + ): + status, body = await self.start(self.add_body(["a.png"], ["x"])) + self.assertEqual((status, body["code"]), (409, "busy")) + async def test_record_changing_excludes_running_scan(self): self.png("a.png") gate = asyncio.Event() diff --git a/tests/test_init_package.py b/tests/test_init_package.py index df95d4e..a80a493 100644 --- a/tests/test_init_package.py +++ b/tests/test_init_package.py @@ -12,6 +12,7 @@ import json import os import sys import tempfile +import threading import types import unittest import unittest.mock as mock @@ -368,10 +369,29 @@ class TestConsoleBanner(unittest.TestCase): self.assertIn("Loaded:", banner) +class PriorFileHandler(logging.FileHandler): + """A caller-owned sink that stays closed once its owner closes it. + + ``FileHandler.emit`` reopens the file when ``stream`` is None, so a record + that selected this handler before ``close()`` would reopen prior.log after + it, and Windows then refuses the temp directory cleanup (WinError 32). + ``Handler.handle`` and ``FileHandler.close`` hold the same handler lock, so + checking ``_closed`` in ``emit`` is race free (as in the product's + ``_OwnedRotatingFileHandler``). + """ + + def emit(self, record): + if not self._closed: + super().emit(record) + + class TestPackageLoggerIsolation(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) + # Runs after restore_logging, before the cleanup Windows refuses while + # prior.log is open; /proc makes Linux fail the same way. + self.addCleanup(self.assert_tmp_has_no_open_handles) self.logger = logging.getLogger("prompt_manager") self.previous = ( self.logger.handlers[:], @@ -381,7 +401,7 @@ class TestPackageLoggerIsolation(unittest.TestCase): ) self.addCleanup(self.restore_logging) self.path = os.path.join(self.tmp.name, "prior.log") - self.handler = logging.FileHandler(self.path) + self.handler = PriorFileHandler(self.path) self.logger.handlers = [self.handler] self.logger.setLevel(logging.DEBUG) self.logger.propagate = True @@ -398,16 +418,20 @@ class TestPackageLoggerIsolation(unittest.TestCase): self.server.PromptServer.instance.prompt_queue.task_done = lambda *a, **k: None def restore_logging(self): - for handler in self.logger.handlers: - if handler not in self.previous[0]: - handler.close() - self.handler.close() + # Detach before closing, so new records stop selecting these handlers. + retired = [h for h in self.logger.handlers if h not in self.previous[0]] ( self.logger.handlers, self.logger.level, self.logger.propagate, self.logger.disabled, ) = self.previous + for handler in retired: + handler.close() + self.handler.close() + + def assert_tmp_has_no_open_handles(self): + self.assertEqual(open_handles_under(self.tmp.name), []) def assert_logging_restored(self, handlers, registration): for key, module in self.saved_modules.items(): @@ -456,6 +480,49 @@ class TestPackageLoggerIsolation(unittest.TestCase): self.assertEqual(open_at_disposal, [True]) self.assert_logging_restored(handlers, registration) + def test_record_in_flight_at_restore_cannot_reopen_prior_log(self): + """A record that selected prior.log before restore never reopens it. + + Leaked daemon threads of other tests (image workers, watchdog) log + under ``prompt_manager.*``. One that reaches the prior handler while + restore_logging closes it would, with a plain FileHandler, reopen the + file on emit and keep it open through TemporaryDirectory.cleanup(). + """ + message = "record selected before restore_logging" + selected = threading.Event() + resume = threading.Event() + errors = [] + + def pause_before_handler_lock(record): + if record.getMessage() == message: + selected.set() + if not resume.wait(5): + raise RuntimeError("logging thread was not released") + return True + + def log_from_background_thread(): + try: + logging.getLogger("prompt_manager.late_emitter").warning(message) + except BaseException as error: + errors.append(error) + + self.handler.addFilter(pause_before_handler_lock) + worker = threading.Thread(target=log_from_background_thread, daemon=True) + worker.start() + try: + self.assertTrue(selected.wait(5), "record did not select prior.log") + self.restore_logging() + self.assertIsNone(self.handler.stream) + finally: + resume.set() + worker.join(5) + self.assertFalse(worker.is_alive(), "logging thread did not finish") + self.assertEqual(errors, []) + self.assertIsNone(self.handler.stream) + self.assert_tmp_has_no_open_handles() + with open(self.path, encoding="utf-8") as log: + self.assertNotIn(message, log.read()) + def test_failed_package_import_closes_handlers_and_restores_logging(self): name = "prompt_manager_pkg_logger_failure" captured = {} diff --git a/tests/test_job_admission.py b/tests/test_job_admission.py new file mode 100644 index 0000000..59e4821 --- /dev/null +++ b/tests/test_job_admission.py @@ -0,0 +1,552 @@ +"""Registry admission classes and the legacy routes that hold the registry. + +Record-changing work never runs together (Activity design): a ``records`` +job is refused while any ``records`` or ``guarded`` job runs, a ``guarded`` +job (thumbnails) while a ``records`` job runs, and ``read`` jobs only share +the one-per-kind rule. The legacy admin and gallery routes that change +records or files take a registry hold for their duration, so a workspace job +and a legacy-page operation refuse each other, naming the one that runs. + +Fake runners and gated executor calls only; ordering is driven by events. +""" + +import asyncio +import hashlib +import json +import os +import sys +import threading +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +_mock_server = MagicMock() +_mock_server.PromptServer.instance.routes = MagicMock() +sys.modules.setdefault("server", _mock_server) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from py.api.jobs import ( # noqa: E402 + ADMISSIONS, + Job, + JobBusy, + JobRegistry, +) + +try: + from tests.test_api_admin import AdminAPITestCase # noqa: E402 +except ImportError: # discovered with tests/ as the top-level directory + from test_api_admin import AdminAPITestCase # noqa: E402 + + +def _gated(gate): + async def runner(job): + await gate.wait() + + return runner + + +async def _noop(job): + return None + + +class TestAdmissionClasses(unittest.IsolatedAsyncioTestCase): + + def _start(self, registry, kind, admission, gate=None, **kwargs): + return registry.start( + kind, + kind.title(), + admission=admission, + scope={}, + runner=_gated(gate) if gate is not None else _noop, + **kwargs, + ) + + async def test_admission_names_and_explicit_read(self): + self.assertEqual(ADMISSIONS, ("read", "guarded", "records")) + registry = JobRegistry() + job = registry.start( + "report", "Report", admission="read", scope={}, runner=_noop + ) + await asyncio.wait_for(job.task, 2) + self.assertEqual(job.admission, "read") + self.assertFalse(job.record_changing) + with self.assertRaises(ValueError): + registry.start("x", "X", admission="exclusive", scope={}, runner=_noop) + with self.assertRaises(ValueError): + registry.start( + "x", + "X", + admission="read", + record_changing=True, + scope={}, + runner=_noop, + ) + + async def test_start_requires_an_admission_class(self): + # A job that forgets its class must not silently run as "read" beside + # a scan or restore: every start call decides. + registry = JobRegistry() + with self.assertRaises(TypeError) as raised: + registry.start("review", "Review", scope={}, runner=_noop) + self.assertIn("admission", str(raised.exception)) + self.assertEqual(registry.list(), []) + job = registry.start( + "review", "Review", record_changing=False, scope={}, runner=_noop + ) + await asyncio.wait_for(job.task, 2) + self.assertEqual(job.admission, "read") + + async def test_record_changing_is_an_alias_for_records(self): + registry = JobRegistry() + gate = asyncio.Event() + job = registry.start( + "scan", "Scan", record_changing=True, scope={}, runner=_gated(gate) + ) + self.assertEqual(job.admission, "records") + self.assertTrue(job.record_changing) + self.assertEqual(job.summary()["admission"], "records") + self.assertIs( + registry.blocker("image-tags", record_changing=True), job + ) # the existing call shape keeps working + gate.set() + await asyncio.wait_for(job.task, 2) + + async def test_records_job_refused_while_guarded_runs(self): + registry = JobRegistry() + gate = asyncio.Event() + thumbs = self._start(registry, "thumbnails", "guarded", gate) + with self.assertRaises(JobBusy) as raised: + self._start(registry, "scan", "records") + self.assertIs(raised.exception.running, thumbs) + self.assertIs(registry.blocker("restore", admission="records"), thumbs) + gate.set() + await asyncio.wait_for(thumbs.task, 2) + scan = self._start(registry, "scan", "records") + await asyncio.wait_for(scan.task, 2) + self.assertEqual(scan.state, "finished") + + async def test_guarded_refused_while_records_runs(self): + registry = JobRegistry() + gate = asyncio.Event() + scan = self._start(registry, "scan", "records", gate) + with self.assertRaises(JobBusy) as raised: + self._start(registry, "thumbnails", "guarded") + self.assertIs(raised.exception.running, scan) + # A second guarded kind may run beside a guarded one, not beside records. + gate.set() + await asyncio.wait_for(scan.task, 2) + hold = asyncio.Event() + thumbs = self._start(registry, "thumbnails", "guarded", hold) + backup = self._start(registry, "backup", "guarded") + await asyncio.wait_for(backup.task, 2) + self.assertEqual(backup.state, "finished") + hold.set() + await asyncio.wait_for(thumbs.task, 2) + + async def test_read_jobs_run_in_parallel(self): + registry = JobRegistry() + gate = asyncio.Event() + scan = self._start(registry, "scan", "records", gate) + health = self._start(registry, "health", "read", gate) + duplicates = self._start(registry, "duplicates", "read", gate) + self.assertTrue(all(j.running for j in (scan, health, duplicates))) + with self.assertRaises(JobBusy) as raised: # one job of a kind + self._start(registry, "health", "read") + self.assertIs(raised.exception.running, health) + gate.set() + await asyncio.wait_for( + asyncio.gather(scan.task, health.task, duplicates.task), 2 + ) + + async def test_non_cancellable_job_ignores_cancel(self): + registry = JobRegistry() + gate = asyncio.Event() + job = self._start(registry, "restore", "records", gate, cancellable=False) + self.assertIs(job.summary()["cancellable"], False) + self.assertFalse(registry.cancel(job.id)) + self.assertFalse(job.cancelled) + gate.set() + await asyncio.wait_for(job.task, 2) + self.assertEqual(job.state, "finished") + + async def test_hold_registers_a_running_non_cancellable_job(self): + registry = JobRegistry() + held = registry.hold( + "restore", "Database restore (admin page)", admission="records" + ) + async with held as job: + self.assertIs(registry.active("restore"), job) + self.assertEqual(job.state, "running") + self.assertEqual( + (job.summary()["cancellable"], job.summary()["admission"]), + (False, "records"), + ) + self.assertFalse(registry.cancel(job.id)) + with self.assertRaises(JobBusy) as raised: + self._start(registry, "scan", "records") + self.assertIn("Database restore (admin page)", str(raised.exception)) + self.assertEqual(job.state, "finished") + self.assertIsNone(registry.active("restore")) + self.assertIs(registry.list()[0], job) + + async def test_hold_refused_without_waiting(self): + registry = JobRegistry() + gate = asyncio.Event() + scan = self._start(registry, "scan", "records", gate) + with self.assertRaises(JobBusy) as raised: + async with registry.hold("maintenance", "Maintenance", admission="records"): + self.fail("a refused hold must not run its body") + self.assertIs(raised.exception.running, scan) + self.assertEqual([j.kind for j in registry.list()], ["scan"]) + gate.set() + await asyncio.wait_for(scan.task, 2) + + async def test_hold_released_on_exception(self): + registry = JobRegistry() + with self.assertRaises(RuntimeError): + async with registry.hold( + "maintenance", "Maintenance", admission="records" + ) as job: + raise RuntimeError("boom") + self.assertEqual(job.state, "failed") + again = registry.hold("maintenance", "Maintenance", admission="records") + async with again as second: + second.finish("failed") # the body may end it itself + self.assertEqual(second.state, "failed") + + async def test_hold_admits_on_entry_not_at_the_call(self): + # A hold created but never entered (an await or an exception between + # the call and ``async with``) must not leave a running records job + # behind that blocks all record work until a restart. + registry = JobRegistry() + held = registry.hold("restore", "Restore", admission="records") + self.assertEqual(registry.list(), []) + self.assertIsNone(registry.blocker("scan", admission="records")) + del held + async with registry.hold("restore", "Restore", admission="records") as job: + self.assertIs(registry.active("restore"), job) + self.assertEqual(job.state, "finished") + + def test_job_defaults_stay_read_and_cancellable(self): + job = Job("duplicates", "Duplicate scan") + self.assertEqual((job.admission, job.cancellable), ("read", True)) + + +class _LegacyHoldCase(AdminAPITestCase): + """The admin test app plus a running workspace job on demand.""" + + def _run_job(self, kind="scan", title="Scan for metadata", admission="records"): + gate = asyncio.Event() + job = self.api._jobs.start( + kind, title, admission=admission, scope={}, runner=_gated(gate) + ) + self.addAsyncCleanup(self._release, gate, job) + return gate, job + + @staticmethod + async def _release(gate, job): + gate.set() + await asyncio.wait_for(job.task, 5) + + def _db_digest(self): + path = Path(self.api.db.model.db_path) + return { + suffix: hashlib.sha256(Path(f"{path}{suffix}").read_bytes()).hexdigest() + for suffix in ("", "-wal") + if Path(f"{path}{suffix}").exists() + } + + async def _busy(self, resp, title="Scan for metadata"): + body = await resp.json() + self.assertEqual(resp.status, 409) + self.assertEqual(body, {"success": False, "error": f"{title} already running"}) + + +class TestLegacyRoutesHoldTheRegistry(_LegacyHoldCase): + + async def test_hold_is_listed_as_running_job(self): + held = self.api._jobs.hold( + "restore", "Database restore (admin page)", admission="records" + ) + async with held: + resp = await self.client.request("GET", "/prompt_manager/jobs") + jobs = (await resp.json())["jobs"] + self.assertEqual(len(jobs), 1) + job = jobs[0] + self.assertEqual( + (job["kind"], job["title"], job["state"]), + ("restore", "Database restore (admin page)", "running"), + ) + self.assertIs(job["cancellable"], False) + self.assertEqual(job["admission"], "records") + + async def test_legacy_maintenance_and_cleanup_refused_while_scan_runs(self): + self.api.db.save_prompt(text="keep me", prompt_hash="h1") + before = self._db_digest() + self._run_job() + resp = await self.client.request( + "POST", + "/prompt_manager/maintenance", + json={"operations": ["cleanup_duplicates", "vacuum"]}, + ) + await self._busy(resp) + resp = await self.client.request("POST", "/prompt_manager/cleanup") + await self._busy(resp) + self.assertEqual(self._db_digest(), before) + + async def test_read_only_maintenance_runs_during_scan(self): + # The legacy statistics and consistency checks only read: they hold + # the registry as "read", so a long scan does not block them. + self.api.db.save_prompt(text="keep me", prompt_hash="h1") + _gate, scan = self._run_job() + listed = [] + real = self.api._run_maintenance_operations + + def observed(operations): + listed.extend( + (job.kind, job.admission, job.state) for job in self.api._jobs.list() + ) + return real(operations) + + self.api._run_maintenance_operations = observed + resp = await self.client.request( + "POST", + "/prompt_manager/maintenance", + json={ + "operations": [ + "statistics", + "check_consistency", + "check_hash_duplicates", + ] + }, + ) + body = await resp.json() + self.assertEqual(resp.status, 200) + self.assertIs(body["success"], True) + self.assertEqual( + set(body["results"]), + {"statistics", "check_consistency", "check_hash_duplicates"}, + ) + self.assertIn(("maintenance", "read", "running"), listed) + self.assertEqual(scan.state, "running") + maintenance = self.api._jobs.list()[-1] + self.assertEqual( + (maintenance.kind, maintenance.admission, maintenance.state), + ("maintenance", "read", "finished"), + ) + + async def test_mixed_maintenance_selection_is_refused_during_scan(self): + # One write operation among read-only ones makes the run "records". + self.api.db.save_prompt(text="keep me", prompt_hash="h1") + before = self._db_digest() + self._run_job() + resp = await self.client.request( + "POST", + "/prompt_manager/maintenance", + json={"operations": ["statistics", "vacuum"]}, + ) + await self._busy(resp) + self.assertEqual(self._db_digest(), before) + + async def test_default_maintenance_selection_is_refused_during_scan(self): + # No "operations" key means the default list, which vacuums. + self.api.db.save_prompt(text="keep me", prompt_hash="h1") + before = self._db_digest() + self._run_job() + resp = await self.client.request("POST", "/prompt_manager/maintenance", json={}) + await self._busy(resp) + self.assertEqual(self._db_digest(), before) + self.assertEqual([j.kind for j in self.api._jobs.list()], ["scan"]) + + async def test_legacy_maintenance_refused_while_image_tags_runs(self): + self._run_job("image-tags", "Add tags to 3 images") + resp = await self.client.request("POST", "/prompt_manager/maintenance") + await self._busy(resp, "Add tags to 3 images") + + async def test_scan_refused_while_legacy_maintenance_holds(self): + entered = threading.Event() + release = threading.Event() + real = self.api._run_maintenance_operations + + def gated(operations): + entered.set() + release.wait(10) + return real(operations) + + self.api._run_maintenance_operations = gated + loop = asyncio.get_running_loop() + maintenance = asyncio.ensure_future( + self.client.request( + "POST", "/prompt_manager/maintenance", json={"operations": ["vacuum"]} + ) + ) + self.assertTrue(await loop.run_in_executor(None, entered.wait, 10)) + resp = await self.client.request("POST", "/prompt_manager/jobs/scan") + body = await resp.json() + self.assertEqual((resp.status, body["code"]), (409, "busy")) + self.assertEqual(body["running"]["title"], "Maintenance (admin page)") + release.set() + self.assertEqual((await maintenance).status, 200) + listed = ( + await (await self.client.request("GET", "/prompt_manager/jobs")).json() + )["jobs"] + self.assertEqual( + [(j["kind"], j["state"]) for j in listed], [("maintenance", "finished")] + ) + + async def test_legacy_duplicate_delete_refused_while_scan_runs(self): + victim = self.output_dir / "dup.png" + victim.write_bytes(b"x") + self._run_job() + resp = await self.client.request( + "POST", + "/prompt_manager/delete_duplicate_images", + json={"image_paths": [str(victim)]}, + ) + await self._busy(resp) + self.assertTrue(victim.exists()) + # The duplicate family lock was not taken by the refused request. + self.assertIsNotNone(self.api._acquire_long_job(self.api.LONG_JOB_DUPLICATES)) + + async def test_lora_scan_refused_while_scan_runs(self): + from py.config import IntegrationConfig + + lm = Path(self.tmpdir) / "custom_nodes" / "ComfyUI-Lora-Manager" + (lm / "py").mkdir(parents=True) + self._run_job() + imported = [] + with ( + patch.multiple( + IntegrationConfig, LORA_MANAGER_ENABLED=True, LORA_MANAGER_PATH=str(lm) + ), + patch("py.lora_utils.resolve_lora_manager_path", return_value=str(lm)), + patch.object( + self.api, "_lora_scan_stream", side_effect=lambda *a: imported.append(a) + ), + ): + resp = await self.client.request("POST", "/prompt_manager/lora/scan") + text = await resp.text() + self.assertEqual(resp.status, 200) # the legacy page reads a stream + events = [json.loads(line[6:]) for line in text.split("\n\n") if line] + # The admin page's modal shows ``status`` and closes on ``type: error``. + message = "Scan for metadata already running" + self.assertEqual( + events, + [{"type": "error", "status": message, "progress": 0, "message": message}], + ) + self.assertEqual(imported, []) + + async def test_legacy_autotag_refused_while_scan_runs(self): + # Both pages read this route as a stream (a 409 reads as a lost + # connection there), so a refusal by another job is one error frame: + # the admin page keys on ``type``, the gallery on ``status``. + self._run_job() + resp = await self.client.request( + "POST", "/prompt_manager/autotag/start", json={} + ) + text = await resp.text() + self.assertEqual(resp.status, 200) + self.assertEqual(resp.headers["Content-Type"], "text/event-stream") + events = [json.loads(line[6:]) for line in text.split("\n\n") if line] + message = "Scan for metadata already running" + self.assertEqual( + events, [{"type": "error", "status": "error", "message": message}] + ) + self.assertEqual( + [j.kind for j in self.api._jobs.list() if j.kind == "autotag"], [] + ) + + async def test_second_legacy_autotag_batch_keeps_its_json_409(self): + from py.api.autotag_routes import _BUSY_ERROR + + self._run_job("autotag", "Auto-tag images (legacy page)") + resp = await self.client.request( + "POST", "/prompt_manager/autotag/start", json={} + ) + self.assertEqual(resp.status, 409) + self.assertEqual(await resp.json(), {"success": False, "error": _BUSY_ERROR}) + + async def test_legacy_payloads_unchanged_when_idle(self): + resp = await self.client.request( + "POST", "/prompt_manager/maintenance", json={"operations": ["vacuum"]} + ) + body = await resp.json() + self.assertEqual(resp.status, 200) + self.assertEqual( + set(body), + {"success", "operations_completed", "all_successful", "results", "message"}, + ) + resp = await self.client.request("POST", "/prompt_manager/cleanup") + body = await resp.json() + self.assertEqual(resp.status, 200) + self.assertEqual(set(body), {"success", "message", "duplicates_removed"}) + resp = await self.client.request( + "POST", "/prompt_manager/delete_duplicate_images", json={"image_paths": []} + ) + self.assertEqual( + (resp.status, await resp.json()), + (400, {"success": False, "error": "No image paths provided"}), + ) + # Each hold ended with the request; refused input never took one. + listed = await self._listed() + self.assertEqual( + [(j["kind"], j["state"]) for j in listed], + [("maintenance", "finished"), ("maintenance", "finished")], + ) + self.assertEqual( + [j["title"] for j in listed], + ["Prompt cleanup (admin page)", "Maintenance (admin page)"], + ) + + async def _listed(self): + resp = await self.client.request("GET", "/prompt_manager/jobs") + return (await resp.json())["jobs"] + + +class TestHeldJobStates(_LegacyHoldCase): + """Activity lists what a held legacy operation really did (Minor 7).""" + + async def _listed(self): + resp = await self.client.request("GET", "/prompt_manager/jobs") + return [(j["kind"], j["state"]) for j in (await resp.json())["jobs"]] + + async def test_refused_input_takes_no_hold(self): + resp = await self.client.request( + "POST", "/prompt_manager/delete_duplicate_images", json={} + ) + self.assertEqual(resp.status, 400) + resp = await self.client.request( + "POST", + "/prompt_manager/images/generate-thumbnails", + data=b"{not json", + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(resp.status, 400) + self.assertEqual(await self._listed(), []) + + async def test_thumbnail_stream_that_ends_in_error_is_failed(self): + with patch.object(self.api, "_get_all_output_dirs", return_value=[]): + resp = await self.client.request( + "POST", "/prompt_manager/images/generate-thumbnails/progress" + ) + text = await resp.text() + self.assertEqual(resp.status, 200) + self.assertIn("event: error", text) + self.assertEqual(await self._listed(), [("thumbnails", "failed")]) + + async def test_legacy_autotag_that_ends_in_error_is_failed(self): + service = MagicMock() + service.get_models_status.return_value = {} # model not downloaded + with patch("py.autotag.get_autotag_service", return_value=service): + resp = await self.client.request( + "POST", "/prompt_manager/autotag/start", json={} + ) + text = await resp.text() + self.assertEqual(resp.status, 200) + self.assertIn('"type": "error"', text) + self.assertEqual(await self._listed(), [("autotag", "failed")]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_jobs.py b/tests/test_jobs.py index f561be0..6848fa8 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -182,8 +182,13 @@ class TestJobRegistry(unittest.IsolatedAsyncioTestCase): "scope", "listed", "log_length", + "cancellable", + "admission", }, ) + self.assertEqual( + (summary["cancellable"], summary["admission"]), (True, "records") + ) self.assertEqual(summary["scope"], {"files": "new", "output_folders": 2}) # What the detail would list, so a view reads it only when that changed. self.assertEqual((summary["listed"], summary["log_length"]), ({}, 0)) diff --git a/tests/test_local_inspect_parity.py b/tests/test_local_inspect_parity.py index 9fdfdd9..04d4ad5 100644 --- a/tests/test_local_inspect_parity.py +++ b/tests/test_local_inspect_parity.py @@ -51,12 +51,19 @@ from utils.prompt_inspector import ( read_inspection_file, ) +from utils.prompt_inspector import inspect_prompt_evidence, unreadable_evidence +from utils.prompt_pair_resolver import INSPECTION_PROFILE + try: from tests import inspection_parity as parity + from tests import local_resolver_cases as resolver_cases + from tests import local_resolver_selection_cases as selection_cases from tests.open_handles import assert_closed from tests.test_utils_video_metadata import HAVE_FFMPEG, write_video except ImportError: # pragma: no cover - discover -s tests puts tests/ first import inspection_parity as parity + import local_resolver_cases as resolver_cases + import local_resolver_selection_cases as selection_cases from open_handles import assert_closed from test_utils_video_metadata import HAVE_FFMPEG, write_video @@ -1111,5 +1118,306 @@ class LocalProjectionParityTests(unittest.TestCase): self.assertTrue({"parsed", "malformed", "limited"} <= seen) +def _server_evidence(records): + return inspect_prompt_evidence(records, None, profile=INSPECTION_PROFILE) + + +class LocalResolverParityTests(unittest.TestCase): + """``resolveLocalEvidence`` answers as ``inspect_prompt_evidence`` (L3 Tasks 8-9). + + Python is the oracle: every expected value is the server's own answer on the + same record texts. Task 8 ported one sampler's tracing; Task 9 ports + candidate selection (several samplers, the identical-branch rule of #270, + preview-only stages, detailer stages) and A1111 ``parameters``, so no + record set is left to the server any more. + """ + + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.addCleanup(assert_closed, self, temporary.name) + self.directory = Path(temporary.name) + + def _compare(self, cases): + """Assert each record set's local evidence equal; return the server answers.""" + observed = _run_oracle( + self, + [ + {"name": name, "mode": "resolve", "records": records} + for name, records in cases.items() + ], + ) + answers = {} + for name, records in cases.items(): + with self.subTest(case=name): + local = observed[name] + server = _server_evidence(records) + answers[name] = server + self.assertEqual( + _canonical_bytes(local["evidence"]), _canonical_bytes(server) + ) + return answers + + def test_local_roles_match_execution_and_single_chain_vectors(self): + files = {} + for name in parity.COMMITTED: + files[name] = parity.fixture_path(name) + for name in parity.TEST_TIME: + path = parity.fixture_path(name, self.directory) + parity.write_png(path, parity.build(name)) + files[name] = path + observed = _run_oracle( + self, + [ + {"name": name, "mode": "resolve", "path": str(path)} + for name, path in files.items() + ], + ) + vectors = { + vector["fixture"]: vector + for vector in json.loads(parity.VECTORS_PATH.read_text(encoding="utf-8")) + if vector["profile"] == "inspection" + } + compared = 0 + for name, path in files.items(): + with self.subTest(fixture=name): + local = observed[name] + try: + records = read_inspection_file(path).metadata + except MetadataReadError: + self.assertTrue(local["unreadable"]) + continue + # The single-chain subset (Task 8); the rest is Task 9's test below. + if not resolver_cases.single_chain(records): + continue + server = _server_evidence(records) + self.assertEqual( + _canonical_bytes(local["evidence"]), _canonical_bytes(server) + ) + # The committed vector agrees with the server read here. + expect = vectors[name]["expect"] + self.assertEqual(server["resolution"]["state"], expect["state"]) + self.assertEqual( + server["resolution"]["reason_code"], expect["reason_code"] + ) + compared += 1 + self.assertGreaterEqual(compared, 20) + + def test_local_roles_match_inspection_vectors(self): + """Every inspection vector: local prompts, resolution and facts equal Images'. + + The facts are ``extractGenerationFacts`` (with the legacy fill) on the + local payload, against the facts the committed vectors pin for the + server's response on the same file. + """ + vectors = [ + vector + for vector in json.loads(parity.VECTORS_PATH.read_text(encoding="utf-8")) + if vector["profile"] == "inspection" + ] + files = {} + for vector in vectors: + name = vector["fixture"] + if name in parity.TEST_TIME: + path = parity.fixture_path(name, self.directory) + parity.write_png(path, parity.build(name)) + else: + path = parity.fixture_path(name) + files[name] = path + observed = _run_oracle( + self, + [ + {"name": name, "mode": "resolve", "path": str(path)} + for name, path in files.items() + ], + ) + for vector in vectors: + name = vector["fixture"] + with self.subTest(fixture=name): + local, expect = observed[name], vector["expect"] + try: + server = _server_evidence( + read_inspection_file(files[name]).metadata + ) + except MetadataReadError: + server = unreadable_evidence() + self.assertTrue(local["unreadable"]) + self.assertEqual( + _canonical_bytes(local["evidence"]), _canonical_bytes(server) + ) + resolution, prompts = ( + local["evidence"]["resolution"], + local["evidence"]["prompts"], + ) + self.assertEqual( + ( + resolution["state"], + resolution["reason"], + resolution["reason_code"], + ), + (expect["state"], expect["reason"], expect["reason_code"]), + ) + self.assertEqual( + [ + parity._role_text( + prompts[role]["status"], prompts[role]["text"] + ) + for role in ("positive", "negative") + ], + [expect["positive"], expect["negative"]], + ) + self.assertEqual(local["facts"], expect["facts"]) + self.assertEqual(len(vectors), 28) + + def test_local_roles_match_selection_record_sets(self): + answers = self._compare(selection_cases.selection_cases()) + reasons = {answer["resolution"]["reason"] for answer in answers.values()} + self.assertTrue( + { + "Every sampler branch uses the same prompt pair", + "Multiple sampler branches", + "Sampler stages have different prompts", + "Cyclic sampler stage ancestry", + "Expected an image link", + "Unsupported sampler stage ancestry", + "Resolution depth limit reached", + "A1111 settings footer is not unique", + "Multiple negative prompt delimiters", + "Negative prompt delimiter is absent", + "Parameters exceed the length limit", + "No A1111 parameters", + } + <= reasons + ) + # A stage reaching only previews is dropped: one sampler is named. + self.assertEqual( + answers["select-preview-dropped"]["resolution"]["sampler_id"], "3" + ) + + def test_local_roles_match_fuzzed_selection_sets(self): + cases = {} + for seed in range(4): + cases.update(selection_cases.selection_fuzz_cases(seed, 150)) + answers = self._compare(cases) + reasons = [answer["resolution"]["reason"] for answer in answers.values()] + self.assertGreater(reasons.count("Multiple sampler branches"), 20) + self.assertGreater(reasons.count("Sampler stages have different prompts"), 0) + self.assertGreater( + reasons.count("Every sampler branch uses the same prompt pair"), 0 + ) + self.assertGreater(reasons.count("A1111 settings footer is not unique"), 0) + + def test_local_roles_match_golden_record_sets(self): + answers = self._compare(resolver_cases.golden_cases()) + codes = {answer["resolution"]["reason_code"] for answer in answers.values()} + self.assertTrue( + { + "malformed", + "unsupported-node", + "runtime-value", + "composite", + "work-limit", + "depth-limit", + "text-limit", + "graph-ceiling", + "no-sampler", + None, + } + <= codes + ) + sources = { + side["source"] + for answer in answers.values() + for side in answer["prompts"].values() + } + self.assertTrue( + {"execution", "graph-inferred", "zeroed-conditioning"} <= sources + ) + + def test_committed_resolver_vectors_are_current(self): + """The Node unit test's committed answers are still the server's.""" + committed = json.loads(resolver_cases.VECTORS_PATH.read_text(encoding="utf-8")) + self.assertEqual( + committed, + json.loads(json.dumps(resolver_cases.committed_vectors())), + "regenerate with: python3 -m tests.local_resolver_cases --write", + ) + + def test_local_roles_match_at_every_work_bound(self): + answers = self._compare(resolver_cases.boundary_cases()) + for name in ("visits", "depth", "scanned"): + self.assertEqual(answers[f"{name}-edge"]["resolution"]["state"], "resolved") + self.assertIn( + answers[f"{name}-over"]["resolution"]["reason_code"], + ("work-limit", "depth-limit"), + ) + self.assertEqual( + answers["named-astral"]["resolution"]["node"]["class_type"], + "\U0001f600" * 64, + ) + + def test_local_json_depth_cap_at_its_boundary(self): + """Both kinds of deep record at the browser's JSON depth cap (1,000). + + Locally a record nested past the cap is invalid JSON: a prompt is + malformed and a workflow is ignored, as the server answers when + ``json.loads`` raises RecursionError. The server's own limit depends on + the Python (TESTING.md, "Deep JSON"): about 990 on 3.10 and 3.11, so it + agrees past the cap there; about 10,000 on 3.12 and 3.13 and over + 50,000 on 3.14, so it agrees up to the cap there and reads deeper. + """ + cases = resolver_cases.depth_cases() + observed = _run_oracle( + self, + [ + {"name": name, "mode": "resolve", "records": records} + for name, _kind, _depth, records in cases + ], + ) + newer = sys.version_info >= (3, 12) + compared = 0 + for name, kind, depth, records in cases: + with self.subTest(case=name): + local = observed[name]["evidence"]["resolution"] + within = depth <= resolver_cases.JSON_MAX_DEPTH + if kind == "prompt": + expected = ( + ("resolved", None) if within else ("unresolved", "malformed") + ) + else: + # A workflow that is read finds the overwritten pair (R1). + expected = ( + ("unresolved", "malformed") if within else ("resolved", None) + ) + self.assertEqual((local["state"], local["reason_code"]), expected) + server = _server_evidence(records) + if server["resolution"]["state"] == local["state"]: + self.assertEqual( + _canonical_bytes(observed[name]["evidence"]), + _canonical_bytes(server), + ) + compared += 1 + else: + # The documented divergence only: an older server refuses + # a little below the cap, a newer one reads past it. + self.assertEqual(within, not newer) + self.assertGreaterEqual(compared, 2) + + def test_local_roles_match_fuzzed_record_sets(self): + cases = {} + for seed in range(4): + cases.update(resolver_cases.fuzz_cases(seed, 150)) + answers = self._compare(cases) + states = [answer["resolution"]["state"] for answer in answers.values()] + # Coherent graphs reach real results, not only early refusals. + self.assertGreater(states.count("resolved"), len(states) // 5) + self.assertGreater(states.count("ambiguous"), 0) + self.assertGreater(states.count("unresolved"), len(states) // 10) + self.assertGreaterEqual( + len({answer["resolution"]["reason_code"] for answer in answers.values()}), + 5, + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_workspace_concept_fixture.py b/tests/test_workspace_concept_fixture.py index f301de6..f855211 100644 --- a/tests/test_workspace_concept_fixture.py +++ b/tests/test_workspace_concept_fixture.py @@ -1,7 +1,9 @@ """The concept fixture files carry the concept inspector facts (app.js:400-414).""" import json +import math import tempfile +import time import unittest from pathlib import Path @@ -15,6 +17,8 @@ except ImportError: # discovered with tests/ as the top-level directory from tests.browser.workspace_concept_fixture import ( CONCEPT_MODEL, CONCEPT_PROMPTS, + MAX_EPOCH_SECONDS, + _instant, write_concept_png, ) from utils.prompt_inspector import inspect_prompt_evidence, read_inspection_file @@ -80,5 +84,29 @@ class TestWorkspaceConceptFixture(unittest.TestCase): self.assertEqual((image["width"], image["height"]), size) +class TestConceptFixtureInstant(unittest.TestCase): + """One "now" for every relative time the fixture writes (PR #284's midnight).""" + + def test_a_chosen_instant_is_kept(self): + self.assertEqual(_instant(1_792_713_598), 1_792_713_598.0) + self.assertEqual(_instant(1_792_713_598.25), 1_792_713_598.25) + + def test_the_wall_clock_when_omitted(self): + before = time.time() + now = _instant(None) + self.assertLessEqual(before, now) + self.assertLessEqual(now, time.time()) + + def test_a_non_number_is_refused(self): + for value in (True, "1792713598", [1]): + with self.subTest(value=value), self.assertRaises(TypeError): + _instant(value) + + def test_an_instant_out_of_range_is_refused(self): + for value in (0, -1, MAX_EPOCH_SECONDS, math.inf, math.nan): + with self.subTest(value=value), self.assertRaises(ValueError): + _instant(value) + + if __name__ == "__main__": unittest.main() diff --git a/tools/vendor/frontend-coverage-baseline.json b/tools/vendor/frontend-coverage-baseline.json index 31ab8b3..7d82941 100644 --- a/tools/vendor/frontend-coverage-baseline.json +++ b/tools/vendor/frontend-coverage-baseline.json @@ -1,9 +1,9 @@ { "admin.js": { - "lines": 28, + "lines": 31, "branches": 58, - "functions": 23, - "statements": 28 + "functions": 26, + "statements": 31 }, "api-client.js": { "lines": 98, @@ -42,10 +42,10 @@ "statements": 98 }, "gallery.js": { - "lines": 39, + "lines": 40, "branches": 63, - "functions": 43, - "statements": 39 + "functions": 44, + "statements": 40 }, "html-escape.js": { "lines": 98, @@ -157,7 +157,7 @@ }, "workspace/activity-job-view.mjs": { "lines": 97, - "branches": 83, + "branches": 86, "functions": 98, "statements": 97 }, @@ -174,14 +174,14 @@ "statements": 98 }, "workspace/activity.mjs": { - "lines": 95, + "lines": 96, "branches": 87, "functions": 98, - "statements": 95 + "statements": 96 }, "workspace/file-inspector.mjs": { "lines": 96, - "branches": 92, + "branches": 93, "functions": 92, "statements": 96 }, @@ -296,7 +296,7 @@ "workspace/inspect.mjs": { "lines": 98, "branches": 92, - "functions": 92, + "functions": 93, "statements": 98 }, "workspace/inspection-state.mjs": { @@ -341,12 +341,42 @@ "functions": 98, "statements": 98 }, + "workspace/local-parameters.mjs": { + "lines": 98, + "branches": 94, + "functions": 98, + "statements": 98 + }, "workspace/local-projection.mjs": { "lines": 97, "branches": 96, "functions": 98, "statements": 97 }, + "workspace/local-resolver-stages.mjs": { + "lines": 94, + "branches": 93, + "functions": 98, + "statements": 94 + }, + "workspace/local-resolver-support.mjs": { + "lines": 96, + "branches": 80, + "functions": 98, + "statements": 96 + }, + "workspace/local-resolver.mjs": { + "lines": 96, + "branches": 86, + "functions": 98, + "statements": 96 + }, + "workspace/local-semantics.mjs": { + "lines": 98, + "branches": 98, + "functions": 98, + "statements": 98 + }, "workspace/local-session.mjs": { "lines": 98, "branches": 92, @@ -389,6 +419,12 @@ "functions": 98, "statements": 98 }, + "workspace/py-json.mjs": { + "lines": 97, + "branches": 92, + "functions": 98, + "statements": 97 + }, "workspace/recorded-run.mjs": { "lines": 95, "branches": 83, diff --git a/utils/prompt_inspector.py b/utils/prompt_inspector.py index 9c1bb08..e7ab375 100644 --- a/utils/prompt_inspector.py +++ b/utils/prompt_inspector.py @@ -20,6 +20,7 @@ from .prompt_pair_resolver import ( _FOOTER, EMPTY_POSITIVE_REASON, INSPECTION_PROFILE, + MALFORMED_EVIDENCE_REASON, ResolverProfile, resolve_prompt_pair, ) @@ -548,7 +549,7 @@ def inspect_prompt_evidence( resolution = { "state": "unresolved", "sampler_id": None, - "reason": "Prompt evidence is malformed", + "reason": MALFORMED_EVIDENCE_REASON, "reason_code": "malformed", "node": None, } diff --git a/utils/prompt_pair_resolver.py b/utils/prompt_pair_resolver.py index f64fee9..c6e229d 100644 --- a/utils/prompt_pair_resolver.py +++ b/utils/prompt_pair_resolver.py @@ -1237,10 +1237,13 @@ class _GraphResolver: def non_file_outputs(self) -> set: names = self.semantics.non_file_outputs + # Only a string names a class: a list or object is never looked up (#286). return { node_id for node_id, node in self.graph.items() - if isinstance(node, dict) and node.get("class_type") in names + if isinstance(node, dict) + and isinstance(node.get("class_type"), str) + and node["class_type"] in names } def producers_of(self, starts: list, previews: set) -> set: @@ -1832,6 +1835,8 @@ def _shared_pair(candidates: list) -> Optional[Dict[str, Any]]: # Bounds for walking a UI workflow's subgraph instances (R1 only). _MAX_SUBGRAPH_DEPTH = 8 +# The inspector's own line for evidence it cannot read (``inspect_prompt_evidence``). +MALFORMED_EVIDENCE_REASON = "Prompt evidence is malformed" _MAX_WORKFLOW_NODES = 100_000 @@ -1862,7 +1867,9 @@ def _prompt_manager_widgets(workflow: Any) -> Dict[str, str]: Nodes inside a subgraph run as ``":"`` API IDs (nested instances chain their IDs); a definition without an instance runs nothing. - The walk is bounded in depth and in nodes visited. + The walk is bounded in depth and in nodes visited. A node whose ``type`` is + a list or an object is skipped: it is neither a subgraph instance nor a + PromptManager node (#286). """ nodes = workflow.get("nodes") if isinstance(workflow, dict) else None if not isinstance(nodes, list): @@ -1882,6 +1889,9 @@ def _prompt_manager_widgets(workflow: Any) -> Dict[str, str]: if not isinstance(node_id, (str, int)) or isinstance(node_id, bool): continue api_id = f"{prefix}{node_id}" + if isinstance(kind, (list, dict)): + # JSON's unhashable values are never a node type (#286). + continue if kind in subgraphs and depth < _MAX_SUBGRAPH_DEPTH: stack.append((subgraphs[kind], api_id + ":", depth + 1)) continue diff --git a/web/js/admin.js b/web/js/admin.js index da699ac..e74d347 100644 --- a/web/js/admin.js +++ b/web/js/admin.js @@ -1180,6 +1180,11 @@ `Done — ${data.imported} imported, ${data.skipped} skipped`; this.loadStatistics(); setTimeout(() => this.hideModal("loraImportModal"), 2000); + } else if (data.type === "error") { + // A refusal (another job runs) or a failed import: name it and close the modal. + document.getElementById("loraImportStatus").textContent = `Error: ${data.message}`; + this.showNotification(`LoRA import failed: ${data.message}`, "error"); + setTimeout(() => this.hideModal("loraImportModal"), 3000); } } catch (e) { /* skip malformed SSE */ } } @@ -1231,10 +1236,13 @@ } this.hideModal("settingsModal"); } else { - throw new Error("Failed to save settings"); + // A refusal names its reason (a corrupt config.json, a changed file): show it. + const data = await response.json().catch(() => ({})); + throw new Error(data.error ? `Failed to save settings: ${data.error}` : "Failed to save settings"); } } catch (error) { - this.showNotification("Failed to save settings", "error"); + const reason = error.message.startsWith("Failed to save settings:") ? error.message : "Failed to save settings"; + this.showNotification(reason, "error"); } } @@ -1848,7 +1856,9 @@ throw new Error(data.error || 'Maintenance failed'); } } else { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); + // A refusal names the job that runs ("Scan for metadata already running"). + const data = await response.json().catch(() => ({})); + throw new Error(data.error || `HTTP ${response.status}: ${response.statusText}`); } } catch (error) { console.error('Maintenance error:', error); diff --git a/web/js/gallery.js b/web/js/gallery.js index 56596f7..ee9f24b 100644 --- a/web/js/gallery.js +++ b/web/js/gallery.js @@ -17,6 +17,14 @@ this.dupResults = null; this.api = ApiClient.createApiClient(); + // The page's lifetime ends when it starts to leave: WebKit fails a fetch a leaving page starts + // ("Fetch API cannot load … due to access control checks.") and logs it as a page error. + this.pageLifetime = new AbortController(); + window.addEventListener("beforeunload", () => this.pageLifetime.abort()); + window.addEventListener("pagehide", () => this.pageLifetime.abort()); + window.addEventListener("pageshow", (e) => { + if (e.persisted && this.pageLifetime.signal.aborted) this.pageLifetime = new AbortController(); + }); // Only the newest page request may update the grid this.imagesRequest = ApiClient.latestOnly(); @@ -2129,8 +2137,11 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; headers: { 'Content-Type': 'application/json' } }); - if (response.ok) { - const data = await response.json(); + if (!response.ok) { + // Refused (a running job holds the thumbnails): say why and claim nothing. + const data = await response.json().catch(() => ({})); + this.showNotification(`Thumbnails were not cleared: ${data.error || `HTTP ${response.status}`}`, 'error'); + return; } } catch (e) { console.warn('Could not clear server thumbnails:', e); @@ -2531,8 +2542,11 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; return; } + // A leaving page starts no read, and one it aborts is not reported. + const signal = this.pageLifetime?.signal; + if (signal?.aborted) return; try { - const data = await this.api.get('/prompt_manager/images/thumbnails/status'); + const data = await this.api.get('/prompt_manager/images/thumbnails/status', { signal }); const total = data.total_images; const pending = data.pending; if (!data.success || !Number.isSafeInteger(total) || diff --git a/web/js/workspace/activity-job-view.mjs b/web/js/workspace/activity-job-view.mjs index 9808b7a..f853cb4 100644 --- a/web/js/workspace/activity-job-view.mjs +++ b/web/js/workspace/activity-job-view.mjs @@ -274,7 +274,8 @@ export function mountActivityJobView(main, { store, scope, api, jobId }) { const lost = job.state === 'lost'; if (lost && document.activeElement === download) parts.h1.focus(); download.disabled = lost; - if (job.state === 'running') { + // Truth rule: a job that cannot be cancelled (a held legacy-page job) shows no Cancel. + if (job.state === 'running' && job.cancellable) { if (!cancel.isConnected) parts.actions.append(cancel); } else if (cancel.isConnected) { // A focused Cancel that goes away hands its focus to the job's heading, never to the page body. diff --git a/web/js/workspace/activity-store.mjs b/web/js/workspace/activity-store.mjs index 3df6405..72eb6ab 100644 --- a/web/js/workspace/activity-store.mjs +++ b/web/js/workspace/activity-store.mjs @@ -103,6 +103,8 @@ export function parseJob(raw) { scope: plainObject(raw.scope) ? { ...raw.scope } : {}, listed, log_length: count(raw.log_length), + // A held legacy-page job cannot be cancelled; an older server sends no key (cancellable then). + cancellable: raw.cancellable !== false, }; } diff --git a/web/js/workspace/activity.mjs b/web/js/workspace/activity.mjs index 48b83c4..eb7bc14 100644 --- a/web/js/workspace/activity.mjs +++ b/web/js/workspace/activity.mjs @@ -153,11 +153,15 @@ function itemFor(job, scope) { progress.textContent = `${next.progress}%`; progress.setAttribute('aria-label', `${next.title} progress`); if (!progress.isConnected) item.insertBefore(progress, actions.isConnected ? actions : null); - if (!actions.isConnected) item.append(actions); } else { + progress.remove(); + } + // Truth rule: a job that cannot be cancelled (a held legacy-page job) shows no Cancel job. + if (running && next.cancellable) { + if (!actions.isConnected) item.append(actions); + } else if (actions.isConnected) { // A focused Cancel job that goes away hands its focus to the item's title link, never to the page body. if (actions.contains(document.activeElement)) link.focus(); - progress.remove(); actions.remove(); } }; diff --git a/web/js/workspace/file-inspector.mjs b/web/js/workspace/file-inspector.mjs index 11309cb..2d7a032 100644 --- a/web/js/workspace/file-inspector.mjs +++ b/web/js/workspace/file-inspector.mjs @@ -5,6 +5,7 @@ import { createSendButton } from './prompt-sender.mjs'; import { createWorkspaceIcon } from './icons.mjs'; import { navigateTo } from './state.mjs'; import { showWorkspaceToast } from './toast.mjs'; +import { openCopyFallback } from './workspace-dialogs.mjs'; const RAW_KEYS = ['prompt', 'workflow', 'parameters', 'comfyassets_prompt_pairs']; const RAW_LIMIT = 'Raw metadata exceeds the response limit. Download the original file to inspect it externally.'; @@ -26,6 +27,26 @@ const REVEAL_BUSY = 'The folder is already opening.'; const REVEAL_FAILED = 'The folder could not be opened. Try again.'; const FACTS = [['seed', 'Seed'], ['steps', 'Steps'], ['cfg', 'CFG'], ['sampler', 'Sampler'], ['scheduler', 'Scheduler'], ['model', 'Model']]; +// D117 (LI-4): the legacy metadata page's COPY ALL (web/metadata.html copyAllData), its lines and placeholders. +const SUMMARY_LINES = [['Checkpoint', 'model'], ['Positive Prompt', 'positive'], ['Negative Prompt', 'negative'], ['Steps', 'steps'], + ['CFG Scale', 'cfg'], ['Sampler', 'sampler'], ['Seed', 'seed']]; +const SUMMARY_MISSING = {positive:'No prompt found', negative:'No negative prompt found'}; + +/** + * The one-click settings summary (D117): the legacy COPY ALL text for what this inspector shows. A confirmed prompt + * and a known fact are copied verbatim; anything else, an explicitly empty prompt included, takes the legacy page's + * placeholder, as its parse does (`hasValue` treats '' as absent). + */ +export function settingsSummary(record) { + const roles = promptRoles(record?.prompts); + const value = key => { + if (Object.hasOwn(SUMMARY_MISSING, key)) return roles[key] ? roles[key] : SUMMARY_MISSING[key]; + const fact = record?.generation?.[key]; + return fact?.status === 'known' && typeof fact.value === 'string' && fact.value !== '' ? fact.value : 'Unknown'; + }; + return SUMMARY_LINES.map(([label, key]) => `${label}: ${value(key)}`).join('\n'); +} + function safeFilename(value) { return String(value || 'file').replace(/[\\/\x00-\x1f\x7f<>:"|?*]/g, '_').replace(/^\.+|[. ]+$/g, '') || 'file'; } @@ -175,7 +196,7 @@ function prefixedChip(prefix, text) { * `variant: 'full'` (the viewer's `#viewer-details`, imageDetails(i, true)) leaves out the cover and adds "File & * provenance" and "Workflow & raw metadata" with the downloads. `inline: true` means `panel`; neither means `full`. */ -export function mountFileInspector(container, {api, sender, parentScope, variant, inline = false, promptContext = '', onTagFilter, onRetry, onRefresh, onInteraction = () => {}}) { +export function mountFileInspector(container, {api, sender, parentScope, variant, inline = false, promptContext = '', onTagFilter, onRetry, onRefresh, onInteraction = () => {}, copyFallback = openCopyFallback}) { if (parentScope.signal.aborted) return {setState() {}, dispose() {}}; const scope = childScope(parentScope); let contentScope; @@ -184,7 +205,7 @@ export function mountFileInspector(container, {api, sender, parentScope, variant scope.listen(container, 'keydown', onInteraction); scope.listen(container, 'click', onInteraction); scope.listen(container, 'wheel', onInteraction, {passive:true}); - scope.cleanup(() => {contentScope?.dispose(); container.replaceChildren(); container.classList.remove('file-inspector');}); + scope.cleanup(() => {contentScope?.dispose(); container.replaceChildren(); container.classList.remove('file-inspector'); container.removeAttribute('aria-busy');}); const full = (variant ?? (inline ? 'panel' : 'full')) === 'full'; function statusView(state, parent) { @@ -203,6 +224,8 @@ export function mountFileInspector(container, {api, sender, parentScope, variant if (scope.signal.aborted) return; contentScope?.dispose(); contentScope = childScope(scope); container.replaceChildren(); + // A local read resolves on this thread in bounded steps; the busy state stays until its answer lands. + if (state.loading) container.setAttribute('aria-busy', 'true'); else container.removeAttribute('aria-busy'); render(state); } /** @@ -244,7 +267,7 @@ export function mountFileInspector(container, {api, sender, parentScope, variant inner.append(tags); } inner.append(...generationFacts(record, 'meta-grid', 'small')); - if (full) inner.append(provenance(record, badge), rawMetadata(record)); + if (full) inner.append(summaryButton(record), provenance(record, badge), rawMetadata(record)); const footer = full ? container : el('div', undefined, 'detail-footer'), roles = promptRoles(record.prompts); if (Object.hasOwn(roles, 'positive') && Object.hasOwn(roles, 'negative')) { const pair = createButton('', 'send'); @@ -264,6 +287,23 @@ export function mountFileInspector(container, {api, sender, parentScope, variant } if (!full && footer.childNodes.length) container.append(footer); } + /** + * D117 (LI-4): "Copy settings summary" after the six facts, the legacy COPY ALL. It copies as the concept's prompt + * copy does: the shared toast, or the select-to-copy dialog where the clipboard is unavailable or refused. + */ + function summaryButton(record) { + const button = createButton('Copy settings summary', 'settings-summary'), text = settingsSummary(record); + contentScope.listen(button, 'click', async () => { + try { + if (!navigator.clipboard) throw new Error('Clipboard requires a secure context'); + await navigator.clipboard.writeText(text); + if (!contentScope.signal.aborted) showWorkspaceToast('Settings summary copied.'); + } catch { + if (!contentScope.signal.aborted) copyFallback(text, {scope:contentScope, trigger:button, title:'Copy settings summary'}); + } + }); + return button; + } /** The concept's plain cover; the card image opens the viewer (D19). Video keeps its own element. */ function cover(record) { const {file} = record, video = file?.media_type === 'video'; diff --git a/web/js/workspace/inspect.mjs b/web/js/workspace/inspect.mjs index cd6f2f7..719b067 100644 --- a/web/js/workspace/inspect.mjs +++ b/web/js/workspace/inspect.mjs @@ -7,6 +7,7 @@ import { pageHeading } from './heading.mjs'; import { createWorkspaceIcon } from './icons.mjs'; import { createLocalSession, LOCAL_SESSION_MESSAGES } from './local-session.mjs'; import { localFileAdapter } from './local-inspection.mjs'; +import { loadInspectionSemantics } from './local-semantics.mjs'; import { createMediaViewer } from './media-viewer.mjs'; import { showWorkspaceToast, TOAST_MS } from './toast.mjs'; @@ -39,36 +40,24 @@ function element(tag, className, text) { // The status shown for the selection; AVI/WMV's note and an empty status keep the toast's "metadata is available". const TOASTABLE = new Set(['', LOCAL_SESSION_MESSAGES.containerOnly]); -// How long a released video still starting may take to settle before its deferred reset is dropped. -const DEFERRED_RESET_MS = 15000; +// How long a replaced video still starting may stay in place before it is released anyway. +const STARTING_VIDEO_MS = 15000; -/** - * Resets a released video once it settles. Removing `src` alone never aborts a load already under way, and WebKit - * (GStreamer) blocked the page when a pipeline still starting was reset, so load() waits for metadata or an error. - * The fallback only drops the waiting listeners (a WebKit load cut off by the revoked URL stalls and never settles), - * so the detached node is never pinned. - */ -function resetWhenSettled(node) { - const waiting = new AbortController(); - const fallback = setTimeout(() => waiting.abort(), DEFERRED_RESET_MS); - const reset = () => { waiting.abort(); clearTimeout(fallback); node.load(); }; - for (const type of ['loadedmetadata', 'error']) node.addEventListener(type, reset, { once: true, signal: waiting.signal }); +/** A video whose pipeline is still starting: it has a source but neither metadata nor an error yet. */ +function isStarting(node) { + return node instanceof HTMLMediaElement && node.hasAttribute('src') && node.readyState < HTMLMediaElement.HAVE_METADATA && node.error === null; } /** - * Stops a preview from holding its blob: URL: its own listeners end, media is paused and its src dropped before the - * session revokes the URL, and the node leaves the page. A settled video (metadata read, or failed) is then reset with - * load() to free its decoder at once; one still starting is reset as soon as it settles. + * Stops a preview from holding its blob: URL: its own listeners end, media is paused, its src dropped and it is reset + * with load() (which frees its decoder), and the node leaves the page, all before its URL is revoked. */ function releaseMedia(preview) { if (!preview) return; const { node, listeners } = preview; listeners.abort(); if (node instanceof HTMLMediaElement) { - const settled = node.readyState >= HTMLMediaElement.HAVE_METADATA || node.error !== null; - node.pause(); node.removeAttribute('src'); - if (settled) node.load(); - else resetWhenSettled(node); + node.pause(); node.removeAttribute('src'); node.load(); } else node.removeAttribute('src'); node.remove(); } @@ -84,6 +73,8 @@ export function mountInspect(main, { parentScope, api, sender }) { if (parentScope.signal.aborted) { scope.dispose(); return { dispose }; } parentScope.signal.addEventListener('abort', dispose, { once: true }); scope.cleanup(() => parentScope.signal.removeEventListener('abort', dispose)); + // The node table the local roles resolve with, read before any file is chosen (one request per page). + loadInspectionSemantics(); // The concept DOM: section.surface > h2, p; div.file-drop.spaced-20 > icon, label, input, span.small; #local-preview. const surface = element('section', 'surface'); @@ -108,7 +99,33 @@ export function mountInspect(main, { parentScope, api, sender }) { // The session ends after the viewer has closed and the inline media is gone, so no element holds its URL then. // The page owns page leave for the same reason: the session listens for no `pagehide` of its own. const sessionScope = createScope(); - const session = createLocalSession({ parentScope: sessionScope, pageTarget: null }); + // Replaced videos still starting, by URL. WebKit (GStreamer) deadlocked the page when a video left the page or + // the viewport before its pipeline had started, so such a video stays where it is, inert, until it reports + // metadata or an error; only then is it released and its URL, which the session asked to revoke, revoked. + const retiring = new Map(); + function finishRetiring(url) { + const entry = retiring.get(url); + if (!entry) return; + retiring.delete(url); + entry.waiting.abort(); clearTimeout(entry.timer); + releaseMedia(entry.preview); + if (entry.revoke) URL.revokeObjectURL(url); + } + function retire(preview, url) { + const waiting = new AbortController(); + const entry = { preview, waiting, revoke: false, timer: setTimeout(() => finishRetiring(url), STARTING_VIDEO_MS) }; + retiring.set(url, entry); + preview.listeners.abort(); + preview.node.inert = true; preview.node.setAttribute('aria-hidden', 'true'); + for (const type of ['loadedmetadata', 'error']) preview.node.addEventListener(type, () => finishRetiring(url), { once: true, signal: waiting.signal }); + } + const finishAllRetiring = () => { for (const url of [...retiring.keys()]) finishRetiring(url); }; + const revokeObjectURL = url => { + const entry = retiring.get(url); + if (entry) entry.revoke = true; + else URL.revokeObjectURL(url); + }; + const session = createLocalSession({ parentScope: sessionScope, pageTarget: null, revokeObjectURL }); const viewer = createMediaViewer({ api, container: main, parentScope: scope, inspectionController: session.controller, sender, adapter: localFileAdapter, onRestoreFocus: () => { if (open.isConnected) open.focus({ preventScroll: true }); } }); @@ -153,7 +170,9 @@ export function mountInspect(main, { parentScope, api, sender }) { function showMedia(state) { const url = state.preview?.url ?? null; if (url === mediaUrl) return; - releaseMedia(media); media = null; mediaUrl = url; + if (media && isStarting(media.node) && !retiring.has(mediaUrl)) retire(media, mediaUrl); + else releaseMedia(media); + media = null; mediaUrl = url; if (!url) return; const video = state.preview.media === 'video'; const node = element(video ? 'video' : 'img', 'local-file-preview'); @@ -173,7 +192,10 @@ export function mountInspect(main, { parentScope, api, sender }) { }, { once: true, signal: current.listeners.signal }); node.src = url; media = current; - preview.prepend(node); + // A video still starting keeps its place, so the new preview goes after it and never moves it. + const anchor = [...retiring.values()].map(entry => entry.preview.node).filter(item => item.parentNode === preview).at(-1); + if (anchor) anchor.after(node); + else preview.prepend(node); } scope.cleanup(session.subscribe(state => { showMedia(state); @@ -190,6 +212,7 @@ export function mountInspect(main, { parentScope, api, sender }) { // viewer (on abort), drops the media, then ends the session. scope.cleanup(() => viewer.dispose()); scope.cleanup(() => { releaseMedia(media); media = null; mediaUrl = null; }); + scope.cleanup(finishAllRetiring); scope.cleanup(() => sessionScope.dispose()); // Page leave: the viewer closes and the inline media goes before the session revokes the URL, and the chooser is @@ -198,6 +221,7 @@ export function mountInspect(main, { parentScope, api, sender }) { clearRejection(); viewer.close(); releaseMedia(media); media = null; mediaUrl = null; + finishAllRetiring(); session.clear(); chosen = null; input.value = ''; } diff --git a/web/js/workspace/local-inspection.mjs b/web/js/workspace/local-inspection.mjs index 3ccae32..e68889b 100644 --- a/web/js/workspace/local-inspection.mjs +++ b/web/js/workspace/local-inspection.mjs @@ -6,12 +6,11 @@ import { readVideoTags, selectVideoCarrier } from './local-video.mjs'; import { projectMetadata, fitInspectionPayload, MAX_RESPONSE_BYTES } from './local-projection.mjs'; import { projectInspectionPayload } from './inspection-state.mjs'; +import { resolveLocalEvidence, unreadableEvidence } from './local-resolver.mjs'; +import { loadInspectionSemantics } from './local-semantics.mjs'; export const LOCAL_OBSERVATION = 'selected-local-file'; export const LOCAL_SOURCE_LABEL = 'Selected local file'; -// Interim roles until PR L3 ports the inspection resolver: every role is open, and -// the legacy fill in projectInspectionPayload shows what the legacy pages show. -const LOCAL_RESOLUTION_REASON = 'Resolved in this browser from the workflow.'; const LOCAL_LIBRARY = Object.freeze({ status: 'unavailable', scope: 'saved-library', tags: [], category_scope: null, tags_scope: null }); const LOCAL_SCOPE = 'local'; const PREFIX_BYTES = 32; @@ -19,6 +18,24 @@ const MAX_FILENAME_CHARS = 255; const MEDIA_TYPES = new Set(['image', 'video']); const object = value => value !== null && typeof value === 'object' && !Array.isArray(value); +const abortError = () => new DOMException('The local selection ended', 'AbortError'); + +/** `promise`, or an AbortError as soon as `signal` aborts (the work behind `promise` is shared and keeps going). */ +function untilAborted(promise, signal) { + if (!signal) return promise; + if (signal.aborted) return Promise.reject(abortError()); + return new Promise((resolve, reject) => { + const onAbort = () => reject(abortError()); + signal.addEventListener('abort', onAbort, { once: true }); + promise.then(value => { signal.removeEventListener('abort', onAbort); resolve(value); }, + error => { signal.removeEventListener('abort', onAbort); reject(error); }); + }); +} + +/** One turn of the event loop between bounded steps, so the busy state paints and input is handled. */ +function nextTurn(signal) { + return untilAborted(new Promise(resolve => setTimeout(resolve, 0)), signal); +} const pngReader = () => globalThis.PngMetadata || globalThis.window?.PngMetadata; /** A display name with no path separators or control characters; never empty. */ @@ -88,8 +105,19 @@ export function toInspectionRead(classification, result) { const dimension = value => Number.isSafeInteger(value) && value > 0 ? value : null; -/** The `inspection_version: 1` wire for one local read, before the response budget. */ -export function buildLocalPayload({ read, file, classification }) { +/** The roles for one local read (`{prompts, resolution}`): the bounded parse and resolution step of buildLocalPayload. */ +export function localEvidence(read, classification, semantics = null) { + const inspection = toInspectionRead(classificationOf(classification), read); + return inspection.read === null ? unreadableEvidence() : resolveLocalEvidence(inspection.read.records, semantics); +} + +/** + * The `inspection_version: 1` wire for one local read, before the response budget. Its roles are the server + * inspector's for the same records (local-resolver.mjs with `semantics`, the compiled node table; null answers + * "unavailable" as the server does), so the legacy fill in projectInspectionPayload applies exactly where it does + * for Images. + */ +export function buildLocalPayload({ read, file, classification, semantics = null, evidence = localEvidence(read, classification, semantics) }) { const classified = classificationOf(classification); const inspection = toInspectionRead(classified, read); const filename = sanitizeLocalFilename(file?.name); @@ -101,8 +129,8 @@ export function buildLocalPayload({ read, file, classification }) { observation: LOCAL_OBSERVATION, source_label: LOCAL_SOURCE_LABEL, media_type: classified.media, extension: localExtension(filename), format: inspection.format, size, width: dimension(inspection.width), height: dimension(inspection.height), modified_time: modified, }, - prompts: { positive: { status: 'unresolved' }, negative: { status: 'unresolved' } }, - resolution: { state: 'unresolved', sampler_id: null, reason: LOCAL_RESOLUTION_REASON, reason_code: null, node: null }, + prompts: evidence.prompts, + resolution: evidence.resolution, metadata: projectMetadata(inspection.read), library: { ...LOCAL_LIBRARY, tags: [] }, }; @@ -166,7 +194,12 @@ export const localFileAdapter = Object.freeze({ scope(handle) { owned(handle); return LOCAL_SCOPE; }, // An inert sentinel: the controller's same-key check works and no URL is ever built. inspectionPath(handle) { return `local-file:${owned(handle).key}`; }, - async read(handle, { signal } = {}) { + /** + * Reads, resolves and projects one selection on the page's thread in bounded steps (each bound by the 2 MiB role + * and 4 MiB workflow budgets and the resolver's work limits) with a turn of the event loop between them, so the + * inspector's busy state paints. Every wait ends with the selection's `signal`; `yieldControl` is for tests. + */ + async read(handle, { signal, yieldControl = nextTurn } = {}) { const secret = owned(handle); let result = null; try { @@ -176,7 +209,14 @@ export const localFileAdapter = Object.freeze({ // MetadataReadError, an unreadable record. Abort and programming errors still reject. if (!(error instanceof DOMException) || error.name === 'AbortError' || signal?.aborted) throw error; } - return fitLocalPayload(buildLocalPayload({ read: result, file: { name: secret.filename, size: secret.file.size, lastModified: secret.file.lastModified }, classification: secret.classification }), MAX_RESPONSE_BYTES, { signal }); + const semantics = await untilAborted(loadInspectionSemantics(), signal); + await yieldControl(signal); + const evidence = localEvidence(result, secret.classification, semantics); + await yieldControl(signal); + const file = { name: secret.filename, size: secret.file.size, lastModified: secret.file.lastModified }; + const payload = buildLocalPayload({ read: result, file, classification: secret.classification, semantics, evidence }); + await yieldControl(signal); + return fitLocalPayload(payload, MAX_RESPONSE_BYTES, { signal }); }, toRecord(result, handle, extractFacts) { const secret = owned(handle); diff --git a/web/js/workspace/local-parameters.mjs b/web/js/workspace/local-parameters.mjs new file mode 100644 index 0000000..5e268fd --- /dev/null +++ b/web/js/workspace/local-parameters.mjs @@ -0,0 +1,55 @@ +// A1111 `parameters` for local-resolver.mjs (#249 local Inspect, PR L3 Task 9): a port of +// utils/prompt_pair_resolver.py `_parameters`, `_footer` and `_trailer_lines` for the inspection profile. +// Python's `re` differs from JavaScript's in three ways that matter here, so the patterns spell them out: +// multiline `^`/`$` break only at "\n" (not "\r", U+2028 or U+2029), `\s` is the `str.isspace()` set and `\d` +// is any Unicode decimal digit (Nd). Lengths count code points. No DOM. +import { PY_SPACE, pyLen, pyStrip } from './py-json.mjs'; +import { EMPTY_POSITIVE_REASON, result } from './local-resolver-support.mjs'; + +const START = '(?<=^|\\n)', END = '(?=\\n|$)', SPACE = `[${PY_SPACE}]*`; +// `_FOOTER` and `_BARE_FOOTER` (inspection reads a bare `Steps: N` when there is no full footer). +const FOOTER = new RegExp(`${START}Steps:${SPACE}\\p{Nd}+${SPACE},[^\\n]*${END}`, 'gu'); +const BARE_FOOTER = new RegExp(`${START}Steps:${SPACE}\\p{Nd}+${SPACE}${END}`, 'gu'); +const TRAILERS = ['Template:', 'Negative Template:']; +const DELIMITER = '\nNegative prompt:'; + +/** `_trailer_lines`: empty, or at most one `Template:` line then at most one `Negative Template:` line. */ +function trailerLines(text) { + const stripped = pyStrip(text); + const lines = stripped ? stripped.split(/\r?\n|\r/) : []; + const prefixes = lines.map(line => { + const index = TRAILERS.findIndex(name => line.startsWith(name)); + return index === -1 ? null : index; + }); + return !prefixes.includes(null) && prefixes.every((prefix, index) => index === 0 || prefix > prefixes[index - 1]); +} + +/** `_footer(value, inspection=True)`: the unique settings footer match, or null. */ +function footer(value) { + let footers = [...value.matchAll(FOOTER)]; + if (!footers.length) footers = [...value.matchAll(BARE_FOOTER)]; + if (footers.length !== 1) return null; + const [match] = footers; + return trailerLines(value.slice(match.index + match[0].length)) ? match : null; +} + +const rstripLineBreaks = text => text.replace(/[\r\n]+$/, ''); + +/** `_parameters(value, max_text_length, inspection=True)`. */ +export function parameters(value, maxTextLength) { + if (typeof value !== 'string' || !value) return result('absent', { reason: 'No A1111 parameters' }); + if (pyLen(value) > maxTextLength) return result('unresolved', { reason: 'Parameters exceed the length limit', reasonCode: 'text-limit' }); + const match = footer(value); + if (match === null) return result('unresolved', { reason: 'A1111 settings footer is not unique', reasonCode: 'malformed' }); + // R4: text that starts with the delimiter has an empty positive (#165). + const body = `\n${rstripLineBreaks(value.slice(0, match.index))}`; + const parts = body.split(DELIMITER); + if (parts.length > 2) return result('ambiguous', { reason: 'Multiple negative prompt delimiters' }); + const positive = pyStrip(parts[0]); + if (!positive) return result('unresolved', { reason: EMPTY_POSITIVE_REASON }); + const provenance = { format: 'a1111', field: 'parameters', text_source: 'embedded-parameters' }; + const side = (text, role) => ({ text, storage_text: text, provenance: { ...provenance, role } }); + if (parts.length === 1) return result('absent', { positive: side(positive, 'positive'), reason: 'Negative prompt delimiter is absent' }); + const negative = pyStrip(parts[1]); + return result('resolved', { positive: side(positive, 'positive'), negative: side(negative, 'negative') }); +} diff --git a/web/js/workspace/local-resolver-stages.mjs b/web/js/workspace/local-resolver-stages.mjs new file mode 100644 index 0000000..351d560 --- /dev/null +++ b/web/js/workspace/local-resolver-stages.mjs @@ -0,0 +1,174 @@ +// Candidate selection for local-resolver.mjs (#249 local Inspect, PR L3 Task 9): ports of +// utils/prompt_pair_resolver.py `_GraphResolver` `stage`, `upstream_stages`, `final_stages`, `file_stages`, +// `non_file_outputs`, `producers_of`, `consumers`, `reaches_output`, plus `_outcome_key`, `_recipe_key` and +// `_shared_pair` (#270). Each function takes the resolver whose budgets and caches it spends, so the order of +// work, and with it every limit, is the server's. No DOM. +import { isDict, pyGet, pyStr } from './py-json.mjs'; +import { DETAILER, IDENTICAL_BRANCHES_REASON, Unresolved, isLink, result } from './local-resolver-support.mjs'; + +const roleText = side => side !== null && typeof side === 'object' ? side.text : null; +const storageText = side => side !== null && typeof side === 'object' ? side.storage_text : null; +/** `_outcome_key`: what a reader sees of a result, the state and the two role texts. */ +export const outcomeKey = item => JSON.stringify([item.state, roleText(item.positive), roleText(item.negative)]); +/** `_recipe_key`: the outcome plus each side's stored recipe. */ +const recipeKey = item => JSON.stringify([item.state, roleText(item.positive), roleText(item.negative), storageText(item.positive), storageText(item.negative)]); + +/** `_shared_pair`: the one pair every sampler branch resolved to, or null (#270). */ +export function sharedPair(candidates) { + const first = candidates[0]; + if (first.state !== 'resolved') return null; + if (new Set(candidates.map(recipeKey)).size !== 1) return null; + return result('resolved', { positive: first.positive, negative: first.negative, reason: IDENTICAL_BRANCHES_REASON, candidates }); +} + +/** `stage`: one sampler's result; a detailer is compared with its upstream stages. */ +export function stage(resolver, samplerId, later) { + if (later.size >= resolver.maxDepth) return result('unresolved', { samplerId, reason: 'Resolution depth limit reached', reasonCode: 'depth-limit' }); + const own = resolver.sides(samplerId); + if (own.state === 'unresolved') return own; + const [name, inputs] = resolver.node(samplerId); + const entry = resolver.semantics.samplers.get(name); + if (entry === undefined || entry.strategy !== DETAILER) return own; + let upstream; + try { + upstream = upstreamStages(resolver, samplerId, pyGet(inputs, entry.image_stage)); + if (upstream.some(nodeId => later.has(nodeId))) throw new Unresolved('Cyclic sampler stage ancestry', 'malformed'); + } catch (error) { + if (!(error instanceof Unresolved)) throw error; + return result('unresolved', { samplerId, reason: error.message, reasonCode: error.code, node: error.node }); + } + if (!upstream.length) return own; + const deeper = new Set([...later, samplerId]); + const stages = upstream.map(nodeId => stage(resolver, nodeId, deeper)); + const blocked = stages.find(item => item.state === 'unresolved'); + // A stage that cannot be read cannot be compared: never pick one. + if (blocked !== undefined) return result('unresolved', { samplerId, reason: blocked.reason, reasonCode: blocked.reason_code, node: blocked.node }); + const flat = []; + for (const item of [own, ...stages]) flat.push(...(item.state === 'ambiguous' ? item.candidates : [item])); + if (new Set(flat.map(outcomeKey)).size === 1) return own; + return result('ambiguous', { samplerId, reason: 'Sampler stages have different prompts', candidates: flat, reasonCode: 'multistage-differs' }); +} + +/** `upstream_stages`: samplers reached from a detailer's image input, breadth first; each node draws a visit. */ +function upstreamStages(resolver, origin, value) { + if (!isLink(value)) throw new Unresolved('Expected an image link', 'malformed'); + const found = [], start = String(value[0]), queue = [[start, 0]], queued = new Set([start]); + for (let head = 0; head < queue.length; head++) { + const [nodeId, depth] = queue[head]; + if (nodeId === origin) throw new Unresolved('Cyclic sampler stage ancestry', 'malformed'); + if (depth >= resolver.maxDepth) throw new Unresolved('Resolution depth limit reached', 'depth-limit'); + resolver.budget.spend(); + const [name, inputs] = resolver.node(nodeId); + if (resolver.stageClasses.has(name)) { found.push(nodeId); continue; } + if (resolver.stageShaped(name, inputs)) throw resolver.unsupported('Unsupported sampler stage ancestry', nodeId); + resolver.enqueue(queue, queued, inputs, depth + 1); + } + return found; +} + +const strategyOf = (resolver, nodeId) => resolver.semantics.samplers.get(resolver.graph.get(nodeId).get('class_type')).strategy; + +/** `final_stages`: drop samplers that only feed detailers (each is judged in that chain). */ +export function finalStages(resolver, samplerIds) { + samplerIds = fileStages(resolver, samplerIds); + const detailers = samplerIds.filter(nodeId => strategyOf(resolver, nodeId) === DETAILER); + if (!detailers.length) return samplerIds; + let reverse; + try { reverse = consumers(resolver); } catch (error) { + if (!(error instanceof Unresolved)) throw error; + return samplerIds; + } + const absorbed = new Set(); + for (const detailer of detailers) { + resolver.budget.reset(resolver.perCandidateVisits); + try { + const [name, inputs] = resolver.node(detailer); + const image = pyGet(inputs, resolver.semantics.samplers.get(name).image_stage); + for (const nodeId of upstreamStages(resolver, detailer, image)) { + if (!reachesOutput(resolver, nodeId, reverse)) absorbed.add(nodeId); + } + } catch (error) { + if (!(error instanceof Unresolved)) throw error; + } + } + const kept = samplerIds.filter(nodeId => !absorbed.has(nodeId)); + return kept.length ? kept : samplerIds; +} + +/** `file_stages`: stages not proven preview-only; all of them when every one is. */ +function fileStages(resolver, samplerIds) { + if (samplerIds.length < 2) return samplerIds; + let toFile, toPreview; + try { + const reverse = consumers(resolver), previews = nonFileOutputs(resolver); + const terminals = [...resolver.graph].filter(([nodeId, node]) => isDict(node) && !previews.has(nodeId) && !reverse.get(nodeId)?.length).map(([nodeId]) => nodeId); + toFile = producersOf(resolver, terminals, previews); + toPreview = producersOf(resolver, [...previews], previews); + } catch (error) { + if (!(error instanceof Unresolved)) throw error; + return samplerIds; + } + const kept = samplerIds.filter(nodeId => toFile.has(nodeId) || !toPreview.has(nodeId)); + return kept.length ? kept : samplerIds; +} + +function nonFileOutputs(resolver) { + const names = resolver.semantics.nonFileOutputs, found = new Set(); + for (const [nodeId, node] of resolver.graph) { + // `node.get("class_type") in names`: only a string can be a listed class. + if (isDict(node) && names.has(pyGet(node, 'class_type'))) found.add(nodeId); + } + return found; +} + +/** `producers_of`: nodes whose output reaches one of `starts`, never through a preview. */ +function producersOf(resolver, starts, previews) { + const found = new Set(starts), queue = [...starts]; + for (let head = 0; head < queue.length; head++) { + const inputs = pyGet(resolver.graph.get(queue[head]), 'inputs'); + if (!isDict(inputs)) continue; + resolver.scan(inputs.size); + for (const value of inputs.values()) { + if (!isLink(value)) continue; + const producer = String(value[0]); + if (found.has(producer) || previews.has(producer) || !isDict(pyGet(resolver.graph, producer))) continue; + found.add(producer); + queue.push(producer); + } + } + return found; +} + +/** `consumers`: reverse links, node ID to the IDs of nodes reading one of its outputs. */ +function consumers(resolver) { + const reverse = new Map(); + for (const [nodeId, node] of resolver.graph) { + if (!isDict(node) || !isDict(pyGet(node, 'inputs'))) continue; + const inputs = node.get('inputs'); + resolver.scan(inputs.size); + for (const value of inputs.values()) { + if (!isLink(value)) continue; + const producer = String(value[0]); + if (!reverse.has(producer)) reverse.set(producer, []); + reverse.get(producer).push(nodeId); + } + } + return reverse; +} + +/** `reaches_output`: whether a sampler's output reaches a terminal node besides later stages. */ +function reachesOutput(resolver, nodeId, reverse) { + const seen = new Set([nodeId]), queue = [nodeId]; + for (let head = 0; head < queue.length; head++) { + for (const consumer of reverse.get(queue[head]) ?? []) { + if (seen.has(consumer)) continue; + seen.add(consumer); + resolver.budget.spend(); + const name = pyStr(pyGet(resolver.graph.get(consumer), 'class_type', '')); + if (resolver.stageClasses.has(name) || resolver.semantics.nonFileOutputs.has(name)) continue; + if (!reverse.get(consumer)?.length) return true; + queue.push(consumer); + } + } + return false; +} diff --git a/web/js/workspace/local-resolver-support.mjs b/web/js/workspace/local-resolver-support.mjs new file mode 100644 index 0000000..f180e07 --- /dev/null +++ b/web/js/workspace/local-resolver-support.mjs @@ -0,0 +1,255 @@ +// Tables, evidence reader, bounds and outcome lines for local-resolver.mjs (#249 local Inspect, PR L3 Tasks 8-9): +// the constants of utils/prompt_pair_resolver.py and utils/inspection_outcomes.py, utils/pair_execution.py +// `read_pair_execution` and the semantics table check of utils/inspection_semantics.py. No DOM. +import { PyFloat, PyValueError, isDict, isList, isInt, pyJsonLoads, pyLen, pySlice, pyStrip, pyTruthy, pyEqualsOne, pyStr, pyGet } from './py-json.mjs'; + +/** The semantics table is unusable: the server's `SemanticsError`. */ +export class SemanticsError extends Error { + constructor(message) { + super(message); + this.name = 'SemanticsError'; + } +} + +// INSPECTION_PROFILE (prompt_pair_resolver.py 152-166); its drop_preview_only_stages is always on here. +export const PROFILE = Object.freeze({ + perCandidateVisits: 2000, totalVisits: 20000, maxDepth: 64, maxTextLength: 100000, nodeCeiling: 20000, + maxPathEntries: 64 * 20000, maxBuiltChars: 16 * 100000, maxScannedUnits: 8000000, +}); +export const EXECUTION_KEY = 'comfyassets_prompt_pairs'; +export const ROLE_METADATA_KEYS = new Set(['prompt', 'parameters', EXECUTION_KEY]); +// pair_execution.py +export const MAX_EXECUTION_RECORDS = 16, MAX_EXECUTION_NODES = 64, MAX_EXECUTION_TEXT = 100000, MAX_EXECUTION_JSON = 500000; +export const EXECUTION_FIELDS = ['text', 'negative_text', 'positive_text', 'negative_effective_text', 'prepend_text', 'append_text']; + +export const CANDIDATE_STRATEGIES = new Set(['ksampler', 'custom-advanced', 'detailer-stage']); +export const DETAILER = 'detailer-stage'; +export const GUIDER_ABSENT_REASON = 'Guider has no negative input'; +export const ZEROED_SOURCE = 'zeroed-conditioning'; +export const MAX_BRANCH_CANDIDATES = 32; +export const RUNTIME_REASON = 'Text is decided at run time'; +export const EMPTY_POSITIVE_REASON = 'Positive prompt is empty'; +export const IDENTICAL_BRANCHES_REASON = 'Every sampler branch uses the same prompt pair'; +export const MULTISTAGE_UNATTRIBUTED_REASON = 'Multistage sampler ancestry is ambiguous'; +export const MALFORMED_EVIDENCE_REASON = 'Prompt evidence is malformed'; +export const UNREADABLE_METADATA_REASON = 'Media metadata could not be read'; +export const SEMANTICS_UNAVAILABLE_REASON = 'Inspection node semantics are unavailable'; +export const INCOMPATIBLE_SLOT = 'Output slot has an incompatible type'; +export const OVERWRITTEN_PROMPT_REASON = 'Saved PromptManager text was overwritten by another node'; +export const UNVERIFIABLE_CODES = new Set(['unsupported-node', 'runtime-value']); +export const MAX_JOIN_INPUTS = 1000; +export const SAMPLER_LIKE_INPUTS = ['latent_image', 'guider', 'positive', 'negative']; +export const NO_SAMPLER_NAMES = 3; +export const CLASS_NAME_LENGTH = 64; +export const MAX_SUBGRAPH_DEPTH = 8, MAX_WORKFLOW_NODES = 100000; + +// inspection_outcomes.py +export const OUTCOME_NAME_LENGTH = 200; +export const LIMIT_CODES = ['work-limit', 'depth-limit', 'text-limit', 'graph-ceiling']; +export const NAMED_LINES = { + 'unsupported-node': 'Prompt uses a node the inspector does not support', + 'runtime-value': 'Prompt is chosen at run time', + composite: 'Prompt combines several texts', +}; +export const CODED_LINES = { + ...Object.fromEntries(LIMIT_CODES.map(code => [code, 'Prompt exceeds inspection limits.'])), + 'no-sampler': 'No supported sampler was found.', + malformed: 'Prompt metadata in this file is malformed.', + 'multistage-differs': 'Sampler stages have different prompts.', +}; +export const UNCODED_LINES = { + [EMPTY_POSITIVE_REASON]: 'Positive prompt is empty.', + [MULTISTAGE_UNATTRIBUTED_REASON]: 'Several sampler stages were found; their prompts were not compared.', + [SEMANTICS_UNAVAILABLE_REASON]: "The inspector's node table is unavailable.", + [UNREADABLE_METADATA_REASON]: 'Metadata in this file could not be read.', +}; + +// Output adapters (prompt_pair_resolver.py 44-105): [inputName, kind, {compose, storageInput, comma, strip, default}]. +export const adapter = (inputName, kind, options = {}) => Object.freeze({ inputName, kind, compose: false, storageInput: false, comma: false, strip: false, default: null, ...options }); +export const slotKey = (name, slot) => `${name}\u0000${String(slot)}`; +export const ADAPTERS = new Map(); +for (const name of ['PromptManager', 'PromptManagerFocused', 'PromptManagerSplit']) { + ADAPTERS.set(slotKey(name, 0), adapter('text', 'conditioning', { compose: true })); + ADAPTERS.set(slotKey(name, 1), adapter('text', 'string', { compose: true })); +} +ADAPTERS.set(slotKey('PromptManagerPair', 0), adapter('text', 'conditioning', { compose: true })); +ADAPTERS.set(slotKey('PromptManagerPair', 2), adapter('text', 'string', { compose: true })); +ADAPTERS.set(slotKey('PromptManagerPair', 1), adapter('negative_text', 'conditioning', { storageInput: true })); +ADAPTERS.set(slotKey('PromptManagerPair', 3), adapter('negative_text', 'string', { storageInput: true })); +ADAPTERS.set(slotKey('PromptManagerText', 0), adapter('text', 'string', { compose: true })); +ADAPTERS.set(slotKey('CLIPTextEncode', 0), adapter('text', 'conditioning')); +for (const name of ['PrimitiveString', 'PrimitiveStringMultiline']) ADAPTERS.set(slotKey(name, 0), adapter('value', 'string')); +for (const name of ['Text Multiline', 'String Literal', 'ShowText|pysssss']) ADAPTERS.set(slotKey(name, 0), adapter('text', 'string')); +// Historical PromptManager classes (inspection only). +export const COMMA = { comma: true }, NEGATIVE = { strip: true, default: '' }; +const historical = [ + ['PromptManagerV2', 0, 'text', 'conditioning', COMMA], ['PromptManagerV2', 1, 'negative_text', 'conditioning', NEGATIVE], + ['PromptManagerV2', 2, 'text', 'string', COMMA], ['PromptManagerV2', 3, 'negative_text', 'string', NEGATIVE], + ['PromptManagerV2Text', 0, 'text', 'string', COMMA], ['PromptManagerV2Text', 1, 'negative_text', 'string', NEGATIVE], + ['PromptManagerNegative', 0, 'text', 'conditioning', { strip: true }], ['PromptManagerNegative', 1, 'text', 'string', { strip: true }], + ['PromptManagerPositiveText', 0, 'text', 'string', COMMA], ['PromptManagerNegativeText', 0, 'text', 'string', COMMA], +]; +for (const name of ['PromptManagerPositive', 'PromptManagerDetailer']) historical.push([name, 0, 'text', 'conditioning', COMMA], [name, 1, 'text', 'string', COMMA]); +for (const [name, slot, input, kind, options] of historical) ADAPTERS.set(slotKey(name, slot), adapter(input, kind, options)); +// `known_slots` in execution_record: every adapter slot per class. +export const ADAPTER_SLOTS = new Map(); +for (const key of ADAPTERS.keys()) { + const [name, slot] = key.split('\u0000'); + if (!ADAPTER_SLOTS.has(name)) ADAPTER_SLOTS.set(name, new Set()); + ADAPTER_SLOTS.get(name).add(slot); +} + +export class Unresolved extends Error { + constructor(message, code = null, node = null) { + super(message); + this.code = code; + this.node = node; + } +} +export class Diverging extends Error { + constructor(key) { + super('diverging'); + this.key = key; + } +} + +/** `_link`: a two-item list of (str or int) and a non-negative int, never a bool. */ +export function isLink(value) { + if (!isList(value) || value.length !== 2) return false; + const [source, slot] = value; + return (typeof source === 'string' || isInt(source)) && isInt(slot) && (typeof slot === 'bigint' ? slot >= 0n : slot >= 0); +} +export const nodeRef = (nodeId, classType) => ({ id: pySlice(nodeId, CLASS_NAME_LENGTH), class_type: typeof classType === 'string' ? pySlice(classType, CLASS_NAME_LENGTH) : null }); + +export function result(state, { positive = null, negative = null, samplerId = null, reason = '', candidates = [], reasonCode = null, node = null } = {}) { + return { state, positive, negative, sampler_id: samplerId, reason, candidates, reason_code: reasonCode, node }; +} + +export class Budget { + constructor(total) { + this.total = total; + this.allowance = total; + } + reset(allowance) { this.allowance = allowance; } + spend() { + if (this.total <= 0 || this.allowance <= 0) throw new Unresolved('Resolution visit limit reached', 'work-limit'); + this.total -= 1; + this.allowance -= 1; + } + charge(cost) { + if (cost > this.allowance) throw new Unresolved('Resolution visit limit reached', 'work-limit'); + this.allowance -= cost; + } +} + +/** `read_pair_execution`: one distinct record for a traced node, or null; malformed evidence throws `Unresolved`. */ +export function readPairExecution(metadata, nodeId) { + const evidence = message => new Unresolved(message, 'malformed'); + if (!isDict(metadata) || !metadata.has(EXECUTION_KEY)) return null; + let envelope = metadata.get(EXECUTION_KEY); + if (typeof envelope === 'string') { + if (pyLen(envelope) > MAX_EXECUTION_JSON) throw evidence('Execution evidence exceeds the size limit'); + try { envelope = pyJsonLoads(envelope); } catch (error) { + if (error instanceof PyValueError) throw evidence('Malformed execution evidence JSON'); + throw error; + } + } + if (!isDict(envelope) || !pyEqualsOne(pyGet(envelope, 'version'))) throw evidence('Unsupported execution evidence format'); + const nodes = pyGet(envelope, 'nodes'); + if (pyTruthy(pyGet(envelope, 'truncated')) || !isDict(nodes) || nodes.size > MAX_EXECUTION_NODES) throw evidence('Truncated or malformed execution evidence'); + if (!nodes.has(nodeId)) return null; + const node = nodes.get(nodeId); + if (!isDict(node) || pyTruthy(pyGet(node, 'truncated'))) throw evidence('Truncated or malformed node execution evidence'); + const records = pyGet(node, 'records'); + if (!isList(records) || !records.length || records.length > MAX_EXECUTION_RECORDS) throw evidence('Malformed node execution records'); + const unique = new Map(); + for (const record of records) { + if (!isDict(record) || EXECUTION_FIELDS.some(key => typeof pyGet(record, key) !== 'string' || pyLen(record.get(key)) > MAX_EXECUTION_TEXT)) { + throw evidence('Malformed execution text values'); + } + const clean = Object.fromEntries(EXECUTION_FIELDS.map(key => [key, record.get(key)])); + if (clean.negative_effective_text !== clean.negative_text) throw evidence('Negative execution text must be unchanged'); + unique.set(JSON.stringify(EXECUTION_FIELDS.map(key => clean[key])), clean); + } + if (unique.size !== 1) throw evidence('Multiple distinct executions have no image anchor'); + return [...unique.values()][0]; +} + +/** + * The validated semantics table (utils/inspection_semantics.py), from the parsed JSON document. Throws + * `SemanticsError` for any shape the server rejects; the caller then answers "semantics unavailable". + */ +export function compileSemantics(doc) { + const fail = message => { throw new SemanticsError(message); }; + const plain = value => value !== null && typeof value === 'object' && !Array.isArray(value); + if (!plain(doc) || doc.schema !== 1) fail('unsupported semantics table'); + const owners = new Map(); + const claim = (name, category) => { + if (owners.has(name) && owners.get(name) !== category) fail(`duplicate class ${name}`); + owners.set(name, category); + }; + const byClass = category => { + if (!plain(doc[category])) fail(`category ${category} must be an object`); + const table = new Map(); + for (const [name, entry] of Object.entries(doc[category])) { + if (!plain(entry) || typeof entry.strategy !== 'string') fail(`${category}[${name}]`); + claim(name, category); + table.set(name, entry); + } + return table; + }; + const bySlot = category => { + if (!Array.isArray(doc[category])) fail(`category ${category} must be a list`); + const table = new Map(); + for (const row of doc[category]) { + if (!plain(row) || typeof row.class !== 'string' || !Number.isInteger(row.slot) || row.slot < 0 || row.slot > 255) fail(`${category} row`); + const key = slotKey(row.class, row.slot); + if (table.has(key)) fail(`${category}: duplicate output key`); + claim(row.class, category); + const { class: _name, slot: _slot, ...entry } = row; + table.set(key, entry); + } + return table; + }; + const bySet = category => { + if (!Array.isArray(doc[category])) fail(`category ${category} must be a list`); + const set = new Set(); + for (const name of doc[category]) { + if (typeof name !== 'string' || set.has(name)) fail(`${category}: duplicate class`); + claim(name, category); + set.add(name); + } + return set; + }; + const samplers = byClass('samplers'), guiders = byClass('guiders'), selectors = byClass('selectors'); + const composites = byClass('composites'), savers = byClass('savers'); + const conditioning = bySlot('conditioning'), encoders = bySlot('encoders'), strings = bySlot('strings'); + const runtime = bySet('runtime'), nonFileOutputs = bySet('non_file_outputs'); + const stageInputs = doc.stage_inputs; + if (!plain(stageInputs) || !Array.isArray(stageInputs.shaped) || !Array.isArray(stageInputs.with_model)) fail('stage_inputs'); + return Object.freeze({ + samplers, guiders, selectors, composites, savers, conditioning, encoders, strings, runtime, nonFileOutputs, + stageInputs: { shaped: [...stageInputs.shaped], withModel: [...stageInputs.with_model] }, allClasses: new Set(owners.keys()), + }); +} + +/** `outcome_message`: one line for an unresolved outcome, or null to keep the status copy. */ +export function outcomeMessage(resolution) { + if (resolution?.state !== 'unresolved') return null; + const code = typeof resolution.reason_code === 'string' ? resolution.reason_code : null; + if (Object.hasOwn(NAMED_LINES, code)) { + const node = resolution.node, raw = node !== null && typeof node === 'object' ? node.class_type : null; + const name = typeof raw === 'string' && raw ? pySlice(raw, OUTCOME_NAME_LENGTH) : null; + return name ? `${NAMED_LINES[code]} (${name}).` : `${NAMED_LINES[code]}.`; + } + if (code === null) return Object.hasOwn(UNCODED_LINES, resolution.reason) ? UNCODED_LINES[resolution.reason] : null; + return Object.hasOwn(CODED_LINES, code) ? CODED_LINES[code] : null; +} + +export const sideOf = (status, text = null, source = null) => ({ status, text, source }); +export function explained(prompts, resolution) { + const message = outcomeMessage(resolution); + if (message === null) return prompts; + return Object.fromEntries(Object.entries(prompts).map(([role, side]) => [role, side.status === 'unresolved' && !Object.hasOwn(side, 'message') ? { ...side, message } : side])); +} +export const EMPTY_POSITIVE = outcomeMessage({ state: 'unresolved', reason: EMPTY_POSITIVE_REASON }); diff --git a/web/js/workspace/local-resolver.mjs b/web/js/workspace/local-resolver.mjs new file mode 100644 index 0000000..3e2b5bb --- /dev/null +++ b/web/js/workspace/local-resolver.mjs @@ -0,0 +1,772 @@ +// The server's inspection resolver in this browser (#249 local Inspect, PR L3 Task 8, decision LI-1): a port of +// utils/prompt_inspector.py `inspect_prompt_evidence` with utils/prompt_pair_resolver.py (INSPECTION_PROFILE) and +// utils/pair_execution.py `read_pair_execution`, driven by web/js/inspection-semantics.json. Python is the oracle: +// tests/test_local_inspect_parity.py compares every answer with the server's on the same records. Values follow +// Python's model (py-json.mjs): dict order, int versus float, code-point lengths, `str.strip()`. +// +// Task 8 ported execution evidence and graph tracing for one sampler; Task 9 adds candidate selection among several +// samplers and detailer stages (local-resolver-stages.mjs) and A1111 `parameters` (local-parameters.mjs). No DOM. +import { PyValueError, isDict, isList, isInt, pyJsonLoads, pyLen, pySlice, pyStrip, pyStr, pyGet } from './py-json.mjs'; +import { + ADAPTERS, + ADAPTER_SLOTS, + Budget, + CANDIDATE_STRATEGIES, + CLASS_NAME_LENGTH, + Diverging, + EMPTY_POSITIVE, + EMPTY_POSITIVE_REASON, + GUIDER_ABSENT_REASON, + INCOMPATIBLE_SLOT, + MALFORMED_EVIDENCE_REASON, + MAX_BRANCH_CANDIDATES, + MAX_JOIN_INPUTS, + MAX_SUBGRAPH_DEPTH, + MAX_WORKFLOW_NODES, + MULTISTAGE_UNATTRIBUTED_REASON, + NO_SAMPLER_NAMES, + OVERWRITTEN_PROMPT_REASON, + PROFILE, + ROLE_METADATA_KEYS, + RUNTIME_REASON, + SAMPLER_LIKE_INPUTS, + SEMANTICS_UNAVAILABLE_REASON, + UNREADABLE_METADATA_REASON, + UNVERIFIABLE_CODES, + Unresolved, + ZEROED_SOURCE, + compileSemantics, + explained, + isLink, + nodeRef, + readPairExecution, + result, + sideOf, + slotKey, +} from './local-resolver-support.mjs'; +import { finalStages, outcomeKey, sharedPair, stage } from './local-resolver-stages.mjs'; +import { parameters } from './local-parameters.mjs'; + +export { SemanticsError, compileSemantics, readPairExecution, outcomeMessage, isLink } from './local-resolver-support.mjs'; + +class GraphResolver { + constructor(graph, metadata, semantics, overwritten) { + this.graph = graph; + this.overwritten = overwritten; + this.perCandidateVisits = PROFILE.perCandidateVisits; + this.budget = new Budget(PROFILE.totalVisits); + this.budget.reset(PROFILE.perCandidateVisits); + this.pathEntries = PROFILE.maxPathEntries; + this.builtChars = PROFILE.maxBuiltChars; + this.scanned = PROFILE.maxScannedUnits; + this.checked = new Map(); + this.costs = new Map(); + this.heights = new Map(); + this.deepest = 0; + this.maxDepth = PROFILE.maxDepth; + this.maxTextLength = PROFILE.maxTextLength; + this.memo = new Map(); + this.active = new Set(); + this.metadata = metadata ?? new Map(); + this.textSources = new Map(); + this.choices = new Map(); + this.tainted = false; + this.latentWalks = new Map(); + this.latentCharged = new Set(); + this.semantics = semantics; + this.samplerClasses = new Set([...semantics.samplers].filter(([, entry]) => CANDIDATE_STRATEGIES.has(entry.strategy)).map(([name]) => name)); + this.stageClasses = new Set(semantics.samplers.keys()); + } + + node(nodeId) { + if (!this.checked.has(nodeId)) { + try { + this.checked.set(nodeId, this.checkedNode(nodeId)); + } catch (error) { + if (!(error instanceof Unresolved) || error.code === 'work-limit') throw error; + this.checked.set(nodeId, error); + } + } + const checked = this.checked.get(nodeId); + if (checked instanceof Unresolved) throw new Unresolved(checked.message, checked.code, checked.node); + return checked; + } + checkedNode(nodeId) { + const node = pyGet(this.graph, nodeId); + if (!isDict(node) || !isDict(pyGet(node, 'inputs'))) throw new Unresolved('Missing or malformed graph node', 'malformed'); + const inputs = node.get('inputs'); + this.scan(inputs.size); + return [pyStr(pyGet(node, 'class_type', '')), inputs]; + } + scan(units) { + if (units > this.scanned) { + this.scanned = -1; + throw new Unresolved('Resolution work limit reached', 'work-limit'); + } + this.scanned -= units; + } + unsupported(message, nodeId) { return this.named(message, 'unsupported-node', nodeId); } + named(message, code, nodeId) { + const node = pyGet(this.graph, nodeId); + const classType = isDict(node) ? pyGet(node, 'class_type') : null; + return new Unresolved(message, code, nodeRef(nodeId, classType)); + } + bounded(text) { + if (pyLen(text) > this.maxTextLength) throw new Unresolved('Resolved text exceeds the length limit', 'text-limit'); + return text; + } + built(text) { + this.builtChars -= pyLen(text); + if (this.builtChars < 0) throw new Unresolved('Resolution work limit reached', 'work-limit'); + return text; + } + + /** `text`: [text, path, storageText] for a string value or a typed link. */ + text(value, kind, depth = 0) { + this.deepest = Math.max(this.deepest, depth); + if (depth >= this.maxDepth) throw new Unresolved('Resolution depth limit reached', 'depth-limit'); + if (typeof value === 'string' && kind === 'string') return [this.bounded(value), [], value]; + if (!isLink(value)) throw new Unresolved('Expected a typed graph link or text input', 'malformed'); + const nodeId = String(value[0]), slot = value[1]; + const key = `${slotKey(nodeId, slot)}\u0000${kind}`; + if (this.active.has(key)) throw new Unresolved('Cyclic prompt graph', 'malformed'); + if (this.memo.has(key)) { + const height = this.heights.get(key); + if (depth + height >= this.maxDepth) throw new Unresolved('Resolution depth limit reached', 'depth-limit'); + this.budget.charge(this.costs.get(key)); + this.deepest = Math.max(this.deepest, depth + height); + return this.memo.get(key); + } + const allowance = this.budget.allowance, outerDeepest = this.deepest, outerTainted = this.tainted; + this.tainted = false; + this.deepest = depth; + try { + this.budget.spend(); + const [name, inputs] = this.node(nodeId); + this.scan(inputs.size); + if (this.overwritten.has(nodeId)) throw new Unresolved(OVERWRITTEN_PROMPT_REASON, 'malformed'); + this.active.add(key); + let text, path, storageText; + const adapted = ADAPTERS.get(slotKey(name, slot)); + if (name === 'Reroute' && slot === 0) { + const keys = ['value', 'input', 'in'].filter(item => inputs.has(item)); + if (keys.length !== 1) throw new Unresolved('Reroute input is not uniquely identifiable', 'malformed'); + [text, path, storageText] = this.text(inputs.get(keys[0]), kind, depth + 1); + this.inheritSource(nodeId, slot, [path]); + } else if (adapted) { + if (adapted.kind !== kind) throw new Unresolved(INCOMPATIBLE_SLOT, 'malformed'); + const evidence = name === 'PromptManagerPair' ? this.executionRecord(nodeId, inputs) : null; + if (evidence !== null) { + text = evidence[adapted.inputName === 'text' ? 'positive_text' : 'negative_effective_text']; + storageText = evidence[adapted.inputName]; + path = []; + this.textSources.set(slotKey(nodeId, slot), 'execution'); + } else if (adapted.comma || adapted.strip) { + [text, path, storageText] = this.historical(adapted, inputs, depth); + } else { + [text, path, storageText] = this.text(pyGet(inputs, adapted.inputName), 'string', depth + 1); + if (adapted.compose || adapted.storageInput) storageText = text; + if (adapted.compose) { + const [before, beforePath] = this.text(pyGet(inputs, 'prepend_text', ''), 'string', depth + 1); + const [after, afterPath] = this.text(pyGet(inputs, 'append_text', ''), 'string', depth + 1); + this.scan(pyLen(before) + pyLen(after) + beforePath.length + path.length + afterPath.length); + text = this.built(this.bounded([pyStrip(before), text, pyStrip(after)].filter(part => part).join(' '))); + path = [...beforePath, ...path, ...afterPath]; + } + } + } else if ((name === 'StringConcatenate' || name === 'Text Concatenate') && slot === 0 && kind === 'string') { + [text, path] = this.concatenate(name, inputs, depth); + storageText = text; + } else if (this.follows(name, slot, kind)) { + [text, path, storageText] = this.semantic(nodeId, name, slot, kind, inputs, depth); + } else { + throw this.unsupported('Unsupported node semantics or output slot', nodeId); + } + this.scan(path.length + 1); + path = [{ node_id: nodeId, output_slot: slot }, ...path]; + const pathLimit = this.maxDepth * Math.max(1, this.perCandidateVisits); + this.pathEntries -= path.length; + if (path.length > pathLimit || this.pathEntries < 0) throw new Unresolved('Provenance path limit reached', 'work-limit'); + const traced = [text, path, storageText]; + if (!this.tainted) { + this.memo.set(key, traced); + this.costs.set(key, allowance - this.budget.allowance); + this.heights.set(key, this.deepest - depth); + } + return traced; + } finally { + this.deepest = Math.max(outerDeepest, this.deepest); + this.tainted = outerTainted || this.tainted; + this.active.delete(key); + } + } + + /** `StringConcatenate` / `Text Concatenate` (the `text` branch). */ + concatenate(name, inputs, depth) { + let keys, delimiter, clean; + if (name === 'StringConcatenate') { + keys = ['string_a', 'string_b']; + delimiter = pyGet(inputs, 'delimiter', ''); + clean = false; + } else { + keys = [...inputs.keys()].filter(item => /^text_[a-z]$/.test(item)).sort(); + delimiter = pyGet(inputs, 'delimiter', ' '); + clean = pyStr(pyGet(inputs, 'clean_whitespace', 'true')).toLowerCase() === 'true'; + } + if (!keys.length || typeof delimiter !== 'string') throw new Unresolved('Malformed concatenation', 'malformed'); + const parts = [], path = []; + let size = 0; + for (const inputName of keys) { + let [part, followed] = this.text(pyGet(inputs, inputName, ''), 'string', depth + 1); + this.scan(pyLen(part) + followed.length); + if (clean) part = pyStrip(part); + path.push(...followed); + if (part || name === 'StringConcatenate') { + size += pyLen(part) + (parts.length ? pyLen(delimiter) : 0); + if (size > this.maxTextLength) throw new Unresolved('Resolved text exceeds the length limit', 'text-limit'); + parts.push(part); + } + } + return [this.built(parts.join(delimiter)), path]; + } + + /** `prompt_manager_*.py` at 07a5af3: strip, then join with ", ". */ + historical(adapted, inputs, depth) { + let [text, path] = this.text(pyGet(inputs, adapted.inputName, adapted.default), 'string', depth + 1); + this.scan(pyLen(text)); + text = pyStrip(text); + const storageText = text; + if (adapted.comma) { + let [before, beforePath] = this.text(pyGet(inputs, 'prepend_text') ?? '', 'string', depth + 1); + let [after, afterPath] = this.text(pyGet(inputs, 'append_text') ?? '', 'string', depth + 1); + this.scan(pyLen(before) + pyLen(after) + beforePath.length + afterPath.length); + before = pyStrip(before); after = pyStrip(after); + if (before) text = `${before}${text && !before.endsWith(',') ? ', ' : ''}${text}`; + if (after) text = `${text}${text && !text.endsWith(',') ? ', ' : ''}${after}`; + text = this.built(this.bounded(text)); + path = [...beforePath, ...path, ...afterPath]; + } + return [text, path, storageText]; + } + + follows(name, slot, kind) { + const semantics = this.semantics, key = slotKey(name, slot); + if (semantics.runtime.has(name)) return true; + if (semantics.encoders.has(key) || semantics.strings.has(key)) return true; + if (slot === 0 && semantics.selectors.has(name)) return true; + return kind === 'conditioning' && (semantics.conditioning.has(key) || (slot === 0 && semantics.composites.has(name))); + } + + semantic(nodeId, name, slot, kind, inputs, depth) { + const semantics = this.semantics, key = slotKey(name, slot); + if (semantics.runtime.has(name)) throw this.named(RUNTIME_REASON, 'runtime-value', nodeId); + for (const [table, expected, build] of [[semantics.encoders, 'conditioning', 'encoded'], [semantics.strings, 'string', 'stringed']]) { + const listed = table.get(key); + if (listed !== undefined) { + if (kind !== expected) throw new Unresolved(INCOMPATIBLE_SLOT, 'malformed'); + return this[build](nodeId, listed, inputs, depth); + } + } + const entry = kind === 'conditioning' ? semantics.conditioning.get(key) : undefined; + if (entry !== undefined && entry.strategy === 'zeroed') { + this.textSources.set(slotKey(nodeId, slot), ZEROED_SOURCE); + return ['', [], '']; + } + if (entry !== undefined) { + const traced = this.text(pyGet(inputs, entry.input), kind, depth + 1); + this.inheritSource(nodeId, slot, [traced[1]]); + return traced; + } + if (semantics.selectors.has(name)) return this.select(nodeId, semantics.selectors.get(name), inputs, kind, depth); + const parts = semantics.composites.get(name).inputs.map(inputName => this.text(pyGet(inputs, inputName), kind, depth + 1)); + if (new Set(parts.map(part => part[0])).size !== 1) throw this.named('Combined conditioning has different texts', 'composite', nodeId); + return this.merged(nodeId, slot, parts); + } + + select(nodeId, entry, inputs, kind, depth) { + const condition = pyGet(inputs, entry.condition), branches = entry.branches; + let choice; + if (typeof condition === 'boolean') choice = condition; + else if (!isLink(condition)) throw new Unresolved('Malformed selector condition', 'malformed'); + else if (this.choices.has(slotKey(String(condition[0]), condition[1]))) { + choice = this.choices.get(slotKey(String(condition[0]), condition[1])); + this.tainted = true; + } else choice = null; + if (choice !== null) { + const traced = this.text(pyGet(inputs, branches[choice ? 'true' : 'false']), kind, depth + 1); + this.inheritSource(nodeId, 0, [traced[1]]); + return traced; + } + const parts = ['true', 'false'].map(label => this.text(pyGet(inputs, branches[label]), kind, depth + 1)); + if (parts[0][0] !== parts[1][0]) throw new Diverging(slotKey(String(condition[0]), condition[1])); + return this.merged(nodeId, 0, parts); + } + + merged(nodeId, slot, parts) { + const traced = this.joinedPaths(parts, parts); + this.inheritSource(nodeId, slot, parts.map(part => part[1])); + return traced; + } + + joinedPaths(chosen, parts) { + const text = chosen[0][0]; + const storages = new Set(chosen.map(part => part[2])); + const storageText = storages.size === 1 ? chosen[0][2] : text; + const paths = parts.map(part => part[1]); + this.scan(paths.reduce((sum, path) => sum + path.length, 0)); + const merged = [], seen = new Set(); + for (const path of paths) { + for (const step of path) { + const identity = slotKey(step.node_id, step.output_slot); + if (!seen.has(identity)) { seen.add(identity); merged.push(step); } + } + } + return [text, merged, storageText]; + } + + encoded(nodeId, entry, inputs, depth) { + const names = entry.inputs; + if (entry.strategy === 'single') return this.text(pyGet(inputs, names[0]), 'string', depth + 1); + if (entry.strategy === 'flag-parts') return this.flagged(nodeId, entry, inputs, depth); + if (entry.strategy !== 'parts-equal') throw this.unsupported('Unsupported encoder strategy', nodeId); + const parts = names.map(name => this.text(pyGet(inputs, name), 'string', depth + 1)); + this.scan(parts.reduce((sum, part) => sum + pyLen(part[0]), 0)); + if (new Set(parts.map(part => part[0])).size === 1) return this.joinedPaths(parts, parts); + const filled = parts.filter(part => part[0] !== ''); + if (filled.length === 1) return this.joinedPaths(filled, parts); + throw this.named('Encoder parts have different texts', 'composite', nodeId); + } + + flagged(nodeId, entry, inputs, depth) { + const names = entry.inputs; + if (!this.setting(nodeId, inputs, entry.params.flag, 'bool')) return this.text(pyGet(inputs, names[0]), 'string', depth + 1); + const parts = names.map(name => this.text(pyGet(inputs, name), 'string', depth + 1)); + this.scan(parts.reduce((sum, part) => sum + pyLen(part[0]), 0)); + if (new Set(parts.map(part => part[0])).size === 1) return this.joinedPaths(parts, parts); + throw this.named(RUNTIME_REASON, 'runtime-value', nodeId); + } + + stringed(nodeId, entry, inputs, depth) { + const strategy = entry.strategy, source = entry.inputs[0]; + if (strategy === 'literal') return this.text(pyGet(inputs, source), 'string', depth + 1); + if (strategy === 'strip-newlines') { + const flag = this.setting(nodeId, inputs, entry.params.flag, 'bool'); + const traced = this.text(pyGet(inputs, source), 'string', depth + 1); + if (!flag) return traced; + this.scan(pyLen(traced[0])); + const text = this.built(this.bounded(pyStrip(traced[0].replaceAll('\n', '')))); + return [text, traced[1], text]; + } + if (strategy === 'join-multi') return this.joinMulti(nodeId, entry, inputs, depth); + throw this.unsupported('Unsupported string strategy', nodeId); + } + + /** A literal node setting of exactly `type(value) is kind`; linked is `unsupported-node`, else malformed. */ + setting(nodeId, inputs, name, kind) { + if (!inputs.has(name)) throw new Unresolved('Missing string node setting', 'malformed'); + const value = inputs.get(name); + if (isLink(value)) throw this.unsupported('Linked string node setting', nodeId); + const typed = { bool: typeof value === 'boolean', int: isInt(value), str: typeof value === 'string' }[kind]; + if (!typed) throw new Unresolved('Malformed string node setting', 'malformed'); + return value; + } + + joinMulti(nodeId, entry, inputs, depth) { + const params = entry.params, flag = params.list_flag; + if (!inputs.has(flag)) throw new Unresolved('Missing string node setting', 'malformed'); + if (isLink(inputs.get(flag)) || inputs.get(flag) === true) throw this.named(RUNTIME_REASON, 'runtime-value', nodeId); + if (inputs.get(flag) !== false) throw new Unresolved('Malformed string node setting', 'malformed'); + const count = this.setting(nodeId, inputs, params.count, 'int'); + const delimiter = this.setting(nodeId, inputs, params.delimiter, 'str'); + if (!(count >= 1 && count <= MAX_JOIN_INPUTS)) throw new Unresolved('Malformed string node setting', 'malformed'); + const total = Number(count); + this.scan(total); + const names = [entry.inputs[0]]; + for (let index = 2; index <= total; index++) names.push(`${params.input_prefix}${index}`); + const parts = [], path = [], seen = new Set(); + let size = 0; + names.forEach((inputName, position) => { + if (position && !inputs.has(inputName)) return; + const [part, followed] = this.text(pyGet(inputs, inputName), 'string', depth + 1); + this.scan(pyLen(part) + followed.length); + for (const step of followed) { + const identity = slotKey(step.node_id, step.output_slot); + if (!seen.has(identity)) { seen.add(identity); path.push(step); } + } + if (position && !part) return; + size += pyLen(part) + (parts.length ? pyLen(delimiter) : 0); + if (size > this.maxTextLength) throw new Unresolved('Resolved text exceeds the length limit', 'text-limit'); + parts.push(part); + }); + const text = this.built(parts.join(delimiter)); + return [text, path, text]; + } + + sourceOf(path) { + if (!path.length) return 'graph-inferred'; + return this.textSources.get(slotKey(path[0].node_id, path[0].output_slot)) ?? 'graph-inferred'; + } + inheritSource(nodeId, slot, paths) { + const sources = new Set(paths.map(path => this.sourceOf(path))); + if (sources.size === 1) this.textSources.set(slotKey(nodeId, slot), [...sources][0]); + else this.textSources.delete(slotKey(nodeId, slot)); + } + unverifiable(error) { return UNVERIFIABLE_CODES.has(error.code); } + + executionRecord(nodeId, inputs) { + const record = readPairExecution(this.metadata, nodeId); + if (record === null) return null; + for (const name of ['text', 'negative_text', 'prepend_text', 'append_text']) { + let expected; + try { + expected = this.text(pyGet(inputs, name, ''), 'string')[0]; + } catch (error) { + if (error instanceof Unresolved && this.unverifiable(error)) { + const value = pyGet(inputs, name); + if (isLink(value)) { + const [sourceName] = this.node(String(value[0])); + const known = ADAPTER_SLOTS.get(sourceName); + if (known && !known.has(String(value[1]))) throw error; + if (['StringConcatenate', 'Text Concatenate', 'Reroute'].includes(sourceName) && value[1] !== 0) throw error; + } + continue; + } + throw error; + } + if (name === 'prepend_text' || name === 'append_text') expected = pyStrip(expected); + if (expected !== record[name]) throw new Unresolved('Execution evidence does not match recorded node inputs', 'malformed'); + } + return record; + } + + side(value, samplerId, guiderId = null, depth = 0) { + const [text, path, storageText] = this.text(value, 'conditioning', depth); + const origin = path[0]; + const provenance = { + format: 'comfyui', sampler_id: samplerId, ...origin, path, + text_source: this.textSources.get(slotKey(origin.node_id, origin.output_slot)) ?? 'graph-inferred', + }; + if (guiderId !== null) provenance.guider_id = guiderId; + return { text, storage_text: storageText, provenance }; + } + + guider(value) { + const seen = new Set(); + let depth = 0; + for (;;) { + if (depth >= this.maxDepth) throw new Unresolved('Resolution depth limit reached', 'depth-limit'); + if (!isLink(value)) throw new Unresolved('Expected a guider link', 'malformed'); + const nodeId = String(value[0]), slot = value[1]; + if (seen.has(nodeId)) throw new Unresolved('Cyclic guider ancestry', 'malformed'); + seen.add(nodeId); + this.budget.spend(); + const [name, inputs] = this.node(nodeId); + this.scan(inputs.size); + depth += 1; + if (name === 'Reroute' && slot === 0) { + const keys = ['value', 'input', 'in'].filter(item => inputs.has(item)); + if (keys.length !== 1) throw new Unresolved('Reroute input is not uniquely identifiable', 'malformed'); + value = inputs.get(keys[0]); + continue; + } + const entry = this.semantics.guiders.get(name); + if (entry === undefined || slot !== 0) throw this.unsupported('Unsupported guider', nodeId); + return [nodeId, inputs, { positive: entry.positive, negative: entry.negative ?? null }, entry.absent_reason ?? GUIDER_ABSENT_REASON, depth]; + } + } + + pair(samplerId) { + this.budget.reset(this.perCandidateVisits); + this.latentCharged = new Set(); + if (this.budget.total <= 0) return result('unresolved', { samplerId, reason: 'Resolution visit limit reached', reasonCode: 'work-limit' }); + return this.branches(samplerId); + } + + branches(samplerId) { + const pending = [new Map()], results = []; + try { + while (pending.length) { + this.choices = pending.shift(); + try { + results.push(stage(this, samplerId, new Set())); + } catch (error) { + if (!(error instanceof Diverging)) throw error; + if (results.length + pending.length + 2 > MAX_BRANCH_CANDIDATES) throw new Unresolved('Too many selector branches', 'work-limit'); + for (const choice of [true, false]) pending.push(new Map([...this.choices, [error.key, choice]])); + } + } + } catch (error) { + if (!(error instanceof Unresolved)) throw error; + return result('unresolved', { samplerId, reason: error.message, reasonCode: error.code, node: error.node }); + } finally { + this.choices = new Map(); + } + if (results.every(item => item.state === 'unresolved')) return results[0]; + if (new Set(results.map(outcomeKey)).size === 1) return results[0]; + return result('ambiguous', { samplerId, reason: 'Selector branches have different prompts', candidates: results }); + } + + sides(samplerId) { + let positive = null, negative = null; + try { + let [name, inputs] = this.node(samplerId); + const entry = this.semantics.samplers.get(name); + this.checkOverrides(samplerId, entry, inputs); + const latent = entry.latent; + if (latent !== undefined && latent !== null && inputs.has(latent)) this.latentChecked(samplerId, inputs.get(latent)); + let guiderId = null, depth = 0, conditioning = entry.conditioning, absentReason = 'Negative input is absent'; + if (entry.strategy === 'custom-advanced') { + let guiderReason; + [guiderId, inputs, conditioning, guiderReason, depth] = this.guider(pyGet(inputs, conditioning.guider)); + if (conditioning.negative === null) absentReason = guiderReason; + } + positive = this.side(pyGet(inputs, conditioning.positive), samplerId, guiderId, depth); + if (!pyStrip(positive.text)) throw new Unresolved(EMPTY_POSITIVE_REASON); + const negativeName = conditioning.negative; + if (negativeName === null || negativeName === undefined || !inputs.has(negativeName)) { + return result('absent', { positive, samplerId, reason: absentReason }); + } + negative = this.side(inputs.get(negativeName), samplerId, guiderId, depth); + return result('resolved', { positive, negative, samplerId }); + } catch (error) { + if (!(error instanceof Unresolved)) throw error; + return result('unresolved', { positive, negative, samplerId, reason: error.message, reasonCode: error.code, node: error.node }); + } + } + + checkOverrides(samplerId, entry, inputs) { + for (const name of entry.prompt_overrides ?? []) { + const value = pyGet(inputs, name); + if (value === null || value === '') continue; + if (isLink(value)) throw this.unsupported('Unsupported prompt override', String(value[0])); + throw this.named(RUNTIME_REASON, 'runtime-value', samplerId); + } + } + + latentChecked(samplerId, value) { + let error; + if (this.latentWalks.has(samplerId)) { + const [walked, cost] = this.latentWalks.get(samplerId); + error = walked; + if (!this.latentCharged.has(samplerId)) { + this.budget.charge(cost); + this.latentCharged.add(samplerId); + } + } else { + const before = this.budget.allowance; + try { + this.latentStages(samplerId, value); + error = null; + } catch (raised) { + if (!(raised instanceof Unresolved) || raised.code === 'work-limit') throw raised; + error = raised; + } + this.latentWalks.set(samplerId, [error, before - this.budget.allowance]); + this.latentCharged.add(samplerId); + } + if (error !== null) throw new Unresolved(error.message, error.code, error.node === null ? null : { ...error.node }); + } + + enqueue(queue, queued, inputs, depth) { + this.scan(inputs.size); + for (const item of inputs.values()) { + if (!isLink(item)) continue; + const key = String(item[0]); + if (!queued.has(key)) { queued.add(key); queue.push([key, depth]); } + } + } + + stageShaped(name, inputs) { + if (this.semantics.allClasses.has(name)) return false; + const { shaped, withModel } = this.semantics.stageInputs; + if (shaped.some(key => isLink(pyGet(inputs, key)))) return true; + return isLink(pyGet(inputs, 'model')) && withModel.some(key => isLink(pyGet(inputs, key))); + } + + latentStages(origin, value) { + if (!isLink(value)) throw new Unresolved('Unknown latent ancestry', 'malformed'); + const start = String(value[0]), queue = [[start, 0]], queued = new Set([start]); + for (let head = 0; head < queue.length; head++) { + const [nodeId, depth] = queue[head]; + if (nodeId === origin) throw new Unresolved('Cyclic latent ancestry', 'malformed'); + if (depth >= this.maxDepth) throw new Unresolved('Resolution depth limit reached', 'depth-limit'); + this.budget.spend(); + const [name, inputs] = this.node(nodeId); + if (this.stageClasses.has(name)) throw new Unresolved(MULTISTAGE_UNATTRIBUTED_REASON); + if (this.stageShaped(name, inputs)) throw this.unsupported('Unsupported latent ancestry', nodeId); + this.enqueue(queue, queued, inputs, depth + 1); + } + } +} + + +/** `_sampler_like_classes`: distinct classes, in graph order, of nodes shaped like samplers or guiders. */ +function samplerLikeClasses(graph) { + const names = []; + for (const node of graph.values()) { + if (!isDict(node) || !isDict(pyGet(node, 'inputs'))) continue; + const name = pyGet(node, 'class_type'); + if (typeof name !== 'string' || !name || names.includes(name)) continue; + if (SAMPLER_LIKE_INPUTS.some(key => isLink(pyGet(node.get('inputs'), key)))) { + names.push(pySlice(name, CLASS_NAME_LENGTH)); + if (names.length === NO_SAMPLER_NAMES) break; + } + } + return names; +} + +function noSampler(graph) { + const seen = samplerLikeClasses(graph); + const reason = `No supported sampler pair found${seen.length ? ` (saw ${seen.join(', ')})` : ''}`; + return result('unresolved', { reason, reasonCode: 'no-sampler' }); +} + +/** `_prompt_manager_widgets`: API node ID to widget text for every PromptManager node that runs. */ +function promptManagerWidgets(workflow) { + const nodes = isDict(workflow) ? pyGet(workflow, 'nodes') : null; + if (!isList(nodes)) return new Map(); + const definitions = pyGet(workflow, 'definitions'); + const listed = isDict(definitions) ? pyGet(definitions, 'subgraphs') : null; + const subgraphs = new Map(); + for (const subgraph of isList(listed) ? listed : []) { + if (isDict(subgraph) && typeof pyGet(subgraph, 'id') === 'string' && isList(pyGet(subgraph, 'nodes'))) subgraphs.set(subgraph.get('id'), subgraph.get('nodes')); + } + const widgets = new Map(), stack = [[nodes, '', 0]]; + let visited = 0; + while (stack.length) { + const [members, prefix, depth] = stack.pop(); + for (const node of members) { + visited += 1; + if (visited > MAX_WORKFLOW_NODES) return widgets; + if (!isDict(node)) continue; + const kind = pyGet(node, 'type'), nodeId = pyGet(node, 'id'); + if (!(typeof nodeId === 'string' || isInt(nodeId))) continue; + const apiId = `${prefix}${String(nodeId)}`; + // A list or object `type` is neither a subgraph instance nor a PromptManager node (#286). + if (isList(kind) || isDict(kind)) continue; + if (typeof kind === 'string' && subgraphs.has(kind) && depth < MAX_SUBGRAPH_DEPTH) { + stack.push([subgraphs.get(kind), `${apiId}:`, depth + 1]); + continue; + } + const values = pyGet(node, 'widgets_values'); + const text = isList(values) && values.length && typeof values[0] === 'string' ? values[0] : + isDict(values) && typeof pyGet(values, 'text') === 'string' ? values.get('text') : null; + if (typeof kind === 'string' && kind !== 'PromptManagerPair' && kind.toLowerCase().includes('promptmanager') && text !== null) widgets.set(apiId, text); + } + } + return widgets; +} + +/** `_overwritten_prompt_nodes` (R1). */ +function overwrittenPromptNodes(workflow, graph) { + const widgets = promptManagerWidgets(workflow), byText = new Map(); + for (const [nodeId] of widgets) { + const node = pyGet(graph, nodeId); + const inputs = isDict(node) ? pyGet(node, 'inputs') : null; + const text = isDict(inputs) ? pyGet(inputs, 'text') : null; + if (typeof text === 'string') { + if (!byText.has(text)) byText.set(text, []); + byText.get(text).push(nodeId); + } + } + const found = new Set(); + for (const ids of byText.values()) { + if (ids.length > 1 && new Set(ids.map(id => widgets.get(id))).size > 1) ids.forEach(id => found.add(id)); + } + return found; +} + +/** `_resolve` for an unanchored inspection (output_node_id None). */ +function resolve(metadata, semantics) { + if (!isDict(metadata) || !metadata.size) return result('absent', { reason: 'No parsed prompt metadata' }); + let graph; + if (metadata.has('prompt')) graph = metadata.get('prompt'); + else if (metadata.has('parameters')) return parameters(metadata.get('parameters'), PROFILE.maxTextLength); + else graph = metadata; + if (!isDict(graph) || !graph.size) return result('unresolved', { reason: 'API graph is missing or malformed', reasonCode: 'malformed' }); + if (graph.size > PROFILE.nodeCeiling) return result('unresolved', { reason: 'Prompt graph exceeds the inspection node ceiling', reasonCode: 'graph-ceiling' }); + if (semantics === null) return result('unresolved', { reason: SEMANTICS_UNAVAILABLE_REASON }); + let overwritten = new Set(); + if (metadata.has('prompt')) { + overwritten = overwrittenPromptNodes(pyGet(metadata, 'workflow'), graph); + } + const resolver = new GraphResolver(graph, metadata.has('prompt') ? metadata : null, semantics, overwritten); + const samplerIds = finalStages(resolver, [...graph].filter(([, node]) => isDict(node) && typeof pyGet(node, 'class_type') === 'string' && + resolver.samplerClasses.has(node.get('class_type'))).map(([id]) => id)); + if (!samplerIds.length) return noSampler(graph); + if (samplerIds.length > 1) { + const candidates = samplerIds.map(id => resolver.pair(id)); + return sharedPair(candidates) ?? result('ambiguous', { reason: 'Multiple sampler branches', candidates }); + } + return resolver.pair(samplerIds[0]); +} + + +/** `_may_hold_prompt_manager`. */ +function mayHoldPromptManager(canonical) { + const graph = pyGet(canonical, 'prompt'); + if (!isDict(graph)) return false; + for (const node of graph.values()) { + if (!isDict(node)) continue; + const name = pyGet(node, 'class_type'), inputs = pyGet(node, 'inputs'); + if (typeof name === 'string' && name.toLowerCase().includes('promptmanager')) return true; + if (isDict(inputs) && inputs.has('search_text')) return true; + } + return false; +} + +/** `_parsed_workflow`: the UI workflow as a dict, or null. */ +function parsedWorkflow(value) { + if (typeof value === 'string') { + try { value = pyJsonLoads(value); } catch (error) { + if (error instanceof PyValueError) return null; + throw error; + } + } + return isDict(value) ? value : null; +} + +/** `unreadable_evidence`: both roles when the file's metadata could not be read at all. */ +export function unreadableEvidence() { + const resolution = { state: 'unresolved', sampler_id: null, reason: UNREADABLE_METADATA_REASON, reason_code: null, node: null }; + return { prompts: explained({ positive: sideOf('unresolved'), negative: sideOf('unresolved') }, resolution), resolution }; +} + +/** + * `inspect_prompt_evidence(records, None, profile=INSPECTION_PROFILE)`: `{prompts, resolution}` for the records a + * local read found (`{prompt, workflow, parameters, comfyassets_prompt_pairs}` texts). `semantics` is + * `compileSemantics(...)`'s table, or null when it could not be loaded. + */ +export function resolveLocalEvidence(records, semantics) { + const source = records !== null && typeof records === 'object' ? records : {}; + const metadata = new Map(Object.entries(source).filter(([key]) => ROLE_METADATA_KEYS.has(key))); + const malformedResolution = { state: 'unresolved', sampler_id: null, reason: MALFORMED_EVIDENCE_REASON, reason_code: 'malformed', node: null }; + if (!metadata.size) { + return { + prompts: { positive: sideOf('absent'), negative: sideOf('absent') }, + resolution: { state: 'absent', sampler_id: null, reason: 'No parsed prompt metadata', reason_code: null, node: null }, + }; + } + const malformed = { prompts: explained({ positive: sideOf('unresolved'), negative: sideOf('unresolved') }, malformedResolution), resolution: malformedResolution }; + if (!metadata.has('prompt') && !metadata.has('parameters')) return malformed; + if (typeof metadata.get('prompt') === 'string') { + try { metadata.set('prompt', pyJsonLoads(metadata.get('prompt'))); } catch (error) { + if (error instanceof PyValueError) return malformed; + throw error; + } + } + const workflow = mayHoldPromptManager(metadata) ? parsedWorkflow(source.workflow ?? null) : null; + if (workflow !== null && metadata.has('prompt')) metadata.set('workflow', workflow); + const resolved = resolve(metadata, semantics); + const state = resolved.state, prompts = {}; + for (const role of ['positive', 'negative']) { + const side = resolved[role]; + const present = side !== null && typeof side === 'object'; + if (state !== 'ambiguous' && present && (role === 'negative' || pyStrip(side.text))) prompts[role] = sideOf('resolved', side.text, side.provenance.text_source); + else if (state === 'resolved' && present) prompts[role] = { ...sideOf('unresolved'), message: EMPTY_POSITIVE }; + else prompts[role] = sideOf(state !== 'resolved' ? state : 'unresolved'); + } + const resolution = { state, sampler_id: resolved.sampler_id, reason: resolved.reason, reason_code: resolved.reason_code, node: resolved.node }; + return { prompts: explained(prompts, resolution), resolution }; +} + diff --git a/web/js/workspace/local-semantics.mjs b/web/js/workspace/local-semantics.mjs new file mode 100644 index 0000000..bfda05d --- /dev/null +++ b/web/js/workspace/local-semantics.mjs @@ -0,0 +1,27 @@ +// The node table local Inspect resolves with (#249 local Inspect, PR L3 Task 10): web/js/inspection-semantics.json, +// the very table the server's inspector reads, fetched once per page through the API client (the proxy-mount rule). +// The Inspect page asks for it as it mounts, before any file is chosen; it is the only static file Inspect fetches, +// and the request carries nothing about a selection. A table that cannot be fetched or used is null: the roles then +// answer as the server does when its table is unusable, and a later selection asks again. +import { compileSemantics } from './local-resolver.mjs'; + +export const SEMANTICS_PATH = '/prompt_manager/js/inspection-semantics.json'; +let pending = null; + +async function fetchSemantics() { + const client = globalThis.window?.ApiClient; + if (typeof client?.resolveUrl !== 'function' || typeof globalThis.fetch !== 'function') return null; + const response = await globalThis.fetch(client.resolveUrl(SEMANTICS_PATH), { credentials: 'same-origin' }); + if (!response.ok) throw new Error(`Inspection node table request failed (${response.status})`); + return compileSemantics(await response.json()); +} + +/** The compiled table (one request per page), or null when it is unavailable. */ +export function loadInspectionSemantics() { + if (pending !== null) return pending; + const current = pending = fetchSemantics().catch(() => null).then(table => { + if (table === null && pending === current) pending = null; + return table; + }); + return current; +} diff --git a/web/js/workspace/local-session.mjs b/web/js/workspace/local-session.mjs index 0765ff6..646f77e 100644 --- a/web/js/workspace/local-session.mjs +++ b/web/js/workspace/local-session.mjs @@ -70,7 +70,7 @@ function chosenFile(input) { } export function createLocalSession({ - parentScope, createObjectURL = file => URL.createObjectURL(file), revokeObjectURL = url => URL.revokeObjectURL(url), + parentScope, createObjectURL = file => URL.createObjectURL(file), revokeObjectURL = URL.revokeObjectURL.bind(URL), schedulerYield = defaultSchedulerYield(), setTimer = setTimeout, clearTimer = clearTimeout, pageTarget = globalThis.window, extractFacts, pngMetadata, } = {}) { diff --git a/web/js/workspace/local-video.mjs b/web/js/workspace/local-video.mjs index f414cd5..0cc26e4 100644 --- a/web/js/workspace/local-video.mjs +++ b/web/js/workspace/local-video.mjs @@ -17,6 +17,7 @@ // server folds them. import { INT_MAX_STR_DIGITS } from "./local-projection.mjs"; +import { JSON_MAX_DEPTH } from "./py-json.mjs"; // Role records (prompt, parameters, the pair execution record) share one budget: // past it the read fails. A workflow has its own budget; past it only the workflow @@ -40,7 +41,6 @@ const MAX_NAME_BYTES = 64; // the longest carrier name is 24 bytes; longer names export const VIDEO_READ_CEILING = MAX_VIDEO_ELEMENTS * (MAX_HEADER_BYTES + MAX_NAME_BYTES) + VIDEO_TAG_READ_BYTES; const PREFIX_BYTES = 32; const YIELD_INTERVAL_MS = 8; -const JSON_MAX_DEPTH = 1000; // Python's default recursion limit const ROLE_KEYS = ["prompt", "parameters", "comfyassets_prompt_pairs"]; const METADATA_KEYS = ["prompt", "workflow", "parameters", "comfyassets_prompt_pairs"]; @@ -826,8 +826,9 @@ export function pythonJsonLoads(text) { } } function value(depth) { - if (depth > JSON_MAX_DEPTH) throw new PythonJsonError("Maximum recursion depth exceeded", true); const char = text[pos]; + // The shared cap (py-json.mjs): `depth` containers enclose this value, and it would be one more. + if ((char === "{" || char === "[") && depth >= JSON_MAX_DEPTH) throw new PythonJsonError("Maximum recursion depth exceeded", true); if (char === '"') return string(); if (char === "{") { pos += 1; diff --git a/web/js/workspace/py-json.mjs b/web/js/workspace/py-json.mjs new file mode 100644 index 0000000..5c87291 --- /dev/null +++ b/web/js/workspace/py-json.mjs @@ -0,0 +1,249 @@ +// Python's value model for the local inspection resolver (#249 local Inspect, PR L3): the server's resolver +// (utils/prompt_pair_resolver.py) runs on `json.loads` output, so the browser port reads the same text the same way. +// Objects are `Map`s (insertion order, a repeated key keeps its first position and its last value, as a dict does), +// integers are numbers (or BigInt past 2^53 - 1), floats are `PyFloat`, None is null. Strings are measured, sliced and +// stripped as Python's `str` is: by code point, with `str.isspace()` whitespace. No DOM. + +/** A Python float: distinct from an int (`1.0` is not a link slot, `type(x) is int` is false). */ +export class PyFloat { + constructor(value) { + this.value = value; + Object.freeze(this); + } +} + +/** `ValueError` from `json.loads` (malformed text, or an integer past `sys.int_info.str_digits_check_threshold`). */ +export class PyValueError extends Error { + constructor(message) { + super(message); + this.name = 'PyValueError'; + } +} + +// CPython's default int max str digits (PEP 651 / gh-95778). +const INT_MAX_STR_DIGITS = 4300; +/** + * The nesting both browser CPython-JSON parsers accept (this one and local-video.mjs's): every array or object counts, + * an empty one too, as CPython's scanner enters a recursion level for each. Past it `json.loads` raises RecursionError, + * which the server treats as invalid JSON; its real limit depends on the Python (TESTING.md, "Deep JSON"). + */ +export const JSON_MAX_DEPTH = 1000; +const MAX_SAFE_DIGITS = '9007199254740991'; +const NUMBER = /-?(?:0|[1-9][0-9]*)(\.[0-9]+)?([eE][-+]?[0-9]+)?/y; +const STRING_STOP = /["\\\u0000-\u001f]/g; +const HEX4 = /^[0-9a-fA-F]{4}$/; +const ESCAPES = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' }; +// The C scanner's constants, in its order of checks. +const CONSTANTS = [['null', null], ['true', true], ['false', false], ['NaN', new PyFloat(NaN)], + ['Infinity', new PyFloat(Infinity)], ['-Infinity', new PyFloat(-Infinity)]]; + +export const isDict = value => value instanceof Map; +export const isList = value => Array.isArray(value); +/** `isinstance(value, int) and not isinstance(value, bool)`. */ +export const isInt = value => (typeof value === 'number' && Number.isInteger(value)) || typeof value === 'bigint'; + +function numberValue(match) { + const [literal, fraction, exponent] = match; + if (fraction || exponent) return new PyFloat(Number(literal)); + const digits = literal[0] === '-' ? literal.slice(1) : literal; + if (digits.length > INT_MAX_STR_DIGITS) throw new PyValueError('Exceeds the limit for integer string conversion'); + if (digits.length > MAX_SAFE_DIGITS.length || (digits.length === MAX_SAFE_DIGITS.length && digits > MAX_SAFE_DIGITS)) return BigInt(literal); + // `-0` is the int 0. + return Number(literal) || 0; +} + +/** `json.loads(text)` with Python's defaults (C scanner): NaN/Infinity accepted, a repeated key's last value wins. */ +export function pyJsonLoads(text) { + if (typeof text !== 'string') throw new TypeError('JSON text must be a string'); + if (text.startsWith('')) throw new PyValueError('Unexpected UTF-8 BOM (decode using utf-8-sig)'); + let pos = 0; + const fail = message => new PyValueError(`${message}: char ${pos}`); + const skip = () => { + while (pos < text.length) { + const c = text.charCodeAt(pos); + if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d) break; + pos++; + } + }; + function string() { + pos++; + let out = ''; + for (;;) { + STRING_STOP.lastIndex = pos; + const stop = STRING_STOP.exec(text); + if (!stop) throw fail('Unterminated string starting at'); + out += text.slice(pos, stop.index); + pos = stop.index + 1; + if (stop[0] === '"') return out; + if (stop[0] !== '\\') throw fail('Invalid control character at'); + const escape = text[pos]; + if (escape === 'u') { + const hex = text.slice(pos + 1, pos + 5); + if (!HEX4.test(hex)) throw fail('Invalid \\uXXXX escape'); + pos += 5; + const code = parseInt(hex, 16); + if (code >= 0xd800 && code <= 0xdbff && text[pos] === '\\' && text[pos + 1] === 'u') { + const low = text.slice(pos + 2, pos + 6); + if (!HEX4.test(low)) throw fail('Invalid \\uXXXX escape'); + const lowCode = parseInt(low, 16); + if (lowCode >= 0xdc00 && lowCode <= 0xdfff) { + out += String.fromCharCode(code, lowCode); + pos += 6; + continue; + } + } + out += String.fromCharCode(code); + } else if (Object.hasOwn(ESCAPES, escape)) { + out += ESCAPES[escape]; + pos++; + } else throw fail('Invalid \\escape'); + } + } + function key() { + if (text[pos] !== '"') throw fail('Expecting property name enclosed in double quotes'); + const name = string(); + skip(); + if (text[pos] !== ':') throw fail("Expecting ':' delimiter"); + pos++; + skip(); + return name; + } + function scalar() { + if (text[pos] === '"') return string(); + for (const [literal, value] of CONSTANTS) { + if (text.startsWith(literal, pos)) { + pos += literal.length; + return value; + } + } + NUMBER.lastIndex = pos; + const match = NUMBER.exec(text); + if (!match) throw fail('Expecting value'); + pos += match[0].length; + return numberValue(match); + } + skip(); + const stack = []; + const enter = () => { + if (stack.length >= JSON_MAX_DEPTH) throw fail('Maximum JSON nesting depth exceeded'); + pos++; skip(); + }; + let result; + for (;;) { + let value; + if (text[pos] === '{') { + enter(); + const frame = { value: new Map(), object: true, key: null }; + if (text[pos] === '}') pos++; + else { frame.key = key(); stack.push(frame); continue; } + value = frame.value; + } else if (text[pos] === '[') { + enter(); + const frame = { value: [], object: false }; + if (text[pos] === ']') pos++; + else { stack.push(frame); continue; } + value = frame.value; + } else value = scalar(); + for (;;) { + if (!stack.length) { result = value; break; } + const frame = stack[stack.length - 1]; + if (frame.object) frame.value.set(frame.key, value); + else frame.value.push(value); + skip(); + if (text[pos] === ',') { + pos++; skip(); + if (frame.object) frame.key = key(); + break; + } + if (text[pos] !== (frame.object ? '}' : ']')) throw fail("Expecting ',' delimiter"); + pos++; + stack.pop(); + value = frame.value; + } + if (!stack.length) break; + } + skip(); + if (pos !== text.length) throw fail('Extra data'); + return result; +} + +/** `len(text)`: code points (a lone surrogate counts once). */ +export function pyLen(text) { + let pairs = 0; + for (let index = 0; index < text.length - 1; index++) { + const code = text.charCodeAt(index); + if (code >= 0xd800 && code <= 0xdbff) { + const next = text.charCodeAt(index + 1); + if (next >= 0xdc00 && next <= 0xdfff) { pairs++; index++; } + } + } + return text.length - pairs; +} + +/** `text[:count]` by code points. */ +export function pySlice(text, count) { + if (text.length <= count) return text; + let end = 0; + for (let taken = 0; taken < count && end < text.length; taken++) { + const code = text.charCodeAt(end); + const next = text.charCodeAt(end + 1); + end += code >= 0xd800 && code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff ? 2 : 1; + } + return text.slice(0, end); +} + +// `str.isspace()`: Unicode Zs plus the bidi classes B, S and WS (not U+FEFF, unlike `trim()`). +export const PY_SPACE = '\\t\\n\\x0b\\x0c\\r\\x1c-\\x1f \\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000'; +const LEADING = new RegExp(`^[${PY_SPACE}]+`), TRAILING = new RegExp(`[${PY_SPACE}]+$`); +/** `text.strip()`. */ +export const pyStrip = text => text.replace(LEADING, '').replace(TRAILING, ''); + +/** `bool(value)`. */ +export function pyTruthy(value) { + if (value === null || value === undefined || value === false) return false; + if (value === true) return true; + if (typeof value === 'number') return value !== 0; + if (typeof value === 'bigint') return value !== 0n; + if (value instanceof PyFloat) return value.value !== 0; + if (typeof value === 'string' || isList(value)) return value.length > 0; + if (isDict(value)) return value.size > 0; + return true; +} + +/** `value == 1` (True and 1.0 equal it too). */ +export const pyEqualsOne = value => value === 1 || value === 1n || value === true || (value instanceof PyFloat && value.value === 1); + +/** `repr(float)`: the shortest round-trip digits, fixed for exponents -4..15, else `d.ddde+XX`. */ +export function pyFloatRepr(value) { + if (Number.isNaN(value)) return 'nan'; + if (!Number.isFinite(value)) return value > 0 ? 'inf' : '-inf'; + if (value === 0) return Object.is(value, -0) ? '-0.0' : '0.0'; + const [mantissa, exponentText] = value.toExponential().split('e'); + const exponent = Number(exponentText); + const sign = mantissa.startsWith('-') ? '-' : ''; + const digits = mantissa.replace(/^-/, '').replace('.', ''); + if (exponent < -4 || exponent >= 16) { + const rest = digits.slice(1); + return `${sign}${digits[0]}${rest ? `.${rest}` : ''}e${exponent < 0 ? '-' : '+'}${String(Math.abs(exponent)).padStart(2, '0')}`; + } + if (exponent < 0) return `${sign}0.${'0'.repeat(-exponent - 1)}${digits}`; + const whole = digits.slice(0, exponent + 1).padEnd(exponent + 1, '0'), fraction = digits.slice(exponent + 1); + return `${sign}${whole}.${fraction || '0'}`; +} + +/** + * `str(value)` where the resolver compares the result with node class names (`str(node.get("class_type", ""))`). + * A container's text never equals a class name, so it is a marker only. + */ +export function pyStr(value) { + if (typeof value === 'string') return value; + if (value === null || value === undefined) return 'None'; + if (value === true) return 'True'; + if (value === false) return 'False'; + if (typeof value === 'number' || typeof value === 'bigint') return String(value); + if (value instanceof PyFloat) return pyFloatRepr(value.value); + return '\u0000container'; +} + +/** `mapping.get(key, fallback)`. */ +export const pyGet = (mapping, key, fallback = null) => mapping.has(key) ? mapping.get(key) : fallback; diff --git a/web/js/workspace/workspace-dialogs.mjs b/web/js/workspace/workspace-dialogs.mjs index bdfa84f..39b35e6 100644 --- a/web/js/workspace/workspace-dialogs.mjs +++ b/web/js/workspace/workspace-dialogs.mjs @@ -286,14 +286,17 @@ export function openImportDialog({ scope, onInspect, onRescan, onMetadataScan = return { setRescanBusy }; } -/** "Copy prompt": the text in a readonly, pre-selected textarea for a manual copy. */ -export function openCopyFallback(text, { scope, trigger = null } = {}) { +/** + * "Copy prompt": the text in a readonly, pre-selected textarea for a manual copy. `title` names another copy's dialog + * (D117 "Copy settings summary"). + */ +export function openCopyFallback(text, { scope, trigger = null, title = 'Copy prompt' } = {}) { requireScope(scope); if (typeof text !== 'string') throw new TypeError('Copy text must be a string'); const area = el('textarea'); area.readOnly = true; area.value = text; - show('Copy prompt', [ + show(title, [ field('Select and copy this text', area, 'copy-text'), el('p', 'Automatic clipboard access requires a secure browser context.', 'small'), ], { scope, trigger, focusTarget: area }); diff --git a/web/js/workspace/workspace.css b/web/js/workspace/workspace.css index 2e87e8b..7117376 100644 --- a/web/js/workspace/workspace.css +++ b/web/js/workspace/workspace.css @@ -212,6 +212,8 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; } /* #249 D20: the viewer details' pair send and history buttons wrap inside the narrow rail at 200% zoom with wider fonts, where the toolkit's `nowrap` would push them past its edge. */ .ca-tools #viewer-details > button { white-space: normal; /* D20 */ } +/* LI-4: "Copy settings summary" after the facts wraps the same way in the narrow rail. */ +.ca-tools #viewer-details .settings-summary { white-space: normal; /* D117 */ } /* The Images page narrows to one column inside a narrow container (the Library-embedded gallery). */ .images-main { container: images-page / inline-size; } @container images-page (inline-size < 600px) {