kylebrodeur commited on
Commit
c3fc85f
·
verified ·
1 Parent(s): 45f6662

deploy: updated core + ingestion pipeline (1,304 refs, 25 lessons, 260 Lane C obs locally)

Browse files
DEPLOYMENT.md ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # The Chief Engineer — HF Space Deployment Guide
2
+
3
+ **Space:** `build-small-hackathon/microfactory-lab`
4
+ **Hardware:** zero-a10g (ZeroGPU)
5
+ **Status:** 🟢 LIVE — https://build-small-hackathon-microfactory-lab.hf.space
6
+
7
+ ---
8
+
9
+ ## Quick Summary
10
+
11
+ The Gradio app was failing on HF Spaces due to three platform-specific issues:
12
+ 1. **SSR crash** — Gradio 6's Node.js SSR proxy crashes on zero-a10g
13
+ 2. **Missing `@spaces.GPU`** — ZeroGPU hardware requires at least one decorated function
14
+ 3. **Theme/CSS/head lost** — pySpaces calls `demo.launch()` without our custom args (Gradio 6 moved these from `Blocks()` to `launch()`)
15
+
16
+ All fixed. The app runs on **ZeroGPU** with Gemma 4 E2B (2B params, Apache 2.0, not gated).
17
+
18
+ ---
19
+
20
+ ## Dual-Config Architecture
21
+
22
+ The codebase supports **both** local development and Space deployment from a single `app.py`:
23
+
24
+ | Environment | Launch Trigger | Theme Injection | ZeroGPU |
25
+ |-------------|----------------|-----------------|---------|
26
+ | **Local dev** | `if __name__ == "__main__": demo.launch(...)` | Explicit args + monkey-patch defaults | `@spaces.GPU` no-op |
27
+ | **HF Space** | pySpaces imports module → calls `demo.launch()` | Monkey-patch `setdefault()` fills THEME/CSS/VP_HEAD/ssr_mode | `@spaces.GPU` wraps function |
28
+
29
+ **Key mechanisms:**
30
+ - `demo = build()` at module level — pySpaces finds it
31
+ - `if __name__ == "__main__":` guarded — only runs locally
32
+ - `@spaces.GPU` decorator no-ops when `Config.zero_gpu=False` (local)
33
+ - `_patched_launch` uses `kwargs.setdefault()` — respects explicit local args, fills defaults on Space
34
+
35
+ ---
36
+
37
+ ## Space Variables (set via `hf spaces variables`)
38
+
39
+ | Variable | Value | Purpose |
40
+ |----------|-------|---------|
41
+ | `GRADIO_SSR_MODE` | `False` | Disables SSR to prevent Node.js crash |
42
+ | `CHIEF_ENGINEER_BACKEND` | `zerogpu` | Enables live inference path |
43
+ | `CHIEF_ENGINEER_HF_MODEL` | `google/gemma-4-E2B-it` | Model for ZeroGPU inference |
44
+
45
+ ---
46
+
47
+ ## Requirements Strategy
48
+
49
+ HF Spaces builds mount **only `requirements.txt`** to `/tmp/requirements.txt`. The `-r requirements-zerogpu.txt` pattern fails because the included file isn't mounted.
50
+
51
+ **Solution:** ZeroGPU deps are **inlined** in `requirements.txt`:
52
+
53
+ ```txt
54
+ # Core deps
55
+ gradio>=6.17,<7
56
+ ollama>=0.4
57
+ pydantic>=2.7
58
+ trimesh>=4.4
59
+ shapely>=2.0
60
+
61
+ # ZeroGPU deps (inlined — build mounts only requirements.txt)
62
+ spaces>=0.30 # @spaces.GPU decorator
63
+ torch>=2.4
64
+ transformers>=4.49 # Gemma 3/4 family
65
+ accelerate>=0.34
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Code Changes in `app.py`
71
+
72
+ ### 1. Module-level `demo` for pySpaces
73
+ ```python
74
+ def build() -> gr.Blocks:
75
+ with gr.Blocks(title="The Chief Engineer") as demo:
76
+ # ... build UI ...
77
+ return demo
78
+
79
+ demo = build() # Module level — pySpaces finds this
80
+ ```
81
+
82
+ ### 2. `@spaces.GPU` on inference function
83
+ ```python
84
+ import spaces
85
+
86
+ @spaces.GPU
87
+ def get_recommendation(part, material, description, temp, humidity, bed_position):
88
+ # Heavy inference — GPU allocated on call via ZeroGPU
89
+ ...
90
+ ```
91
+
92
+ `import spaces` is wrapped in a `try/except` shim in `app.py`: when the package
93
+ is absent (local base env / offline), `@spaces.GPU` no-ops and returns the function
94
+ unchanged. On the Space, HF provides `spaces` and the real decorator allocates the GPU.
95
+
96
+ ### 3. Monkey-patch `demo.launch` for theme/CSS/head
97
+ ```python
98
+ # After demo = build()
99
+ _original_launch = demo.launch
100
+
101
+ def _patched_launch(**kwargs):
102
+ kwargs.setdefault("theme", THEME)
103
+ kwargs.setdefault("css", CSS)
104
+ kwargs.setdefault("head", VP_HEAD)
105
+ kwargs.setdefault("ssr_mode", False)
106
+ return _original_launch(**kwargs)
107
+
108
+ demo.launch = _patched_launch
109
+ ```
110
+
111
+ - **Local:** `__main__` calls `demo.launch(ssr_mode=False, server_name="0.0.0.0", ...)` → explicit args win
112
+ - **Space:** pySpaces calls `demo.launch()` → `setdefault()` injects THEME/CSS/VP_HEAD/ssr_mode
113
+
114
+ ### 4. Guarded `__main__` for local dev only
115
+ ```python
116
+ if __name__ == "__main__":
117
+ print(f"[chief-engineer] {llm.backend_status()} | seeded {_loaded} lessons this run")
118
+ demo.launch(ssr_mode=False, server_name="0.0.0.0", server_port=7860, share=False)
119
+ ```
120
+
121
+ Runs locally; skipped on Space (pySpaces imports module, `__name__ != "__main__"`).
122
+
123
+ ---
124
+
125
+ ## Build & Deploy Commands
126
+
127
+ ```bash
128
+ # From chief-engineer/
129
+ hf upload build-small-hackathon/microfactory-lab ./app.py app.py --repo-type space
130
+ hf upload build-small-hackathon/microfactory-lab ./requirements.txt requirements.txt --repo-type space
131
+
132
+ # Set variables (triggers rebuild)
133
+ hf spaces variables add build-small-hackathon/microfactory-lab \
134
+ -e GRADIO_SSR_MODE=False \
135
+ -e CHIEF_ENGINEER_BACKEND=zerogpu \
136
+ -e CHIEF_ENGINEER_HF_MODEL=google/gemma-4-E2B-it
137
+
138
+ # Factory reboot if needed
139
+ hf spaces restart build-small-hackathon/microfactory-lab --factory-reboot
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Local Development
145
+
146
+ ```bash
147
+ # In chief-engineer/
148
+ make setup # uv sync + generate assets
149
+ ollama serve & # For live Gemma (optional; fallback works offline)
150
+ make run # uv run python app.py → http://localhost:7860
151
+ ```
152
+
153
+ - Full Astrometrics theme applied via monkey-patch defaults
154
+ - Falls back to deterministic advisor if Ollama unavailable
155
+ - All core tests pass offline: `uv run python test_core.py`
156
+
157
+ ---
158
+
159
+ ## Pre-flight / Verification
160
+
161
+ ```bash
162
+ # Core tests (offline, fast)
163
+ uv run python test_core.py
164
+
165
+ # Preflight (needs Ollama for live path)
166
+ uv run python -m scripts.preflight
167
+
168
+ # Space logs
169
+ hf spaces logs build-small-hackathon/microfactory-lab --tail 30
170
+ hf spaces logs build-small-hackathon/microfactory-lab --build --tail 30
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Troubleshooting
176
+
177
+ | Symptom | Cause | Fix |
178
+ |---------|-------|-----|
179
+ | `RUNTIME_ERROR` + "No @spaces.GPU function detected" | Missing GPU decorator on zero-a10g | Add `@spaces.GPU` to at least one function |
180
+ | SSR crash ("Stopping Node.js server...") | Gradio 6 SSR incompatible | Set `GRADIO_SSR_MODE=False` + `ssr_mode=False` |
181
+ | Theme/CSS missing on Space | pySpaces calls `launch()` without args | Monkey-patch `demo.launch` with `setdefault()` |
182
+ | Build fails: "No such file: requirements-zerogpu.txt" | `-r` not mounted | Inline deps in `requirements.txt` |
183
+ | 503 on zero-a10g, works on cpu-basic | Hardware-specific SSR/GPU issue | Apply SSR fix + `@spaces.GPU` |
184
+ | "ModuleNotFoundError: spaces" locally | ZeroGPU package not installed | None needed — `app.py` shims `spaces` to a no-op when absent. Install the real path only to test ZeroGPU locally: `uv sync --extra zerogpu`. |
185
+
186
+ ---
187
+
188
+ ## Model Details
189
+
190
+ - **Model:** `google/gemma-4-E2B-it` (Gemma 4, Efficient 2B, instruction-tuned)
191
+ - **Parameters:** ~2 billion (E2B = Efficient 2B)
192
+ - **License:** Apache 2.0 — **not gated**, no `HF_TOKEN` needed
193
+ - **Architecture:** Dense, multimodal (text + image), 128K context
194
+ - **Endpoint:** Loaded on first `analyze` call via ZeroGPU
195
+
196
+ ---
197
+
198
+ ## File Tree (Space Root)
199
+
200
+ ```
201
+ build-small-hackathon/microfactory-lab/
202
+ ├── app.py # Entry point (demo = build())
203
+ ├── README.md # Space metadata (sdk: gradio, sdk_version: 6.17.3)
204
+ ├── requirements.txt # All deps inlined
205
+ ├── assets/ # .glb meshes (benchy, bridge, cube, overhang, vase)
206
+ ├── core/ # Core library (llm, ledger, spine, widgets, viewer, etc.)
207
+ ├── learn/ # Policy learning loop
208
+ ├── ingest/ # Config distillation (Klipper/Prusa/Marlin)
209
+ ├── sim/ # Virtual printer + outcome simulator
210
+ ├── scripts/ # Helpers (preflight, bench, demo, capture)
211
+ ├── data/ # seed_lessons.jsonl (gitignored: lessons.jsonl, policy.json)
212
+ └── test_core.py # Offline core tests
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Key Files for Future Maintenance
218
+
219
+ | File | Purpose |
220
+ |------|---------|
221
+ | `app.py` | Space entry point + local launch |
222
+ | `core/llm.py` | Backend abstraction (Ollama / ZeroGPU / fallback) |
223
+ | `core/llm_zerogpu.py` | ZeroGPU transformers path |
224
+ | `core/chief_engineer.py` | Brain: precedent eval + settings proposal |
225
+ | `core/spine.py` | Deterministic veto (material bounds) |
226
+ | `learn/policy.py` | Learned parametric policy (env-bucketed) |
227
+ | `core/theme.py` | Astrometrics THEME + CSS + VP_HEAD |
228
+ | `scripts/preflight.py` | GO/NO-GO gate for releases |
229
+
230
+ ---
231
+
232
+ ## Next Steps / Frontiers
233
+
234
+ - [ ] Weight-level fine-tuning on accumulated ledger (named "Well-Tuned" — currently not built)
235
+ - [ ] Real distributed multi-node execution (currently single Space)
236
+ - [ ] Physical interfaces: g-code streaming, env sensors, camera defect CV
237
+ - [ ] Larger Gemma variants (E4B, 26B A4B, 31B) on larger GPUs
238
+
239
+ ---
240
+
241
+ *Last updated: 2026-06-11 — Space live on zero-a10g with ZeroGPU Gemma 4 E2B*
README.md CHANGED
@@ -52,9 +52,9 @@ outcome comes from outside the model. The honest signal is what makes the compou
52
  prior jobs the LLM reasons over) and a **learned parametric policy** (`learn/policy.py`) that stores
53
  setting offsets per (material, geometry, *environment-bucket*) and updates from each outcome. Because
54
  cells are bucketed, a lesson from one humid PETG bridge **generalizes** to the next, not-identical
55
- one. The **Learning Loop** tab runs the closed loop live: pick a hard job and watch print quality
56
- climb from failure to clean over a handful of iterations. The one simulated boundary (print
57
- outcomes) is spelled out in *What's real vs frontier* below.
58
 
59
  ## Run it (fully local, no cloud, no API key)
60
 
@@ -71,10 +71,14 @@ Local dev uses [uv](https://docs.astral.sh/uv); the HF Space installs via `requi
71
  and the simulator / learning-loop / ingestion packages in `sim/`, `learn/`, `ingest/`. See
72
  `docs/RUNBOOK.md` for the full run-and-test order of operations.
73
 
 
 
 
 
74
  Falls back to a deterministic advisor if Ollama is unreachable, so the demo never crashes; the
75
  UI always shows which model ran. Set `CHIEF_ENGINEER_MODEL=gemma4:e2b` if CPU latency is high.
76
 
77
- **Resetting demo state.** The Learning Loop and the 🧪 *Simulate outcome* button accumulate runtime
78
  state — earned/sim lessons in `data/lessons.jsonl` and the learned cells in `data/policy.json` (both
79
  gitignored). For a clean slate between demos:
80
 
@@ -82,18 +86,26 @@ gitignored). For a clean slate between demos:
82
  rm -f data/lessons.jsonl data/policy.json # seed corpus reloads automatically on next launch
