deploy: updated core + ingestion pipeline (1,304 refs, 25 lessons, 260 Lane C obs locally)
Browse files- DEPLOYMENT.md +241 -0
- README.md +28 -16
- app.py +287 -183
- core/inspector.py +194 -0
- core/llm.py +1 -1
- core/llm_zerogpu.py +2 -2
- core/models.py +1 -0
- core/theme.py +28 -6
- core/viewer.py +58 -3
- ingest/samples/research_lessons.jsonl +7 -0
- scripts/capture.py +32 -24
- sim/calibration/observations.modal.jsonl +260 -0
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 **
|
| 56 |
-
climb from failure to clean over a handful of iterations
|
| 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
|
| 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:
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
2. **
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
3. **
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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
|
| 32 |
from learn.policy import LearnedPolicy, cell_key
|
| 33 |
from sim.outcome import simulate
|
| 34 |
from core.viewer import (
|
| 35 |
-
|
| 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 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 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}** ·
|
| 98 |
)
|
| 99 |
|
| 100 |
|
| 101 |
-
def
|
| 102 |
-
"""Immediate feedback the instant
|
| 103 |
-
|
| 104 |
-
|
| 105 |
if not (part and part.get("geometry")):
|
| 106 |
-
gr.Warning("
|
| 107 |
-
return (gr.update(),) *
|
| 108 |
return (
|
| 109 |
-
gr.Tabs(selected="
|
| 110 |
-
"⏳ **
|
| 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 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
| 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()
|
| 137 |
from pathlib import Path as _P
|
| 138 |
-
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 148 |
def _sensor_readout(t, h, pos) -> str:
|
| 149 |
-
return ("<div class='ce-sub' style='font-size:13px;'>SIMULATED
|
| 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
|
| 170 |
if not (part and part.get("geometry")): # guard: empty start, no part chosen
|
| 171 |
-
return ("", "", "⚠ **
|
| 172 |
-
"or
|
| 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
|
| 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
|
| 222 |
-
"""
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
key = cell_key(material, geometry_type, env)
|
| 226 |
-
|
| 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"**{
|
| 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
|
| 240 |
-
"the policy and ledger learned
|
| 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,
|
| 246 |
-
quality_curve_html(traj),
|
| 247 |
-
policy_html,
|
| 248 |
-
iteration_log_html(sess.records),
|
| 249 |
-
ledger_html(),
|
| 250 |
-
render_node_cards(env, working=False),
|
|
|
|
| 251 |
)
|
| 252 |
|
| 253 |
|
| 254 |
def simulate_outcome(state):
|
| 255 |
-
"""
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
|
| 275 |
def record_outcome(outcome, state):
|
| 276 |
-
if not state:
|
| 277 |
-
|
|
|
|
| 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 |
-
# ─────────────────────────
|
| 304 |
with gr.Tab("⬡ STUDIO", id="studio"):
|
| 305 |
-
gr.HTML("<div class='ce-sub'>
|
| 306 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
with gr.Row():
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 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("③
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
with gr.Row():
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 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 |
-
# ─────────────────
|
| 350 |
-
with gr.Tab("◇
|
| 351 |
backend = gr.Markdown()
|
| 352 |
-
gr.HTML("<div class='ce-sub'>
|
| 353 |
-
"
|
| 354 |
with gr.Row():
|
| 355 |
-
with gr.Column():
|
| 356 |
-
gr.HTML(rule("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 368 |
-
gr.HTML(rule("
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
gr.
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 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("①
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
with gr.Column(scale=3):
|
| 406 |
gr.HTML(rule("② QUALITY PER ITERATION"))
|
| 407 |
-
|
| 408 |
gr.HTML(rule("③ LEARNED POLICY CELL"))
|
| 409 |
-
|
|
|
|
|
|
|
| 410 |
gr.HTML(rule("④ ITERATION LOG"))
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
# ─────────────────
|
| 414 |
-
with gr.Tab("▤
|
| 415 |
-
gr.HTML("<div class='ce-sub'>The
|
| 416 |
-
"
|
| 417 |
-
"
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
|
|
|
|
|
|
|
|
|
| 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], [
|
| 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/
|
| 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/
|
| 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/
|
| 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 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 251 |
return (
|
| 252 |
-
"<div style='
|
|
|
|
| 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
|
| 256 |
-
"<span>LLM PROPOSES · <b style='color:var(--ao-orange);'>SPINE DISPOSES</b> ·
|
| 257 |
-
"<span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"""
|
| 40 |
-
accordion
|
| 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
|
| 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 →
|
| 62 |
-
set deterministic
|
| 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
|
| 78 |
-
"""Beat 3b —
|
| 79 |
-
page.get_by_role("button", name="
|
| 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
|
| 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
|
| 97 |
_pill(page, "corner") # worst plate position
|
|
|
|
| 98 |
time.sleep(slow)
|
| 99 |
-
page.get_by_role("button", name="
|
| 100 |
time.sleep(4.5) # PLACEMENT risk + suggested alignment appear
|
| 101 |
|
| 102 |
|
| 103 |
def beat_close_loop(page, slow):
|
| 104 |
-
"""Beat 4a — simulate
|
| 105 |
-
page.get_by_role("
|
|
|
|
| 106 |
time.sleep(3.0)
|
| 107 |
|
| 108 |
|
| 109 |
def beat_loop(page, slow):
|
| 110 |
-
"""Beat 4b —
|
| 111 |
-
page.get_by_role("tab", name="
|
| 112 |
-
page.get_by_role("button", name="RUN
|
| 113 |
time.sleep(5.0) # the curve fills in
|
| 114 |
|
| 115 |
|
| 116 |
def beat_ledger(page, slow):
|
| 117 |
-
"""Beat 4c —
|
| 118 |
-
page.get_by_role("tab", name="
|
| 119 |
time.sleep(3.0)
|
| 120 |
|
| 121 |
|
| 122 |
BEATS = {
|
| 123 |
"studio": [beat_studio],
|
| 124 |
"benchy": [beat_studio, beat_benchy],
|
| 125 |
-
"3": [beat_studio,
|
| 126 |
-
"scrub": [beat_studio,
|
|
|
|
| 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,
|
| 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}
|