blender-visual-review · diff
git:20261001.74a473f to git:20261001.e42d2f4
112 added, 112 removed. Audit A to A.
---
name: blender-visual-review
description: "Use when: looks off, check the model, look at it, review the render, inverted normals, exploded parts, scale drift. Multi-angle Blender MCP render-and-look. Complements blender-assembly (numeric only). Not DayZ winding: dayz-p3d-audit."
---
# Blender Visual Review
`verify_bounds()` and `audit_all()` (from the `blender-assembly` skill) prove a model's *topology* is clean — parts overlap, transforms are applied, nothing is silently double-scaled. They cannot prove the model is *right*. A chair facing backwards, a quad sunk into the ground, a roof at half scale, faces lit from the inside — every one of those passes a numeric audit and fails the instant a human looks. Numbers verify topology; only eyes verify intent.
This skill is the missing half of the loop: **render the model, look at the images, diagnose what you see, fix one thing, re-render the same angles to confirm.** Run the numeric checks in `blender-assembly`; run the visual checks here.
## When to use this
After you build, import, or modify any model; before you call a model "done" or export it; whenever the user says it "looks off" or asks you to "check" / "look at" / "review" a model; and — for DayZ — before you spend an in-game test cycle, because a render is free and an in-game iteration is not.
## The loop
1. **Set up an honest view** — frame the object, add a scale reference, pick diagnostic shading.
2. **Capture from several angles.** One angle hides defects: a gap invisible head-on, a normal only wrong on one side.
3. **Look, against the checklist.** Don't free-associate — walk the categories below so you don't miss a whole class of defect.
4. **Fix one diagnosis at a time.**
5. **Re-render the *same* angles** and compare before/after. A fix you didn't re-capture is a fix you didn't verify.
## Bounded loop: typed outcomes + stop policy (added 2026-07-30, adapted from img2threejs v1.4.3, Apache-2.0 — provenance in `references/NOTICE-img2threejs.md`)
Step 4 of the loop says "fix one diagnosis at a time" — this section bounds how many times. Unbounded visual iteration is a documented failure mode (racing-game rip: N rounds on a red visual before escalating to a human; DZ-R5's in-game hard stop exists for the same reason). Every review iteration ends by choosing exactly one action:
- `continue` — this aspect is good; move to the next check or part.
- `refine-spec` — the plan was wrong or shallow (missing detail row, wrong connection-map entry, wrong route): go back to `blender-assembly` Phase 1/1.5, fix the plan, rebuild from it. Patching geometry around a wrong plan burns iterations without converging.
- `refine-code` — the plan is sound; the build doesn't match it. Fix the geometry/material.
- `request-input` — show the user the evidence (before/after renders, what was tried, what still fails) and stop iterating.
- `stop` — the target is not reachable from this input (unusable reference, wrong approach); say so instead of faking progress.
Keep a per-model iteration history — a JSON list, one entry per iteration: `{"fidelity": <your visual 0-1 score>, "defectTags": ["short-stable-labels"], "reverted": <true if the fix made it worse and was undone>}`, stored next to the renders (`_review/history.json`). After appending each iteration, run the stop policy (always `--json`: the plain output crashes cp1252 consoles):
python references/correction_loop.py --history _review/history.json --json
| Condition (priority order) | Verdict |
|---|---|
| fidelity ≥ target and no open defect tags | stop — `continue` |
| same defect tag survived 2 consecutive fixes | stop — `refine-spec`: the plan is wrong, not the code |
| oscillating — 2 reverted fixes, or score direction flip | stop — `refine-spec` |
| progress plateaued (Δ < 0.02, below target) | stop — `request-input` |
| 6 iterations | stop — `request-input`, non-bypassable |
The script is the cycle counter nobody keeps under pressure. NEVER argue past its stop verdict with "one more try will fix it" — that is the exact failure it exists to catch. Escalate with the evidence instead.
### Per-feature gates: a global "looks right" cannot rescue a failed critical feature
The detail list from `blender-assembly` Phase 1.5 is this review's contract. Its identity-defining rows — the ones that make the object *this* object (a grille shape, a stock profile, a hinge) — each get their own pass/fail during checklist step 3, judged on a native-res crop (LL-153). A render that reads well overall with one wrong critical feature fails the iteration; record that feature's tag in `defectTags`.
### Report honestly: "improved" is not "done"
Each iteration's note states what changed (with numbers — "guard edge extended −0.56→−0.48"), why, what still doesn't match, and what the current check is blind to ("passes front view; top view not re-rendered"). A feature that got closer is "improved", never "done" — imprecise language here is how a defect survives into the next session.
## Two ways to see — and when to use each
You have two distinct ways to get pixels back, and they answer different questions.
**Quick viewport screenshot** — `get_screenshot_of_area_as_image(area_ui_type="VIEW_3D")` returns the current viewport straight to you in one call, no file step. Use it for "what does it look like right now", and especially for the **Face Orientation overlay** (below), which shows only in the viewport, never in a render.
**Reproducible multi-angle render** — place a camera at canonical angles and render each to a file, then `Read` the files. Use it when you need identical framing every iteration, clean lighting, and before/after evidence. The helper below renders all angles in one call and also returns the measurable numbers (dimensions, lowest vertex, counts), so you get numeric and visual in one shot.
Rule of thumb: **screenshot to glance, render to judge.**
## Setup that keeps captures honest
**Scale reference.** Proportion errors are invisible without a yardstick. Drop a 1 m cube at the origin so every render carries a known unit (size=2 so scale = half-extent, per `blender-assembly`):
```python
import bpy
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0.5))
ref = bpy.context.active_object
ref.name = "VR_ScaleRef_1m"
ref.scale = (0.5, 0.5, 0.5) # a 1 m cube sitting on z=0
bpy.ops.object.transform_apply(scale=True)
ref.display_type = 'WIRE' # wireframe so it never hides the model
```
**Diagnostic shading.** For shape and topology, Solid + cavity reveals facets and pinching that smooth shading hides. For normal *direction*, the Face Orientation overlay paints front faces blue and back faces red — the fastest inverted-normal detector there is (viewport only). Set it, then take a viewport screenshot:
```python
import bpy
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
sp = area.spaces.active
sp.shading.type = 'SOLID'
sp.shading.show_cavity = True # facets / pinching
sp.overlay.show_face_orientation = True # front = blue, back = red
break
```
Workbench needs no scene lights (it has its own studio light), which sidesteps the classic "render came out black because the scene has no lamp" trap. Use Workbench for diagnosis; switch to EEVEE or Cycles only when you are specifically judging materials.
## Multi-angle capture (the workhorse)
Run this via `execute_blender_code`. It renders the object from front / right / top / iso to files you can read, restores the scene's original camera and engine, and returns both the file paths and the measurable diagnostics. Then `Read` each path and actually look.
```python
import bpy, os, tempfile
from mathutils import Vector
def vr_capture(obj_name, tag="iter1", out_dir=None, res=900):
obj = bpy.data.objects[obj_name]
scene = bpy.context.scene
# world-space bounds of the base mesh (good enough to frame and measure)
vs = [obj.matrix_world @ v.co for v in obj.data.vertices]
lo = Vector((min(v.x for v in vs), min(v.y for v in vs), min(v.z for v in vs)))
hi = Vector((max(v.x for v in vs), max(v.y for v in vs), max(v.z for v in vs)))
center, dims = (lo + hi) * 0.5, (hi - lo)
max_dim = max(dims.x, dims.y, dims.z) or 1.0
if out_dir is None: # a folder you can also Read afterwards
base = bpy.path.abspath("//") if bpy.data.filepath else tempfile.gettempdir()
out_dir = os.path.join(base, "_review")
os.makedirs(out_dir, exist_ok=True)
prev = (scene.render.engine, scene.camera, scene.render.filepath,
scene.display.shading.show_cavity)
scene.render.engine = 'BLENDER_WORKBENCH' # fast, self-lit
scene.display.shading.show_cavity = True # facets / pinching in the render
scene.render.image_settings.file_format = 'PNG'
scene.render.resolution_x = scene.render.resolution_y = res
scene.render.resolution_percentage = 100
cam_data = bpy.data.cameras.new("VR_Cam")
cam = bpy.data.objects.new("VR_Cam", cam_data)
scene.collection.objects.link(cam)
scene.camera = cam
dist = max_dim * 2.2
views = {"front": (0,-1,0), "right": (1,0,0), "top": (0,0,1), "iso": (1,-1,0.8)}
renders = []
for name, off in views.items():
cam.location = center + Vector(off).normalized() * dist
cam.rotation_euler = (center - cam.location).to_track_quat('-Z', 'Y').to_euler()
path = os.path.join(out_dir, f"{obj_name}__{name}__{tag}.png")
scene.render.filepath = path
bpy.ops.render.render(write_still=True)
renders.append(path)
(scene.render.engine, scene.camera, scene.render.filepath,
scene.display.shading.show_cavity) = prev # restore, then drop temp camera
bpy.data.objects.remove(cam, do_unlink=True)
if cam_data.users == 0:
bpy.data.cameras.remove(cam_data)
return {"renders": renders,
"dims": [round(c, 4) for c in dims],
"lowest_z": round(lo.z, 4),
"center": [round(c, 4) for c in center],
"counts": {"verts": len(obj.data.vertices), "faces": len(obj.data.polygons)}}
result = vr_capture("YOUR_OBJECT") # set the object name; optional: tag=, out_dir=, res=
```
`Read` each path in `result["renders"]` and look. Use `result["dims"]`, `result["lowest_z"]`, and `result["counts"]` as the measurable backbone for the checklist. (Bounds come from the base mesh; if a modifier changes the silhouette, apply it or evaluate the dependency graph first.)
## Diagnostic checklist
Walk every category — the point of a checklist is to catch the defect you weren't already worried about. Each item is a concrete, checkable signal, not a vibe: an invariant written as loose prose gets nodded at and skipped (LL-062), so tie each one to a number or a specific visual cue.
### A. General 3D correctness (every model)
- **Orientation** — does "forward" point where it should; is anything upside down or mirrored? Read it against the scale cube and the world axes, not memory.
- **Proportion / scale** — measure against the 1 m cube and `result["dims"]`. "Looks small" is a prompt to check the number, not a conclusion.
- **Inverted / black normals** — Face Orientation overlay: any red facing the camera is a back-face you are seeing through. Also watch for surfaces lit as if from inside.
- **Shading artifacts** — black facets, pinching, smooth shading bleeding across a hard edge, n-gons on curved areas.
- **Exploded / floating parts** — gaps between parts that should touch; a part hovering off its mount. (`blender-assembly`'s `verify_overlap` catches the quantifiable ones; the eye catches the rest.)
- **Doubled geometry** — a shimmer that flickers between angles means two faces occupy the same place (z-fighting).
- **Animated parts: clearance per keyframe, not per render** — presentation renders only show posed frames; impacts hide behind walls or closed doors. Evaluate the depsgraph mesh at every keyframe AND at intermediate points of each travel span: `BVHTree.overlap` between each moving mesh and each fixed mesh, a sweep of fixed-geometry points (vertices, face and edge centres) that fall inside the part's at-rest footprint and travel range, and section cuts on the travel planes. A flush contact also reports as overlap — separate it from a penetration by measuring how deep the geometry enters the footprint, not how many triangles cross. [EXACT] Measured on an animated rock platform (2026-10-01): its 9 presentation cameras showed nothing, while the sweep found a 12 × 14.6 m platform crossing a lip 0.27 m into its footprint and an elevator cabin crossing the top threshold (0.14 m) on every ride, visible only from inside at frame 145.
- **Workbench `color_type = 'TEXTURE'` paints one image of the material, not its shading** [EXACT] — on a vanilla rock the `rock_*_mask` input rendered as flat RGB mask colours and looked like a defect. Judge materials in EEVEE with the file's own lights. (measured 2026-10-01)
### B. DayZ parity vs a vanilla reference
Import a known-good vanilla model into the same scene at 1:1 and compare side by side (the import path lives in `dayz-model-pipeline`). Anchor each check to the reference, not to taste:
- **Orientation / scale** — same forward axis and roughly the same footprint as the vanilla equivalent.
- **Ride-height (measurable)** — for ground items, the lowest vertex (`result["lowest_z"]`) sits at ≈ the ground plane (z≈0); for vehicles, the wheel contact is ≈ ground and the chassis belly clears it well (a vanilla sedan's belly clearance is ~0.43 m; a too-small wheel sinks the body). Measure it — don't eyeball "looks grounded" (LL-062; `vehicle-structural-parity`).
- **Proxy / slot presence** — wheels, FireGeo bulk, and lights are visibly where the vanilla has them.
### C. Reference-image comparison
When matching a photo or concept: load the reference as a camera background image (`cam_data.show_background_images = True`, then `cam_data.background_images.new().image = bpy.data.images.load(path)`), match the camera to the reference's angle, render, and compare **named landmarks** — roofline, wheel arch, handle position — not overall impression. Report silhouette deviation, missing or extra masses, and proportion drift by pointing at specific points.
### D. Photo-resemblance scoring: calibrate the camera first (added 2026-09-25, SP-427)
- **One Blender camera per reference photo, calibrated by silhouette IoU** — maximize the IoU between the Workbench-alpha silhouette and the photo mask (Nelder-Mead over loc/yaw/pitch/roll/focal, initialized from a known-size feature such as the tyres from the spec sheet). Judge only photo|render crops taken with that same camera: with an eyeballed "similar" camera the proportion errors (a wing out of place, a dome floating 5 cm) stay invisible.
- **Poor or partial views** — low-res or off-angle photos converge to a false camera (IoU 0.81 on a brochure front view); solve PnP on 6-8 triangulated points across other photos instead, and reject any refinement that LOWERS silhouette IoU (a 2-anchor bundle shifted the camera 38 cm along the view rays and dropped IoU 0.936 to 0.923).
- **Silhouette renders** — set `render.use_compositing = False`: an Alpha-Over compositor makes the whole silhouette opaque when `film_transparent` is on.
- **Judge blind, cross-family.** [EXACT] Same-family self-scores ran 1-2 checklist points above a blind audit by another model family over the same 27 image sets (measured in Blender 5.1.1 headless against studio photos, 2026-09-25); the working score is the per-criterion minimum of self and blind.
## Hard guardrail: a Blender render cannot judge DayZ winding
Blender (and any Three.js viewer) renders **right-handed**; DayZ culls **left-handed**. A model that looks perfectly solid in Blender can render inside-out in-game. This is not hypothetical — it is a logged self-error: a render was used to declare "normals not inverted", and the exterior turned out transparent in-game (`lessons-learned.md` LL-029-era entry; `dayz-p3d-audit/SKILL.md:474-476`).
- **NEVER conclude that DayZ face winding or normal direction is correct from a Blender render or screenshot.** The render genuinely cannot tell you. Absolute winding is decided by `dayz-p3d-audit` (its edge-pair topology check), not by eye.
- The *only* valid visual winding signal is **relative**: the model winds the same way as a vanilla reference imported beside it. "Same as vanilla" is meaningful; "looks fine to me" is not.
- The Blender→DayZ transform `(x,y,z)→(x,z,-y)` is a proper rotation (det=+1) and **preserves** winding — do not "flip to be safe" (LL-020; authority: `dayz-model-pipeline`).
Everything else here — orientation, proportion, scale, exploded parts, ride-height, reference match — the render judges well. Winding is the one thing it cannot; hand that to `dayz-p3d-audit`.
### Winding-flip detection workflow (added 2026-07-30, applies pending append 2026-06-24; LL-162)
A standard lit render cannot expose a winding flip (the guardrail above) because declared/custom split normals mask it: lighting follows declared normals while engines cull by triangle winding. Reproduced on the MercedesAMGLF Phase 2 shell — all 4 views passed here, in-game only back faces textured (LL-162). Two viewport/render mechanisms DO read winding-derived orientation, unaffected by declared normals:
1. **Face Orientation overlay** (already in Setup above) — from OUTSIDE the model, any red face is winding-flipped. Screenshot the 4 canonical angles with the overlay on; a uniformly red exterior means the whole mesh is flipped.
2. **Backface-culling re-render** — set `mat.use_backface_culling = True` on every material (EEVEE honors it), re-render the same 4 angles, and diff each against its original render (`references/vr_delta.py` per angle). Faces that vanish (holes; silhouette IoU < 0.95 on any view) were winding-flipped ⇒ verdict `FLIPPED_LIKELY` — do not approve the export.
Do NOT "verify" a suspected flip by running `normals_make_consistent` and re-rendering: recalc rewrites the winding itself, so the re-render comes back clean and the check false-negatives. Recalc is a repair tool; the repair decision for a DayZ export belongs to the import-transform rules in `dayz-model-pipeline` (LL-020: the canonical `(x,y,z)→(x,z,-y)` transform preserves winding; a `det=−1` variant is what flips it).
When to run: any import from glTF/FBX/OBJ, any pipeline with a configurable `reverse_winding`, any build destined for a winding-culling engine (DayZ / Arma / BI). Absolute authority on the exported `.p3d` remains `dayz-p3d-audit`'s topology check; this is the free early warning that saves the in-game cycle.
#### Quantified, and why checklist Q5 must never be scored by eye (measured 2026-08-21)
The guardrail above was reproduced from the other direction, on a purpose-built probe whose
winding is reversed by construction (`q5_normals_build.py` pattern: same object, same camera,
same shading, only the winding differs):
| capture | faces reversed | pixels changed |
|---|---|---|
| this skill's own settings (Workbench + `show_cavity`, culling **off**) | 62 (face-0 normal dot **-1.0001**) | **0.00%** |
| identical, backface culling **on** | 62 | **36.92%** |
Two variants at different severities came out byte-identical to each other. The defect is
**absent from the pixels**, so no reviewer can answer from a lit render -- which is exactly
what the section above says, now with a number.
**Consequence for `references/checks_hardsurface.json`.** Its entry 4 -- *"Are there any black,
inside-out or wrongly shaded faces visible?"* -- asks a VLM the one question this skill states
a render cannot answer, over non-culled captures. It carries `skip` and must be routed to the
culling diff above, never sent to or scored by the VLM. A gate that cannot go red for the
cause it targets is decoration.
**And it is not a local-model limitation.** Given the culled pair where the flip changes 36.92%
of the image and the interior of the cylinder is plainly visible, `ornith-1.5:9b`,
`qwen3-vl:30b` and `muse-glimmer:30b` each answered NO on **both** variants across 3 seeds --
0/3 separation, all three -- and justified it fluently: *"the visible faces are the front cap
and the exterior side of the cylinder"*, said of a render where the front cap is culled away
and the interior is what is on screen. The failure mode is a confident green, not a refusal.
#### What the local judges DO and DO NOT hold up on (same tanda)
Whole shipped checklist, over a pair differing only in facet count (48 vs 6 sides), 3 seeds.
Only the faceting question should move; everything else should hold still:
| model | correct | the miss |
|---|---|---|
| `qwen3-vl:30b` | **8/8** | -- |
| `ornith-1.5:9b` | 7/8 | calls the SMOOTH variant holed, 2 of 3 seeds |
| `muse-glimmer:30b` | 7/8 | reads the FACETED variant as bevelled, 3 of 3 seeds |
So the incumbent is the cleanest of the three, and a bigger or newer model did not beat it.
Scope: one object, one pair, one framing where the round part fills the frame -- the condition
where the faceting question is already known to work. It screens models; it does not rank them.
## Deterministic delta check (report-only; added 2026-07-30)
Before judging a fix by eye, run the pixel-math comparator on the before/after pair of the SAME angle:
python references/vr_delta.py _review/model__front__iter1.png _review/model__front__iter2.png --json
It reports silhouette IoU, bbox scale/aspect deltas, SSIM, edge overlap, tonal/blowout/flat parity, and which signals moved beyond same-camera noise. Two uses:
- **Prove the fix landed** — an intended edit must move at least one signal; `identicalImages: true` after an edit means the old scene got re-rendered (a real reproduced failure mode: the MCP render-depsgraph staleness documented in `blender-assembly`).
- **Catch collateral drift** — signals moving that the edit shouldn't touch (a tonal shift after a pure geometry fix; a silhouette change after a material tweak).
Per LL-153 these numbers are a change filter, NEVER a correctness verdict — correctness is decided by eyes on the renders and native-res crops. Same-camera pairs only: scoring a render against a reference photo is not calibrated here (framing/background mismatch breaks every signal; that comparison stays with §C's named-landmark method).
## Free local second opinion — SHADOW MODE (added 2026-07-30)
A local vision model served by Ollama answers checklist questions about renders at zero credit cost, so agent budget goes to gates and final judgment instead of every inner-loop glance. `references/vr_score.py`:
python references/vr_score.py ask _review/model__iso__iter3.png --checklist references/checks_hardsurface.json --model gemma4:26b --json
python references/vr_score.py score _review/model__iso__iter3.png --reference ref.jpg --model gemma4:26b --json
Prefer `ask` over `score`: concrete yes/no questions are measurably easier for a small VLM than open judgment, and each "no" is a specific pointer instead of an opaque number. Derive the questions from the model's own detail list (`blender-assembly` Phase 1.5) — `checks_hardsurface.json` is the generic starter set.
**This is SHADOW MODE and it is not negotiable**: the local model's answers are evidence logged beside your own, NEVER a gate, NEVER a substitute for looking. Every call appends to a JSONL shadow log; that log IS the calibration dataset, built free during real work. Promotion path: shadow → pre-filter (it discards the obviously broken, you judge the survivors) → never final judge. Run `references/vr_calibrate.py report` against ≥15 renders you have judged yourself before delegating any single question; it reports agreement per question, which is the unit that gets delegated — not the model as a whole.
- Measured 2026-07-30 (RTX 3090, n=2 renders — provisional): `gemma4:26b` ≈15 s warm per 8-question checklist, `qwen3.5:27b` ≈132 s for comparable quality, so gemma is the default. **⚠ Esa brecha de 9x quedó REFUTADA en su causa el 2026-08-16: no era el modelo, era el `num_ctx` de fábrica** tirando a qwen el 37% a CPU. Con `think:false` + `num_ctx=8192` son 8,3 s y 17,2 s — brecha de 2x, y qwen vuelve a ser viable como segunda opinión de otra familia. Ver §Pre-filtro más abajo. Both correctly flagged a faceted circle and both answered "unsure" rather than guessing when the view could not decide — but they gave **opposite** bevel verdicts on the same renders, so treat bevel/chamfer questions as agent-side until calibration says otherwise. Judge fine surface questions on a native-res crop, never a full-frame render (LL-153).
+ Measured 2026-07-30 (RTX 3090, n=2 renders — provisional): `gemma4:26b` ≈15 s warm per 8-question checklist, `qwen3.5:27b` ≈132 s for comparable quality, so gemma is the default. **⚠ That 9x gap was REFUTED in its cause on 2026-08-16: it was not the model, it was the factory `num_ctx`** offloading 37% of qwen to CPU. With `think:false` + `num_ctx=8192` it is 8.3 s and 17.2 s — 2x gap, and qwen becomes viable again as second opinion from another family. See §Pre-filter below. Both correctly flagged a faceted circle and both answered "unsure" rather than guessing when the view could not decide — but they gave **opposite** bevel verdicts on the same renders, so treat bevel/chamfer questions as agent-side until calibration says otherwise. Judge fine surface questions on a native-res crop, never a full-frame render (LL-153).
Setup notes: models live wherever `OLLAMA_MODELS` points; a 17 GB model fills a 24 GB card, so models load one at a time and compete with Blender for VRAM — unload before a heavy render. Always address the server as `127.0.0.1`, never `localhost`: on Windows that resolves to IPv6 first and a stray IPv6-bound `ollama serve` will answer from a different model library.
## Output / evidence convention
Render into a folder you can also read (the helper defaults to a `_review/` folder next to the .blend, or the OS temp dir if the file is unsaved) and name files `<model>__<angle>__<iter>.png`. Keep the before/after pair for any angle you changed, so the diagnosis is auditable and you can prove the fix actually landed.
- ## Checklist VLM: cinco preguntas activas, tres excluidas (added 2026-08-16, SP-277; datos de calibrado en SP-270)
+ ## VLM Checklist: five active questions, three excluded (added 2026-08-16, SP-277; calibration data in SP-270)
- La sección de sombra de arriba manda para cualquier pregunta que no esté en esta tabla. El JSON conserva
- ocho entradas: cinco llegan al modelo y tres llevan `skip`; solo las filas **pre-filtro** cambian el bucle.
+ The shadow section above governs any question not in this table. The JSON preserves
+ eight entries: five reach the model and three have `skip`; only the **pre-filter** rows change the loop.
- **El filtro RECHAZA, nunca APRUEBA.** Un «no» dispara un arreglo antes de enseñar nada; un pase
- limpio **no** significa «está bien», significa «ya merece tus ojos». La regla de que el modelo
- local nunca es juez final sigue intacta: se le ha delegado el poder de parar, no el de aprobar.
+ **The filter REJECTS, never APPROVES.** A "no" triggers a fix before showing anything; a clean
+ pass does **not** mean "it is fine", it means "it now deserves your eyes". The rule that the local
+ model is never the final judge remains intact: it has been delegated the power to stop, not to approve.
- ### Qué está calibrado y qué no
+ ### What is calibrated and what is not
- Medido sobre `mk47_mutant`, con el par roto/arreglado del MISMO objeto, tres modelos, el agrupado
- real de `vr_score.py`. El checklist entero saca **77,8% contra un suelo de respuesta constante del
- 58,3%**:
+ Measured on `mk47_mutant`, with the broken/fixed pair of the SAME object, three models, the actual
+ grouping of `vr_score.py`. The entire checklist achieves **77.8% against a constant response floor of
+ 58.3%**:
- | pregunta | acierto | en el bucle |
+ | question | accuracy | in the loop |
|---|---|---|
- | conectividad / piezas flotando | **12/12** | **pre-filtro** — la más fuerte con diferencia |
- | huecos o agujeros en la superficie | 10/12 | **pre-filtro** |
- | ~~caras negras, del revés o mal sombreadas~~ | ~~10/12~~ | **NO delegar** — `skip`: el defecto no está en la captura; usar el diff mecánico de culling |
- | objeto acabado vs cajas peladas | 10/12 | **pre-filtro** |
- | ~~proporciones consistentes~~ | ~~10-12/12~~ | **NO delegar** (retractado 2026-08-22) — ese 10-12/12 es una puntuación **infalsificada**: el par roto/arreglado con el que se midió no contenía ningún defecto de proporción, así que contestar «yes» siempre puntúa perfecto. Ver la nota de abajo |
- | **biseles / chamfered edges** | **6/12** | **NO delegar** — está en el suelo de la respuesta constante. Confirma el aviso de 2026-07-30: los dos modelos daban veredictos opuestos de bisel |
- | **cilindros facetados** | **al azar en los 8 encuadres** | **NO delegar, y no la reescribas** — se probaron cuatro redacciones (2026-08-17) y la que ya está es la mejor: 6/6 en encuadres donde la pieza redonda llena el marco, 1-2 de 3 en el de objeto completo. **El encuadre pesa más que la redacción.** El fallo de fondo es que el objeto no puede contestarla, ver la nota de abajo |
+ | connectivity / floating parts | **12/12** | **pre-filter** — by far the strongest |
+ | gaps or holes in the surface | 10/12 | **pre-filter** |
+ | ~~black, flipped or improperly shaded faces~~ | ~~10/12~~ | **DO NOT delegate** — `skip`: defect is not in capture; use mechanical culling diff |
+ | finished object vs bare boxes | 10/12 | **pre-filter** |
+ | ~~consistent proportions~~ | ~~10-12/12~~ | **DO NOT delegate** (retracted 2026-08-22) — that 10-12/12 is an **unfalsified** score: broken/fixed pair against which it was measured contained no proportion defect, so answering "yes" always scores perfect. See note below |
+ | **bevels / chamfered edges** | **6/12** | **DO NOT delegate** — is at constant response floor. Confirms 2026-07-30 warning: both models gave opposite bevel verdicts |
+ | **faceted cylinders** | **at random across 8 framings** | **DO NOT delegate, and do not rewrite it** — four phrasings tested (2026-08-17) and the existing one is best: 6/6 in framings where round piece fills frame, 1-2 of 3 in whole object framing. **Framing outweighs phrasing.** Underlying failure is that object cannot answer it, see note below |
- ### Antes de creerte una puntuación alta de esta tabla (2026-08-22)
+ ### Before believing a high score from this table (2026-08-22)
- Una pregunta sí/no tiene dos modos de fallo que su propia puntuación esconde, y la fila
- de proporciones cayó en los dos:
+ A yes/no question has two failure modes that its own score hides, and the
+ proportions row fell into both:
- 1. **La constante.** Si el oro del conjunto de prueba **nunca cambia de valor**, la
- respuesta constante puntúa perfecto y la métrica queda *infalsificada*, no validada.
- La pregunta lucía 10-12/12 porque el par con el que se midió no tenía ningún defecto
- de proporción. **La comprobación es una línea: ¿cambia el oro de valor en el
- conjunto?** Si no, no has medido nada.
- 2. **La confabulación.** Al pedirle motivo no duda, lo inventa. Ante una brida un 37 %
- más ancha de lo debido, un modelo la declaró coherente con «standard fastener
- geometry» y otro «appropriately scaled». Una nota segura y falsa es peor que un
- «unsure».
+ 1. **The constant.** If test set gold **never changes value**, constant
+ response scores perfect and metric remains *unfalsified*, not validated.
+ The question looked 10-12/12 because pair against which it was measured had no
+ proportion defect. **The check is one line: does gold change value in the
+ set?** If not, you measured nothing.
+ 2. **Confabulation.** When asked for rationale it does not hesitate, it invents. Facing a flange 37%
+ wider than expected, one model declared it consistent with "standard fastener
+ geometry" and another "appropriately scaled". A confident and false note is worse than
+ "unsure".
- Medido el 2026-08-17 con `gemma4:26b`, `qwen3.8:27b` y `qwen3.5:27b`: con dos defectos
- construidos, la pregunta contestó **«yes» en las 9 celdas** (3 modelos × 3 variantes).
- Pedir en su lugar **la medida** —tamaño y posición de cada pieza como fracción del
- marco, puntuada por dirección de cambio— acertó **6/6 sobre los mismos renders**.
+ Measured on 2026-08-17 with `gemma4:26b`, `qwen3.8:27b`, and `qwen3.5:27b`: with two constructed
+ defects, the question answered **"yes" in all 9 cells** (3 models × 3 variants).
+ Asking instead for **the measurement** —size and position of each piece as fraction of
+ frame, scored by direction of change— got **6/6 right on the same renders**.
- El sustituto de medida **todavía no está implementado aquí**: `probe_truth.py` no existe
- en `references/` y la pregunta sigue en `checks_hardsurface.json:29` tal cual. Hasta que
- lo esté, esta fila se mira a ojo y no se delega. Y al implementarlo, la trampa que ya
- mordió: **declarar una convención de coordenadas no gana al prior arriba-abajo del
- modelo** — pedir y=0 abajo y espejar sus respuestas bajó el error vertical de 0,107 a
- 0,017 de marco. Usa la convención de imagen o detecta el espejo antes de puntuar.
+ The measurement substitute **is not yet implemented here**: `probe_truth.py` does not exist
+ in `references/` and the question remains in `checks_hardsurface.json:29` as-is. Until
+ it is, this row is checked by eye and not delegated. And when implementing it, the trap that already
+ bit: **declaring a coordinate convention does not beat the model's up-down
+ prior** — asking for y=0 at bottom and mirroring its responses reduced vertical error from 0.107 to
+ 0.017 of frame. Use image convention or detect mirror before scoring.
- ### Dónde entra en el bucle
+ ### Where it enters the loop
- Entre el paso 2 (capturar ángulos) y el paso 3 (mirar) de §The loop:
+ Between step 2 (capture angles) and step 3 (look) of §The loop:
python references/vr_score.py ask --checklist references/checks_hardsurface.json \
--model gemma4:26b --json \
--view assembled__iso _review/m__iso__iter3.png \
--view assembled__profile_R _review/m__profile_R__iter3.png \
--view assembled__profile_L _review/m__profile_L__iter3.png \
--view zoom_receiver__right_iso _review/m__receiver__iter3.png \
--view zoom_muzzle__front_iso _review/m__muzzle__iter3.png
- Si dispara alguna de las cinco activas: arreglar y re-renderizar **antes** de gastar los ojos del usuario.
- Si no dispara ninguna: mirar igualmente — el filtro no ha aprobado nada. Las tres excluidas no
- aparecen en las respuestas del modelo: caras invertidas van al diff mecánico; proporciones y
- cilindros mantienen declarado el hueco hasta que existan `probe_truth.py` y `facet_report.py`.
+ If any of the five active ones triggers: fix and re-render **before** spending user's eyes.
+ If none triggers: look anyway — filter has not approved anything. The three excluded do
+ not appear in model responses: inverted faces go to mechanical diff; proportions and
+ cylinders keep gap declared until `probe_truth.py` and `facet_report.py` exist.
- ### El gate de calibración cambia: por pregunta, con par roto/arreglado
+ ### Calibration gate changes: per question, with broken/fixed pair
- La sección de sombra pide **≥15 renders ya juzgados** antes de delegar una pregunta. Eso es
- calibrar por acumulación y **no habría encontrado nada de esto**: la misma pregunta pasa de 7/12 a
- 11/12 solo cambiando el encuadre, y de 9/12 a 18/18 solo cambiando con qué otras preguntas viaja
- en la llamada. Se sustituye por un gate más barato y que además atribuye:
+ The shadow section asks for **≥15 already-judged renders** before delegating a question. That is
+ calibrating by accumulation and **would have found none of this**: the same question goes from 7/12 to
+ 11/12 just by changing framing, and from 9/12 to 18/18 just by changing which other questions travel
+ in the call. It is replaced by a cheaper gate that also attributes:
- **Coge dos renders del MISMO objeto, uno con el defecto y otro con el defecto arreglado, y haz la
- misma pregunta a los dos.** No necesita oro. Mide dos cosas que no son la misma:
+ **Take two renders of the SAME object, one with defect and another with defect fixed, and ask
+ the same question to both.** Needs no gold. Measures two things that are not the same:
- - **sensibilidad** — ¿cambia la respuesta donde debe cambiar?
- - **especificidad** — ¿se queda quieta donde no debe cambiar?
+ - **sensitivity** — does response change where it should change?
+ - **specificity** — does it stay still where it should not change?
- Un juez que contesta igual a las dos versiones no está leyendo el modelo, tenga el marcador que
- tenga. Cualquier mod con un antes/después sirve de banco: no hay que fabricar nada.
+ A judge answering identically to both versions is not reading the model, whatever score it
+ may have. Any mod with a before/after serves as testbench: nothing needs to be manufactured.
- ### Tres cosas que invalidan una comparación de encuadres
+ ### Three things that invalidate a framing comparison
- 1. **El lote de la llamada cambia la respuesta.** Misma pregunta, misma imagen, mismo modelo:
- **18/18** en una llamada con otras cuatro de sombreado, **9/12** en la llamada con las ocho del
- checklist. Comparar encuadres exige mantener el lote fijo. Y **no es el tamaño**: sacar una
- pregunta del lote empeoró a las dos que se quedaron.
- 2. **Un encuadre sin render arreglado no es comparable** con uno que lo tenga: puntúa sobre la
- mitad de las celdas y ahí una respuesta constante saca 6/6.
- 3. **La temperatura está fijada a 0,1** en el payload, así que dos muestras de la misma celda salen
- casi idénticas: el `n` efectivo es el número de celdas, no el de muestras. No confundir repetir
- con medir.
+ 1. **The batch of the call changes the response.** Same question, same image, same model:
+ **18/18** in a call with four other shading questions, **9/12** in call with eight of the
+ checklist. Comparing framings requires keeping batch fixed. And **it is not size**: removing a
+ question from batch worsened the two that stayed.
+ 2. **A framing without fixed render is not comparable** to one that has it: scores over
+ half of cells and there a constant response gets 6/6.
+ 3. **Temperature is set to 0.1** in payload, so two samples of same cell turn out
+ almost identical: effective `n` is number of cells, not number of samples. Do not confuse repeating
+ with measuring.
- ### Alcance, para no repetir el error que originó esto
+ ### Scope, so as not to repeat the error that originated this
- Todo lo anterior sale de **un solo objeto**. El día antes registré que estos modelos «no detectan
- defectos geométricos» — y era falso: medía mi arnés, no los modelos, y cerró una línea de trabajo
- que funcionaba. Antes de escribir que un modelo no sabe hacer algo, haber variado **redacción,
- encuadre y lote**, y decir cuál de los tres se varió. Origen y evidencia en `LL-289`.
+ All of the above comes from **a single object**. The day before I recorded that these models "do not detect
+ geometric defects" — and it was false: it measured my harness, not the models, and closed a line of work
+ that worked. Before writing that a model cannot do something, have varied **phrasing,
+ framing, and batch**, and state which of the three was varied. Origin and evidence in `LL-289`.
- ## Reglas promovidas del corpus de lecciones (added 2026-07-27)
+ ## Rules promoted from the lessons corpus (added 2026-07-27)
- Promovidas desde `AI/20_Knowledge/lessons-learned.md` para que lleguen por trigger en vez
- de depender de que alguien recuerde buscarlas. Cada regla cita su `LL-NNN` de origen;
- la entrada completa (síntoma, origen, evidencia) vive allí. No quites la cita: el índice
- `lessons-index.md` detecta la promoción buscando esa referencia dentro de las skills.
+ Promoted from `AI/20_Knowledge/lessons-learned.md` so that they arrive via trigger instead
+ of relying on someone remembering to look them up. Each rule cites its source `LL-NNN`;
+ the full entry (symptom, origin, evidence) lives there. Do not remove the citation: the index
+ `lessons-index.md` detects promotion by searching for that reference inside the skills.
- - **LL-153** — Juzga toda zona visual crítica en un crop a resolución nativa. Usa RMS y scores solo como filtro de cambio; nunca como veredicto de corrección, y conserva el crop full-res como evidencia.
+ - **LL-153** — Judge every critical visual zone on a native-resolution crop. Use RMS and scores only as change filter; never as correctness verdict, and keep full-res crop as evidence.
- ## Q3 cerrada: no reescribir ni delegar el facetado (SP-282, added 2026-08-31)
+ ## Q3 closed: do not rewrite or delegate faceting (SP-282, added 2026-08-31)
- La redacción actual de cilindros es la mejor de las cuatro medidas, pero eso no la vuelve un
- gate. Preguntada sola acertó 6/6; dentro del lote de ocho de `vr_score.py` cayó a 3/6. En el
- objeto real produjo 8 falsas alarmas de 12: la pieza octogonal visible es intencional y los
- cilindros auténticos, suavizados, tienen una sagitta subpíxel en esos encuadres. Por tanto:
+ Current cylinders phrasing is best of four measured, but that does not make it a
+ gate. Asked alone it got 6/6; within the batch of eight of `vr_score.py` it fell to 3/6. On
+ actual object it produced 8 false alarms out of 12: visible octagonal piece is intentional and
+ authentic smoothed cylinders have subpixel sagitta in those framings. Therefore:
- - conserva la pregunta sin reescribir, con `skip`; no entra en checklist activo ni pre-filtro;
- - no describas el resultado como azar: el fallo cambia con el encuadre y con el agrupado;
- - si se pide contar un perfil, encuadra la **sección** hasta que llene el marco. La distancia
- genérica `max_dim * 2.2` de un objeto alargado hace que la vista axial no tenga información;
- - el sustituto correcto es determinista y **reporta, no juzga**: pieza, lados, radio, desviación
- relativa y sagitta, con allowlist para prismas intencionales. La intención sigue siendo una
- decisión del modelador.
+ - keep question without rewriting, with `skip`; does not enter active checklist or pre-filter;
+ - do not describe result as chance: failure changes with framing and grouping;
+ - if asked to count a profile, frame the **section** until it fills the frame. Generic
+ distance `max_dim * 2.2` of an elongated object leaves axial view without information;
+ - correct substitute is deterministic and **reports, does not judge**: part, sides, radius, relative
+ deviation, and sagitta, with allowlist for intentional prisms. Intention remains a
+ decision of the modeler.
- `facet_report.py` continúa sin distribuirse en esta skill. Esta sección fija su contrato y sus
- límites; no afirma que el ejecutable esté instalado.
+ `facet_report.py` continues without being distributed in this skill. This section establishes its contract and its
+ limits; does not claim that executable is installed.
- ## Contrato calibrado del reporte de facetado (SP-284, added 2026-08-31)
+ ## Calibrated contract of faceting report (SP-284, added 2026-08-31)
- Si se incorpora `facet_report.py`, la regla calibrada de candidatos es `lados < 12` y
- `radio >= 6 mm`, nunca el umbral provisional de 4 mm. Además, exige una sección transversal casi
- cuadrada: relación entre lados del bbox transversal `<= 1.30`. Ese límite admite pentágonos y
- hexágonos, pero rechaza formas planas que el filtro del 20 % exterior confunde con un cilindro.
- Una allowlist silencia perfiles prismáticos deliberados.
+ If `facet_report.py` is incorporated, calibrated candidate rule is `sides < 12` and
+ `radius >= 6 mm`, never provisional 4 mm threshold. Furthermore, requires an almost square
+ cross section: ratio between sides of transversal bbox `<= 1.30`. That limit admits pentagons and
+ hexagons, but rejects planar shapes that the 20% outer filter confuses with a cylinder.
+ An allowlist silences deliberate prismatic profiles.
- El reporte de **cobertura** es tan importante como las marcas. Debe publicar:
+ The **coverage** report is as important as flags. Must publish:
- - mallas visibles en render y piezas con perfil circular;
- - polígonos examinados y porcentaje sobre los visibles;
- - la mayor malla no examinada y su porcentaje.
+ - meshes visible in render and parts with circular profile;
+ - examined polygons and percentage over visible ones;
+ - largest unexamined mesh and its percentage.
- `0 marcadas` es **NO CONCLUYENTE** si no hay mallas visibles o si una sola malla no examinada
- concentra `>=30 %` de los polígonos. El porcentaje global cubierto no decide el veredicto: un
- asset compuesto principalmente por cajas y raíles puede tener cobertura baja y ser válido para
- las piezas examinadas.
+ `0 marked` is **INCONCLUSIVE** if there are no visible meshes or if a single unexamined mesh
+ concentrates `>=30%` of polygons. Overall percentage covered does not decide verdict: an
+ asset composed mainly of boxes and rails can have low coverage and be valid for
+ examined parts.
- Calibración declarada: 0 falsas alarmas en 41 piezas de perfil circular de tres assets reales y
- las tres piezas del defecto sintético siguieron marcadas; las sondas de 12 y 48 lados quedaron
- limpias. Límites: trabaja por objeto, no ve tubos dentro de una malla fusionada, puede perder
- perfiles finos o muy ranurados y no certifica que lo no detectado sea redondo. El corpus no
- incluye un mod publicado y verificado in-game; no eleves este reporte a gate final.
+ Declared calibration: 0 false alarms across 41 circular-profile parts from three real assets and
+ all three parts of synthetic defect remained marked; 12- and 48-sided probes remained
+ clean. Limits: works per object, does not see tubes inside merged mesh, can miss
+ thin or heavily slotted profiles, and does not certify that undetected geometry is round. Corpus does
+ not include a published mod verified in-game; do not elevate this report to final gate.