83
  ```
84
 
85
- ## Workspaces
86
-
87
- 1. **Studio** — define the job: pick a reference part type, quick-load 3DBenchy, generate a
88
- primitive, or upload a mesh; choose material; set run conditions. Live 3D preview + a
89
- layer-by-layer *virtual print* of the actual part.
90
- 2. **Analysis** — the load-bearing moment, on its own page: the model's precedent evaluation and
91
- reasoning, predicted failure regions, the Spine-validated settings + G-code as instrument
92
- readouts, then close the loop (🧪 *Simulate outcome* or record a real one).
93
- 3. **Learning Loop** — the compounding made literal: run the closed simulated loop and watch the
94
- quality curve climb while the learned policy cell fills in.
95
- 4. **Knowledge + Mesh** — the node mesh (one node executes, others as context) and the live ledger
96
- where lessons accumulate (seed → earned → sim).
 
 
 
 
 
 
 
 
97
 
98
  ## Badges
99
 
 
52
  prior jobs the LLM reasons over) and a **learned parametric policy** (`learn/policy.py`) that stores
53
  setting offsets per (material, geometry, *environment-bucket*) and updates from each outcome. Because
54
  cells are bucketed, a lesson from one humid PETG bridge **generalizes** to the next, not-identical
55
+ one. The **Print** workspace runs the closed loop live on the job you built: watch print quality
56
+ climb from failure to clean over a handful of iterations while a separate QA Inspector grades each
57
+ run. The one simulated boundary (print outcomes) is spelled out in *What's real vs frontier* below.
58
 
59
  ## Run it (fully local, no cloud, no API key)
60
 
 
71
  and the simulator / learning-loop / ingestion packages in `sim/`, `learn/`, `ingest/`. See
72
  `docs/RUNBOOK.md` for the full run-and-test order of operations.
73
 
74
+ **HF Space Deployment:** See [`DEPLOYMENT.md`](DEPLOYMENT.md) for the complete guide covering
75
+ Space variables, dual-config architecture (local + Space), build strategy, troubleshooting, and
76
+ the key code changes (`@spaces.GPU`, monkey-patched `demo.launch`, module-level `demo`).
77
+
78
  Falls back to a deterministic advisor if Ollama is unreachable, so the demo never crashes; the
79
  UI always shows which model ran. Set `CHIEF_ENGINEER_MODEL=gemma4:e2b` if CPU latency is high.
80
 
81
+ **Resetting demo state.** The Print loop and the 🧪 *Simulate one print* button accumulate runtime
82
  state — earned/sim lessons in `data/lessons.jsonl` and the learned cells in `data/policy.json` (both
83
  gitignored). For a clean slate between demos:
84
 
 
86
  rm -f data/lessons.jsonl data/policy.json # seed corpus reloads automatically on next launch
87
  ```
88
 
