fix(images): bound digit-string seeds and isolate each card's parse
A seed recorded as a 5000-digit string reached int(), which raises ValueError past Python's integer string limit, and the listing answered 500. A digit-string seed now has at most 20 digits (2**64 fits), and the conversion falls back to no seed on ValueError. Each card's parse is also guarded in the page builder: an unexpected error in one file's records logs a warning and gives that card no seed and no first-line title, and the rest of the page still lists.
This commit is contained in:
+20
-3
@@ -2351,6 +2351,23 @@ class ImageRoutesMixin:
|
||||
self.logger.warning("Workspace link summaries could not be read")
|
||||
return {}
|
||||
|
||||
def _card_parse(self, path: Path, records: dict | None) -> tuple:
|
||||
"""One card's ``(seed, parsed positive)``; ``(None, None)`` on any error.
|
||||
|
||||
One file's unexpected parse failure must not fail the whole page: its
|
||||
card shows no seed and withholds a first-line title.
|
||||
"""
|
||||
try:
|
||||
params = header_params(records)
|
||||
return card_seed(params), parsed_positive(params)
|
||||
except Exception:
|
||||
self.logger.warning(
|
||||
"Workspace card facts could not be parsed for %s",
|
||||
path.name,
|
||||
exc_info=True,
|
||||
)
|
||||
return None, None
|
||||
|
||||
def _format_workspace_page(
|
||||
self, page: list[tuple], *, linked: bool = True
|
||||
) -> list[dict]:
|
||||
@@ -2384,10 +2401,10 @@ class ImageRoutesMixin:
|
||||
continue
|
||||
# Parsed after the handle closes, so a concurrent rename or delete
|
||||
# (which Windows refuses on open files) is not held up by it.
|
||||
params = header_params(records)
|
||||
image["seed"] = card_seed(params)
|
||||
seed, positive = self._card_parse(path, records)
|
||||
image["seed"] = seed
|
||||
summary = summaries.get(str(path)) or _unavailable_link()
|
||||
image["linked"] = _card_link(summary, parsed_positive(params))
|
||||
image["linked"] = _card_link(summary, positive)
|
||||
if linked:
|
||||
# Image cards carry the star; Library previews show none.
|
||||
favorites = self._workspace_favorites(page)
|
||||
|
||||
@@ -643,6 +643,48 @@ class TestWorkspaceImages(ImageAPITestCase):
|
||||
else:
|
||||
self.assertEqual(shown, {"status": "known", "value": card})
|
||||
|
||||
async def test_unbounded_digit_string_seed_still_lists(self):
|
||||
records_png(
|
||||
self.output_dir / "huge.png", seeded_records("a fox", "9" * 5000), 200
|
||||
)
|
||||
records_png(self.output_dir / "ok.png", seeded_records("a fox", 5))
|
||||
response, data = await self.get()
|
||||
self.assertEqual(response.status, 200, data)
|
||||
items = {item["filename"]: item for item in data["images"]}
|
||||
self.assertIsNone(items["huge.png"]["seed"])
|
||||
self.assertEqual(items["ok.png"]["seed"], "5")
|
||||
|
||||
async def test_one_cards_parse_error_spares_the_page(self):
|
||||
prompt = self._save_prompt("a fox")
|
||||
for name, mtime in (("bad.png", 200), ("ok.png", 100)):
|
||||
path = records_png(
|
||||
self.output_dir / name, seeded_records("a fox", 5), mtime
|
||||
)
|
||||
self.api.db.link_image_to_prompt(prompt, str(path))
|
||||
real_parse = header_params_of_images()
|
||||
|
||||
def parsing(info):
|
||||
if parsing.calls == 0:
|
||||
parsing.calls += 1
|
||||
raise RuntimeError("unexpected parser bug")
|
||||
return real_parse(info)
|
||||
|
||||
parsing.calls = 0
|
||||
with (
|
||||
patch("py.api.images.header_params", side_effect=parsing),
|
||||
self.assertLogs(self.api.logger, "WARNING") as logged,
|
||||
):
|
||||
response, data = await self.get()
|
||||
self.assertEqual(response.status, 200, data)
|
||||
items = {item["filename"]: item for item in data["images"]}
|
||||
self.assertIsNone(items["bad.png"]["seed"])
|
||||
self.assertIsNone(items["bad.png"]["linked"]["title"])
|
||||
self.assertEqual(items["bad.png"]["linked"]["status"], "linked")
|
||||
self.assertEqual(items["ok.png"]["seed"], "5")
|
||||
self.assertEqual(items["ok.png"]["linked"]["title"], "a fox")
|
||||
self.assertNotIn("unexpected parser bug", json.dumps(data))
|
||||
self.assertIn("unexpected parser bug", "\n".join(logged.output))
|
||||
|
||||
async def test_library_previews_skip_card_facts(self):
|
||||
records_png(self.output_dir / "a.png", seeded_records("a fox", 5))
|
||||
page = await self.api._collect_output_images(
|
||||
|
||||
@@ -165,6 +165,22 @@ class CardSeed(unittest.TestCase):
|
||||
self.assertEqual(card_seed({"seed": value}), str(value))
|
||||
self.assertEqual(card_seed({"seed": "18446744073709551616"}), str(2**64))
|
||||
|
||||
def test_digit_string_seed_is_bounded(self):
|
||||
# 2**64 has 20 digits; a longer string is no seed, and an unbounded one
|
||||
# must not reach int() (Python's int max-str-digits raises ValueError).
|
||||
self.assertEqual(card_seed({"seed": "9" * 20}), "9" * 20)
|
||||
self.assertEqual(card_seed({"seed": "-" + "9" * 20}), "-" + "9" * 20)
|
||||
self.assertIsNone(card_seed({"seed": "1" * 21}))
|
||||
self.assertIsNone(card_seed({"seed": "9" * 5000}))
|
||||
self.assertIsNone(info_seed(graph(seeded("9" * 5000))))
|
||||
|
||||
def test_seed_conversion_error_gives_none(self):
|
||||
class Huge(int):
|
||||
def __str__(self):
|
||||
raise ValueError("Exceeds the limit for integer string conversion")
|
||||
|
||||
self.assertIsNone(card_seed({"seed": Huge(5)}))
|
||||
|
||||
def test_workflow_only_record(self):
|
||||
case = next(
|
||||
case
|
||||
|
||||
+22
-8
@@ -20,12 +20,21 @@ from .generation_params import (
|
||||
)
|
||||
from .inspection_response import project_graph
|
||||
|
||||
__all__ = ["MAX_RECORD_BYTES", "card_seed", "header_params", "parsed_positive"]
|
||||
__all__ = [
|
||||
"MAX_RECORD_BYTES",
|
||||
"MAX_SEED_DIGITS",
|
||||
"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")
|
||||
# A digit-string seed has at most this many digits (2**64 has 20). Longer text
|
||||
# is no seed, and never reaches int(), whose max-str-digits guard raises.
|
||||
MAX_SEED_DIGITS = 20
|
||||
_INTEGER = re.compile(r"-?[0-9]{1,%d}\Z" % MAX_SEED_DIGITS)
|
||||
|
||||
|
||||
def _utf8_size_over(text: str, limit: int) -> bool:
|
||||
@@ -76,18 +85,23 @@ def card_seed(params: dict | None) -> str | 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.
|
||||
recorded seed (the inspector shows it too), up to ``MAX_SEED_DIGITS``
|
||||
digits. A placeholder, other or longer 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))
|
||||
try:
|
||||
if isinstance(value, int):
|
||||
return str(value)
|
||||
if isinstance(value, str) and _INTEGER.match(value):
|
||||
return str(int(value))
|
||||
except ValueError:
|
||||
# Python's integer string conversion limit: no seed, never a 500.
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user