Workspace items gain `seed`, read with the shared generation-parameter parser from the prompt/workflow records the header open already loaded (each at most 2 MiB; no extra file reads, videos give None). A placeholder or non-integer seed gives None. JSON integers beyond 2**53 are recovered exactly, so the card prints the digits the inspector shows. The same parse confirms first-line card titles: a `text` title shows only when the file's parsed positive gives the same title. One helper applies that rule on the card and in the inspector, which now reads the positive from its own inspection records with the same parser.
128 lines
4.5 KiB
Python
128 lines
4.5 KiB
Python
"""Workspace card facts from the metadata records a header open already loaded.
|
|
|
|
The card shows what the inspector shows, so it reads the same shared parser
|
|
(``extract_generation_params``, the Python twin of the browser parser the
|
|
inspector fills from since #251) and nothing else: 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 json
|
|
import math
|
|
import re
|
|
from collections.abc import Mapping
|
|
|
|
from .generation_params import (
|
|
PLACEHOLDERS,
|
|
GenerationParamsError,
|
|
_number_string,
|
|
extract_from_records,
|
|
extract_generation_params,
|
|
)
|
|
|
|
__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")
|
|
_MAX_SAFE_INTEGER = 2**53 - 1
|
|
|
|
|
|
def header_params(info) -> dict | None:
|
|
"""The shared parser's result for the ``prompt``/``workflow`` text in ``info``.
|
|
|
|
``info`` is a Pillow ``Image.info`` (or the inspection read's metadata).
|
|
None when neither record is present, a record is not text or is over
|
|
``MAX_RECORD_BYTES``, or the browser parser would throw on it.
|
|
"""
|
|
if not isinstance(info, Mapping):
|
|
return None
|
|
records = {key: info.get(key) for key in _RECORD_KEYS}
|
|
if all(text is None for text in records.values()):
|
|
return None
|
|
for text in records.values():
|
|
if text is None:
|
|
continue
|
|
if not isinstance(text, str):
|
|
return None
|
|
if len(text.encode("utf-8", "surrogatepass")) > MAX_RECORD_BYTES:
|
|
return None
|
|
try:
|
|
params = extract_from_records(records["prompt"], records["workflow"])
|
|
except GenerationParamsError:
|
|
return None
|
|
return _with_exact_seed(params, records)
|
|
|
|
|
|
def _reject_constant(name):
|
|
raise ValueError(f"{name} is not JSON")
|
|
|
|
|
|
def _exact_json(text):
|
|
"""``text`` parsed with integers kept exact, or None where JSON.parse throws."""
|
|
if not isinstance(text, str):
|
|
return None
|
|
try:
|
|
return json.loads(text, parse_constant=_reject_constant)
|
|
except (ValueError, RecursionError):
|
|
return None
|
|
|
|
|
|
def _with_exact_seed(params: dict, records: dict) -> dict:
|
|
"""``params`` with a rounded seed replaced by the integer the file records.
|
|
|
|
``JSON.parse`` rounds integers beyond 2**53, but the inspector reads the
|
|
record's own digits (its projection carries big numbers as text). ComfyUI's
|
|
randomized seeds are mostly that large, so the same parser is run once more
|
|
on an exact parse of the same records, and its seed is kept only when it is
|
|
the one that rounded to the parsed value. Anything else leaves the seed
|
|
as parsed.
|
|
"""
|
|
seed = params.get("seed")
|
|
if not isinstance(seed, float) or not math.isfinite(seed):
|
|
return params
|
|
if abs(seed) <= _MAX_SAFE_INTEGER or not seed.is_integer():
|
|
return params
|
|
try:
|
|
exact = extract_generation_params(
|
|
{
|
|
key: _exact_json(text)
|
|
for key, text in records.items()
|
|
if text is not None
|
|
}
|
|
)["seed"]
|
|
except GenerationParamsError:
|
|
return params
|
|
if isinstance(exact, bool) or not isinstance(exact, int) or float(exact) != seed:
|
|
return params
|
|
return {**params, "seed": exact}
|
|
|
|
|
|
def card_seed(params: dict | None) -> int | None:
|
|
"""The parsed seed as the integer the browser prints, else None.
|
|
|
|
A placeholder, text, boolean or non-integral number gives None. A double
|
|
that ``header_params`` could not make exact gives the integer whose decimal
|
|
form equals the browser's ``String(seed)``.
|
|
"""
|
|
if not isinstance(params, Mapping):
|
|
return None
|
|
value = params.get("seed")
|
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
return None
|
|
if isinstance(value, float) and not math.isfinite(value):
|
|
return None
|
|
text = _number_string(value)
|
|
return int(text) if _INTEGER.match(text) else 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
|