89
+ ## Workspaces — the real print workflow
90
+
91
+ 1. **Studio** — define + preview the job: quick-load 3DBenchy, generate a primitive, or drop a
92
+ mesh (the engineer *infers* the part class itself — you don't pick it); choose material; the
93
+ simulated **environment** (temp/humidity/plate position on a Creality Ender 3 V2) populates.
94
+ 2. **Build** — the pre-flight check, **before it prints**: the slicer / layer-by-layer virtual
95
+ print, the engineer's precedent read + reasoning + predicted failure regions, the Spine-validated
96
+ settings + G-code (terminal readout), and a **second opinion** from a separate QA Inspector.
97
+ 3. **Print** — run *this* job through the closed loop and watch quality compound fail→clean: the
98
+ Engineer proposes → the Spine vetoes → a simulated world prints → the **Inspector grades each
99
+ run** → policy + ledger learn. Simulate one print, or record a real outcome.
100
+ 4. **Review** — the compounding made visible: the live ledger (seed → earned → sim), the capability
101
+ mesh, and the Inspector's verdict on the whole run.
102
+
103
+ **The hybrid evaluator (honest by design).** The Engineer never grades its own work. The
104
+ deterministic world (`sim/outcome.py`) produces the ground-truth pass/fail; a *separate* **QA
105
+ Inspector** persona (`core/inspector.py`) reads what the Engineer claimed vs what actually
106
+ happened and writes the verdict — a second opinion in **Build**, a per-print grade in **Print**,
107
+ a run verdict in **Review**. Physics + a skeptical second voice — never the proposer marking its
108
+ own homework.
109
 
110
  ## Badges
111
 
app.py CHANGED
@@ -16,33 +16,43 @@ from __future__ import annotations
16
  import random
17
 
18
  import gradio as gr
19
- import spaces
20
 
 
 
 
 
 
 
 
 
 
 
 
21
  from core import llm
22
  from core import seed_lessons
23
- from core.theme import THEME, CSS, rule, command_bar, footer_bar
24
  from core.widgets import virtual_printer_html, layer_image, SCRUB_LAYERS, VP_HEAD
25
  from core.chief_engineer import advise
26
  from core.ledger import LedgerManager
27
- from core.models import BED_POSITIONS, Environment, GEOMETRY_TYPES, Job, MATERIALS, PrintSettings
28
  from core.nodes import render_node_cards
29
  from core.reflect import reflect_on_job
30
  from core.spine import SpineValidator
31
- from learn.loop import run_iteration, run_session
32
  from learn.policy import LearnedPolicy, cell_key
33
  from sim.outcome import simulate
34
  from core.viewer import (
35
- PART_LABEL,
36
  benchy_mesh,
37
  gcode_panel_html,
38
  generate_primitive,
 
39
  iteration_log_html,
40
  placement_callout,
41
  policy_cell_html,
42
  precedent_eval_html,
43
  quality_curve_html,
44
  risk_callouts_html,
45
- sample_mesh,
46
  settings_panel_html,
47
  steepest_overhang_hint,
48
  )
@@ -84,46 +94,46 @@ def ledger_html() -> str:
84
  return head + "".join(rows)
85
 
86
 
87
- _DEFAULT_PART = {"geometry": "overhang", "mesh": sample_mesh("overhang"), "label": "OVERHANG TEST"}
88
-
89
-
90
- def _set_part(geometry: str, mesh, label: str):
91
- """Shared STUDIO preview update → (part_state, model3d, status). The virtual
92
- print is NOT rendered here — it initializes on ANALYZE (see get_recommendation),
93
- so it doesn't loop perpetually on the Studio preview."""
94
  return (
95
- {"geometry": geometry, "mesh": mesh, "label": label},
96
  gr.update(value=mesh),
97
- f"▣ ACTIVE PART · **{label}** · class `{geometry}`",
98
  )
99
 
100
 
101
- def analyze_start(part):
102
- """Immediate feedback the instant ANALYZE is clicked: jump to the Analysis tab
103
- and show spinners while the model runs (e4b warm ~15-20s on CPU). The heavy
104
- get_recommendation runs right after via .then(). No part selected → stay put."""
105
  if not (part and part.get("geometry")):
106
- gr.Warning("Select a part in Studio first — pick a type, load Benchy, generate, or upload.")
107
- return (gr.update(),) * 7
108
  return (
109
- gr.Tabs(selected="analysis"),
110
- "⏳ **Analyzing the job** — evaluating precedent and proposing settings…", # reasoning
111
  "<div class='ce-sub'>⏳ retrieving precedent…</div>", # precedent
112
  "", # risks
113
  "", # settings_html
114
  "", # gcode_html
115
  "<div class='ce-sub'>⏳ slicing the part…</div>", # vprint
 
 
 
116
  )
117
 
118
 
119
- def select_type(geo): # reference part-type pill chosen
120
- return _set_part(geo, sample_mesh(geo), PART_LABEL.get(geo, geo.upper()))
121
-
122
-
123
  def load_benchy():
124
  mesh = benchy_mesh()
125
- label = "3DBENCHY (CC0)" if mesh else "BENCHY MISSING — add assets/benchy.glb"
126
- return _set_part("overhang", mesh, label)
 
 
 
127
 
128
 
129
  def generate_part(kind, size):
@@ -133,9 +143,10 @@ def generate_part(kind, size):
133
 
134
  def upload_part(f):
135
  if not f:
136
- return (gr.update(), gr.update(), gr.update(), gr.update())
137
  from pathlib import Path as _P
138
- return _set_part("overhang", f.name, f"UPLOAD · {_P(f.name).name[:26]}")
 
139
 
140
 
141
  def scrub_layer(idx, part):
@@ -144,15 +155,28 @@ def scrub_layer(idx, part):
144
  return layer_image(mesh, idx)
145
 
146
 
147
- # ── simulated sensors (this is a sim lab — conditions are generated, overridable) ──
 
 
 
148
  def _sensor_readout(t, h, pos) -> str:
149
- return ("<div class='ce-sub' style='font-size:13px;'>SIMULATED SENSORS · "
150
  f"🌡 <b style='color:var(--ao-blue);'>{float(t):.0f}°C</b> · "
151
  f"💧 <b style='color:var(--ao-blue);'>{float(h):.0f}%RH</b> · "
152
- f"⌖ <b style='color:var(--ao-blue);'>{pos}</b> "
 
153
  "<span style='opacity:.6;'>(🎲 re-roll or ⚙ override)</span></div>")
154
 
155
 
 
 
 
 
 
 
 
 
 
156
  def randomize_sensors():
157
  """Roll a plausible ambient + plate position — the lab's simulated sensor feed."""
158
  t = random.choice([18, 20, 22, 24, 26, 28, 30, 32])
@@ -166,14 +190,14 @@ def sync_readout(t, h, pos):
166
 
167
 
168
  @spaces.GPU
169
- def get_recommendation(part, material, description, temp, humidity, bed_position):
170
  if not (part and part.get("geometry")): # guard: empty start, no part chosen
171
- return ("", "", "⚠ **Select a part in Studio** (pick a type, load Benchy, generate, "
172
- "or upload) before analyzing.", "", "", "", "", gr.update(visible=False),
173
  gr.update(), {}, "", gr.update(), gr.update())
174
  geometry_type, mesh = part["geometry"], part.get("mesh")
175
  job = Job(geometry_type=geometry_type, material=material, description=description or "",
176
- bed_position=bed_position or "center")
177
  env = Environment(temp=float(temp), humidity=float(humidity))
178
 
179
  retrieved = LEDGER.retrieve(material, geometry_type, env.temp, env.humidity)
@@ -182,7 +206,7 @@ def get_recommendation(part, material, description, temp, humidity, bed_position
182
  rec = advise(job, env, retrieved, references, policy_note)
183
  spine = SPINE.check(rec.advice.settings, material)
184
  hint = steepest_overhang_hint(mesh) if geometry_type in ("overhang", "bridge") else None
185
- vp_html = virtual_printer_html(mesh, f"{material} · {geometry_type}") # init on ANALYZE
186
 
187
  precedent = precedent_eval_html(retrieved, env)
188
  if retrieved:
@@ -199,7 +223,8 @@ def get_recommendation(part, material, description, temp, humidity, bed_position
199
  confirm_vis = gr.update(visible=spine.requires_approval)
200
  approval_md = ("⏸ **HITL gate:** the Spine clamped a boundary setting — review, then **Confirm & Print**."
201
  if spine.requires_approval else "✅ within safe envelope — ready when you are.")
202
- state = {"job": job.model_dump(), "env": env.model_dump(), "settings": spine.settings.model_dump()}
 
203
 
204
  return (
205
  f"{rec.backend}{fb}", # backend status
@@ -218,63 +243,134 @@ def get_recommendation(part, material, description, temp, humidity, bed_position
218
  )
219
 
220
 
221
- def run_learning(material, geometry_type, temp, humidity, iterations):
222
- """Primary demo: run the closed simulated loop and watch quality compound."""
223
- job = Job(geometry_type=geometry_type, material=material, description="")
224
- env = Environment(temp=float(temp), humidity=float(humidity))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  key = cell_key(material, geometry_type, env)
226
- before = POLICY.cell_stats(material, geometry_type, env)
227
- before_html = policy_cell_html(before, key)
228
 
229
  sess = run_session(job, env, int(iterations), POLICY, LEDGER)
230
  after = POLICY.cell_stats(material, geometry_type, env)
 
 
231
 
232
  traj = sess.trajectory
233
  first = sess.first_success
234
  headline = (
235
- f"**{material}/{geometry_type} @ {env.temp:.0f}°C / {env.humidity:.0f}% RH** — "
236
  f"started at quality **{traj[0]:.2f}** ({sess.records[0].result.outcome}); "
237
  + (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."
238
  if first else f"still improving — best **{max(traj):.2f}** after {len(traj)} runs.")
239
- + " The model proposed; a separate simulated world reported the outcome; "
240
- "the policy and ledger learned from it. *(Simulated — see SIMULATION.md.)*"
241
  )
242
  policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);'>▼ learned ▼</div>"
243
  f"{policy_cell_html(after, key)}")
244
  return (
245
- headline, # l_headline
246
- quality_curve_html(traj), # l_curve
247
- policy_html, # l_policy
248
- iteration_log_html(sess.records), # l_log
249
- ledger_html(), # ledger_panel
250
- render_node_cards(env, working=False), # node_cards
 
251
  )
252
 
253
 
254
  def simulate_outcome(state):
255
- """Analysis: close the loop in the world (deterministic), then learn from it."""
256
- if not state:
257
- return "Run a recommendation first.", ledger_html()
 
 
 
258
  job = Job(**state["job"])
259
  env = Environment(**state["env"])
260
  settings = PrintSettings(**state["settings"])
 
 
261
  result = simulate(settings, job, env)
 
262
  learned = POLICY.update(job.material, job.geometry_type, env, result)
263
  POLICY.save()
264
- # record the simulated outcome to the ledger as precedent
265
  from learn.loop import _record_lesson
266
  _record_lesson(job, env, settings, result, LEDGER)
267
- icon = "✓" if result.outcome == "success" else "✗"
268
- return (
269
- f"🧪 **Simulated world** (stand-in for printer + sensors): {icon} **{result.detail}**. \n"
270
- f"↳ policy update: *{learned}* \n*(The model never saw this outcome in advance — SIMULATION.md.)*",
271
- ledger_html(),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  )
 
 
 
 
273
 
274
 
275
  def record_outcome(outcome, state):
276
- if not state:
277
- return "Run a recommendation first.", ledger_html(), render_node_cards(Environment(temp=22, humidity=45))
 
278
  job = Job(**state["job"])
279
  env = Environment(**state["env"])
280
  settings = PrintSettings(**state["settings"])
@@ -297,124 +393,123 @@ def build() -> gr.Blocks:
297
  "and tells you where a print fails <b>before</b> it runs · gemma4 · fully local</div>"
298
  )
299
  state = gr.State()
300
- part = gr.State({"geometry": None, "mesh": None, "label": None})
301
 
302
  with gr.Tabs() as tabs:
303
- # ─────────────────────────── STUDIO (define) ─��─────────────────────────
304
  with gr.Tab("⬡ STUDIO", id="studio"):
305
- gr.HTML("<div class='ce-sub'>DEFINE THE JOB · ① part → ② material → "
306
- "③ conditions → <b>ANALYZE</b></div>")
 
 
 
 
307
  with gr.Row():
308
- with gr.Column(scale=2, elem_classes=["ce-console"]):
309
- gr.HTML(rule("① SELECT PART"))
310
- gr.HTML("<div class='ce-sub'>REFERENCE PART TYPE — what the engineer reasons about</div>")
311
- type_radio = gr.Radio(GEOMETRY_TYPES, value=None, show_label=False,
312
- elem_classes=["ce-pills"])
313
- gr.HTML("<div class='ce-sub' style='margin-top:6px;'>OR LOAD A MODEL</div>")
314
- with gr.Row():
315
- benchy_btn = gr.Button("⛴ QUICK-LOAD BENCHY", elem_classes=["ce-pillbtn"])
316
- with gr.Accordion("⚙ GENERATE A PRIMITIVE", open=False):
317
- gen_kind = gr.Radio(["box", "cylinder", "cone", "sphere"], value="box",
318
- show_label=False, elem_classes=["ce-pills"])
319
- gen_size = gr.Number(value=30, label="SIZE (mm)", elem_classes=["ce-num"])
320
- gen_btn = gr.Button("GENERATE", elem_classes=["ce-pillbtn"])
321
- mesh_in = gr.File(file_types=[".stl", ".glb", ".obj"],
322
- label="UPLOAD YOUR OWN MESH", elem_classes=["ce-drop"])
323
- part_status = gr.Markdown("▣ **no part selected** — pick a type, load Benchy, "
324
- "generate, or upload to begin")
325
 
 
 
326
  gr.HTML(rule("② MATERIAL"))
327
  material = gr.Radio(MATERIALS, value="PLA", show_label=False, elem_classes=["ce-pills"])
328
-
329
- gr.HTML(rule("③ SIMULATED SENSORS"))
330
- sensors_readout = gr.HTML()
331
- roll_btn = gr.Button("🎲 RANDOMIZE SENSORS", elem_classes=["ce-pillbtn"])
332
- with gr.Accordion("⚙ OVERRIDE SENSORS", open=False):
 
 
 
 
 
333
  with gr.Row():
334
- temp = gr.Number(value=22, label="AMBIENT °C", elem_classes=["ce-num"])
335
- humidity = gr.Number(value=45, label="HUMIDITY %RH", elem_classes=["ce-num"])
336
- gr.HTML("<div class='ce-sub'>BUILD-PLATE POSITION — edges/corners run "
337
- "cooler → warp/adhesion risk</div>")
338
- bed_position = gr.Radio(BED_POSITIONS, value="center", show_label=False,
339
- elem_classes=["ce-pills"])
340
- description = gr.Textbox(label="NOTES (OPTIONAL)",
341
- placeholder="e.g. 45° bracket, 60mm tall")
342
- run_btn = gr.Button("▶ ANALYZE JOB", variant="primary")
343
- with gr.Column(scale=3):
344
- gr.HTML(rule("PART PREVIEW"))
345
- model3d = gr.Model3D(value=None, label="", height=320)
346
- gr.HTML("<div class='ce-sub'>Rotate/zoom the part. The layer-by-layer "
347
- "<b>virtual print</b> renders on the Analysis page when you ANALYZE.</div>")
348
 
349
- # ─────────────────────────── ANALYSIS (results) ────────────────────────
350
- with gr.Tab("◇ ANALYSIS", id="analysis"):
351
  backend = gr.Markdown()
352
- gr.HTML("<div class='ce-sub'>① the engineer's read → ② validated settings + "
353
- "g-code → ③ virtual print → ④ close the loop</div>")
354
  with gr.Row():
355
- with gr.Column():
356
- gr.HTML(rule("① PRECEDENT EVALUATION"))
 
 
 
 
 
 
 
357
  precedent = gr.HTML(elem_id="ce-precedent")
358
- gr.HTML(rule("ENGINEER'S REASONING"))
359
  reasoning = gr.Markdown(elem_id="ce-reasoning")
360
- gr.HTML(rule("PREDICTED FAILURE REGIONS"))
361
  risks = gr.HTML()
 
 
362
  with gr.Column():
363
- gr.HTML(rule("② VALIDATION + G-CODE"))
364
  spine_notes = gr.Markdown()
365
  settings_html = gr.HTML()
 
366
  gcode_html = gr.HTML()
367
- confirm_btn = gr.Button("✅ Confirm & Print", elem_id="ce-confirm", visible=False)
368
- gr.HTML(rule("③ VIRTUAL PRINT"))
369
- vprint = gr.HTML()
370
- gr.HTML("<div class='ce-sub'>Slide through the layers — each is a real "
371
- "cross-section of <i>this</i> part at full mesh fidelity.</div>")
372
- vp_slider = gr.Slider(1, SCRUB_LAYERS, value=1, step=1, label="LAYER")
373
- vp_layer = gr.Image(label="", height=380, show_label=False)
374
- gr.HTML(rule("④ CLOSE THE LOOP"))
375
- gr.HTML("<div class='ce-sub'>Simulate the print (deterministic stand-in for printer + "
376
- "sensors), or record a real outcome. Either way the model never grades itself.</div>")
377
- with gr.Row(elem_id="ce-sim"):
378
- sim_btn = gr.Button("🧪 Simulate outcome (deterministic world)")
379
- with gr.Row(elem_id="ce-outcomes"):
380
- b_clean = gr.Button("✓ Printed clean")
381
- b_sag = gr.Button("✗ Sagged")
382
- b_string = gr.Button("✗ Stringing")
383
- outcome_msg = gr.Markdown()
384
-
385
- # ─────────────────────────── LEARNING LOOP ─────────────────────────────
386
- with gr.Tab("◳ LEARNING LOOP", id="loop"):
387
- gr.HTML(
388
- "<div class='ce-sub'>THE COMPOUNDING LOOP · the model proposes → a <b>simulated world</b> "
389
- "reports the outcome → the <b>learned policy</b> and <b>ledger</b> update → the next run is "
390
- "better. Pick a hard job and watch quality climb from failure to clean. "
391
- "<i>Simulated; physical interfaces framed in SIMULATION.md.</i></div>"
392
- )
393
  with gr.Row():
394
  with gr.Column(scale=2, elem_classes=["ce-console"]):
395
- gr.HTML(rule("① SIMULATION CONTROLS"))
396
- l_geometry = gr.Radio(GEOMETRY_TYPES, value="bridge", show_label=False,
397
- elem_classes=["ce-pills"])
398
- l_material = gr.Radio(MATERIALS, value="PETG", show_label=False, elem_classes=["ce-pills"])
399
- with gr.Row():
400
- l_temp = gr.Number(value=29, label="AMBIENT °C", elem_classes=["ce-num"])
401
- l_humidity = gr.Number(value=62, label="HUMIDITY %RH", elem_classes=["ce-num"])
402
- l_iters = gr.Slider(1, 16, value=8, step=1, label="ITERATIONS")
403
- l_run = gr.Button("▶ RUN SIMULATED LEARNING", variant="primary")
404
- l_headline = gr.Markdown()
405
  with gr.Column(scale=3):
406
  gr.HTML(rule("② QUALITY PER ITERATION"))
407
- l_curve = gr.HTML()
408
  gr.HTML(rule("③ LEARNED POLICY CELL"))
409
- l_policy = gr.HTML()
 
 
410
  gr.HTML(rule("④ ITERATION LOG"))
411
- l_log = gr.HTML()
412
-
413
- # ─────────────────────────── KNOWLEDGE + MESH ──────────────────────────
414
- with gr.Tab("▤ KNOWLEDGE + MESH", id="ledger"):
415
- gr.HTML("<div class='ce-sub'>The capability mesh (one node executes, the others "
416
- "stand by as context) and the live ledger where knowledge compounds "
417
- "(seed → earned → sim).</div>")
 
 
 
418
  with gr.Row():
419
  with gr.Column():
420
  gr.HTML(rule("CAPABILITY MESH"))
@@ -424,39 +519,48 @@ def build() -> gr.Blocks:
424
  ledger_panel = gr.HTML(ledger_html())
425
  refresh = gr.Button("⟳ REFRESH LEDGER", elem_classes=["ce-pillbtn"])
426
 
427
- gr.HTML(footer_bar())
428
 
429
  # ── wiring ──
430
  preview_outs = [part, model3d, part_status]
431
- type_radio.change(select_type, [type_radio], preview_outs)
432
- benchy_btn.click(load_benchy, None, preview_outs)
433
- gen_btn.click(generate_part, [gen_kind, gen_size], preview_outs)
434
- mesh_in.upload(upload_part, [mesh_in], preview_outs)
 
435
 
436
- # Simulated sensors: roll on load, re-roll on demand, keep the readout in sync.
437
  sensor_outs = [temp, humidity, bed_position, sensors_readout]
438
- demo.load(randomize_sensors, None, sensor_outs)
439
- roll_btn.click(randomize_sensors, None, sensor_outs)
440
  for c in (temp, humidity, bed_position):
441
- c.change(sync_readout, [temp, humidity, bed_position], [sensors_readout])
442
-
443
- # Two-step ANALYZE: instant tab-switch + spinners, then the heavy model call.
444
- analyze_outs = [backend, precedent, reasoning, risks, settings_html, spine_notes,
445
- gcode_html, confirm_btn, node_cards, state, vprint, vp_slider, vp_layer]
446
- run_btn.click(analyze_start, [part],
447
- [tabs, reasoning, precedent, risks, settings_html, gcode_html, vprint]).then(
448
- get_recommendation, [part, material, description, temp, humidity, bed_position],
449
- analyze_outs)
 
 
 
450
  vp_slider.change(scrub_layer, [vp_slider, part], [vp_layer])
451
-
 
 
 
 
 
 
 
 
 
452
  for btn, oc in [(b_clean, "success"), (b_sag, "failed_sag"), (b_string, "failed_stringing")]:
453
- btn.click(record_outcome, [gr.State(oc), state], [outcome_msg, ledger_panel, node_cards])
454
- sim_btn.click(simulate_outcome, [state], [outcome_msg, ledger_panel])
455
  refresh.click(lambda: ledger_html(), outputs=[ledger_panel])
456
 
457
- l_run.click(run_learning, [l_material, l_geometry, l_temp, l_humidity, l_iters],
458
- [l_headline, l_curve, l_policy, l_log, ledger_panel, node_cards])
459
-
460
  return demo
461
 
462
 
 
16
  import random
17
 
18
  import gradio as gr
 
19
 
20
+ try:
21
+ import spaces # ZeroGPU @spaces.GPU decorator — HF-provided on the Space.
22
+ except ImportError: # not installed locally (base env / offline); decorator no-ops.
23
+ class _SpacesShim:
24
+ @staticmethod
25
+ def GPU(fn=None, **_kw):
26
+ return fn if fn is not None else (lambda f: f)
27
+
28
+ spaces = _SpacesShim() # type: ignore
29
+
30
+ from core import inspector
31
  from core import llm
32
  from core import seed_lessons
33
+ from core.theme import THEME, CSS, rule, command_bar, footer_bar, inspector_panel
34
  from core.widgets import virtual_printer_html, layer_image, SCRUB_LAYERS, VP_HEAD
35
  from core.chief_engineer import advise
36
  from core.ledger import LedgerManager
37
+ from core.models import Advice, BED_POSITIONS, Environment, Job, MATERIALS, PrintSettings
38
  from core.nodes import render_node_cards
39
  from core.reflect import reflect_on_job
40
  from core.spine import SpineValidator
41
+ from learn.loop import run_session
42
  from learn.policy import LearnedPolicy, cell_key
43
  from sim.outcome import simulate
44
  from core.viewer import (
45
+ GEO_READS,
46
  benchy_mesh,
47
  gcode_panel_html,
48
  generate_primitive,
49
+ infer_geometry,
50
  iteration_log_html,
51
  placement_callout,
52
  policy_cell_html,
53
  precedent_eval_html,
54
  quality_curve_html,
55
  risk_callouts_html,
 
56
  settings_panel_html,
57
  steepest_overhang_hint,
58
  )
 
94
  return head + "".join(rows)
95
 
96
 
97
+ def _set_part(geometry: str, mesh, label: str, read: str | None = None):
98
+ """Shared STUDIO preview update → (part_state, model3d, status). The user never
99
+ picks the class — the engineer infers it from the mesh (see infer_geometry).
100
+ The virtual print is NOT rendered here; it initializes on BUILD."""
101
+ read = read or GEO_READS.get(geometry, geometry)
 
 
102
  return (
103
+ {"geometry": geometry, "mesh": mesh, "label": label, "read": read},
104
  gr.update(value=mesh),
105
+ f"▣ ACTIVE PART · **{label}** · the engineer reads this as *{read}* → reasons about `{geometry}`",
106
  )
107
 
108
 
109
+ def build_start(part):
110
+ """Immediate feedback the instant BUILD is clicked: jump to the Build tab and
111
+ show spinners while the model runs (e4b warm ~15-20s on CPU). The heavy
112
+ build_job runs right after via .then(). No part loaded → stay put."""
113
  if not (part and part.get("geometry")):
114
+ gr.Warning("Load a part in Studio first — quick-load Benchy, generate a primitive, or drop a mesh.")
115
+ return (gr.update(),) * 10
116
  return (
117
+ gr.Tabs(selected="build"),
118
+ "⏳ **Building the job** — evaluating precedent and proposing settings…", # reasoning
119
  "<div class='ce-sub'>⏳ retrieving precedent…</div>", # precedent
120
  "", # risks
121
  "", # settings_html
122
  "", # gcode_html
123
  "<div class='ce-sub'>⏳ slicing the part…</div>", # vprint
124
+ "", # second_opinion_panel (clear stale verdict)
125
+ gr.update(interactive=True), # to_print_btn (un-gate)
126
+ gr.update(visible=False), # override_btn (hide)
127
  )
128
 
129
 
 
 
 
 
130
  def load_benchy():
131
  mesh = benchy_mesh()
132
+ if not mesh:
133
+ return ({"geometry": None, "mesh": None, "label": None, "read": None},
134
+ gr.update(value=None), "▣ **BENCHY MISSING** — add assets/benchy.glb")
135
+ geo, read = infer_geometry(mesh)
136
+ return _set_part(geo, mesh, "3DBENCHY (CC0)", read)
137
 
138
 
139
  def generate_part(kind, size):
 
143
 
144
  def upload_part(f):
145
  if not f:
146
+ return (gr.update(), gr.update(), gr.update())
147
  from pathlib import Path as _P
148
+ geo, read = infer_geometry(f.name)
149
+ return _set_part(geo, f.name, f"UPLOAD · {_P(f.name).name[:26]}", read)
150
 
151
 
152
  def scrub_layer(idx, part):
 
155
  return layer_image(mesh, idx)
156
 
157
 
158
+ # ── simulated environment (this is a sim lab — conditions are generated, overridable) ──
159
+ PRINTER = "Creality Ender 3 V2"
160
+
161
+
162
  def _sensor_readout(t, h, pos) -> str:
163
+ return ("<div class='ce-sub' style='font-size:13px;'>ENVIRONMENT (SIMULATED) · "
164
  f"🌡 <b style='color:var(--ao-blue);'>{float(t):.0f}°C</b> · "
165
  f"💧 <b style='color:var(--ao-blue);'>{float(h):.0f}%RH</b> · "
166
+ f"⌖ <b style='color:var(--ao-blue);'>{pos}</b> · "
167
+ f"🖨 <b style='color:var(--ao-outline);'>{PRINTER}</b> "
168
  "<span style='opacity:.6;'>(🎲 re-roll or ⚙ override)</span></div>")
169
 
170
 
171
+ def status_footer(part, material, t, h, pos):
172
+ """Live job context for the sticky footer strip."""
173
+ p = part or {}
174
+ label = p.get("label") or "no part"
175
+ geo = p.get("geometry") or "—"
176
+ return footer_bar(job=f"{label} · {material} · {geo}",
177
+ env=f"{float(t):.0f}°C / {float(h):.0f}%RH · {pos} · {PRINTER}")
178
+
179
+
180
  def randomize_sensors():
181
  """Roll a plausible ambient + plate position — the lab's simulated sensor feed."""
182
  t = random.choice([18, 20, 22, 24, 26, 28, 30, 32])
 
190
 
191
 
192
  @spaces.GPU
193
+ def build_job(part, material, description, temp, humidity, bed_position):
194
  if not (part and part.get("geometry")): # guard: empty start, no part chosen
195
+ return ("", "", "⚠ **Load a part in Studio** (quick-load Benchy, generate, "
196
+ "or drop a mesh) before building.", "", "", "", "", gr.update(visible=False),
197
  gr.update(), {}, "", gr.update(), gr.update())
198
  geometry_type, mesh = part["geometry"], part.get("mesh")
199
  job = Job(geometry_type=geometry_type, material=material, description=description or "",
200
+ bed_position=bed_position or "center", mesh_path=mesh)
201
  env = Environment(temp=float(temp), humidity=float(humidity))
202
 
203
  retrieved = LEDGER.retrieve(material, geometry_type, env.temp, env.humidity)
 
206
  rec = advise(job, env, retrieved, references, policy_note)
207
  spine = SPINE.check(rec.advice.settings, material)
208
  hint = steepest_overhang_hint(mesh) if geometry_type in ("overhang", "bridge") else None
209
+ vp_html = virtual_printer_html(mesh, f"{material} · {geometry_type}") # init on BUILD
210
 
211
  precedent = precedent_eval_html(retrieved, env)
212
  if retrieved:
 
223
  confirm_vis = gr.update(visible=spine.requires_approval)
224
  approval_md = ("⏸ **HITL gate:** the Spine clamped a boundary setting — review, then **Confirm & Print**."
225
  if spine.requires_approval else "✅ within safe envelope — ready when you are.")
226
+ state = {"job": job.model_dump(), "env": env.model_dump(), "settings": spine.settings.model_dump(),
227
+ "advice": rec.advice.model_dump(), "label": part.get("label")}
228
 
229
  return (
230
  f"{rec.backend}{fb}", # backend status
 
243
  )
244
 
245
 
246
+ def second_opinion(state):
247
+ """BUILD: a SEPARATE Inspector persona critiques the plan before any print runs.
248
+ A 'dispute' verdict GATES → PRINT until the objection is acknowledged; 'caution'
249
+ and 'concur' are advisory (Print stays open)."""
250
+ if not state or "advice" not in state:
251
+ return ("<div class='ce-sub'>Build a job first — then I'll give the plan a second look.</div>",
252
+ gr.update(interactive=True), gr.update(visible=False))
253
+ job = Job(**state["job"])
254
+ env = Environment(**state["env"])
255
+ settings = PrintSettings(**state["settings"])
256
+ advice = Advice(**state["advice"])
257
+ verdict = inspector.second_opinion(job, env, settings, advice)
258
+ panel = inspector_panel(verdict, label="QA INSPECTOR · SECOND OPINION (PRE-PRINT)")
259
+ if verdict.stance.lower() == "dispute":
260
+ panel += ("<div style='margin-top:6px;padding:6px 10px;border-left:3px solid var(--ao-red,#d9534f);"
261
+ "background:var(--ao-surface);font-family:ui-monospace,monospace;font-size:12px;"
262
+ "color:var(--ao-text);'>⛔ <b>The Inspector disputes this plan.</b> → PRINT is held. "
263
+ "Review the objection, then acknowledge to proceed anyway.</div>")
264
+ return panel, gr.update(interactive=False), gr.update(visible=True)
265
+ return panel, gr.update(interactive=True), gr.update(visible=False)
266
+
267
+
268
+ def ack_override():
269
+ """Human overrides the Inspector's dispute — re-open → PRINT (on the operator's call)."""
270
+ return gr.update(interactive=True), gr.update(visible=False)
271
+
272
+
273
+ def job_readout(state):
274
+ """The job Print inherits from Studio/Build (no re-picking — it prints THIS job)."""
275
+ if not state or "job" not in state:
276
+ return ("<div class='ce-sub'>No job built yet. Go to <b>Studio</b> → define a part → "
277
+ "<b>Build</b>, then return here to print it.</div>")
278
+ j, e = state["job"], state["env"]
279
+ return (f"<div class='ce-sub' style='font-size:13px;'>PRINTING · "
280
+ f"<b style='color:var(--ao-orange);'>{state.get('label') or j['geometry_type']}</b> · "
281
+ f"{j['material']}/{j['geometry_type']} · ⌖ {j.get('bed_position','center')} · "
282
+ f"🌡 {e['temp']:.0f}°C / 💧 {e['humidity']:.0f}%RH · 🖨 {PRINTER}</div>")
283
+
284
+
285
+ def run_print(state, iterations):
286
+ """PRINT: run THIS job (inherited from Build) through the closed loop. Each
287
+ iteration: policy proposes → Spine vetoes → the deterministic world prints →
288
+ the Inspector grades that outcome → policy + ledger learn. Quality compounds."""
289
+ if not state or "job" not in state:
290
+ gr.Warning("Build a job first (Studio → Build), then print it here.")
291
+ return (gr.update(),) * 7
292
+ job = Job(**state["job"])
293
+ env = Environment(**state["env"])
294
+ material, geometry_type = job.material, job.geometry_type
295
  key = cell_key(material, geometry_type, env)
296
+ before_html = policy_cell_html(POLICY.cell_stats(material, geometry_type, env), key)
 
297
 
298
  sess = run_session(job, env, int(iterations), POLICY, LEDGER)
299
  after = POLICY.cell_stats(material, geometry_type, env)
300
+ verdicts = [inspector.grade_iteration(geometry_type, r.result) for r in sess.records]
301
+ run_summary = inspector.summarize_run(sess.records, material=material, geometry=geometry_type)
302
 
303
  traj = sess.trajectory
304
  first = sess.first_success
305
  headline = (
306
+ f"**{state.get('label') or geometry_type} · {material} @ {env.temp:.0f}°C / {env.humidity:.0f}% RH** — "
307
  f"started at quality **{traj[0]:.2f}** ({sess.records[0].result.outcome}); "
308
  + (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."
309
  if first else f"still improving — best **{max(traj):.2f}** after {len(traj)} runs.")
310
+ + " The Engineer proposed; a separate simulated world reported the outcome; the **Inspector** "
311
+ "graded each run; the policy and ledger learned. *(Simulated — see SIMULATION.md.)*"
312
  )
313
  policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);'>▼ learned ▼</div>"
