Tie back the Joke Shop curtain bays and add the describe_image vision tool
This commit is contained in:
1 parent
e7d733bc93
commit
e4065c2068
6 files changed
+160
-15
No files matched your search
@@ -201,6 +201,10 @@ licence-free zone only.
|
|||||||
the `CanopySignatures` label is white, Godot 276/276, `12-joke-shop.png` + the three-up
|
the `CanopySignatures` label is white, Godot 276/276, `12-joke-shop.png` + the three-up
|
||||||
board recaptured. Note: fix 2 (blade sign) is already a circular disc on a roof pole in the
|
board recaptured. Note: fix 2 (blade sign) is already a circular disc on a roof pole in the
|
||||||
code (`37f925c`); the ranking note was stale.
|
code (`37f925c`); the ranking note was stale.
|
||||||
|
**Fix 5 landed:** the red curtain bays are now seven tied-back drapes (hourglass cinch, dark
|
||||||
|
recess between bays, scalloped hem) instead of four flat red slabs; vision-judged as
|
||||||
|
"tied-back/gathered fabric" via `tools/describe_image.py`. Joke Shop map is now 25,828 tris /
|
||||||
|
518 meshes (under the 150k/700 budget); Godot 277/277, `12-joke-shop.png` + board recaptured.
|
||||||
- [ ] Lighting: `lighting`/`lookdev` pass on sconces, canopy, marquee and street lamps with
|
- [ ] Lighting: `lighting`/`lookdev` pass on sconces, canopy, marquee and street lamps with
|
||||||
day/late captures. (Attenuation is now gated: `test_venue_lights_use_physical_falloff`
|
day/late captures. (Attenuation is now gated: `test_venue_lights_use_physical_falloff`
|
||||||
asserts every `OmniLight3D`/`SpotLight3D` in all three venues uses `attenuation = 1.0`,
|
asserts every `OmniLight3D`/`SpotLight3D` in all three venues uses `attenuation = 1.0`,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 116 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"board": "jokeshop-day-board.jpg",
|
"board": "jokeshop-day-board.jpg",
|
||||||
"caption": "Joke Shop frontage, day. Photo: local-only research frame (tmp/research/comedy-store). Concept: photo-anchored (gemini + gpt). Capture: dark stucco wall tile + white signature band.",
|
"caption": "Joke Shop frontage, day. Photo: local-only research frame (tmp/research/comedy-store). Concept: photo-anchored (gemini + gpt). Capture: dark stucco wall tile + white signature band + tied-back curtain bays.",
|
||||||
"photo": "tmp/research/comedy-store/01.jpg",
|
"photo": "tmp/research/comedy-store/01.jpg",
|
||||||
"concept": "docs/screens/concepts/joke_shop/joke_shop_entrance_day-v2.png",
|
"concept": "docs/screens/concepts/joke_shop/joke_shop_entrance_day-v2.png",
|
||||||
"capture": "docs/screenshots/12-joke-shop.png"
|
"capture": "docs/screenshots/12-joke-shop.png"
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 553 KiB After Width: | Height: | Size: 540 KiB |
+34
-14
@@ -498,20 +498,40 @@ func _build_comedy_store_building() -> void:
|
|||||||
step_body.shape_owner_set_transform(step_owner,
|
step_body.shape_owner_set_transform(step_owner,
|
||||||
Transform3D(Basis(), Vector3(door_x, 0.07, front + 0.92)))
|
Transform3D(Basis(), Vector3(door_x, 0.07, front + 0.92)))
|
||||||
root.add_child(step_body)
|
root.add_child(step_body)
|
||||||
# Red curtains hang between the entrance and the signature wall. Three
|
# Red tied-back curtains: seven bays between the entrance and the east end.
|
||||||
# narrow folds and a tieback keep them from reading as flat red slabs.
|
# Each drape is cinched at a tieback into an hourglass, leaving the dark
|
||||||
for x in [-3.2, 1.9, 7.0, 11.2]:
|
# recess visible on either side, and finishes in a scalloped hem. The taper
|
||||||
for fold in [-0.27, 0.0, 0.27]:
|
# plus tone variation makes them read as gathered fabric, not flat slabs.
|
||||||
var fold_color := red.lightened(0.10 if fold > 0.0 else 0.0)
|
for bay in range(7):
|
||||||
_add_box(building, Vector3(0.24, 3.10, 0.18),
|
var x := -3.2 + float(bay) * 2.4
|
||||||
Vector3(x + fold, 2.70, front + 4.56 + absf(fold) * 0.08),
|
# Top drape tapering down to the tieback (widest under the valence).
|
||||||
fold_color, true, null, Vector2.ONE, "cloth")
|
for top in [[4.05, 1.00], [3.55, 0.80], [3.05, 0.60], [2.55, 0.42]]:
|
||||||
_add_box(building, Vector3(0.96, 0.16, 0.22),
|
_add_box(building, Vector3(top[1], 0.52, 0.20),
|
||||||
Vector3(x, 2.13, front + 4.60), Color(0.24, 0.02, 0.03),
|
Vector3(x, top[0], front + 4.56),
|
||||||
false, null, Vector2.ONE, "cloth")
|
red.lightened(0.05), true, null, Vector2.ONE, "cloth")
|
||||||
_add_box(building, Vector3(1.06, 0.52, 0.20),
|
# Tieback cord at the narrowest point.
|
||||||
Vector3(x, 1.34, front + 4.54), Color(0.34, 0.025, 0.035),
|
_add_box(building, Vector3(0.60, 0.14, 0.24), Vector3(x, 2.15, front + 4.58),
|
||||||
false, null, Vector2.ONE, "cloth")
|
Color(0.24, 0.02, 0.03), true, null, Vector2.ONE, "cloth")
|
||||||
|
# Bottom drape flaring down to the hem.
|
||||||
|
for bottom in [[1.85, 0.56], [1.48, 0.82]]:
|
||||||
|
_add_box(building, Vector3(bottom[1], 0.52, 0.20),
|
||||||
|
Vector3(x, bottom[0], front + 4.56),
|
||||||
|
red.lightened(0.11), true, null, Vector2.ONE, "cloth")
|
||||||
|
# Scalloped hem: overlapping shallow spheres along the bottom edge.
|
||||||
|
for hem_i in range(5):
|
||||||
|
var hem_x := -0.46 + 0.23 * float(hem_i)
|
||||||
|
var hem := MeshInstance3D.new()
|
||||||
|
var hem_mesh := SphereMesh.new()
|
||||||
|
hem_mesh.radius = 0.25
|
||||||
|
hem_mesh.height = 0.48
|
||||||
|
hem_mesh.radial_segments = 8
|
||||||
|
hem_mesh.rings = 4
|
||||||
|
hem.mesh = hem_mesh
|
||||||
|
hem.position = Vector3(x + hem_x, 1.24, front + 4.56)
|
||||||
|
hem.scale = Vector3(0.62, 0.36, 0.36)
|
||||||
|
hem.material_override = Materials.flat(red.lightened(0.12), "cloth")
|
||||||
|
hem.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||||
|
building.add_child(hem)
|
||||||
# Small warm sconces along the black wall. Their bulbs are emissive and each
|
# Small warm sconces along the black wall. Their bulbs are emissive and each
|
||||||
# gets a low-energy practical so the facade is not a black hole after dark.
|
# gets a low-energy practical so the facade is not a black hole after dark.
|
||||||
for x in [-12.4, -6.2, 3.0, 9.4]:
|
for x in [-12.4, -6.2, 3.0, 9.4]:
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Ask an OpenRouter vision model to describe an image (free-form).
|
||||||
|
|
||||||
|
Complements the structured judges (`scene_concepts.py realism --judge`,
|
||||||
|
`screen_designs.py judge`): those return scores/verdicts, this one returns an
|
||||||
|
open-ended textual observation for exactly the question asked. Used to review
|
||||||
|
captures and references when the agent harness itself cannot display images.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 tools/describe_image.py <image> [--question "..."] [--model ...]
|
||||||
|
python3 tools/describe_image.py <image> <image2> [--question "..."] # compare
|
||||||
|
|
||||||
|
Requires OPENROUTER_API_KEY (a gitignored `.env` in the repo root holds it;
|
||||||
|
source with `set -a && . ./.env && set +a`, or the tool reads `.env` directly).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(ROOT / "tools"))
|
||||||
|
|
||||||
|
import openrouter_judge # noqa: E402
|
||||||
|
|
||||||
|
CHAT_URL = "https://openrouter.ai/api/v1/chat/completions"
|
||||||
|
|
||||||
|
DEFAULT_QUESTION = (
|
||||||
|
"Describe this image as an art director reviewing a game screenshot: the "
|
||||||
|
"subject, composition, materials, lighting, silhouette, and any obvious "
|
||||||
|
"visual defects or things that read as flat, broken or out of place. Be "
|
||||||
|
"concrete and specific."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_env_file() -> None:
|
||||||
|
env = ROOT / ".env"
|
||||||
|
if not env.is_file():
|
||||||
|
return
|
||||||
|
for line in env.read_text(encoding="utf-8").splitlines():
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
os.environ.setdefault(key.strip(), value.strip().strip("\"'"))
|
||||||
|
|
||||||
|
|
||||||
|
def _data_url(path: Path) -> str:
|
||||||
|
return "data:image/png;base64," + base64.b64encode(Path(path).read_bytes()).decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def _call(body: dict, timeout: int = 180) -> dict:
|
||||||
|
key = os.environ.get("OPENROUTER_API_KEY", "").strip()
|
||||||
|
if not key:
|
||||||
|
raise ValueError("OPENROUTER_API_KEY is not set")
|
||||||
|
req = urllib.request.Request(
|
||||||
|
CHAT_URL, data=json.dumps(body).encode("utf-8"),
|
||||||
|
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json",
|
||||||
|
"HTTP-Referer": "https://git.allucanget.biz/zwitschi/security-game",
|
||||||
|
"X-Title": "Gig Line"}, method="POST")
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
raise ValueError(f"OpenRouter HTTP {exc.code}: {exc.read().decode('utf-8', 'replace')[:300]}")
|
||||||
|
|
||||||
|
|
||||||
|
def build_body(model: str, images: list[Path], question: str) -> dict:
|
||||||
|
content: list[dict] = [{"type": "text", "text": question}]
|
||||||
|
for path in images:
|
||||||
|
content.append({"type": "image_url", "image_url": {"url": _data_url(path)}})
|
||||||
|
return {"model": model, "messages": [{"role": "user", "content": content}],
|
||||||
|
"temperature": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
ap.add_argument("images", nargs="+", help="image file(s) to send")
|
||||||
|
ap.add_argument("--question", default=DEFAULT_QUESTION,
|
||||||
|
help="question to ask (default: a full art-director description)")
|
||||||
|
ap.add_argument("--model", default="auto",
|
||||||
|
help="explicit model, or 'auto' for the cheapest image-capable model")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
|
_load_env_file()
|
||||||
|
images = [Path(p) for p in args.images]
|
||||||
|
for path in images:
|
||||||
|
if not path.is_file():
|
||||||
|
print(f"error: not a file: {path}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
models = openrouter_judge.resolve_models(args.model)
|
||||||
|
if len(images) > 1:
|
||||||
|
args.question = args.question + (
|
||||||
|
f"\n\nI sent {len(images)} images in order. Compare them and describe the "
|
||||||
|
"differences, especially anything that changed or regressed.")
|
||||||
|
try:
|
||||||
|
model, payload = openrouter_judge.call_with_fallback(
|
||||||
|
models, lambda m: build_body(m, images, args.question), _call)
|
||||||
|
except ValueError as exc:
|
||||||
|
print(f"error: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
try:
|
||||||
|
text = payload["choices"][0]["message"]["content"]
|
||||||
|
except (KeyError, IndexError, TypeError):
|
||||||
|
text = "unreadable model response"
|
||||||
|
if isinstance(text, list):
|
||||||
|
text = "".join(p.get("text", "") for p in text if isinstance(p, dict))
|
||||||
|
print(f"[model {model}]")
|
||||||
|
print(text)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in new issue
Block a user