Skip to content

Commit 3ee9a4d

Browse files
sememtacclaude
andcommitted
add Mememage Decode All node for multi-bar composites
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e2086a4 commit 3ee9a4d

3 files changed

Lines changed: 94 additions & 0 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,14 @@ reference decoder's password box) reveals them. Needs the crypto library:
203203
look the record up (e.g. Load Record). To check whether an image *matches* its
204204
record, use **Mememage Verify** — that's the verification node.
205205

206+
**Mememage Decode All**`IMAGE → identifiers[], content_hashes[], count, image`
207+
- Reads **every** bar in a composite (a collage/contact sheet assembled from several
208+
barred images carries one bar each). `identifiers` and `content_hashes` are **list**
209+
outputs — wire either into **Load Record** or **Verify** and that node runs **once per
210+
bar** automatically (ComfyUI list-expansion), no loop node. `count` is how many were
211+
found (0 for a bare image), handy to gate on. For the ordinary one-image-one-bar case
212+
use **Mememage Decode** instead — this is the forensic reader.
213+
206214
**Mememage Reserve ID**`→ identifier`
207215
- A stable identifier **pointer** for iterating one piece. The **🎲 new slot** button
208216
mints a fresh `<prefix>-<16 hex>` (saved with the workflow); wire the output into

nodes.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,49 @@ def run(self, image):
780780
return (bar.identifier, bar.content_hash, image)
781781

782782

783+
class MememageDecodeAll:
784+
"""Read EVERY Mememage bar in an image — for composites that carry more than one.
785+
786+
The plain **Mememage Decode** reads the single bottom bar (the common case: one
787+
image, one record). This reads them all: an image assembled from several barred
788+
pictures (a collage, a contact sheet, a paste-up) carries a bar per source, at
789+
whatever height each sits. A forensic reader — "what provenanced images are in
790+
here" — closer in spirit to the validator's Observatory than to the mint pipeline.
791+
792+
`identifiers` and `content_hashes` are LIST outputs (bottom-most bar first, index-
793+
aligned), so wiring either into Load Record / Verify makes that node **run once
794+
per bar automatically** — no loop node. `count` is the plain number found (0 when
795+
the image has no bar), handy to gate on. The image passes through unchanged.
796+
797+
A false positive would have to beat the magic bytes, CRC-16, and Reed-Solomon at
798+
once, so bar-ish noise is dropped, not misread. Reads the first image of a batch,
799+
same as Decode.
800+
"""
801+
802+
@classmethod
803+
def INPUT_TYPES(cls):
804+
return {"required": {"image": ("IMAGE",)}}
805+
806+
RETURN_TYPES = ("STRING", "STRING", "INT", "IMAGE")
807+
RETURN_NAMES = ("identifiers", "content_hashes", "count", "image")
808+
OUTPUT_IS_LIST = (True, True, False, False) # ids + hashes fan out; count + image are scalar
809+
FUNCTION = "run"
810+
CATEGORY = "Mememage"
811+
DESCRIPTION = ("Read every Mememage bar in an image (for composites). identifiers + "
812+
"content_hashes are lists that fan out to Load Record / Verify; count is "
813+
"how many were found.")
814+
815+
def run(self, image):
816+
import mememage
817+
np, torch, Image = _deps()
818+
pil = _tensor_to_pil(image[0], np, Image)
819+
820+
bars = mememage.decode(pil, all_bars=True) # list of Bar, bottom-most first
821+
identifiers = [b.identifier for b in bars]
822+
content_hashes = [b.content_hash for b in bars]
823+
return (identifiers, content_hashes, len(bars), image)
824+
825+
783826
class MememageReserveId:
784827
"""A reserved identifier — a stable pointer you keep pointing at new versions.
785828
@@ -1476,6 +1519,7 @@ async def _mememage_pick_file(request):
14761519
"MememageField": MememageField,
14771520
"MememageFieldList": MememageFieldList,
14781521
"MememageDecode": MememageDecode,
1522+
"MememageDecodeAll": MememageDecodeAll,
14791523
"MememageLoadRecord": MememageLoadRecord,
14801524
"MememageFindRecord": MememageFindRecord,
14811525
"MememageFetchRecord": MememageFetchRecord,
@@ -1493,6 +1537,7 @@ async def _mememage_pick_file(request):
14931537
"MememageField": "Mememage Field",
14941538
"MememageFieldList": "Mememage Fields", # friendly name — "the node your fields go into"
14951539
"MememageDecode": "Mememage Decode",
1540+
"MememageDecodeAll": "Mememage Decode All",
14961541
"MememageLoadRecord": "Mememage Load Record",
14971542
"MememageFindRecord": "Mememage Find Record",
14981543
"MememageFetchRecord": "Mememage Fetch Record",

test_nodes.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,47 @@ def test_encode_then_decode(self):
384384
_, matched, _, _ = nodes.MememageVerify().run(image=barred, record=record)
385385
self.assertTrue(matched)
386386

387+
def test_decode_all_finds_every_bar(self):
388+
import torch
389+
from mememage.bar import embed_into
390+
from PIL import Image
391+
np, _t, _I = nodes._deps()
392+
393+
# a genuine 2-bar composite: bar one, push it up by appending rows, stamp a
394+
# second at the new bottom (mirrors core's own multi-bar test construction)
395+
one = embed_into(Image.new("RGB", (480, 300), (90, 90, 90)),
396+
"mememage-aa8194d91f1da238", "47f11bad5dcc9ad2")
397+
w, h = one.size
398+
moved = Image.new("RGB", (w, h + 40), (70, 70, 70)); moved.paste(one, (0, 0))
399+
two = embed_into(moved, "mememage-deadbeefcafe1234", "0011223344556677")
400+
401+
tensor = torch.from_numpy(nodes._pil_to_array(two, np))[None, ...]
402+
ids, hashes, count, img_out = nodes.MememageDecodeAll().run(tensor)
403+
self.assertEqual(count, 2)
404+
self.assertEqual(len(ids), 2)
405+
self.assertEqual(set(ids), {"mememage-aa8194d91f1da238", "mememage-deadbeefcafe1234"})
406+
self.assertEqual(len(hashes), 2)
407+
self.assertEqual(img_out.shape, tensor.shape) # image chains onward
408+
409+
def test_decode_all_empty_image_is_zero_not_crash(self):
410+
import torch
411+
img = torch.full((1, 128, 128, 3), 0.5) # no bar
412+
ids, hashes, count, img_out = nodes.MememageDecodeAll().run(img)
413+
self.assertEqual(count, 0)
414+
self.assertEqual(ids, [])
415+
self.assertEqual(hashes, [])
416+
self.assertEqual(img_out.shape, img.shape)
417+
418+
def test_decode_all_single_bar_returns_one(self):
419+
import torch
420+
img = torch.full((1, 512, 768, 3), 0.5)
421+
barred, identifier, _ = nodes.MememageEncode().run(
422+
img, fields_json='{"by": "catmemes"}', embed_workflow=False)
423+
ids, hashes, count, _out = nodes.MememageDecodeAll().run(barred)
424+
self.assertEqual(count, 1)
425+
self.assertEqual(ids, [identifier])
426+
self.assertEqual(len(hashes), 1)
427+
387428
def test_record_core_fields_first_and_still_verifies(self):
388429
import torch
389430
img = torch.full((1, 512, 512, 3), 0.5)

0 commit comments

Comments
 (0)