JSON.parse rounds a number past 2**53, so a listing seed such as 9223372036854788153 reached the page as 9223372036854788000 while the inspector showed the file's digits. Every non-null card seed is now its decimal string. The Node parity harness now JSON.parses the real listing response and compares String(seed) with the seed the inspector shows, including a seed past 2**53. TESTING.md describes the string contract and drops the removed exact re-read.
102 lines
3.7 KiB
Python
102 lines
3.7 KiB
Python
"""Workspace card facts from the metadata records a header open already loaded.
|
|
|
|
The card shows what the inspector shows, so it reads exactly what the
|
|
inspector's browser parser reads: each ``prompt``/``workflow`` record through
|
|
the inspection projection (``project_graph``: strict JSON, the depth and node
|
|
bounds, big integers and decimals as text), and only graphs it parsed, through
|
|
the shared parser (``extract_generation_params``, the Python twin of the
|
|
browser parser the inspector fills from since #251). No separate seed
|
|
heuristic, no graph resolution and no extra file reads. One parse feeds both
|
|
the seed and the positive that confirms a first-line card title.
|
|
"""
|
|
|
|
import re
|
|
from collections.abc import Mapping
|
|
|
|
from .generation_params import (
|
|
PLACEHOLDERS,
|
|
GenerationParamsError,
|
|
extract_generation_params,
|
|
)
|
|
from .inspection_response import project_graph
|
|
|
|
__all__ = ["MAX_RECORD_BYTES", "card_seed", "header_params", "parsed_positive"]
|
|
|
|
# Each record is parsed only up to this many UTF-8 bytes; past it, nothing is read.
|
|
MAX_RECORD_BYTES = 2 * 1024 * 1024
|
|
_RECORD_KEYS = ("prompt", "workflow")
|
|
_INTEGER = re.compile(r"-?[0-9]+\Z")
|
|
|
|
|
|
def _utf8_size_over(text: str, limit: int) -> bool:
|
|
"""True when ``text`` is over ``limit`` UTF-8 bytes, encoding only if unclear.
|
|
|
|
A code point takes one to four bytes, so most records are decided from
|
|
their length alone without copying them.
|
|
"""
|
|
if len(text) > limit:
|
|
return True
|
|
if len(text) * 4 <= limit:
|
|
return False
|
|
return len(text.encode("utf-8", "surrogatepass")) > limit
|
|
|
|
|
|
def header_params(info) -> dict | None:
|
|
"""The shared parser's result for the inspector's view of ``info``'s records.
|
|
|
|
``info`` is a Pillow ``Image.info`` (or the inspection read's metadata).
|
|
A record the projection does not parse is left out, as the inspector
|
|
leaves it out. None when no record parses, a record is not text or is
|
|
over ``MAX_RECORD_BYTES``, or the parser cannot use the graphs.
|
|
"""
|
|
if not isinstance(info, Mapping):
|
|
return None
|
|
graphs = {}
|
|
for key in _RECORD_KEYS:
|
|
text = info.get(key)
|
|
if text is None:
|
|
continue
|
|
if not isinstance(text, str) or _utf8_size_over(text, MAX_RECORD_BYTES):
|
|
return None
|
|
graph = project_graph(text, key)
|
|
if graph["status"] == "parsed":
|
|
graphs[key] = graph["value"]
|
|
if not graphs:
|
|
return None
|
|
try:
|
|
return extract_generation_params(graphs)
|
|
except (GenerationParamsError, RecursionError):
|
|
# Defence in depth: a parser failure gives no card facts, never a 500.
|
|
return None
|
|
|
|
|
|
def card_seed(params: dict | None) -> str | None:
|
|
"""The parsed seed as a decimal string, else None.
|
|
|
|
Always text: ``JSON.parse`` in the browser rounds a number past 2**53, and
|
|
ComfyUI's randomized seeds are often that large. The projection carries
|
|
integers beyond 2**53 as their exact digits, so a digit string is the
|
|
recorded seed (the inspector shows it too). A placeholder, other text, a
|
|
boolean or a decimal gives None.
|
|
"""
|
|
if not isinstance(params, Mapping):
|
|
return None
|
|
value = params.get("seed")
|
|
if isinstance(value, bool):
|
|
return None
|
|
if isinstance(value, int):
|
|
return str(value)
|
|
if isinstance(value, str) and _INTEGER.match(value):
|
|
return str(int(value))
|
|
return None
|
|
|
|
|
|
def parsed_positive(params: dict | None) -> str | None:
|
|
"""The parsed positive prompt text, or None for the placeholder or blank."""
|
|
if not isinstance(params, Mapping):
|
|
return None
|
|
text = params.get("positivePrompt")
|
|
if not isinstance(text, str) or text == PLACEHOLDERS["positivePrompt"]:
|
|
return None
|
|
return text if text.strip() else None
|