* fix(scan): ingest metadata whose graph holds NaN or Infinity
ComfyUI writes its prompt with Python's default json.dumps, so a node whose
IS_CHANGED returns float("nan") saves "is_changed": [NaN] in every PNG. The
exact-pair ingestion stored the snapshots with allow_nan=False, so the scan
failed those files ("Out of range float values are not JSON compliant: nan")
and the monitor dropped their link. The legacy pages replaced NaN with null
before JSON.parse and ingested them.
- utils/json_strict.dumps_strict: strict JSON with non-finite numbers as
null; identical bytes for every other value.
- Pair snapshots and provenance, and the plain prompt link, store strict
JSON (the legacy link wrote bare NaN into the database).
- The browser-parser scan fallback parses records as the legacy pages do
(parse_page_json), so a graph with NaN still traces its prompt.
- The prompt images route answers strict JSON instead of regex-replacing
"NaN" (which also rewrote prompt text such as "ratio: NaN"); the NaN
cleaners also null Infinity.
Prompt text and its hash are unchanged: only numbers become null.
* fix(scan): leave the browser-parser NaN parse to the resolver parity fix
The scan fallback's legacy-page parse (parse_page_json in _graph_prompt_text)
is owned by the resolver parity change, which adds the same helper with the
legacy pages' exact rule (NaN reads as null, Infinity still rejected). This
branch keeps only the strict serialization of stored and answered JSON.
137 lines
5.3 KiB
Python
137 lines
5.3 KiB
Python
"""Atomic metadata enrichment for a single known generation context."""
|
|
|
|
import os
|
|
from typing import Any, Dict
|
|
|
|
from .models import normalize_image_path
|
|
from .operations import _coerce_file_info
|
|
|
|
try:
|
|
from ..utils.hashing import generate_prompt_hash
|
|
from ..utils.json_strict import dumps_strict
|
|
except ImportError:
|
|
from utils.hashing import generate_prompt_hash
|
|
from utils.json_strict import dumps_strict
|
|
|
|
|
|
def ingest_resolved_pair(
|
|
db: Any, image_path: str, metadata: Dict[str, Any], resolution: Dict[str, Any]
|
|
) -> Dict[str, Any]:
|
|
state = resolution.get("state", "unresolved")
|
|
if state != "resolved":
|
|
return {"status": state, "pairs_added": 0, "links_added": 0}
|
|
positive, negative = resolution["positive"], resolution["negative"]
|
|
positive_text = positive.get("storage_text", positive["text"])
|
|
negative_text = negative.get("storage_text", negative["text"])
|
|
normalized = normalize_image_path(image_path)
|
|
provenance = {"resolver_version": 1, "positive": positive, "negative": negative}
|
|
encoded = dumps_strict(provenance, sort_keys=True, separators=(",", ":"))
|
|
with db._pair_transaction() as conn:
|
|
images = conn.execute(
|
|
"SELECT gi.*,p.hash AS positive_hash FROM generated_images gi "
|
|
"JOIN prompts p ON p.id=gi.prompt_id "
|
|
"WHERE gi.file_path=? OR gi.image_path=?",
|
|
(normalized, image_path),
|
|
).fetchall()
|
|
if len(images) > 1 or (
|
|
images and images[0]["positive_hash"] != generate_prompt_hash(positive_text)
|
|
):
|
|
return {"status": "conflict", "pairs_added": 0, "links_added": 0}
|
|
existing = images[0] if images else None
|
|
if existing:
|
|
prior = conn.execute(
|
|
"SELECT n.hash AS negative_hash FROM image_prompt_pairs ip "
|
|
"JOIN prompt_pairs pp ON pp.id=ip.pair_id "
|
|
"LEFT JOIN prompts n ON n.id=pp.negative_prompt_id WHERE ip.image_id=?",
|
|
(existing["id"],),
|
|
).fetchall()
|
|
expected = (
|
|
generate_prompt_hash(negative_text) if negative_text.strip() else None
|
|
)
|
|
if any(row["negative_hash"] != expected for row in prior):
|
|
return {"status": "conflict", "pairs_added": 0, "links_added": 0}
|
|
prior_pair = conn.execute(
|
|
"SELECT pp.id FROM prompt_pairs pp "
|
|
"JOIN prompts p ON p.id=pp.positive_prompt_id "
|
|
"LEFT JOIN prompts n ON n.id=pp.negative_prompt_id "
|
|
"WHERE p.hash=? AND n.hash IS ?",
|
|
(
|
|
generate_prompt_hash(positive_text),
|
|
generate_prompt_hash(negative_text) if negative_text.strip() else None,
|
|
),
|
|
).fetchone()
|
|
pair_id = db._save_pair(conn, positive_text, negative_text)
|
|
pair = conn.execute(
|
|
"SELECT * FROM prompt_pairs WHERE id=?", (pair_id,)
|
|
).fetchone()
|
|
pairs_added = int(prior_pair is None)
|
|
file_size, width, height, image_format = _coerce_file_info(metadata)
|
|
# ComfyUI's NaN/Infinity are stored as null: the snapshot is strict JSON.
|
|
snapshots = {
|
|
column: dumps_strict(metadata[key], sort_keys=True)
|
|
for column, key in (
|
|
("workflow_data", "workflow"),
|
|
("prompt_metadata", "prompt"),
|
|
("parameters", "parameters"),
|
|
)
|
|
if key in metadata
|
|
}
|
|
info = {
|
|
"file_size": file_size,
|
|
"width": width,
|
|
"height": height,
|
|
"format": image_format,
|
|
}
|
|
snapshots.update(
|
|
{key: value for key, value in info.items() if value is not None}
|
|
)
|
|
changed = False
|
|
if existing:
|
|
image_id = existing["id"]
|
|
updates = {
|
|
key: value for key, value in snapshots.items() if existing[key] != value
|
|
}
|
|
if updates:
|
|
conn.execute(
|
|
"UPDATE generated_images SET "
|
|
+ ",".join(f"{key}=?" for key in updates)
|
|
+ " WHERE id=?",
|
|
[*updates.values(), image_id],
|
|
)
|
|
changed = True
|
|
else:
|
|
columns = ["prompt_id", "image_path", "filename", "file_path", *snapshots]
|
|
values = [
|
|
pair["positive_prompt_id"],
|
|
image_path,
|
|
os.path.basename(image_path),
|
|
normalized,
|
|
*snapshots.values(),
|
|
]
|
|
image_id = conn.execute(
|
|
"INSERT INTO generated_images("
|
|
+ ",".join(columns)
|
|
+ ") VALUES("
|
|
+ ",".join("?" for _ in columns)
|
|
+ ")",
|
|
values,
|
|
).lastrowid
|
|
linked = conn.execute(
|
|
"SELECT 1 FROM image_prompt_pairs WHERE pair_id=? "
|
|
"AND image_id=? AND provenance=?",
|
|
(pair_id, image_id, encoded),
|
|
).fetchone()
|
|
db._link_image_to_pair(conn, pair_id, image_id, provenance)
|
|
status = (
|
|
"added"
|
|
if existing is None
|
|
else "enriched" if changed or not linked else "unchanged"
|
|
)
|
|
return {
|
|
"status": status,
|
|
"pairs_added": pairs_added,
|
|
"links_added": int(not linked),
|
|
"pair_id": pair_id,
|
|
"image_id": image_id,
|
|
}
|