From 1ebccb660b8b1e7a7636765364b91bda4f2c77e7 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 8 Aug 2026 12:03:35 +0300 Subject: [PATCH] visual: top-down map renderer, demo --frames-dir mode (step frames + GIF + summary), example artifacts committed --- demo.gif | Bin 0 -> 41178 bytes demo_final_frame.png | Bin 0 -> 12459 bytes demo_summary.png | Bin 0 -> 32335 bytes testbed/README.md | 13 ++++ testbed/demo.py | 173 ++++++++++++++++++++++++++++++++++++++++- testbed/room/render.py | 55 ++++++++++++- 6 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 demo.gif create mode 100644 demo_final_frame.png create mode 100644 demo_summary.png diff --git a/demo.gif b/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..e1614beab0b7b697a745b2abd9317112ddf3960c GIT binary patch literal 41178 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 literal 0 HcmV?d00001 diff --git a/demo_final_frame.png b/demo_final_frame.png new file mode 100644 index 0000000000000000000000000000000000000000..3ff888212a61c191f582edc462adb66c71fa57d3 GIT binary patch literal 12459 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�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 Ra8W$cJ9#y^av8Jm{{y6{bw2 str: return best +class FrameRecorder: + """Visual mode: saves first-person frames and a sensor-built top-down map + with the capsule's path, then stitches a GIF and a summary image. + + Only uses data observed through protocol sensors (world_query, + proprioception, vision) — the map is reconstructed the way the agent sees + the world, not read from bridge internals. + """ + + MAP_SIZE = 320 + ROOM = 16.0 + + def __init__(self, out_dir: str): + self.out_dir = Path(out_dir) + self.out_dir.mkdir(parents=True, exist_ok=True) + self.layout: dict[str, Any] | None = None + self.path: list[tuple[float, float]] = [] + self.map_frames: list[Image.Image] = [] + self.view_frames: list[Image.Image] = [] + self.step = 0 + self._log = lambda msg: print(f"[rec] {msg}") + + async def snap( + self, client: AICCClient, pos: tuple[float, float], yaw: float + ) -> None: + """Capture one visual snapshot (one vision call + sensor layout).""" + if self.layout is None: + wq = (await client.call_tool("world_query", {})).output + self.layout = wq + self._log("world layout captured from sensors") + self.path.append(pos) + view = await self._view_frame(client) + self.view_frames.append(view) + self.step += 1 + map_img = self._map_frame(pos, yaw) + self.map_frames.append(map_img) + view.save(self.out_dir / f"step_{self.step:03d}_view.png") + map_img.save(self.out_dir / f"step_{self.step:03d}_map.png") + + async def _view_frame(self, client: AICCClient) -> Image.Image: + from PIL import Image + + out = (await client.call_tool("vision", {})).output + self._log(f"vision({out['width']}x{out['height']}) frame captured") + return Image.open(io.BytesIO(base64.b64decode(out["png_b64"]))).convert("RGB") + + def _map_frame(self, pos: tuple[float, float], yaw: float) -> Image.Image: + from PIL import Image, ImageDraw + + size = self.MAP_SIZE + scale = size / self.ROOM + + def xy(x: float, z: float) -> tuple[float, float]: + return (x * scale, size - z * scale) + + img = Image.new("RGB", (size, size), (52, 52, 58)) + d = ImageDraw.Draw(img) + for i in range(int(self.ROOM) + 1): + c = 60 if i % 2 == 0 else 54 + d.line([xy(i, 0), xy(i, self.ROOM)], fill=(c, c, c + 6), width=1) + d.line([xy(0, i), xy(self.ROOM, i)], fill=(c, c, c + 6), width=1) + if self.layout: + for ob in self.layout.get("obstacles", []): + x0, z0 = xy(ob["x"] - ob["width"] / 2, ob["z"] - ob["depth"] / 2) + x1, z1 = xy(ob["x"] + ob["width"] / 2, ob["z"] + ob["depth"] / 2) + d.rectangle( + [min(x0, x1), min(z0, z1), max(x0, x1), max(z0, z1)], + fill=(150, 90, 60), + outline=(20, 20, 26), + width=2, + ) + b = self.layout.get("beacon", {}) + bx, bz = xy(b.get("x", 12.5), b.get("z", 12.5)) + d.ellipse( + [bx - 8, bz - 8, bx + 8, bz + 8], + fill=(120, 210, 235), + outline=(20, 20, 26), + width=2, + ) + if len(self.path) > 1: + pts = [xy(x, z) for x, z in self.path] + d.line(pts, fill=(255, 170, 60), width=3) + cx, cz = xy(*pos) + d.ellipse( + [cx - 7, cz - 7, cx + 7, cz + 7], + fill=(240, 240, 250), + outline=(20, 20, 26), + width=2, + ) + rad = math.radians(yaw) + tip = xy(pos[0] + math.sin(rad) * 0.7, pos[1] + math.cos(rad) * 0.7) + d.line([(cx, cz), tip], fill=(30, 30, 40), width=3) + d.text( + (8, 8), + f"step {self.step} pos ({pos[0]:.1f}, {pos[1]:.1f}) yaw {yaw:.0f}", + fill=(230, 230, 230), + ) + return img + + def finish(self, interacted: bool) -> Image.Image: + """Write demo.gif + demo_summary.png; returns the summary image.""" + from PIL import Image, ImageDraw + + if self.map_frames: + gif = self.out_dir / "demo.gif" + self.map_frames[0].save( + gif, + save_all=True, + append_images=self.map_frames[1:], + duration=350, + loop=0, + ) + self._log(f"animation written: {gif}") + summary = Image.new("RGB", (self.MAP_SIZE + 320, 240), (20, 20, 26)) + if self.map_frames: + summary.paste(self.map_frames[-1], (0, 0)) + if self.view_frames: + summary.paste(self.view_frames[-1].resize((320, 240)), (self.MAP_SIZE, 0)) + d = ImageDraw.Draw(summary) + status = "BEACON ACTIVATED" if interacted else "mission not completed" + d.text( + (self.MAP_SIZE + 8, 244 - 16), + status, + fill=(255, 220, 120) if interacted else (255, 120, 120), + ) + out = self.out_dir / "demo_summary.png" + summary.save(out) + self._log(f"summary written: {out}") + return summary + + def vision_digest(png_b64: str, cols: int = 12, rows: int = 9) -> str: """Coarse color-grid summary of a vision frame (agent-side preprocessing).""" try: @@ -151,6 +284,7 @@ async def run_llm_agent( api_key: str, model: str, max_steps: int, + recorder: FrameRecorder | None = None, ) -> dict[str, Any]: from openai import AsyncOpenAI @@ -248,6 +382,8 @@ async def run_llm_agent( for step in range(max_steps): summary["steps"] = step + 1 + if recorder is not None and pos is not None: + await recorder.snap(client, pos, yaw) try: resp = await ac.chat.completions.create( model=model, messages=messages, tools=tools, tool_choice="auto" @@ -357,7 +493,10 @@ async def run_llm_agent( async def run_scripted_agent( - client: AICCClient, manifest: SessionInit, max_steps: int + client: AICCClient, + manifest: SessionInit, + max_steps: int, + recorder: FrameRecorder | None = None, ) -> dict[str, Any]: summary: dict[str, Any] = { "steps": 0, @@ -382,6 +521,10 @@ async def run_scripted_agent( prop = (await client.call_tool("proprioception", {})).output summary["tool_calls"] += 1 pos = prop["position"] + if recorder is not None: + await recorder.snap( + client, (pos["x"], pos["z"]), prop["rotation"]["yaw_deg"] + ) dist = math.hypot(bx - pos["x"], bz - pos["z"]) log( "agent", @@ -474,6 +617,11 @@ async def run_demo(args: argparse.Namespace) -> dict[str, Any]: print(f"[handshake] session {manifest.session_id} world {manifest.world.name}") print(f"[handshake] tools: {[t.id for t in manifest.tools]}\n") + if args.frames_dir: + recorder = FrameRecorder(args.frames_dir) + else: + recorder = None + if args.agent == "llm": summary = await run_llm_agent( client, @@ -482,9 +630,12 @@ async def run_demo(args: argparse.Namespace) -> dict[str, Any]: api_key=args.api_key, model=args.model, max_steps=args.max_steps, + recorder=recorder, ) elif args.agent == "scripted": - summary = await run_scripted_agent(client, manifest, args.max_steps) + summary = await run_scripted_agent( + client, manifest, args.max_steps, recorder=recorder + ) else: # auto from openai import AsyncOpenAI @@ -499,6 +650,7 @@ async def run_demo(args: argparse.Namespace) -> dict[str, Any]: api_key=args.api_key, model=args.model, max_steps=llm_steps, + recorder=recorder, ) if summary.get("interacted"): return summary @@ -506,12 +658,19 @@ async def run_demo(args: argparse.Namespace) -> dict[str, Any]: f"\n[auto] LLM did not finish in {llm_steps} steps " f"({summary.get('result')}); handing off to the scripted agent\n" ) - summary = await run_scripted_agent(client, manifest, args.max_steps) + summary = await run_scripted_agent( + client, manifest, args.max_steps, recorder=recorder + ) except Exception as exc: # noqa: BLE001 - fall back to scripted print( f"\n[auto] LLM endpoint unavailable ({type(exc).__name__}: {exc}); falling back to scripted agent" ) - summary = await run_scripted_agent(client, manifest, args.max_steps) + summary = await run_scripted_agent( + client, manifest, args.max_steps, recorder=recorder + ) + + if recorder is not None: + recorder.finish(summary.get("interacted", False)) frame = (await client.call_tool("vision", {})).output if args.frame: @@ -558,6 +717,12 @@ def main() -> int: default="demo_final_frame.png", help="where to save the final vision frame", ) + parser.add_argument( + "--frames-dir", + default=None, + help="visual mode: save first-person + top-down map frames each step " + "into this directory, plus demo.gif animation and demo_summary.png", + ) args = parser.parse_args() summary = asyncio.run(run_demo(args)) return 0 if summary.get("interacted") else 1 diff --git a/testbed/room/render.py b/testbed/room/render.py index 8accca7..c1800d5 100644 --- a/testbed/room/render.py +++ b/testbed/room/render.py @@ -11,7 +11,7 @@ from __future__ import annotations import math -from PIL import Image +from PIL import Image, ImageDraw from testbed.room.world import ROOM_HEIGHT, ROOM_SIZE, Room @@ -355,6 +355,59 @@ class Raycaster: return grid +def render_topdown(room: Room, size: int = 400) -> Image.Image: + """Bird's-eye view of the whole room (debug helper; uses world state). + + Floor grid, crates, beacon (cyan / warm yellow when active) and the + capsule as a circle with a heading arrow. + """ + scale = size / ROOM_SIZE + img = Image.new("RGB", (size, size), (52, 52, 58)) + draw = ImageDraw.Draw(img) + + def xy(x: float, z: float) -> tuple[float, float]: + return (x * scale, size - z * scale) + + for i in range(int(ROOM_SIZE) + 1): + c = 60 if i % 2 == 0 else 54 + draw.line([xy(i, 0), xy(i, ROOM_SIZE)], fill=(c, c, c + 6), width=1) + draw.line([xy(0, i), xy(ROOM_SIZE, i)], fill=(c, c, c + 6), width=1) + + for box in room.boxes: + xa, za = xy(box.x_min(), box.z_min()) + xb, zb = xy(box.x_max(), box.z_max()) + draw.rectangle( + [min(xa, xb), min(za, zb), max(xa, xb), max(za, zb)], + fill=box.color, + outline=(20, 20, 26), + width=2, + ) + + b = room.beacon + bx, bz = xy(b.x, b.z) + color = (255, 200, 90) if b.active else (120, 210, 235) + glow = (255, 240, 180) if b.active else (160, 235, 250) + r = b.radius * scale * 2.2 + draw.ellipse([bx - r * 2.4, bz - r * 2.4, bx + r * 2.4, bz + r * 2.4], fill=glow) + draw.ellipse( + [bx - r, bz - r, bx + r, bz + r], fill=color, outline=(20, 20, 26), width=2 + ) + + c = room.capsule + cx, cz = xy(c.x, c.z) + rad = c.radius * scale + draw.ellipse( + [cx - rad, cz - rad, cx + rad, cz + rad], + fill=(240, 240, 250), + outline=(20, 20, 26), + width=2, + ) + fx, fz = c.forward() + tip = xy(c.x + fx * 0.7, c.z + fz * 0.7) + draw.line([(cx, cz), tip], fill=(30, 30, 40), width=3) + return img + + def _ray_aabb( ox: float, oz: float,