314
  f"{policy_cell_html(after, key)}")
315
  return (
316
+ headline, # p_headline
317
+ quality_curve_html(traj), # p_curve
318
+ policy_html, # p_policy
319
+ iteration_log_html(sess.records, verdicts), # p_log (with inspector grades)
320
+ ledger_html(), # ledger_panel
321
+ render_node_cards(env, working=False), # node_cards
322
+ inspector_panel(run_summary, label="QA INSPECTOR · RUN VERDICT"), # review_summary
323
  )
324
 
325
 
326
  def simulate_outcome(state):
327
+ """PRINT (single print): close the loop once in the deterministic world, present
328
+ the full outcome — what was simulated, what the model did, the physics pass/fail —
329
+ then the Inspector (hybrid evaluator) grades it. The model never grades itself."""
330
+ if not state or "job" not in state:
331
+ gr.Warning("Build a job first (Studio → Build), then print it here.")
332
+ return gr.update(), ledger_html(), render_node_cards(Environment(temp=22, humidity=45))
333
  job = Job(**state["job"])
334
  env = Environment(**state["env"])
335
  settings = PrintSettings(**state["settings"])
336
+ advice = Advice(**state["advice"]) if "advice" in state else Advice(
337
+ reasoning="(no engineer prediction on file)", settings=settings, risks=[])
338
  result = simulate(settings, job, env)
339
+ verdict = inspector.grade_outcome(job, env, settings, advice, result)
340
  learned = POLICY.update(job.material, job.geometry_type, env, result)
341
  POLICY.save()
 
342
  from learn.loop import _record_lesson
343
  _record_lesson(job, env, settings, result, LEDGER)
344
+
345
+ passed = result.outcome == "success"
346
+ badge_col = "var(--ao-green)" if passed else "var(--ao-red)"
347
+ badge = "PASS" if passed else "FAIL"
348
+ s = settings
349
+ panel = (
350
+ "<div style='font-family:ui-monospace,monospace;background:var(--ao-void);"
351
+ "border:1px solid var(--ao-outline-dim);padding:10px 12px;'>"
352
+ f"<div style='color:var(--ao-orange);font-weight:700;letter-spacing:2px;font-size:11px;'>"
353
+ "🧪 PRINT OUTCOME <span style='color:var(--ao-outline);font-weight:400;'>"
354
+ "(deterministic world — stand-in for printer + sensors)</span></div>"
355
+ f"<div class='ce-sub' style='margin-top:6px;'>WHAT WAS SIMULATED · {job.material}/{job.geometry_type} "
356
+ f"· ⌖ {job.bed_position} · {env.temp:.0f}°C/{env.humidity:.0f}%RH · {PRINTER}</div>"
357
+ f"<div class='ce-sub'>WHAT THE MODEL DID · nozzle {s.nozzle_temp:.0f}°C, bed {s.bed_temp:.0f}°C, "
358
+ f"fan {s.fan_pct:.0f}%, retraction {s.retraction_mm:.1f}mm · "
359
+ f"flagged: {', '.join(r.risk for r in advice.risks) or 'none'}</div>"
360
+ f"<div style='margin-top:6px;font-size:15px;'>PRINT RESULT · "
361
+ f"<span style='color:{badge_col};font-weight:700;'>[{badge}] {result.detail}</span></div>"
362
+ "</div>"
363
  )
364
+ panel += inspector_panel(verdict, label="QA INSPECTOR · GRADE")
365
+ panel += (f"<div class='ce-sub' style='margin-top:6px;'>↳ policy update: <i>{learned}</i> · "
366
+ "lesson written to the ledger. <i>(The model never saw this outcome in advance.)</i></div>")
367
+ return panel, ledger_html(), render_node_cards(env, working=False)
368
 
369
 
370
  def record_outcome(outcome, state):
371
+ if not state or "job" not in state:
372
+ gr.Warning("Build a job first (Studio → Build), then record an outcome here.")
373
+ return gr.update(), ledger_html(), render_node_cards(Environment(temp=22, humidity=45))
374
  job = Job(**state["job"])
375
  env = Environment(**state["env"])
376
  settings = PrintSettings(**state["settings"])
 
393
  "and tells you where a print fails <b>before</b> it runs · gemma4 · fully local</div>"
394
  )
395
  state = gr.State()
396
+ part = gr.State({"geometry": None, "mesh": None, "label": None, "read": None})
397
 
398
  with gr.Tabs() as tabs:
