feat: pool set_mask + route

Write a per-slot grayscale mask sidecar (img_XXXX.mask.png) and record
it on the slot. Add the multipart /grid_pool/set_mask route.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 14:01:26 +02:00
parent cd0b8783dc
commit 6feb2c6e63
4 changed files with 48 additions and 0 deletions
+13
View File
@@ -112,6 +112,19 @@ def set_label(base_dir, pool_id, index, label):
return m return m
def set_mask(base_dir, pool_id, index, mask_bytes):
m = read_manifest(base_dir, pool_id)
if not (0 <= index < len(m["slots"])):
return m
img_name = m["slots"][index]["image"]
mask_name = img_name.replace(".png", ".mask.png")
with open(pool_dir(base_dir, pool_id) / mask_name, "wb") as f:
f.write(mask_bytes)
m["slots"][index]["mask"] = mask_name
write_manifest(base_dir, pool_id, m)
return m
def rebuild_manifest(base_dir, pool_id): def rebuild_manifest(base_dir, pool_id):
d = pool_dir(base_dir, pool_id) d = pool_dir(base_dir, pool_id)
m = empty_manifest() m = empty_manifest()
+15
View File
@@ -45,6 +45,21 @@ async def _label(request):
return web.json_response(handlers.handle_label(_base(), body["pool_id"], int(body["index"]), body["label"])) return web.json_response(handlers.handle_label(_base(), body["pool_id"], int(body["index"]), body["label"]))
@routes.post("/grid_pool/set_mask")
async def _set_mask(request):
reader = await request.multipart()
pool_id, index, data = "default", 0, None
async for part in reader:
if part.name == "pool_id":
pool_id = (await part.text())
elif part.name == "index":
index = int(await part.text())
elif part.name == "mask":
data = await part.read(decode=False)
m = handlers.handle_set_mask(_base(), pool_id, index, data)
return web.json_response(m)
@routes.get("/grid_pool/list") @routes.get("/grid_pool/list")
async def _list(request): async def _list(request):
pool_id = request.query.get("pool_id", "default") pool_id = request.query.get("pool_id", "default")
+12
View File
@@ -119,3 +119,15 @@ def test_read_corrupt_manifest_triggers_rebuild(tmp_path):
(d / "manifest.json").write_text("{ not json") (d / "manifest.json").write_text("{ not json")
m = pool.read_manifest(str(tmp_path), "p1") m = pool.read_manifest(str(tmp_path), "p1")
assert [s["image"] for s in m["slots"]] == ["img_0001.png"] assert [s["image"] for s in m["slots"]] == ["img_0001.png"]
def test_set_mask_writes_sidecar(tmp_path):
pool.add_image(str(tmp_path), "p1", b"a", ts=1)
m = pool.set_mask(str(tmp_path), "p1", 0, b"MASKBYTES")
assert m["slots"][0]["mask"] == "img_0001.mask.png"
assert (tmp_path / "p1" / "img_0001.mask.png").read_bytes() == b"MASKBYTES"
def test_set_mask_out_of_range_noop(tmp_path):
m = pool.set_mask(str(tmp_path), "p1", 0, b"x")
assert m["slots"] == []
+8
View File
@@ -21,3 +21,11 @@ def test_handle_active_label_remove(tmp_path):
assert handlers.handle_active(base, "p1", 1)["active"] == 1 assert handlers.handle_active(base, "p1", 1)["active"] == 1
assert handlers.handle_label(base, "p1", 0, "hi")["slots"][0]["label"] == "hi" assert handlers.handle_label(base, "p1", 0, "hi")["slots"][0]["label"] == "hi"
assert len(handlers.handle_remove(base, "p1", 0)["slots"]) == 1 assert len(handlers.handle_remove(base, "p1", 0)["slots"]) == 1
def test_handle_set_mask(tmp_path):
base = str(tmp_path)
handlers.handle_add(base, "p1", _png_bytes(), "png", ts=1)
m = handlers.handle_set_mask(base, "p1", 0, b"MASKBYTES")
assert m["slots"][0]["mask"] == "img_0001.mask.png"
assert (tmp_path / "p1" / "img_0001.mask.png").read_bytes() == b"MASKBYTES"