Files
ComfyAssets-ComfyUI_PromptM…/tests/browser/workspace_concept_fixture.py
T
Vito Sansevero 08bf160309 test(images): concept comparison for the Images page
images-concept.spec.mjs compares the production Images page with the
immutable concept on the concept-shaped fixture: heading, toolbar, filter
row, caption, grid, the first six cards, continuation and panel box in
Grid, List and Masonry, at 1600x1100 and 390x844, dark and light, normal
and reduced motion; plus hover zoom, the viewer title, focus restore,
Info selection, search and Load more with its toast. mapImagesConcept
applies only recorded deviations (D3, D8 and D16 follow-up PRs, D9, D10,
D14, D15, D17, D29, D31, D32); removed row items flow again from the
concept's own boxes. A mutation check proves the comparison fails on a
.media-info padding change and on removed Inspect buttons.

Corrections the comparison demonstrated:
- the grid and continuation carry the concept's ids (image-grid,
  gallery-continuation);
- bounded rendering reserves the card footer at its fractional height:
  offsetHeight rounded a wrapped 67.5px row up, so at 390px every card
  was a pixel taller and the grid 6px taller than the concept.

Two mismatches wait for a ruling and are pinned on both sides: the
category order (concept prompt order, production alphabetical) and the
singular count line ("1 positive prompt").
2026-10-20 07:13:24 -07:00

440 lines
16 KiB
Python