399
+ # ───────────────────────── STUDIO · define + preview ─────────────────────
400
  with gr.Tab("⬡ STUDIO", id="studio"):
401
+ gr.HTML("<div class='ce-sub'>① ENVIRONMENT · ② MATERIAL · ③ PART → "
402
+ "<b>BUILD</b> (slice + the engineer's pre-flight read)</div>")
403
+ run_btn = gr.Button("→ BUILD JOB", variant="primary", elem_id="ce-run")
404
+
405
+ gr.HTML(rule("① ENVIRONMENT (SIMULATED)"))
406
+ sensors_readout = gr.HTML()
407
  with gr.Row():
408
+ roll_btn = gr.Button("🎲 RANDOMIZE ENVIRONMENT", elem_classes=["ce-pillbtn"])
409
+ with gr.Accordion("⚙ OVERRIDE ENVIRONMENT", open=False):
410
+ with gr.Row():
411
+ temp = gr.Number(value=22, label="AMBIENT °C", elem_classes=["ce-num"])
412
+ humidity = gr.Number(value=45, label="HUMIDITY %RH", elem_classes=["ce-num"])
413
+ gr.HTML("<div class='ce-sub'>BUILD-PLATE POSITION — edges/corners run "
414
+ "cooler → warp/adhesion risk</div>")
415
+ bed_position = gr.Radio(BED_POSITIONS, value="center", show_label=False,
416
+ elem_classes=["ce-pills"])
417
+ description = gr.Textbox(label="NOTES (OPTIONAL)",
418
+ placeholder="e.g. 45° bracket, 60mm tall")
 
 
 
 
 
 
419
 
420
+ with gr.Row():
421
+ with gr.Column(scale=1, elem_classes=["ce-console"]):
422
  gr.HTML(rule("② MATERIAL"))
423
  material = gr.Radio(MATERIALS, value="PLA", show_label=False, elem_classes=["ce-pills"])
424
+ with gr.Column(scale=3, elem_classes=["ce-console"]):
425
+ gr.HTML(rule("③ PART"))
426
+ part_status = gr.Markdown("▣ **no part loaded** — quick-load Benchy, generate a "
427
+ "primitive, or drop a mesh. *The engineer infers the part "
428
+ "class itself — you don't pick it.*")
429
+ with gr.Row():
430
+ benchy_btn = gr.Button("⛴ QUICK-LOAD BENCHY", elem_classes=["ce-pillbtn"])
431
+ mesh_in = gr.File(file_types=[".stl", ".glb", ".obj"],
432
+ label="DROP / UPLOAD MESH", elem_classes=["ce-drop"])
433
+ with gr.Accordion("⚙ GENERATE A PRIMITIVE", open=False):
434
  with gr.Row():
435
+ gen_kind = gr.Radio(["box", "cylinder", "cone", "sphere"], value="box",
436
+ show_label=False, elem_classes=["ce-pills"])
437
+ gen_size = gr.Number(value=30, label="SIZE (mm)", elem_classes=["ce-num"])
438
+ gen_btn = gr.Button("GENERATE", elem_classes=["ce-pillbtn"])
439
+ model3d = gr.Model3D(value=None, label="", height=340)
 
 
 
 
 
 
 
 
 
440
 
441
+ # ───────────────── BUILD · slice + analyze + pre-flight check ─────────────
442
+ with gr.Tab("◇ BUILD", id="build"):
443
  backend = gr.Markdown()
444
+ gr.HTML("<div class='ce-sub'>SLICE + the engineer's read — the <b>pre-flight check, "
445
+ "before it prints</b>. Confirm, get a second opinion, then → PRINT.</div>")
446
  with gr.Row():
447
+ with gr.Column(scale=3):
448
+ gr.HTML(rule("VIRTUAL PRINT / SLICER"))
449
+ vprint = gr.HTML()
450
+ gr.HTML("<div class='ce-sub'>Slide through the layers — each is a real "
451
+ "cross-section of <i>this</i> part at full mesh fidelity.</div>")
452
+ vp_slider = gr.Slider(1, SCRUB_LAYERS, value=1, step=1, label="LAYER")
453
+ vp_layer = gr.Image(label="", height=360, show_label=False)
454
+ with gr.Column(scale=2):
455
+ gr.HTML(rule("THE ENGINEER'S READ"))
456
  precedent = gr.HTML(elem_id="ce-precedent")
 
457
  reasoning = gr.Markdown(elem_id="ce-reasoning")
 
458
  risks = gr.HTML()
459
+ gr.HTML(rule("VALIDATION + G-CODE"))
460
+ with gr.Row(elem_classes=["ce-console"]):
461
  with gr.Column():
 
462
  spine_notes = gr.Markdown()
463
  settings_html = gr.HTML()
464
+ with gr.Column():
465
  gcode_html = gr.HTML()
466
+ confirm_btn = gr.Button("✅ Confirm & Print", elem_id="ce-confirm", visible=False)
467
+ gr.HTML(rule("SECOND OPINION"))
468
+ gr.HTML("<div class='ce-sub'>A separate <b>QA Inspector</b> persona reviews the plan "
469
+ "before it prints — the Engineer is an optimist; the Inspector is not.</div>")
470
+ second_opinion_btn = gr.Button("🔍 GET A SECOND OPINION", elem_classes=["ce-pillbtn"])
471
+ second_opinion_panel = gr.HTML()
472
+ override_btn = gr.Button("⚠ I've reviewed the objection — print anyway",
473
+ visible=False, elem_classes=["ce-pillbtn"])
474
+ to_print_btn = gr.Button("→ PRINT (run iterations)", variant="primary")
475
+
476
+ # ──────────────────── PRINT · run it, iterate, grade ─────────────────────
477
+ with gr.Tab("◳ PRINT", id="print"):
478
+ gr.HTML("<div class='ce-sub'>Print <b>this job</b> (inherited from Build). Each run: the "
479
+ "Engineer proposes → the Spine vetoes → a <b>simulated world</b> prints → the "
480
+ "<b>Inspector grades</b> → policy + ledger learn. Quality compounds fail→clean. "
481
+ "<i>Simulated; physical interfaces framed in SIMULATION.md.</i></div>")
482
+ p_job = gr.HTML(job_readout(None))
 
 
 
 
 
 
 
 
 
483
  with gr.Row():
484
  with gr.Column(scale=2, elem_classes=["ce-console"]):
485
+ gr.HTML(rule("① RUN"))
486
+ p_iters = gr.Slider(1, 16, value=8, step=1, label="ITERATIONS")
487
+ p_run = gr.Button("▶ PRINT (RUN ITERATIONS)", variant="primary")
488
+ sim_btn = gr.Button("🧪 SIMULATE ONE PRINT", elem_classes=["ce-pillbtn"])
489
+ gr.HTML("<div class='ce-sub'>…or record a REAL outcome:</div>")
490
+ with gr.Row(elem_id="ce-outcomes"):
491
+ b_clean = gr.Button("✓ Printed clean")
492
+ b_sag = gr.Button("✗ Sagged")
493
+ b_string = gr.Button("✗ Stringing")
494
+ p_headline = gr.Markdown()
495
  with gr.Column(scale=3):
496
  gr.HTML(rule("② QUALITY PER ITERATION"))
497
+ p_curve = gr.HTML()
498
  gr.HTML(rule("③ LEARNED POLICY CELL"))
499
+ p_policy = gr.HTML()
500
+ gr.HTML(rule("SINGLE-PRINT OUTCOME + INSPECTOR GRADE"))
501
+ outcome_panel = gr.HTML()
502
  gr.HTML(rule("④ ITERATION LOG"))
503
+ p_log = gr.HTML()
504
+
505
+ # ───────────────── REVIEW · compounding + agent verdicts ─────────────────
506
+ with gr.Tab("▤ REVIEW", id="review"):
507
+ gr.HTML("<div class='ce-sub'>The compounding made visible — the live ledger "
508
+ "(seed → earned → sim), the capability mesh, and the <b>Inspector's verdict</b> "
509
+ "on the whole run.</div>")
510
+ gr.HTML(rule("QA INSPECTOR · RUN VERDICT"))
511
+ review_summary = gr.HTML("<div class='ce-sub'>Run the Print loop to get the Inspector's "
512
+ "verdict on the whole run.</div>")
513
  with gr.Row():
514
  with gr.Column():
515
  gr.HTML(rule("CAPABILITY MESH"))
 
519
  ledger_panel = gr.HTML(ledger_html())
520
  refresh = gr.Button("⟳ REFRESH LEDGER", elem_classes=["ce-pillbtn"])
521
 
522
+ footer = gr.HTML(footer_bar())
523
 
524
  # ── wiring ──
525
  preview_outs = [part, model3d, part_status]
526
+ foot_in = [part, material, temp, humidity, bed_position]
527
+ benchy_btn.click(load_benchy, None, preview_outs).then(status_footer, foot_in, [footer])
528
+ gen_btn.click(generate_part, [gen_kind, gen_size], preview_outs).then(status_footer, foot_in, [footer])
529
+ mesh_in.upload(upload_part, [mesh_in], preview_outs).then(status_footer, foot_in, [footer])
530
+ material.change(status_footer, foot_in, [footer])
531
 
532
+ # Simulated environment: roll on load, re-roll on demand, keep the readout + footer in sync.
533
  sensor_outs = [temp, humidity, bed_position, sensors_readout]
534
+ demo.load(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])
535
+ roll_btn.click(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])
536
  for c in (temp, humidity, bed_position):
537
+ c.change(sync_readout, [temp, humidity, bed_position], [sensors_readout]).then(
538
+ status_footer, foot_in, [footer])
539
+
540
+ # Two-step BUILD: instant tab-switch + spinners, then the heavy model call, then
541
+ # refresh the inherited-job readout on the Print tab.
542
+ build_outs = [backend, precedent, reasoning, risks, settings_html, spine_notes,
543
+ gcode_html, confirm_btn, node_cards, state, vprint, vp_slider, vp_layer]
544
+ run_btn.click(build_start, [part],
545
+ [tabs, reasoning, precedent, risks, settings_html, gcode_html, vprint,
546
+ second_opinion_panel, to_print_btn, override_btn]).then(
547
+ build_job, [part, material, description, temp, humidity, bed_position],
548
+ build_outs).then(job_readout, [state], [p_job])
549
  vp_slider.change(scrub_layer, [vp_slider, part], [vp_layer])
550
+ second_opinion_btn.click(second_opinion, [state],
551
+ [second_opinion_panel, to_print_btn, override_btn])
552
+ override_btn.click(ack_override, None, [to_print_btn, override_btn])
553
+ to_print_btn.click(lambda: gr.Tabs(selected="print"), None, [tabs])
554
+ tabs.select(job_readout, [state], [p_job])
555
+
556
+ # PRINT: run the loop on THIS job, or a single print, or record a real outcome.
557
+ p_run.click(run_print, [state, p_iters],
558
+ [p_headline, p_curve, p_policy, p_log, ledger_panel, node_cards, review_summary])
559
+ sim_btn.click(simulate_outcome, [state], [outcome_panel, ledger_panel, node_cards])
560
  for btn, oc in [(b_clean, "success"), (b_sag, "failed_sag"), (b_string, "failed_stringing")]:
561
+ btn.click(record_outcome, [gr.State(oc), state], [outcome_panel, ledger_panel, node_cards])
 
562
  refresh.click(lambda: ledger_html(), outputs=[ledger_panel])
563
 
 
 
 
564
  return demo
565
 
566
 
core/inspector.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The QA Inspector — a SEPARATE reviewer persona (the hybrid evaluator).
2
+
3
+ Integrity, restated: the Chief Engineer never grades its own work. The
4
+ deterministic simulated world (`sim/outcome.py`) produces the ground-truth
5
+ pass/fail. The Inspector is a *distinct* voice — skeptical, conservative —
6
+ that reads what the Engineer claimed and what actually happened and writes a
7
+ verdict. The grade is therefore "hybrid": deterministic physics + a second LLM
8
+ opinion, never the proposer marking its own homework.
9
+
10
+ One persona, three sets of rules depending on where it stands in the workflow:
11
+ • second_opinion(...) — BUILD: critique the PLAN before any print runs.
12
+ • grade_outcome(...) — PRINT: grade one finished (simulated) print vs the
13
+ Engineer's prediction — did the called risk hold?
14
+ • summarize_run(...) — REVIEW: one verdict across a whole iteration run.
15
+
16
+ LLM-backed via `llm.chat_json` with a distinct system prompt; each mode has a
17
+ deterministic fallback so the verdict is always present offline.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass
23
+
24
+ from . import llm
25
+ from .models import Advice, Environment, Job, PrintSettings
26
+ from sim.outcome import SimResult
27
+
28
+ PERSONA = """You are the QA Inspector: a skeptical, conservative print-shop \
29
+ inspector. You did NOT propose these settings — the Chief Engineer did, and the \
30
+ Engineer is an optimist. Your job is to second-guess, not to please. You are \
31
+ terse and physical. You never flatter. You call out optimism, thin margins, and \
32
+ unflagged risks, and you give credit only when the evidence earns it."""
33
+
34
+ # predicted-risk vocabulary → simulated failure_mode it corresponds to
35
+ _RISK_TO_MODE = {
36
+ "sag": "sag", "stringing": "stringing", "adhesion": "adhesion",
37
+ "warping": "warp", "warp": "warp", "delamination": "under_extrusion",
38
+ }
39
+ _MODE_HUMAN = {
40
+ "sag": "sagging", "stringing": "stringing", "adhesion": "first-layer adhesion",
41
+ "warp": "warping", "under_extrusion": "under-extrusion", "none": "no failure",
42
+ }
43
+
44
+
45
+ @dataclass
46
+ class InspectorVerdict:
47
+ stance: str # short label, e.g. "concur" / "caution" / "held" / "missed"
48
+ headline: str # one-line verdict
49
+ detail: str # 1-2 lines of rationale
50
+ agreement: bool | None = None # outcome modes: did the Engineer's prediction match reality?
51
+
52
+ @property
53
+ def color(self) -> str:
54
+ s = self.stance.lower()
55
+ if s in ("dispute", "missed", "fail"):
56
+ return "var(--ao-red, #d9534f)"
57
+ if s in ("caution", "overcautious", "watch"):
58
+ return "var(--ao-amber, #e0a458)"
59
+ return "var(--ao-green)"
60
+
61
+
62
+ def _predicted_modes(advice: Advice) -> set[str]:
63
+ out: set[str] = set()
64
+ for r in advice.risks:
65
+ key = (r.risk or "").strip().lower()
66
+ out.add(_RISK_TO_MODE.get(key, key))
67
+ return out
68
+
69
+
70
+ def _settings_line(s: PrintSettings) -> str:
71
+ return (f"nozzle {s.nozzle_temp:.0f}°C, bed {s.bed_temp:.0f}°C, fan {s.fan_pct:.0f}%, "
72
+ f"first-layer fan {s.first_layer_fan_pct:.0f}%, retraction {s.retraction_mm:.1f}mm")
73
+
74
+
75
+ # ── BUILD: a second opinion on the plan, before anything prints ───────────────
76
+ def second_opinion(job: Job, env: Environment, settings: PrintSettings, advice: Advice) -> InspectorVerdict:
77
+ raw = llm.chat_json(
78
+ PERSONA + "\n\nRespond ONLY with JSON: "
79
+ '{"stance":"concur|caution|dispute","headline":"<one line>","detail":"<1-2 lines>"}',
80
+ "Review this PLAN before it prints — do not re-propose, just critique.\n"
81
+ f"JOB: {job.material}/{job.geometry_type}, bed position {job.bed_position}, "
82
+ f"room {env.temp:.0f}°C/{env.humidity:.0f}%RH on a {env.printer}.\n"
83
+ f"ENGINEER PROPOSED: {_settings_line(settings)}.\n"
84
+ f"ENGINEER REASONING: {advice.reasoning}\n"
85
+ f"ENGINEER FLAGGED RISKS: {[r.risk for r in advice.risks] or 'none'}.\n"
86
+ "Where is the Engineer being optimistic? What would you watch?",
87
+ )
88
+ if raw and {"stance", "headline", "detail"} <= set(raw):
89
+ return InspectorVerdict(str(raw["stance"]), str(raw["headline"]), str(raw["detail"]))
90
+ return _second_opinion_fallback(job, env, settings, advice)
91
+
92
+
93
+ def _second_opinion_fallback(job: Job, env: Environment, settings: PrintSettings, advice: Advice) -> InspectorVerdict:
94
+ geo, mat = job.geometry_type, job.material.upper()
95
+ flags: list[str] = []
96
+ if geo in ("overhang", "bridge") and settings.fan_pct < 60:
97
+ flags.append(f"fan {settings.fan_pct:.0f}% is thin for a {geo} — sagging risk the Engineer may be underweighting")
98
+ if mat == "ABS" and job.bed_position in ("edge", "corner"):
99
+ flags.append(f"ABS off-center ({job.bed_position}) will pull at the edges — I'd second a warp watch and a brim")
100
+ if mat == "ABS" and settings.fan_pct > 40:
101
+ flags.append(f"fan {settings.fan_pct:.0f}% on ABS invites cracking/warp")
102
+ if env.humidity > 55 and mat in ("PETG", "TPU", "ABS") and settings.retraction_mm < 3:
103
+ flags.append(f"humid air ({env.humidity:.0f}%RH) + {settings.retraction_mm:.1f}mm retraction → expect stringing")
104
+ if not advice.risks:
105
+ flags.append("Engineer flagged NO failure regions — verify that's confidence, not optimism")
106
+
107
+ if not flags:
108
+ return InspectorVerdict("concur", "No red flags from a second look.",
109
+ "Plan sits inside sane bounds for this material and room. Cleared to print.")
110
+ stance = "dispute" if len(flags) >= 2 else "caution"
111
+ return InspectorVerdict(stance, f"Second opinion: {flags[0]}.",
112
+ " · ".join(flags[1:]) or "Print it, but watch that region.")
113
+
114
+
115
+ # ── PRINT: grade one finished (simulated) print against the prediction ────────
116
+ def grade_outcome(job: Job, env: Environment, settings: PrintSettings,
117
+ advice: Advice, result: SimResult) -> InspectorVerdict:
118
+ predicted = _predicted_modes(advice)
119
+ raw = llm.chat_json(
120
+ PERSONA + "\n\nRespond ONLY with JSON: "
121
+ '{"stance":"held|missed|overcautious|confirmed","headline":"<one line>","detail":"<1-2 lines>"}',
122
+ "Grade this finished print. The outcome below came from the deterministic "
123
+ "world, not from the Engineer — you are checking the Engineer's CALL against it.\n"
124
+ f"JOB: {job.material}/{job.geometry_type} @ {env.temp:.0f}°C/{env.humidity:.0f}%RH.\n"
125
+ f"ENGINEER PREDICTED RISKS: {[r.risk for r in advice.risks] or 'none'}.\n"
126
+ f"ACTUAL OUTCOME: {result.outcome} — {result.detail} "
127
+ f"(failure mode: {result.failure_mode}).\n"
128
+ "Did the Engineer's prediction hold? Be blunt.",
129
+ )
130
+ agreement = _agreement(predicted, result)
131
+ if raw and {"stance", "headline", "detail"} <= set(raw):
132
+ return InspectorVerdict(str(raw["stance"]), str(raw["headline"]), str(raw["detail"]), agreement)
133
+ return _grade_fallback(predicted, result, agreement)
134
+
135
+
136
+ def _agreement(predicted: set[str], result: SimResult) -> bool:
137
+ if result.failure_mode == "none":
138
+ return True # clean print — nothing to have missed
139
+ return result.failure_mode in predicted
140
+
141
+
142
+ def _grade_fallback(predicted: set[str], result: SimResult, agreement: bool) -> InspectorVerdict:
143
+ mode = _MODE_HUMAN.get(result.failure_mode, result.failure_mode)
144
+ if result.failure_mode == "none":
145
+ if predicted:
146
+ return InspectorVerdict("overcautious", f"Print held (q={result.quality:.2f}).",
147
+ f"Engineer flagged {', '.join(sorted(predicted))}; the settings covered it. "
148
+ "Credit the call — or it was conservative.", True)
149
+ return InspectorVerdict("held", f"Clean print (q={result.quality:.2f}).",
150
+ "No failure flagged, none occurred. Plan and reality agree.", True)
151
+ if agreement:
152
+ return InspectorVerdict("confirmed", f"Failed on {mode} — exactly as called.",
153
+ f"Quality {result.quality:.2f}. The Engineer's risk flag was right; "
154
+ "the loop now has the lesson.", True)
155
+ return InspectorVerdict("missed", f"Failed on {mode} — and it wasn't flagged.",
156
+ f"Quality {result.quality:.2f}. The Engineer didn't predict this mode. "
157
+ "That gap is what the next iteration has to close.", False)
158
+
159
+
160
+ # canonical failure mode each geometry is expected to risk (the loop's implicit
161
+ # prediction — the deterministic policy loop carries no LLM Advice per iteration)
162
+ _GEO_EXPECT = {"overhang": "sag", "bridge": "sag", "stringing": "stringing",
163
+ "adhesion": "adhesion", "vase": "warp"}
164
+
165
+
166
+ def grade_iteration(geometry_type: str, result: SimResult) -> InspectorVerdict:
167
+ """Deterministic-only grade for one loop iteration (no LLM — the loop runs
168
+ many fast, reproducible iterations). Checks the outcome against the failure
169
+ mode this geometry is expected to risk."""
170
+ expected = {_GEO_EXPECT.get(geometry_type, "sag")}
171
+ return _grade_fallback(expected, result, _agreement(expected, result))
172
+
173
+
174
+ # ── REVIEW: one verdict across a whole iteration run ──────────────────────────
175
+ def summarize_run(records: list, *, material: str, geometry: str) -> InspectorVerdict:
176
+ if not records:
177
+ return InspectorVerdict("watch", "No run to review yet.", "Run the Print loop first.")
178
+ qualities = [r.result.quality for r in records]
179
+ first_clean = next((r.n for r in records if r.result.outcome == "success"), None)
180
+ start, end = qualities[0], qualities[-1]
181
+ climbed = end - start
182
+ if first_clean:
183
+ stance, head = "concur", f"Converged to clean by iteration {first_clean}."
184
+ detail = (f"{material}/{geometry} climbed {start:.2f} → {end:.2f}. The compounding is real: "
185
+ "each simulated outcome tightened the policy and the next run was better-informed.")
186
+ elif climbed > 0.05:
187
+ stance, head = "caution", f"Improving but not yet clean (best {max(qualities):.2f})."
188
+ detail = (f"Quality rose {start:.2f} → {end:.2f} over {len(records)} runs but never crossed the "
189
+ "bar. More iterations or a different lever needed — the loop is learning, slowly.")
190
+ else:
191
+ stance, head = "dispute", "No real progress this run."
192
+ detail = (f"Quality stuck around {start:.2f}. Either the job is mis-specified or the policy is "
193
+ "saturated for these conditions — worth a human look before trusting the trend.")
194
+ return InspectorVerdict(stance, head, detail)
core/llm.py CHANGED
@@ -17,7 +17,7 @@ MODEL = os.environ.get("CHIEF_ENGINEER_MODEL", "gemma4:e4b")
17
 
18
  # Backend select. Default "ollama" keeps local/recording behavior IDENTICAL.
19
  # Set CHIEF_ENGINEER_BACKEND=zerogpu ONLY on the published Space (see
20
- # llm_zerogpu.py + docs/plan/08-ZEROGPU-DEPLOY.md). Unknown/import-fail → ollama.
21
  BACKEND = os.environ.get("CHIEF_ENGINEER_BACKEND", "ollama").lower()
22
 
23
  try:
 
17
 
18
  # Backend select. Default "ollama" keeps local/recording behavior IDENTICAL.
19
  # Set CHIEF_ENGINEER_BACKEND=zerogpu ONLY on the published Space (see
20
+ # llm_zerogpu.py + docs/_archive/08-ZEROGPU-DEPLOY.md). Unknown/import-fail → ollama.
21
  BACKEND = os.environ.get("CHIEF_ENGINEER_BACKEND", "ollama").lower()
22
 
23
  try:
core/llm_zerogpu.py CHANGED
@@ -18,14 +18,14 @@ Activation is opt-in and total:
18
  It preserves the Off-the-Grid story: the model CAN run fully local (proven in the
19
  video); ZeroGPU is just the hosted convenience so judges can try it live.
20
 
21
- Model ids RESOLVED 6/10 — see docs/plan/08-ZEROGPU-DEPLOY.md (primary:
22
  google/gemma-4-E2B-it, bf16 transformers-native). Original checklist:
23
  - Set `CHIEF_ENGINEER_HF_MODEL` to the QAT Gemma checkpoint you pick. Google's
24
  Gemma QAT int4 releases are the target. CONFIRM the exact repo id AND that it
25
  loads via `transformers` — some QAT releases ship **GGUF** (for llama.cpp);
26
  for those, use a llama-cpp-python path, not this transformers one.
27
  - Confirm the model is ≤32B (hackathon rule) and note its params for Tiny Titan.
28
- See `docs/plan/08-ZEROGPU-DEPLOY.md`.
29
  """
30
 
31
  from __future__ import annotations
 
18
  It preserves the Off-the-Grid story: the model CAN run fully local (proven in the
19
  video); ZeroGPU is just the hosted convenience so judges can try it live.
20
 
21
+ Model ids RESOLVED 6/10 — see docs/_archive/08-ZEROGPU-DEPLOY.md (primary:
22
  google/gemma-4-E2B-it, bf16 transformers-native). Original checklist:
23
  - Set `CHIEF_ENGINEER_HF_MODEL` to the QAT Gemma checkpoint you pick. Google's
24
  Gemma QAT int4 releases are the target. CONFIRM the exact repo id AND that it
25
  loads via `transformers` — some QAT releases ship **GGUF** (for llama.cpp);
26
  for those, use a llama-cpp-python path, not this transformers one.
27
  - Confirm the model is ≤32B (hackathon rule) and note its params for Tiny Titan.
28
+ See `docs/_archive/08-ZEROGPU-DEPLOY.md`.
29
  """
30
 
31
  from __future__ import annotations
core/models.py CHANGED
@@ -28,6 +28,7 @@ class Job(BaseModel):
28
  class Environment(BaseModel):
29
  temp: float = Field(description="Ambient temperature °C")
30
  humidity: float = Field(description="Ambient humidity %")
 
31
 
32
 
33
  class PrintSettings(BaseModel):
 
28
  class Environment(BaseModel):
29
  temp: float = Field(description="Ambient temperature °C")
30
  humidity: float = Field(description="Ambient humidity %")
31
+ printer: str = "Creality Ender 3 V2" # simulated machine (220×220×250 bed)
32
 
33
 
34
  class PrintSettings(BaseModel):
core/theme.py CHANGED
@@ -246,15 +246,37 @@ def rule(label: str) -> str:
246
  return f"<div class='ce-rule'>{label}</div>"
247
 
248
 
249
- def footer_bar() -> str:
250
- """Replaces the hidden Gradio footer with an Astrometrics status strip."""
 
 
 