"""Concept-shaped real data for every concept comparison (D30 of #249).
The approved concept's six prompts (docs/concepts/workspace/app.js:71-178) and its
24 sample images (app.js:180-239) become real rows and real PNG files in this
server's disposable database and output folder, so production renders the
concept's shapes from its own stores: the same titles, texts, categories, tags,
ratings and negative names, the same image dimensions, filenames and seeds, and
the same sidebar counts (24 images, 6 prompts, 15 tags). Nothing is mocked.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
import math
import os
from pathlib import Path
import shutil
import time
from types import SimpleNamespace
from typing import TYPE_CHECKING
from uuid import UUID, uuid5
from PIL import Image, ImageDraw
from PIL.PngImagePlugin import PngInfo
from utils.execution_contexts import (
ContextBundle,
PromptSnapshot,
RecipeContext,
TextEvidence,
)
from utils.execution_history_events import make_finish, make_start
from utils.execution_outputs import OutputBundle, OutputReference, snapshot_output_roots
from utils.pair_execution import EXECUTION_KEY
if TYPE_CHECKING:
from py.api import PromptManagerAPI
@dataclass(frozen=True)
class ConceptPrompt:
id: str
title: str
category: str
tags: tuple[str, ...]
positive: str
negatives: tuple[tuple[str, str], ...]
color: tuple[str, str, str]
rating: int
# app.js:71-178, in concept order. Negatives are (name, text) pairs.
CONCEPT_PROMPTS = (
ConceptPrompt(
"alpine",
"Alpine observatory",
"Landscapes",
("cinematic", "blue_hour", "architecture"),
"A remote alpine observatory at blue hour, warm light through glass, dramatic "
"mountain peaks, cinematic composition, atmospheric mist, fine detail",
(
("Quality baseline", "blurry, low quality, low resolution"),
(
"Clean architecture",
"blurry, low quality, low resolution, distorted architecture, "
"watermark, text",
),
("No negative", ""),
),
("#14233c", "#9aaac3", "#d8d9dd"),
5,
),
ConceptPrompt(
"dunes",
"Quiet geometry",
"Architecture",
("minimal", "warm_light", "editorial"),
"Monolithic sandstone pavilion in a vast desert, sweeping dunes, warm "
"terracotta, late afternoon sun, architectural photography, quiet geometry",
(
("Natural materials", "plastic, harsh shadows, noise, watermark"),
(
"Clean lines",
"plastic, harsh shadows, noise, watermark, distorted geometry",
),
),
("#6c4948", "#ddad91", "#efe0c5"),
4,
),
ConceptPrompt(
"forest",
"Somewhere in the pines",
"Landscapes",
("forest", "mist", "peaceful"),
"A quiet cedar cabin above a still forest lake, towering evergreens, soft "
"morning mist, muted jade and moss, a small amber window, cinematic still",
(("Soft atmosphere", "oversaturated, blurry, low quality, text"),),
("#163b3d", "#6f9691", "#c0cfc1"),
5,
),
ConceptPrompt(
"city",
"After the rain",
"Environments",
("neon", "night", "cinematic"),
"Rain-soaked city street at night, blue neon and rose reflections, deep "
"perspective, cinematic science fiction, solitary architecture, detailed "
"atmosphere",
(
("Clean atmosphere", "blurry, low resolution, watermark, oversaturated"),
(
"Less signage",
"blurry, low resolution, watermark, oversaturated, text, signs",
),
),
("#1a2143", "#6869a1", "#cf8fbc"),
4,
),
ConceptPrompt(
"lunar",
"The long way home",
"Illustration",
("space", "minimal", "concept_art"),
"An isolated lunar research outpost, a luminous planet above the horizon, "
"sweeping pale terrain, quiet science fiction, finely detailed concept art",
(("Quality baseline", "blurry, low quality, watermark"),),
("#19283b", "#7398ac", "#e2d3c4"),
5,
),
ConceptPrompt(
"coast",
"Between sea and sky",
"Landscapes",
("coast", "soft_light", "peaceful"),
"A solitary lighthouse on a rugged coastal cliff, sea mist, glints of morning "
"light on turquoise water, atmospheric perspective, fine art landscape",
(("Natural light", "oversaturated, flat lighting, watermark, text"),),
("#255565", "#8cbcc2", "#e8d9bf"),
4,
),
)
# app.js:220-227: the image at flat index n has the n % 6 dimensions; with six
# prompts per round that is the prompt's own index.
CONCEPT_DIMENSIONS = (
(1536, 1152),
(1024, 1536),
(1280, 1280),
(1600, 900),
(1152, 1536),
(1536, 1024),
)
CONCEPT_IMAGE_ROUNDS = 4
CONCEPT_RUN_COUNT = 4
# The concept's Model fact (app.js:408).
CONCEPT_MODEL = "illustrative-model.safetensors"
MAX_EXTRA_IMAGES = 120
SESSION = "5c1f9a7e-3f0b-4a55-9d0e-2b8c4f6a7d10"
_TABLES = (
"execution_run_outputs",
"execution_output_capture",
"execution_run_contexts",
"execution_context_capture",
"execution_runs",
"execution_contexts",
"execution_prompt_snapshots",
"execution_context_texts",
"image_favorites",
"image_prompt_pairs",
"prompt_pair_tags",
"prompt_pairs",
"generated_images",
"prompt_tags",
"tags",
"prompt_library_state",
"prompts",
)
def concept_starred(prompt_index: int, round_index: int) -> bool:
"""The concept's favorites (app.js ``starred``): each other prompt's first image."""
return round_index == 0 and prompt_index % 2 == 0
def concept_seed(prompt_index: int, round_index: int) -> int:
"""The concept's sample seed (app.js:185)."""
return 42001 + prompt_index * 100 + round_index
def concept_filename(prompt: ConceptPrompt, round_index: int) -> str:
"""The concept's sample filename (app.js:187)."""
return f"{prompt.id}_{round_index + 1:05}.png"
def _graph(positive: str, negative: str, seed: int) -> dict:
"""A ComfyUI API prompt with the concept's facts (app.js:406-408).
The recipe is a Prompt Manager pair node, so with its embedded execution record
the inspector reports both roles as execution evidence, which the panel badges
"Execution recorded" (D21). One sampler keeps the seed unambiguous.
"""
return {
"1": {
"class_type": "PromptManagerPair",
"inputs": {"text": positive, "negative_text": negative, "clip": ["2", 1]},
},
"2": {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": CONCEPT_MODEL},
},
"3": {
"class_type": "KSampler",
"inputs": {
"model": ["2", 0],
"positive": ["1", 0],
"negative": ["1", 1],
"seed": seed,
"steps": 28,
"cfg": 6.5,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
},
},
"4": {"class_type": "SaveImage", "inputs": {"images": ["3", 0]}},
}
def _execution_record(positive: str, negative: str) -> dict:
"""The pair node's embedded execution evidence (utils/pair_execution.py)."""
record = {
"text": positive,
"negative_text": negative,
"positive_text": positive,
"negative_effective_text": negative,
"prepend_text": "",
"append_text": "",
}
return {"version": 1, "nodes": {"1": {"records": [record]}}}
def _snapshot(text: str) -> PromptSnapshot:
"""A recorded prompt snapshot whose raw and effective texts are ``text``."""
def known(value: str) -> TextEvidence:
return TextEvidence(value, "recorded", "runtime_return", None)
return PromptSnapshot(known(text), known(text), known(""), known(""))
def write_concept_png(path: Path, prompt_index: int, round_index: int) -> dict:
"""Write the concept's sample image ``round_index`` of prompt ``prompt_index``.
Returns its facts: variant, negative, seed and dimensions.
"""
prompt = CONCEPT_PROMPTS[prompt_index]
index = round_index * len(CONCEPT_PROMPTS) + prompt_index
variant = round_index % len(prompt.negatives)
negative = prompt.negatives[variant][1]
seed = concept_seed(prompt_index, round_index)
width, height = size = CONCEPT_DIMENSIONS[index % len(CONCEPT_DIMENSIONS)]
sky, ground, light = prompt.color
info = PngInfo()
info.add_text("prompt", json.dumps(_graph(prompt.positive, negative, seed)))
info.add_text(
EXECUTION_KEY, json.dumps(_execution_record(prompt.positive, negative))
)
with Image.new("RGB", size, sky) as image:
draw = ImageDraw.Draw(image)
draw.rectangle((0, height * 2 // 3, width, height), fill=ground)
radius = min(width, height) // 9
left, top = width * 2 // 3, height // 6
draw.ellipse((left, top, left + radius, top + radius), fill=light)
image.save(path, pnginfo=info)
return {
"variant": variant,
"negative": negative,
"seed": seed,
"width": width,
"height": height,
}
def _clear(api: PromptManagerAPI, root: Path) -> None:
with api.db.model.get_connection() as conn:
for table in _TABLES:
conn.execute(f"DELETE FROM {table}")
for name in ("output", "input"):
shutil.rmtree(root / name)
(root / name).mkdir()
def _record_run(
api: PromptManagerAPI,
*,
epoch: str,
item: int,
context: RecipeContext,
output: OutputReference,
observed_at: int,
) -> str:
"""One finished execution with its recipe context and its one output."""
job = str(uuid5(UUID(SESSION), f"concept-job-{item}"))
start = make_start(SESSION, ((item, job, {}, {}), item), epoch, observed_at)
api.db.write_execution_start(start)
finish = make_finish(
SESSION,
item,
epoch,
SimpleNamespace(messages=[("execution_success", {"prompt_id": job})]),
observed_at + 10,
)
api.db.write_execution_finish(finish)
api.db.write_execution_contexts(
start.run_id, epoch, ContextBundle((context,), "complete", "captured")
)
api.db.write_execution_outputs_for_finish(
finish, OutputBundle((output,), "complete", "reported")
)
return start.run_id
def seed_workspace_concept(
api: PromptManagerAPI, root: Path, *, extra_images: int = 0
) -> dict:
"""Replace this server's disposable rows and files with the concept's data.
``extra_images`` continues the concept's rounds (same prompts, dimensions and
seed rule) after its 24 images, for bounded-rendering and Load more checks.
"""
if isinstance(extra_images, bool) or not isinstance(extra_images, int):
raise TypeError("extra_images must be an integer")
if not 0 <= extra_images <= MAX_EXTRA_IMAGES:
raise ValueError("extra_images is out of range")
_clear(api, root)
db = api.db
epoch = db.get_execution_epoch()
prompts = []
for prompt in CONCEPT_PROMPTS:
first_name, first_text = prompt.negatives[0]
receipt = db.create_library_recipe(
prompt.positive,
first_text,
expected_epoch=epoch,
parent_metadata={
"title": prompt.title,
"category": prompt.category,
"tags": list(prompt.tags),
"rating": prompt.rating,
},
)
positive_id = receipt["positive"]["id"]
pair_ids = [receipt["pair"]["id"]]
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.
db.record_prompt_use(positive_id, times=CONCEPT_RUN_COUNT)
prompts.append(
{"id": prompt.id, "positive_id": positive_id, "pair_ids": pair_ids}
)
folder = root / "output" / "concepts"
folder.mkdir()
roots = snapshot_output_roots("posix", str(root / "output"), str(root / "temp"))
# The API package needs the host stubs the server installs before importing it.
from py.api.images import _favorite_relative_key, _favorite_root_key
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 = [], []
for round_index in range(rounds):
for prompt_index, prompt in enumerate(CONCEPT_PROMPTS):
index = round_index * len(CONCEPT_PROMPTS) + prompt_index
if index >= total:
break
name = concept_filename(prompt, round_index)
path = folder / name
facts = write_concept_png(path, prompt_index, round_index)
variant, negative, seed = facts["variant"], facts["negative"], facts["seed"]
# The concept lists images newest first in this order.
stamp = now - 60 - index * 60
os.utime(path, (stamp, stamp))
owner = prompts[prompt_index]
image_id = db.link_image_to_prompt(owner["positive_id"], str(path))
pair_id = owner["pair_ids"][variant]
db.link_image_to_pair(pair_id, image_id, {"source": "concept fixture"})
if concept_starred(prompt_index, round_index):
db.set_image_favorite(
favorite_root, _favorite_relative_key(f"concepts/{name}")
)
context = RecipeContext(
"3", _snapshot(prompt.positive), _snapshot(negative), "inferred"
)
reference = OutputReference(
"4", 0, "output", "concepts", name, "posix", roots.output_key
)
runs.append(
_record_run(
api,
epoch=epoch,
item=index,
context=context,
output=reference,
observed_at=int(stamp * 1000),
)
)
images.append(
{
"filename": name,
"image_id": image_id,
"pair_id": pair_id,
"prompt": prompt.id,
"variant": variant,
"seed": seed,
"width": facts["width"],
"height": facts["height"],
}
)
with db.model.get_connection() as conn:
counts = conn.execute(
"SELECT COUNT(*), MIN(run_count), MAX(run_count) FROM prompts "
"WHERE id IN (SELECT prompt_id FROM prompt_library_state)"
).fetchone()
tags = [row[0] for row in conn.execute("SELECT name FROM tags ORDER BY id")]
if tuple(counts) != (len(CONCEPT_PROMPTS), CONCEPT_RUN_COUNT, CONCEPT_RUN_COUNT):
raise RuntimeError(f"Concept fixture run counts are not real: {tuple(counts)}")
api._generate_gallery_thumbnails_sync(api._get_all_output_dirs(), (150, 150))
api.invalidate_gallery_cache()
return {
"epoch": epoch,
# The newest file's mtime (epoch seconds), for the D17 caption date.
"newest_modified": now - 60,
"prompts": prompts,
"images": images,
"runs": runs,
"tags": tags,
}