251
  return (
252
- "<div style='display:flex;justify-content:space-between;gap:16px;margin-top:14px;"
 
253
  "padding:6px 14px;border-top:2px solid var(--ao-orange);background:var(--ao-surface-hi);"
254
  "font-size:10px;letter-spacing:1px;text-transform:uppercase;color:var(--ao-outline);'>"
255
- "<span>ASTROMETRICS OS · CHIEF ENGINEER</span>"
256
- "<span>LLM PROPOSES · <b style='color:var(--ao-orange);'>SPINE DISPOSES</b> · HUMAN CONFIRMS</span>"
257
- "<span>FULLY LOCAL · GEMMA4</span></div>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  )
259
 
260
 
 
246
  return f"<div class='ce-rule'>{label}</div>"
247
 
248
 
249
+ def footer_bar(job: str | None = None, env: str | None = None) -> str:
250
+ """Sticky Astrometrics status strip (replaces the hidden Gradio footer).
251
+ When a job/env is passed, the strip carries the live job context."""
252
+ left = f"JOB · {job}" if job else "ASTROMETRICS OS · CHIEF ENGINEER"
253
+ right = env if env else "FULLY LOCAL · GEMMA4"
254
  return (
255
+ "<div id='ce-footer' style='position:sticky;bottom:0;z-index:30;"
256
+ "display:flex;justify-content:space-between;gap:16px;margin-top:14px;"
257
  "padding:6px 14px;border-top:2px solid var(--ao-orange);background:var(--ao-surface-hi);"
258
  "font-size:10px;letter-spacing:1px;text-transform:uppercase;color:var(--ao-outline);'>"
259
+ f"<span style='color:var(--ao-orange);'>{left}</span>"
260
+ "<span>LLM PROPOSES · <b style='color:var(--ao-orange);'>SPINE DISPOSES</b> · INSPECTOR GRADES</span>"
261
+ f"<span>{right}</span></div>"
262
+ )
263
+
264
+
265
+ def inspector_panel(verdict, *, label: str = "QA INSPECTOR · SECOND OPINION") -> str:
266
+ """Render an InspectorVerdict as a distinct reviewer card (separate voice)."""
267
+ col = verdict.color
268
+ agree = ""
269
+ if verdict.agreement is not None:
270
+ agree = ("<span style='float:right;font-size:10px;color:var(--ao-outline);'>"
271
+ f"{'✓ prediction held' if verdict.agreement else '✗ prediction missed'}</span>")
272
+ return (
273
+ "<div style='font-family:ui-monospace,monospace;background:var(--ao-void);"
274
+ f"border:1px solid var(--ao-outline-dim);border-left:3px solid {col};padding:8px 12px;'>"
275
+ f"<div style='color:{col};font-weight:700;letter-spacing:2px;font-size:11px;'>"
276
+ f"🔍 {label} <span style='color:var(--ao-outline);font-weight:400;'>"
277
+ f"[{verdict.stance.upper()}]</span>{agree}</div>"
278
+ f"<div style='color:var(--ao-text);font-size:14px;font-weight:700;margin:4px 0;'>{verdict.headline}</div>"
279
+ f"<div class='ce-sub' style='font-size:12px;'>{verdict.detail}</div></div>"
280
  )
281
 
282
 
core/viewer.py CHANGED
@@ -69,6 +69,55 @@ def generate_primitive(kind: str, size_mm: float = 30.0) -> tuple[str, str]:
69
  return str(out), _PRIM_GEO[kind]
70
 
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  def settings_panel_html(settings: PrintSettings, material: str) -> str:
73
  """Render proposed settings as an LCARS instrument readout (not raw JSON)."""
74
  rows = [
@@ -278,11 +327,17 @@ def quality_curve_html(trajectory: list[float], threshold: float = 0.7) -> str:
278
  )
279
 
280
 
281
- def iteration_log_html(records) -> str:
 
 
282
  rows = []
283
- for r in records:
284
  clean = r.result.outcome == "success"
285
  col = "var(--ao-green)" if clean else "var(--ao-red)"
 
 
 
 
286
  rows.append(
287
  f"<div style='font-family:ui-monospace,monospace;font-size:11px;border-left:3px solid {col};"
288
  f"background:var(--ao-surface);padding:5px 10px;margin:3px 0;color:var(--ao-text);'>"
@@ -290,7 +345,7 @@ def iteration_log_html(records) -> str:
290
  f"noz {r.settings.nozzle_temp:.0f}°C bed {r.settings.bed_temp:.0f}°C fan {r.settings.fan_pct:.0f}% "
291
  f"ret {r.settings.retraction_mm:.1f}mm"
292
  + (" · <span style='color:var(--ao-orange);'>spine-clamped</span>" if r.clamped else "")
293
- + f"<br><span style='color:var(--ao-outline);'>↳ {r.learned}</span></div>"
294
  )
295
  return "".join(rows)
296
 
 
69
  return str(out), _PRIM_GEO[kind]
70
 
71
 
72
+ # one-line human read for each inferred class — surfaced read-only ("the engineer
73
+ # reads this as …"), never a control the user sets.
74
+ GEO_READS = {
75
+ "overhang": "overhang-dominant",
76
+ "bridge": "has unsupported spans (bridging)",
77
+ "vase": "tall thin-wall (vase-like)",
78
+ "adhesion": "wide flat base (adhesion-critical)",
79
+ "stringing": "many travel moves (stringing-prone)",
80
+ }
81
+
82
+
83
+ def infer_geometry(mesh_path: str | None) -> tuple[str, str]:
84
+ """Classify the failure-mode the engineer should reason about, straight from
85
+ the mesh — the user never picks it (the system figures it out). Returns
86
+ (geometry_type, one-line read). Falls back to 'overhang' (the most common
87
+ torture-test failure) when the mesh can't be read."""
88
+ if not mesh_path or not Path(mesh_path).exists():
89
+ return "overhang", GEO_READS["overhang"]
90
+ try:
91
+ import math
92
+
93
+ import trimesh
94
+
95
+ mesh = trimesh.load(mesh_path, force="mesh")
96
+ w, d, h = (float(x) for x in mesh.bounding_box.extents)
97
+ base = max(w, d, 1e-6)
98
+ footprint = max(1e-6, w * d)
99
+ downward = -mesh.face_normals[:, 2] # +1 → horizontal ceiling (downward-facing)
100
+ steep = float(downward.max()) if len(downward) else 0.0
101
+ angle = math.degrees(math.asin(min(1.0, max(0.0, steep)))) # overhang angle from vertical
102
+ ceiling = float(mesh.area_faces[downward > 0.94].sum()) if len(downward) else 0.0
103
+ try:
104
+ solidity = float(mesh.volume) / max(1e-6, w * d * h)
105
+ except Exception:
106
+ solidity = 1.0
107
+
108
+ if h > 2.0 * base and solidity < 0.35: # tall + mostly hollow → thin-wall shell
109
+ return "vase", GEO_READS["vase"]
110
+ if ceiling > 0.10 * footprint: # flat unsupported span → bridging
111
+ return "bridge", GEO_READS["bridge"]
112
+ if angle >= 45: # steep angled face → overhang
113
+ return "overhang", GEO_READS["overhang"]
114
+ if h < 0.5 * base: # wide + low → big bed contact
115
+ return "adhesion", GEO_READS["adhesion"]
116
+ return "stringing", GEO_READS["stringing"]
117
+ except Exception:
118
+ return "overhang", GEO_READS["overhang"]
119
+
120
+
121
  def settings_panel_html(settings: PrintSettings, material: str) -> str:
122
  """Render proposed settings as an LCARS instrument readout (not raw JSON)."""
123
  rows = [
 
327
  )
328
 
329
 
330
+ def iteration_log_html(records, verdicts=None) -> str:
331
+ """Per-iteration log. `verdicts` (optional, aligned to records) adds the QA
332
+ Inspector's terse grade on each run — the second voice in the loop."""
333
  rows = []
334
+ for i, r in enumerate(records):
335
  clean = r.result.outcome == "success"
336
  col = "var(--ao-green)" if clean else "var(--ao-red)"
337
+ insp = ""
338
+ if verdicts and i < len(verdicts) and verdicts[i] is not None:
339
+ v = verdicts[i]
340
+ insp = (f"<br><span style='color:{v.color};'>🔍 inspector [{v.stance}]: {v.headline}</span>")
341
  rows.append(
342
  f"<div style='font-family:ui-monospace,monospace;font-size:11px;border-left:3px solid {col};"
343
  f"background:var(--ao-surface);padding:5px 10px;margin:3px 0;color:var(--ao-text);'>"
 
345
  f"noz {r.settings.nozzle_temp:.0f}°C bed {r.settings.bed_temp:.0f}°C fan {r.settings.fan_pct:.0f}% "
346
  f"ret {r.settings.retraction_mm:.1f}mm"
347
  + (" · <span style='color:var(--ao-orange);'>spine-clamped</span>" if r.clamped else "")
348
+ + f"<br><span style='color:var(--ao-outline);'>↳ {r.learned}</span>{insp}</div>"
349
  )
350
  return "".join(rows)
351
 
ingest/samples/research_lessons.jsonl CHANGED
@@ -1,3 +1,10 @@
1
  {"job_id": "ingest-001", "material": "ABS", "geometry_type": "warping", "env_temp": 20.0, "env_humidity": 40.0, "outcome": "failed_sag", "lesson": "ABS corners lifted in an open, cool room. ABS needs an enclosure and a 100-110C bed; without it, warping is near-certain regardless of settings.", "source": "ingested", "timestamp": "2026-05-22T09:00:00Z"}
2
  {"job_id": "ingest-002", "material": "ABS", "geometry_type": "adhesion", "env_temp": 24.0, "env_humidity": 45.0, "outcome": "success", "lesson": "ABS first layer held with bed at 105C, brim, and fan off for opening layers. Draught control matters more than glue for ABS adhesion.", "source": "ingested", "timestamp": "2026-05-22T09:05:00Z"}
3
  {"job_id": "ingest-003", "material": "TPU", "geometry_type": "stringing", "env_temp": 23.0, "env_humidity": 55.0, "outcome": "failed_stringing", "lesson": "TPU strung badly; it is highly hygroscopic. Dry the spool and print slow (~25mm/s) with minimal retraction — TPU clogs with aggressive retraction.", "source": "ingested", "timestamp": "2026-05-22T09:10:00Z"}
 
 
 
 
 
 
 
 
1
  {"job_id": "ingest-001", "material": "ABS", "geometry_type": "warping", "env_temp": 20.0, "env_humidity": 40.0, "outcome": "failed_sag", "lesson": "ABS corners lifted in an open, cool room. ABS needs an enclosure and a 100-110C bed; without it, warping is near-certain regardless of settings.", "source": "ingested", "timestamp": "2026-05-22T09:00:00Z"}
2
  {"job_id": "ingest-002", "material": "ABS", "geometry_type": "adhesion", "env_temp": 24.0, "env_humidity": 45.0, "outcome": "success", "lesson": "ABS first layer held with bed at 105C, brim, and fan off for opening layers. Draught control matters more than glue for ABS adhesion.", "source": "ingested", "timestamp": "2026-05-22T09:05:00Z"}
3
  {"job_id": "ingest-003", "material": "TPU", "geometry_type": "stringing", "env_temp": 23.0, "env_humidity": 55.0, "outcome": "failed_stringing", "lesson": "TPU strung badly; it is highly hygroscopic. Dry the spool and print slow (~25mm/s) with minimal retraction — TPU clogs with aggressive retraction.", "source": "ingested", "timestamp": "2026-05-22T09:10:00Z"}
4
+ {"job_id": "ingest-004", "material": "PETG", "geometry_type": "stringing", "env_temp": 26.0, "env_humidity": 65.0, "outcome": "failed_stringing", "lesson": "PETG strings heavily in humid conditions. Dry filament at 65C for 4+ hours, then lower nozzle temp 5-10C and increase retraction to 5-6mm. Wet PETG is the #1 cause of stringing.", "source": "ingested", "timestamp": "2026-06-12T12:00:00Z"}
5
+ {"job_id": "ingest-005", "material": "PETG", "geometry_type": "bridge", "env_temp": 24.0, "env_humidity": 44.0, "outcome": "success", "lesson": "PETG bridges cleanly with fan at 80%, nozzle 240C, and retraction at 5mm. PETG likes more cooling than people think — just not on the first layer.", "source": "ingested", "timestamp": "2026-06-12T12:05:00Z"}
6
+ {"job_id": "ingest-006", "material": "PLA", "geometry_type": "overhang", "env_temp": 31.0, "env_humidity": 40.0, "outcome": "failed_sag", "lesson": "PLA overhangs sag in a hot room (31C ambient). Max fan (100%) and lower nozzle to 200C rescued the next print. PLA needs aggressive cooling when ambient is above 28C.", "source": "ingested", "timestamp": "2026-06-12T12:10:00Z"}
7
+ {"job_id": "ingest-007", "material": "PLA", "geometry_type": "vase", "env_temp": 21.0, "env_humidity": 40.0, "outcome": "success", "lesson": "PLA vase mode at 205C/60C bed with 100% fan produced a clean spiral. PLA is forgiving in cool, dry rooms — the baseline material for vase-mode prints.", "source": "ingested", "timestamp": "2026-06-12T12:15:00Z"}
8
+ {"job_id": "ingest-008", "material": "ABS", "geometry_type": "overhang", "env_temp": 25.0, "env_humidity": 42.0, "outcome": "failed_sag", "lesson": "ABS overhangs curled upward at 80% fan — ABS hates part cooling. Keep fan at 0-20% for ABS, rely on enclosure ambient temp instead. Fan on ABS causes layer delamination.", "source": "ingested", "timestamp": "2026-06-12T12:20:00Z"}
9
+ {"job_id": "ingest-009", "material": "TPU", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 35.0, "outcome": "success", "lesson": "TPU printed clean with 3mm retraction at 25mm/s in a dry room. TPU's flexibility means retraction must be slow and short — fast retraction stretches the filament instead of pulling it back.", "source": "ingested", "timestamp": "2026-06-12T12:25:00Z"}
10
+ {"job_id": "ingest-010", "material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 50.0, "outcome": "success", "lesson": "PETG adhesion is strong on PEI at 80C bed — too strong on smooth PEI without release agent. Use textured PEI or glue stick as a release layer, not an adhesion aid, for PETG.", "source": "ingested", "timestamp": "2026-06-12T12:30:00Z"}
scripts/capture.py CHANGED
@@ -36,12 +36,12 @@ def _pill(page, value):
36
 
37
 
38
  def _open_override(page):
39
- """Sensors (temp/humidity/bed-position) live in the collapsed ⚙ OVERRIDE
40
- accordion now — open it before setting them. Idempotent: only opens if the
41
  bed-position pill isn't already visible (so chained beats don't toggle it shut)."""
42
  try:
43
  if not page.locator(".ce-pills label", has_text="corner").first.is_visible():
44
- page.get_by_text("OVERRIDE SENSORS", exact=False).first.click()
45
  time.sleep(0.3)
46
  except Exception:
47
  pass
@@ -58,12 +58,12 @@ def _set_sensors(page, t, h):
58
 
59
 
60
  def beat_studio(page, slow):
61
- """Beat 3a — define the job in STUDIO (empty start → pick part + material;
62
- set deterministic sensors for a repeatable, risk-relevant take)."""
63
  page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
64
- _pill(page, "overhang"); time.sleep(slow) # reference part type (DOM is lowercase)
65
- _pill(page, "PLA"); time.sleep(slow) # material
66
  _open_override(page); _set_sensors(page, 28, 60) # warm + humid → precedent-rich
 
 
67
  time.sleep(1.5) # part preview (3D model) settles
68
 
69
 
@@ -74,14 +74,20 @@ def beat_benchy(page, slow):
74
  time.sleep(3.0) # watch the hull slice live
75
 
76
 
77
- def beat_analyze(page, slow):
78
- """Beat 3b — ANALYZE -> auto-switch to ANALYSIS, let the reasoning land."""
79
- page.get_by_role("button", name="ANALYZE JOB").click()
80
  time.sleep(4.5) # reasoning + settings readout appear
81
 
82
 
 
 
 
 
 
 
83
  def beat_scrub(page, slow):
84
- """Slide through the filled cross-section layers (on the Analysis page)."""
85
  sl = page.locator("input[type=range]").last
86
  for v in (8, 18, 30, 40):
87
  sl.fill(str(v)); sl.dispatch_event("input"); sl.dispatch_event("change")
@@ -91,44 +97,46 @@ def beat_scrub(page, slow):
91
  def beat_placement(page, slow):
92
  """Build-plate beat — ABS in the corner: warp predicted + 'center it' suggestion."""
93
  page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
94
- _pill(page, "adhesion") # a flat part where bed adhesion/warp dominates
95
  _pill(page, "ABS") # high-shrink material
96
- _open_override(page) # bed-position now lives in the override panel
97
  _pill(page, "corner") # worst plate position
 
98
  time.sleep(slow)
99
- page.get_by_role("button", name="ANALYZE JOB").click()
100
  time.sleep(4.5) # PLACEMENT risk + suggested alignment appear
101
 
102
 
103
  def beat_close_loop(page, slow):
104
- """Beat 4a — simulate the outcome (deterministic), ledger grows."""
105
- page.get_by_role("button", name="Simulate outcome").first.click()
 
106
  time.sleep(3.0)
107
 
108
 
109
  def beat_loop(page, slow):
110
- """Beat 4b — the Learning Loop: quality climbs fail->clean."""
111
- page.get_by_role("tab", name="LEARNING LOOP").click(); time.sleep(slow)
112
- page.get_by_role("button", name="RUN SIMULATED LEARNING").click()
113
  time.sleep(5.0) # the curve fills in
114
 
115
 
116
  def beat_ledger(page, slow):
117
- """Beat 4c — Knowledge + Mesh: the ledger where lessons accumulate."""
118
- page.get_by_role("tab", name="KNOWLEDGE + MESH").click()
119
  time.sleep(3.0)
120
 
121
 
122
  BEATS = {
123
  "studio": [beat_studio],
124
  "benchy": [beat_studio, beat_benchy],
125
- "3": [beat_studio, beat_analyze],
126
- "scrub": [beat_studio, beat_analyze, beat_scrub],
 
127
  "placement": [beat_placement, beat_scrub], # ABS-in-corner → warp + "center it"
128
  "4": [beat_close_loop, beat_loop, beat_ledger],
129
  "loop": [beat_loop],
130
  "ledger": [beat_ledger],
131
- "all": [beat_studio, beat_analyze, beat_scrub, beat_close_loop,
132
  beat_placement, beat_loop, beat_ledger],
133
  }
134
 
 
36
 
37
 
38
  def _open_override(page):
39
+ """Temp/humidity/bed-position live in the collapsed ⚙ OVERRIDE ENVIRONMENT
40
+ accordion — open it before setting them. Idempotent: only opens if the
41
  bed-position pill isn't already visible (so chained beats don't toggle it shut)."""
42
  try:
43
  if not page.locator(".ce-pills label", has_text="corner").first.is_visible():
44
+ page.get_by_text("OVERRIDE ENVIRONMENT", exact=False).first.click()
45
  time.sleep(0.3)
46
  except Exception:
47
  pass
 
58
 
59
 
60
  def beat_studio(page, slow):
61
+ """Beat 3a — define the job in STUDIO (empty start → quick-load a part +
62
+ material; set deterministic environment for a repeatable, risk-relevant take)."""
63
  page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
 
 
64
  _open_override(page); _set_sensors(page, 28, 60) # warm + humid → precedent-rich
65
+ _pill(page, "PLA"); time.sleep(slow) # material (engineer infers the part class)
66
+ page.get_by_role("button", name="QUICK-LOAD BENCHY").click()
67
  time.sleep(1.5) # part preview (3D model) settles
68
 
69
 
 
74
  time.sleep(3.0) # watch the hull slice live
75
 
76
 
77
+ def beat_build(page, slow):
78
+ """Beat 3b — BUILD -> auto-switch to BUILD, let the engineer's read land."""
79
+ page.get_by_role("button", name="BUILD JOB").click()
80
  time.sleep(4.5) # reasoning + settings readout appear
81
 
82
 
83
+ def beat_second_opinion(page, slow):
84
+ """The QA Inspector's pre-print critique (the second voice) on the BUILD page."""
85
+ page.get_by_role("button", name="SECOND OPINION").first.click()
86
+ time.sleep(3.0)
87
+
88
+
89
  def beat_scrub(page, slow):
90
+ """Slide through the filled cross-section layers (on the Build page)."""
91
  sl = page.locator("input[type=range]").last
92
  for v in (8, 18, 30, 40):
93
  sl.fill(str(v)); sl.dispatch_event("input"); sl.dispatch_event("change")
 
97
  def beat_placement(page, slow):
98
  """Build-plate beat — ABS in the corner: warp predicted + 'center it' suggestion."""
99
  page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
 
100
  _pill(page, "ABS") # high-shrink material
101
+ _open_override(page) # bed-position lives in the override panel
102
  _pill(page, "corner") # worst plate position
103
+ page.get_by_role("button", name="QUICK-LOAD BENCHY").click()
104
  time.sleep(slow)
105
+ page.get_by_role("button", name="BUILD JOB").click()
106
  time.sleep(4.5) # PLACEMENT risk + suggested alignment appear
107
 
108
 
109
  def beat_close_loop(page, slow):
110
+ """Beat 4a — go to PRINT, simulate one print (deterministic) + inspector grade."""
111
+ page.get_by_role("tab", name="PRINT").click(); time.sleep(slow)
112
+ page.get_by_role("button", name="SIMULATE ONE PRINT").first.click()
113
  time.sleep(3.0)
114
 
115
 
116
  def beat_loop(page, slow):
117
+ """Beat 4b — PRINT: run iterations, quality climbs fail->clean with inspector grades."""
118
+ page.get_by_role("tab", name="PRINT").click(); time.sleep(slow)
119
+ page.get_by_role("button", name="PRINT (RUN ITERATIONS)").click()
120
  time.sleep(5.0) # the curve fills in
121
 
122
 
123
  def beat_ledger(page, slow):
124
+ """Beat 4c — Review: the ledger where lessons accumulate + the run verdict."""
125
+ page.get_by_role("tab", name="REVIEW").click()
126
  time.sleep(3.0)
127
 
128
 
129
  BEATS = {
130
  "studio": [beat_studio],
131
  "benchy": [beat_studio, beat_benchy],
132
+ "3": [beat_studio, beat_build],
133
+ "scrub": [beat_studio, beat_build, beat_scrub],
134
+ "second": [beat_studio, beat_build, beat_second_opinion], # the Inspector's pre-print read
135
  "placement": [beat_placement, beat_scrub], # ABS-in-corner → warp + "center it"
136
  "4": [beat_close_loop, beat_loop, beat_ledger],
137
  "loop": [beat_loop],
138
  "ledger": [beat_ledger],
139
+ "all": [beat_studio, beat_build, beat_second_opinion, beat_scrub, beat_close_loop,
140
  beat_placement, beat_loop, beat_ledger],
141
  }
142
 
sim/calibration/observations.modal.jsonl ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
2
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
3
+ {"material": "PETG", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
4
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
5
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
6
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
7
+ {"material": "PLA", "geometry_type": "vase", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 255.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
8
+ {"material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
9
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
10
+ {"material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 51.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
11
+ {"material": "PLA", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 255.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
12
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 255.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
13
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
14
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 51.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
15
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
16
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
17
+ {"material": "PLA", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 255.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
18
+ {"material": "PETG", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
19
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
20
+ {"material": "ABS", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
21
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
22
+ {"material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
23
+ {"material": "ABS", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
24
+ {"material": "PETG", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
25
+ {"material": "PLA", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
26
+ {"material": "PLA", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 255.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
27
+ {"material": "PLA", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 255.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
28
+ {"material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
29
+ {"material": "PETG", "geometry_type": "vase", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
30
+ {"material": "ABS", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
31
+ {"material": "PETG", "geometry_type": "vase", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
32
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
33
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
34
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
35
+ {"material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
36
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
37
+ {"material": "ABS", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
38
+ {"material": "ABS", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
39
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
40
+ {"material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
41
+ {"material": "PLA", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
42
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
43
+ {"material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
44
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
45
+ {"material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 51.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
46
+ {"material": "PLA", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
47
+ {"material": "PLA", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 255.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
48
+ {"material": "ABS", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
49
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
50
+ {"material": "PLA", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
51
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
52
+ {"material": "ABS", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
53
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
54
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
55
+ {"material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
56
+ {"material": "PLA", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 255.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
57
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
58
+ {"material": "PETG", "geometry_type": "vase", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
59
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
60
+ {"material": "PLA", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
61
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
62
+ {"material": "PETG", "geometry_type": "vase", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
63
+ {"material": "PLA", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 255.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
64
+ {"material": "PETG", "geometry_type": "vase", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
65
+ {"material": "PETG", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
66
+ {"material": "PLA", "geometry_type": "vase", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
67
+ {"material": "PLA", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 255.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
68
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
69
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
70
+ {"material": "PLA", "geometry_type": "vase", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
71
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
72
+ {"material": "ABS", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
73
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
74
+ {"material": "PLA", "geometry_type": "vase", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
75
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
76
+ {"material": "PLA", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 255.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
77
+ {"material": "ABS", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
78
+ {"material": "ABS", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
79
+ {"material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 76.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
80
+ {"material": "ABS", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
81
+ {"material": "PLA", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 100.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
82
+ {"material": "PETG", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 40.0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
83
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 6.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
84
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
85
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.85}
86
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 6.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
87
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
88
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 6.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.75}
89
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
90
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
91
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
92
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
93
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
94
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
95
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 6.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
96
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 6.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
97
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 6.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
98
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 6.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
99
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 6.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
100
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
101
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
102
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
103
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
104
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
105
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
106
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
107
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
108
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
109
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
110
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
111
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
112
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
113
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
114
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
115
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.5}
116
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.7}
117
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.7}
118
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
119
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
120
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
121
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
122
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
123
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
124
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
125
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
126
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
127
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
128
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
129
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
130
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
131
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 220.0, "bed_temp": 60.0, "retraction_mm": 2.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
132
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
133
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
134
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
135
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
136
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
137
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
138
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
139
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
140
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
141
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.7}
142
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.7}
143
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.7}
144
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.7}
145
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.7}
146
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.7}
147
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.7}
148
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.7}
149
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.7}
150
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
151
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
152
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
153
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
154
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
155
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
156
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
157
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
158
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
159
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
160
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
161
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
162
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
163
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
164
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
165
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
166
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
167
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
168
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
169
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
170
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
171
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
172
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
173
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
174
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
175
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
176
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
177
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
178
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
179
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
180
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
181
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
182
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
183
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
184
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
185
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
186
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
187
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
188
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
189
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
190
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
191
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
192
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
193
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
194
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
195
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
196
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
197
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
198
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
199
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
200
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
201
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
202
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
203
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_sag", "quality": 0.6}
204
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 8.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
205
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
206
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
207
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 215.0, "bed_temp": 60.0, "retraction_mm": 8.5, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 0.82}
208
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
209
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
210
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
211
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
212
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
213
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
214
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
215
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
216
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
217
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
218
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
219
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
220
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
221
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
222
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
223
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
224
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
225
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
226
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
227
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 0.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
228
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 0.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
229
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 0.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
230
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 0.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
231
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 0.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
232
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 0.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
233
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 0.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
234
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 0.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
235
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
236
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
237
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
238
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
239
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
240
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
241
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
242
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
243
+ {"material": "PLA", "geometry_type": "overhang", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 5.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "success", "quality": 1.0}
244
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
245
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
246
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
247
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
248
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
249
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
250
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
251
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
252
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
253
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 3.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
254
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
255
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
256
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
257
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
258
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
259
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}
260
+ {"material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "nozzle_temp": 210.0, "bed_temp": 60.0, "retraction_mm": 1.0, "fan_pct": 0, "first_layer_fan_pct": 0, "outcome": "failed_stringing", "quality": 0.7}