deploy: update Space from deploy_preflight --push
Browse files- .codeboarding/logs/wrapper-server.log +141 -0
- app.py +358 -137
- core/chief_engineer.py +4 -4
- core/models.py +1 -0
- core/prompts.py +1 -1
- core/spine.py +1 -0
- core/theme.py +52 -19
- core/viewer.py +9 -3
- core/widgets.py +13 -5
- dist/deliberation/deliberations.jsonl +33 -34
- learn/loop.py +2 -2
- test_core.py +19 -0
.codeboarding/logs/wrapper-server.log
CHANGED
|
@@ -348,3 +348,144 @@ INFO: 127.0.0.1:44498 - "GET /health HTTP/1.1" 200 OK
|
|
| 348 |
[stderr] 2026-06-14 08:27:24 INFO [watchfiles.main:308] 1 change detected
|
| 349 |
[stderr] 2026-06-14 08:27:24 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 350 |
[stderr] 2026-06-14 08:27:24 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
[stderr] 2026-06-14 08:27:24 INFO [watchfiles.main:308] 1 change detected
|
| 349 |
[stderr] 2026-06-14 08:27:24 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 350 |
[stderr] 2026-06-14 08:27:24 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 351 |
+
[stderr] 2026-06-14 08:38:21 INFO [watchfiles.main:308] 1 change detected
|
| 352 |
+
[stderr] 2026-06-14 08:38:21 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 353 |
+
[stderr] 2026-06-14 08:38:21 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 354 |
+
[stderr] 2026-06-14 08:38:51 INFO [watchfiles.main:308] 1 change detected
|
| 355 |
+
[stderr] 2026-06-14 08:38:52 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 356 |
+
[stderr] 2026-06-14 08:38:52 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 357 |
+
[stderr] 2026-06-14 08:39:24 INFO [watchfiles.main:308] 1 change detected
|
| 358 |
+
[stderr] 2026-06-14 08:39:25 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 359 |
+
[stderr] 2026-06-14 08:39:25 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 360 |
+
[stderr] 2026-06-14 08:39:37 INFO [watchfiles.main:308] 1 change detected
|
| 361 |
+
[stderr] 2026-06-14 08:39:37 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 362 |
+
[stderr] 2026-06-14 08:39:37 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 363 |
+
[stderr] 2026-06-14 08:39:43 INFO [watchfiles.main:308] 1 change detected
|
| 364 |
+
[stderr] 2026-06-14 08:39:44 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 365 |
+
[stderr] 2026-06-14 08:39:44 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 366 |
+
[stderr] 2026-06-14 08:40:08 INFO [watchfiles.main:308] 1 change detected
|
| 367 |
+
[stderr] 2026-06-14 08:40:08 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 368 |
+
[stderr] 2026-06-14 08:40:08 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 369 |
+
[stderr] 2026-06-14 08:42:15 INFO [watchfiles.main:308] 1 change detected
|
| 370 |
+
[stderr] 2026-06-14 08:42:15 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 371 |
+
[stderr] 2026-06-14 08:42:15 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 372 |
+
[stderr] 2026-06-14 08:42:17 INFO [watchfiles.main:308] 1 change detected
|
| 373 |
+
[stderr] 2026-06-14 08:42:17 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 374 |
+
[stderr] 2026-06-14 08:42:17 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 375 |
+
[stderr] 2026-06-14 08:42:27 INFO [watchfiles.main:308] 1 change detected
|
| 376 |
+
[stderr] 2026-06-14 08:42:27 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 377 |
+
[stderr] 2026-06-14 08:42:27 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 378 |
+
[stderr] 2026-06-14 08:42:32 INFO [watchfiles.main:308] 1 change detected
|
| 379 |
+
[stderr] 2026-06-14 08:42:33 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 380 |
+
[stderr] 2026-06-14 08:42:33 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 381 |
+
[stderr] 2026-06-14 08:42:37 INFO [watchfiles.main:308] 1 change detected
|
| 382 |
+
[stderr] 2026-06-14 08:42:37 INFO [watchfiles.main:308] 1 change detected
|
| 383 |
+
[stderr] 2026-06-14 08:42:38 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 384 |
+
[stderr] 2026-06-14 08:42:38 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 385 |
+
[stderr] 2026-06-14 08:42:41 INFO [watchfiles.main:308] 1 change detected
|
| 386 |
+
[stderr] 2026-06-14 08:42:42 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 387 |
+
[stderr] 2026-06-14 08:42:42 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 388 |
+
[stderr] 2026-06-14 08:42:49 INFO [watchfiles.main:308] 1 change detected
|
| 389 |
+
[stderr] 2026-06-14 08:42:49 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 390 |
+
[stderr] 2026-06-14 08:42:49 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 391 |
+
[stderr] 2026-06-14 08:42:52 INFO [watchfiles.main:308] 1 change detected
|
| 392 |
+
[stderr] 2026-06-14 08:42:52 INFO [watchfiles.main:308] 1 change detected
|
| 393 |
+
[stderr] 2026-06-14 08:42:53 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 394 |
+
[stderr] 2026-06-14 08:42:53 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 395 |
+
[stderr] 2026-06-14 08:42:58 INFO [watchfiles.main:308] 1 change detected
|
| 396 |
+
[stderr] 2026-06-14 08:42:58 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 397 |
+
[stderr] 2026-06-14 08:42:58 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 398 |
+
[stderr] 2026-06-14 08:43:12 INFO [watchfiles.main:308] 1 change detected
|
| 399 |
+
[stderr] 2026-06-14 08:43:13 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 400 |
+
[stderr] 2026-06-14 08:43:13 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 401 |
+
[stderr] 2026-06-14 08:43:21 INFO [watchfiles.main:308] 1 change detected
|
| 402 |
+
[stderr] 2026-06-14 08:43:22 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 403 |
+
[stderr] 2026-06-14 08:43:22 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 404 |
+
[stderr] 2026-06-14 08:43:25 INFO [watchfiles.main:308] 1 change detected
|
| 405 |
+
[stderr] 2026-06-14 08:43:31 INFO [watchfiles.main:308] 1 change detected
|
| 406 |
+
[stderr] 2026-06-14 08:43:34 INFO [watchfiles.main:308] 1 change detected
|
| 407 |
+
[stderr] 2026-06-14 08:43:34 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 408 |
+
[stderr] 2026-06-14 08:43:34 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 409 |
+
[stderr] 2026-06-14 08:43:50 INFO [watchfiles.main:308] 1 change detected
|
| 410 |
+
[stderr] 2026-06-14 08:43:50 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 411 |
+
[stderr] 2026-06-14 08:43:50 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 412 |
+
[stderr] 2026-06-14 08:43:52 INFO [watchfiles.main:308] 1 change detected
|
| 413 |
+
[stderr] 2026-06-14 08:43:52 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 414 |
+
[stderr] 2026-06-14 08:43:52 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 415 |
+
[stderr] 2026-06-14 08:44:05 INFO [watchfiles.main:308] 1 change detected
|
| 416 |
+
[stderr] 2026-06-14 08:44:05 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 417 |
+
[stderr] 2026-06-14 08:44:05 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 418 |
+
[stderr] 2026-06-14 08:44:14 INFO [watchfiles.main:308] 1 change detected
|
| 419 |
+
[stderr] 2026-06-14 08:44:14 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 420 |
+
[stderr] 2026-06-14 08:44:14 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 421 |
+
[stderr] 2026-06-14 08:44:20 INFO [watchfiles.main:308] 1 change detected
|
| 422 |
+
[stderr] 2026-06-14 08:44:20 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 423 |
+
[stderr] 2026-06-14 08:44:20 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 424 |
+
[stderr] 2026-06-14 08:47:55 INFO [watchfiles.main:308] 2 changes detected
|
| 425 |
+
[stderr] 2026-06-14 08:47:55 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 426 |
+
[stderr] 2026-06-14 08:47:55 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 427 |
+
[stderr] 2026-06-14 08:47:59 INFO [watchfiles.main:308] 1 change detected
|
| 428 |
+
[stderr] 2026-06-14 08:48:00 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 429 |
+
[stderr] 2026-06-14 08:48:00 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 430 |
+
[stderr] 2026-06-14 08:48:08 INFO [watchfiles.main:308] 1 change detected
|
| 431 |
+
[stderr] 2026-06-14 08:48:09 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 432 |
+
[stderr] 2026-06-14 08:48:09 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 433 |
+
[stderr] 2026-06-14 08:48:13 INFO [watchfiles.main:308] 1 change detected
|
| 434 |
+
[stderr] 2026-06-14 08:48:14 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 435 |
+
[stderr] 2026-06-14 08:48:14 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 436 |
+
[stderr] 2026-06-14 08:48:25 INFO [watchfiles.main:308] 1 change detected
|
| 437 |
+
[stderr] 2026-06-14 08:48:25 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 438 |
+
[stderr] 2026-06-14 08:48:25 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 439 |
+
[stderr] 2026-06-14 08:48:31 INFO [watchfiles.main:308] 1 change detected
|
| 440 |
+
[stderr] 2026-06-14 08:48:31 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 441 |
+
[stderr] 2026-06-14 08:48:31 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 442 |
+
[stderr] 2026-06-14 08:48:36 INFO [watchfiles.main:308] 1 change detected
|
| 443 |
+
[stderr] 2026-06-14 08:48:37 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 444 |
+
[stderr] 2026-06-14 08:48:37 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 445 |
+
[stderr] 2026-06-14 08:48:40 INFO [watchfiles.main:308] 1 change detected
|
| 446 |
+
[stderr] 2026-06-14 08:48:41 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 447 |
+
[stderr] 2026-06-14 08:48:41 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 448 |
+
[stderr] 2026-06-14 08:48:43 INFO [watchfiles.main:308] 1 change detected
|
| 449 |
+
[stderr] 2026-06-14 08:48:43 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 450 |
+
[stderr] 2026-06-14 08:48:43 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 451 |
+
[stderr] 2026-06-14 08:48:46 INFO [watchfiles.main:308] 1 change detected
|
| 452 |
+
[stderr] 2026-06-14 08:48:46 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 453 |
+
[stderr] 2026-06-14 08:48:46 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 454 |
+
[stderr] 2026-06-14 08:48:48 INFO [watchfiles.main:308] 1 change detected
|
| 455 |
+
[stderr] 2026-06-14 08:48:48 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 456 |
+
[stderr] 2026-06-14 08:48:48 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 457 |
+
[stderr] 2026-06-14 08:48:50 INFO [watchfiles.main:308] 1 change detected
|
| 458 |
+
[stderr] 2026-06-14 08:48:50 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 459 |
+
[stderr] 2026-06-14 08:48:50 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 460 |
+
[stderr] 2026-06-14 08:48:55 INFO [watchfiles.main:308] 1 change detected
|
| 461 |
+
[stderr] 2026-06-14 08:48:56 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 462 |
+
[stderr] 2026-06-14 08:48:56 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 463 |
+
[stderr] 2026-06-14 08:49:02 INFO [watchfiles.main:308] 1 change detected
|
| 464 |
+
[stderr] 2026-06-14 08:49:50 INFO [watchfiles.main:308] 1 change detected
|
| 465 |
+
[stderr] 2026-06-14 08:54:18 INFO [watchfiles.main:308] 1 change detected
|
| 466 |
+
[stderr] 2026-06-14 08:54:19 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 467 |
+
[stderr] 2026-06-14 08:54:19 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 468 |
+
[stderr] 2026-06-14 08:54:40 INFO [watchfiles.main:308] 1 change detected
|
| 469 |
+
[stderr] 2026-06-14 08:54:40 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 470 |
+
[stderr] 2026-06-14 08:54:40 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 471 |
+
[stderr] 2026-06-14 08:54:45 INFO [watchfiles.main:308] 1 change detected
|
| 472 |
+
[stderr] 2026-06-14 08:54:45 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 473 |
+
[stderr] 2026-06-14 08:54:45 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 474 |
+
[stderr] 2026-06-14 08:55:09 INFO [watchfiles.main:308] 1 change detected
|
| 475 |
+
[stderr] 2026-06-14 08:55:10 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 476 |
+
[stderr] 2026-06-14 08:55:10 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 477 |
+
[stderr] 2026-06-14 08:55:29 INFO [watchfiles.main:308] 1 change detected
|
| 478 |
+
[stderr] 2026-06-14 08:55:29 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 479 |
+
[stderr] 2026-06-14 08:55:29 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 480 |
+
[stderr] 2026-06-14 08:55:56 INFO [watchfiles.main:308] 1 change detected
|
| 481 |
+
[stderr] 2026-06-14 08:55:57 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 482 |
+
[stderr] 2026-06-14 08:55:57 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 483 |
+
[stderr] 2026-06-14 08:56:52 INFO [watchfiles.main:308] 1 change detected
|
| 484 |
+
[stderr] 2026-06-14 08:56:53 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 485 |
+
[stderr] 2026-06-14 08:56:53 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 486 |
+
[stderr] 2026-06-14 08:57:00 INFO [watchfiles.main:308] 1 change detected
|
| 487 |
+
[stderr] 2026-06-14 08:57:00 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 488 |
+
[stderr] 2026-06-14 08:57:00 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 489 |
+
[stderr] 2026-06-14 08:57:08 INFO [watchfiles.main:308] 1 change detected
|
| 490 |
+
[stderr] 2026-06-14 08:57:09 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 491 |
+
[stderr] 2026-06-14 08:57:09 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
app.py
CHANGED
|
@@ -18,7 +18,9 @@ from __future__ import annotations
|
|
| 18 |
|
| 19 |
import os
|
| 20 |
import random
|
|
|
|
| 21 |
import uuid
|
|
|
|
| 22 |
|
| 23 |
import gradio as gr
|
| 24 |
|
|
@@ -48,7 +50,7 @@ from core.models import Advice, BED_POSITIONS, Environment, Job, MATERIALS, Prin
|
|
| 48 |
from core.nodes import render_node_cards
|
| 49 |
from core.reflect import reflect_on_job
|
| 50 |
from core.spine import SpineValidator
|
| 51 |
-
from learn.loop import
|
| 52 |
from learn.policy import LearnedPolicy, cell_key
|
| 53 |
from core.viewer import (
|
| 54 |
GEO_READS,
|
|
@@ -174,7 +176,8 @@ def studio_log_html() -> str:
|
|
| 174 |
|
| 175 |
def reset_learnings():
|
| 176 |
"""Reset the live ledger + learned policy to the curated baseline (seed + ingested),
|
| 177 |
-
clearing this session's accumulated runs.
|
|
|
|
| 178 |
removed = LEDGER.reset_to_baseline()
|
| 179 |
POLICY.reset()
|
| 180 |
gr.Info(f"Reset to baseline β cleared {removed} runtime lesson(s) + the learned policy.")
|
|
@@ -188,6 +191,13 @@ def reset_learnings():
|
|
| 188 |
gr.update(visible=False), # print results_group (re-hide)
|
| 189 |
"", # real_log_msg
|
| 190 |
studio_log_html(), # studio_log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
)
|
| 192 |
|
| 193 |
|
|
@@ -219,21 +229,31 @@ def _set_part(geometry: str, mesh, label: str, read: str | None = None):
|
|
| 219 |
|
| 220 |
|
| 221 |
def build_start(part):
|
| 222 |
-
"""Instant feedback when BUILD is clicked: jump to Build,
|
| 223 |
-
|
|
|
|
| 224 |
if not (part and part.get("geometry")):
|
| 225 |
gr.Warning("Load a part on the BUILD tab first β quick-load Benchy, generate a primitive, or drop a mesh.")
|
| 226 |
-
return (gr.update(),) *
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
return (
|
| 228 |
gr.Tabs(selected="build"), # tabs
|
| 229 |
-
|
| 230 |
-
gr.update(visible=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
gr.update(interactive=True), # to_print_btn (un-gate)
|
| 232 |
gr.update(visible=False), # override_btn (hide)
|
| 233 |
-
"", # second_opinion_panel (clear stale
|
| 234 |
gr.update(value="Engineer's Read"), # read_toggle (reset to the read)
|
| 235 |
-
gr.update(visible=True),
|
| 236 |
-
gr.update(visible=False),
|
| 237 |
)
|
| 238 |
|
| 239 |
|
|
@@ -271,16 +291,30 @@ def _status_html() -> str:
|
|
| 271 |
|
| 272 |
|
| 273 |
def warm_up_pending() -> str:
|
| 274 |
-
|
| 275 |
-
|
|
|
|
| 276 |
|
| 277 |
|
| 278 |
def warm_up_cb() -> str:
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
|
| 282 |
def _model_callout() -> str:
|
| 283 |
-
"""
|
| 284 |
return (
|
| 285 |
"<span class='ce-callout' tabindex='0'>" + icon("info") + " MODEL INFO"
|
| 286 |
"<span class='ce-tip'>The <b>LoRA v2/v3</b> adapters serve via ZeroGPU on the Space; "
|
|
@@ -291,16 +325,14 @@ def _model_callout() -> str:
|
|
| 291 |
|
| 292 |
|
| 293 |
def select_model(choice: str) -> str:
|
| 294 |
-
"""Model switcher
|
| 295 |
-
|
| 296 |
-
through ZeroGPU on the Space (and fall back gracefully if the GPU/deps aren't present);
|
| 297 |
-
Base is the stock Gemma 4 E4B; Modal API is the remote endpoint."""
|
| 298 |
choice = choice or MODEL_OPTIONS[0]
|
| 299 |
try:
|
| 300 |
_apply_model_choice(choice)
|
| 301 |
except Exception:
|
| 302 |
pass
|
| 303 |
-
return
|
| 304 |
|
| 305 |
|
| 306 |
# ββ simulated environment (this is a sim lab β conditions are generated, overridable) ββ
|
|
@@ -357,7 +389,8 @@ def build_job(part, material, description, temp, humidity, bed_position, model_c
|
|
| 357 |
rec = advise(job, env, retrieved, references, policy_note)
|
| 358 |
spine = SPINE.check(rec.advice.settings, material)
|
| 359 |
hint = steepest_overhang_hint(mesh) if geometry_type in ("overhang", "bridge") else None
|
| 360 |
-
vp_html = virtual_printer_html(mesh,
|
|
|
|
| 361 |
|
| 362 |
precedent = precedent_eval_html(retrieved, env)
|
| 363 |
if retrieved:
|
|
@@ -374,9 +407,11 @@ def build_job(part, material, description, temp, humidity, bed_position, model_c
|
|
| 374 |
confirm_vis = gr.update(visible=spine.requires_approval)
|
| 375 |
approval_md = ("**HITL gate:** the Spine clamped a boundary setting β review, then **Confirm & Print**."
|
| 376 |
if spine.requires_approval else "Within safe envelope β ready when you are.")
|
|
|
|
| 377 |
session_id = uuid.uuid4().hex
|
| 378 |
state = {"job": job.model_dump(), "env": env.model_dump(), "settings": spine.settings.model_dump(),
|
| 379 |
-
"advice": rec.advice.model_dump(), "label": part.get("label"), "session_id": session_id
|
|
|
|
| 380 |
|
| 381 |
# ββ field log (Space only β gated on HF_TOKEN; local/offline no-ops) ββ
|
| 382 |
field_log.log_build(
|
|
@@ -401,22 +436,22 @@ def build_job(part, material, description, temp, humidity, bed_position, model_c
|
|
| 401 |
f"**Chief Engineer O'Brien:** {rec.advice.reasoning}", # reasoning
|
| 402 |
risk_callouts_html(rec.advice.risks, hint) + placement_callout(material, bed_position), # risks
|
| 403 |
settings_panel_html(spine.settings, material), # settings (LCARS panel)
|
| 404 |
-
|
| 405 |
gcode_panel_html(spine.settings, material), # g-code (LCARS panel)
|
| 406 |
confirm_vis, # confirm visibility
|
| 407 |
render_node_cards(env, working=True), # node cards
|
| 408 |
state, # state
|
| 409 |
-
|
|
|
|
| 410 |
gr.update(value=1), # reset layer scrubber
|
| 411 |
layer_image(mesh, 1), # initial scrubbed layer
|
| 412 |
-
"", #
|
| 413 |
-
gr.update(visible=True), # build_results (reveal)
|
| 414 |
)
|
| 415 |
|
| 416 |
|
| 417 |
-
def
|
| 418 |
-
"""
|
| 419 |
-
|
| 420 |
if not state or "advice" not in state:
|
| 421 |
return ("<div class='ce-sub'>Build a job first β then I'll give the plan a second look.</div>",
|
| 422 |
gr.update(interactive=True), gr.update(visible=False))
|
|
@@ -442,14 +477,42 @@ def second_opinion(state):
|
|
| 442 |
return panel, gr.update(interactive=True), gr.update(visible=False)
|
| 443 |
|
| 444 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
def toggle_read(choice, state):
|
| 446 |
"""Segmented toggle on Build: flip between Engineer's Read and Second Opinion,
|
| 447 |
-
showing one panel at a time. The opinion is computed lazily on first reveal
|
|
|
|
|
|
|
| 448 |
if str(choice).lower().startswith("engineer"):
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
|
| 454 |
|
| 455 |
def ack_override(state):
|
|
@@ -475,6 +538,79 @@ def job_readout(state):
|
|
| 475 |
f"{icon('printer')} {PRINTER}</div>")
|
| 476 |
|
| 477 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
def _simulated_result_panel(sess, run_summary, material, geometry_type, env, label) -> str:
|
| 479 |
"""Two-zone outcome β the dominant SIMULATED RESULT zone (the compact LOG A REAL
|
| 480 |
PRINT zone is static UI below). Shows the final outcome, the climb, whether the
|
|
@@ -503,36 +639,63 @@ def _simulated_result_panel(sess, run_summary, material, geometry_type, env, lab
|
|
| 503 |
)
|
| 504 |
|
| 505 |
|
| 506 |
-
def run_print(state, iterations):
|
| 507 |
-
"""PRINT: run THIS job (inherited from Build) through the closed loop.
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
print. Results reveal only after the run (progressive reveal)."""
|
| 511 |
if not state or "job" not in state:
|
| 512 |
gr.Warning("Build a job first (BUILD β SLICE), then print it here.")
|
| 513 |
-
|
|
|
|
| 514 |
job = Job(**state["job"])
|
| 515 |
env = Environment(**state["env"])
|
| 516 |
material, geometry_type = job.material, job.geometry_type
|
| 517 |
key = cell_key(material, geometry_type, env)
|
| 518 |
before_html = policy_cell_html(POLICY.cell_stats(material, geometry_type, env), key)
|
| 519 |
|
| 520 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
after = POLICY.cell_stats(material, geometry_type, env)
|
| 522 |
-
|
| 523 |
-
run_summary = inspector.summarize_run(sess.records, material=material, geometry=geometry_type)
|
| 524 |
|
| 525 |
-
traj =
|
| 526 |
-
first =
|
| 527 |
field_log.log_event("print_run", {"material": material, "geometry": geometry_type,
|
| 528 |
"env_temp": env.temp, "env_humidity": env.humidity,
|
| 529 |
-
"iterations": len(
|
| 530 |
"q_end": round(traj[-1], 3), "first_clean": first,
|
| 531 |
-
"inspector_stance": run_summary.stance
|
|
|
|
|
|
|
| 532 |
# ββ deliberation log: World simulates -> La Forge grades, per iteration; then verdict ββ
|
| 533 |
ctx = _delib_ctx(job, env)
|
| 534 |
loop_turns = []
|
| 535 |
-
for r, g in zip(
|
| 536 |
clamp = " (Spine clamped a setting)" if r.clamped else ""
|
| 537 |
loop_turns.append({"agent": "World", "act": "simulate", "stance": r.result.outcome,
|
| 538 |
"content": f"Iteration {r.n}: {r.result.detail}.{clamp} Policy: {r.learned}."})
|
|
@@ -545,7 +708,7 @@ def run_print(state, iterations):
|
|
| 545 |
], ctx)
|
| 546 |
headline = (
|
| 547 |
f"**{state.get('label') or geometry_type} Β· {material} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β "
|
| 548 |
-
f"started at quality **{traj[0]:.2f}** ({
|
| 549 |
+ (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."
|
| 550 |
if first else f"still improving β best **{max(traj):.2f}** after {len(traj)} runs.")
|
| 551 |
+ " The Engineer proposed; a separate simulated world reported the outcome; the **Inspector** "
|
|
@@ -553,16 +716,18 @@ def run_print(state, iterations):
|
|
| 553 |
)
|
| 554 |
policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);font-size:11px;"
|
| 555 |
f"letter-spacing:2px;'>{icon('arrow')} LEARNED</div>{policy_cell_html(after, key)}")
|
| 556 |
-
outcome = _simulated_result_panel(
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
|
|
|
|
|
|
| 566 |
inspector_panel(run_summary, label="LA FORGE Β· RUN VERDICT"), # review_summary
|
| 567 |
)
|
| 568 |
|
|
@@ -591,35 +756,57 @@ def launch(**kw):
|
|
| 591 |
|
| 592 |
|
| 593 |
def _action_bar(reset_btn_label="RESET", primary_label=None, primary_variant="primary",
|
| 594 |
-
primary_id=None):
|
| 595 |
"""Build the consistent top-right action bar (small primary + persistent Reset).
|
| 596 |
-
Both buttons are scale=0 so they DON'T stretch full-width.
|
|
|
|
| 597 |
with gr.Row(elem_classes=["ce-actionbar"]):
|
| 598 |
reset = gr.Button(reset_btn_label, elem_classes=["ce-pillbtn", "ce-act"], scale=0)
|
| 599 |
primary = None
|
| 600 |
if primary_label:
|
|
|
|
|
|
|
|
|
|
| 601 |
primary = gr.Button(primary_label, variant=primary_variant,
|
| 602 |
-
elem_classes=
|
| 603 |
return reset, primary
|
| 604 |
|
| 605 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
# outputs touched by Reset (shared by the persistent Reset on every tab)
|
| 607 |
def _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy, p_log,
|
| 608 |
-
p_headline, outcome_panel, results_group, real_log_msg, studio_log
|
|
|
|
|
|
|
| 609 |
return [ledger_panel, node_cards, review_summary, p_curve, p_policy, p_log,
|
| 610 |
-
p_headline, outcome_panel, results_group, real_log_msg, studio_log
|
|
|
|
| 611 |
|
| 612 |
|
| 613 |
def build() -> gr.Blocks:
|
| 614 |
with gr.Blocks(title="Microfactory Node: 3D Printer") as demo:
|
| 615 |
gr.HTML(command_bar(llm.backend_status()))
|
| 616 |
-
# header row: dropdown + warm button only (NO background tints, NO stretching)
|
| 617 |
with gr.Row(elem_id="ce-modelswitch"):
|
| 618 |
model_select = gr.Dropdown(MODEL_OPTIONS, value=MODEL_OPTIONS[0],
|
| 619 |
show_label=False, container=False,
|
| 620 |
elem_classes=["ce-modeldd"], scale=0)
|
| 621 |
warm_btn = gr.Button("WARM UP", elem_classes=["ce-pillbtn", "ce-icon-bolt"], scale=0)
|
| 622 |
-
model_status = gr.HTML(
|
| 623 |
|
| 624 |
state = gr.State()
|
| 625 |
part = gr.State({"geometry": None, "mesh": None, "label": None, "read": None})
|
|
@@ -627,9 +814,22 @@ def build() -> gr.Blocks:
|
|
| 627 |
with gr.Tabs() as tabs:
|
| 628 |
# βββββββββββββββββββββββββ BUILD Β· define the job ββββββββββββββββββββββββ
|
| 629 |
with gr.Tab("LOAD", id="studio"):
|
| 630 |
-
# Top row: ENVIRONMENT
|
|
|
|
| 631 |
with gr.Row(elem_classes=["ce-actionbar", "ce-envbar"]):
|
| 632 |
sensors_readout = gr.HTML(elem_classes=["ce-envbar-readout"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
roll_btn = gr.Button("RANDOMIZE",
|
| 634 |
elem_classes=["ce-pillbtn", "ce-icon-shuffle"], scale=0)
|
| 635 |
gr.HTML("<button class='ce-pillbtn ce-icon-sliders' "
|
|
@@ -639,26 +839,10 @@ def build() -> gr.Blocks:
|
|
| 639 |
"text-transform:uppercase;letter-spacing:.5px;font-size:12px;"
|
| 640 |
"font-weight:700;cursor:pointer;display:inline-flex;"
|
| 641 |
"align-items:center;gap:6px;'>OVERRIDE</button>")
|
| 642 |
-
# spacer pushes the action buttons to the right edge
|
| 643 |
-
gr.HTML("<div style='flex:1;'></div>")
|
| 644 |
reset_s = gr.Button("RESET", elem_classes=["ce-pillbtn", "ce-act"], scale=0)
|
| 645 |
run_btn = gr.Button("SLICE", variant="primary",
|
| 646 |
elem_classes=["ce-act"], elem_id="ce-run", scale=0)
|
| 647 |
|
| 648 |
-
# 2-column setup: BUILD-PLATE POSITION | MATERIAL
|
| 649 |
-
# (NOTES moved into PART card; ENVIRONMENT moved into action bar above)
|
| 650 |
-
with gr.Row(equal_height=False, elem_classes=["ce-loadrow"]):
|
| 651 |
-
with gr.Column(scale=2, min_width=320):
|
| 652 |
-
gr.HTML(rule("BUILD-PLATE POSITION"))
|
| 653 |
-
gr.HTML("<div class='ce-sub' style='margin:-2px 0 4px;'>"
|
| 654 |
-
"edges/corners run cooler β warp/adhesion risk</div>")
|
| 655 |
-
bed_position = gr.Radio(BED_POSITIONS, value="center", show_label=False,
|
| 656 |
-
elem_classes=["ce-pills"])
|
| 657 |
-
with gr.Column(scale=1, min_width=220):
|
| 658 |
-
gr.HTML(rule("MATERIAL"))
|
| 659 |
-
material = gr.Radio(MATERIALS, value="PLA", show_label=False, elem_classes=["ce-pills"])
|
| 660 |
-
gr.HTML(_model_callout())
|
| 661 |
-
|
| 662 |
# OVERRIDE ENVIRONMENT popup (hidden by default; toggled via JS)
|
| 663 |
gr.HTML("<div class='ce-popup-backdrop' data-popup-backdrop='override'></div>")
|
| 664 |
with gr.Group(elem_classes=["ce-popup"], elem_id="ce-popup-override"):
|
|
@@ -679,17 +863,13 @@ def build() -> gr.Blocks:
|
|
| 679 |
with gr.Column(scale=3, elem_classes=["ce-part-viewer-col"]):
|
| 680 |
part_placeholder = gr.HTML(_viewer_placeholder())
|
| 681 |
model3d = gr.Model3D(value=None, label="", height=360, visible=False)
|
| 682 |
-
# NOTES moved here β lives next to the part view always
|
| 683 |
-
description = gr.Textbox(label="NOTES (OPTIONAL)",
|
| 684 |
-
placeholder="e.g. 45Β° bracket, 60mm tall",
|
| 685 |
-
elem_classes=["ce-notes-inline"])
|
| 686 |
# intro + job log moved DOWN into the part area
|
| 687 |
gr.HTML(tab_intro("Load the part, set the material and the room, then "
|
| 688 |
"<b>SLICE</b> to read the engineer's pre-flight check. "
|
| 689 |
"You give it the part, the material, and the room; it "
|
| 690 |
"infers what kind of part this is."))
|
| 691 |
studio_log = gr.HTML(studio_log_html())
|
| 692 |
-
# RIGHT: dropzone
|
| 693 |
with gr.Column(scale=1, elem_classes=["ce-part-actions-col"]):
|
| 694 |
mesh_in = gr.File(file_types=[".stl", ".glb", ".obj"],
|
| 695 |
label="UPLOAD MESH", show_label=False,
|
|
@@ -704,49 +884,55 @@ def build() -> gr.Blocks:
|
|
| 704 |
show_label=False, elem_classes=["ce-pills"])
|
| 705 |
gen_size = gr.Number(value=30, label="SIZE (mm)", elem_classes=["ce-num"])
|
| 706 |
gen_btn = gr.Button("GENERATE", elem_classes=["ce-pillbtn"], scale=0)
|
|
|
|
|
|
|
|
|
|
| 707 |
|
| 708 |
# βββββββββββββββββ SLICE Β· slice + analyze + pre-flight check βββββββββββββ
|
| 709 |
with gr.Tab("SLICE", id="build"):
|
| 710 |
reset_b, to_print_btn = _action_bar(primary_label="PRINT (RUN ITERATIONS)")
|
| 711 |
gr.HTML(tab_intro("The pre-flight check, <b>before it prints</b>: slice the part, read "
|
| 712 |
"precedent, flag failures, and get a second opinion. Then β PRINT."))
|
| 713 |
-
build_loader = gr.HTML()
|
| 714 |
with gr.Group(visible=False, elem_classes=["ce-part-card"]) as build_results:
|
| 715 |
# slice + motion preview side by side β grouped like LOAD's PART card
|
| 716 |
gr.HTML(rule("SLICE Β· CROSS-SECTION"))
|
| 717 |
with gr.Row(equal_height=False, elem_classes=["ce-part-row"]):
|
| 718 |
with gr.Column(scale=3):
|
| 719 |
vp_layer = gr.Image(label="", height=360, show_label=False)
|
| 720 |
-
with gr.Column(scale=1, elem_classes=["ce-
|
| 721 |
-
vp_slider = gr.Slider(1, SCRUB_LAYERS, value=1, step=1, label="LAYER")
|
| 722 |
-
with gr.Column(scale=1, min_width=280):
|
| 723 |
gr.HTML(rule("MOTION PREVIEW"))
|
| 724 |
vprint = gr.HTML()
|
| 725 |
-
gr.
|
| 726 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
|
| 728 |
# Engineer's Read β Second Opinion (one panel at a time)
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
|
|
|
|
|
|
| 746 |
"the plan before it prints: O'Brien is an optimist, La Forge is not.</div>")
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
|
| 751 |
# ββββββββββββββββββββ PRINT Β· run it, iterate, grade βββββββββββββββββββββ
|
| 752 |
with gr.Tab("PRINT", id="print"):
|
|
@@ -754,27 +940,55 @@ def build() -> gr.Blocks:
|
|
| 754 |
gr.HTML(tab_intro("Print <b>this job</b> (inherited from Build). The Engineer proposes β "
|
| 755 |
"the Spine vetoes β a <b>simulated world</b> prints β the <b>Inspector "
|
| 756 |
"grades</b> β policy + ledger learn. Quality compounds failβclean."))
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
|
| 765 |
with gr.Group(visible=False) as results_group:
|
|
|
|
|
|
|
| 766 |
p_headline = gr.Markdown()
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
with gr.Column(scale=2):
|
| 772 |
-
gr.HTML(rule("ITERATION LOG"))
|
| 773 |
-
p_log = gr.HTML()
|
| 774 |
gr.HTML(rule("LEARNED POLICY CELL"))
|
| 775 |
p_policy = gr.HTML()
|
| 776 |
-
gr.HTML(rule("OUTCOME"))
|
| 777 |
-
outcome_panel = gr.HTML()
|
| 778 |
# compact secondary zone (~20%): log a REAL print back into the ledger
|
| 779 |
with gr.Row(elem_classes=["ce-card"]):
|
| 780 |
with gr.Column(scale=1):
|
|
@@ -810,7 +1024,8 @@ def build() -> gr.Blocks:
|
|
| 810 |
# ββ wiring ββ
|
| 811 |
reset_outs = _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy,
|
| 812 |
p_log, p_headline, outcome_panel, results_group, real_log_msg,
|
| 813 |
-
studio_log
|
|
|
|
| 814 |
preview_outs = [part, model3d, part_status, part_placeholder]
|
| 815 |
foot_in = [part, material, temp, humidity, bed_position]
|
| 816 |
benchy_btn.click(load_benchy, None, preview_outs).then(status_footer, foot_in, [footer])
|
|
@@ -836,11 +1051,12 @@ def build() -> gr.Blocks:
|
|
| 836 |
|
| 837 |
# Two-step BUILD: instant loader + tab-switch, then the heavy model call (reveals
|
| 838 |
# the results), then refresh the inherited-job readout on the Print tab.
|
| 839 |
-
build_start_outs = [tabs,
|
| 840 |
-
|
|
|
|
| 841 |
build_outs = [backend, precedent, reasoning, risks, settings_html, spine_notes,
|
| 842 |
gcode_html, confirm_btn, node_cards, state, vprint, vp_slider, vp_layer,
|
| 843 |
-
|
| 844 |
run_btn.click(build_start, [part], build_start_outs).then(
|
| 845 |
build_job, [part, material, description, temp, humidity, bed_position, model_select],
|
| 846 |
build_outs).then(job_readout, [state], [p_job])
|
|
@@ -848,17 +1064,22 @@ def build() -> gr.Blocks:
|
|
| 848 |
vp_slider.change(scrub_layer, [vp_slider, part], [vp_layer])
|
| 849 |
read_toggle.change(toggle_read, [read_toggle, state],
|
| 850 |
[eng_read_group, second_op_group, second_opinion_panel,
|
| 851 |
-
to_print_btn, override_btn])
|
| 852 |
override_btn.click(ack_override, state, [to_print_btn, override_btn])
|
| 853 |
to_print_btn.click(lambda: gr.Tabs(selected="print"), None, [tabs]).then(
|
| 854 |
None, None, None, js=_SCROLL_TOP)
|
| 855 |
-
tabs.select(
|
| 856 |
|
| 857 |
# PRINT: run the loop on THIS job (slider 1 = single print), or log a real outcome.
|
| 858 |
-
print_outs = [results_group, p_headline, p_curve, p_log, p_policy,
|
| 859 |
ledger_panel, node_cards, review_summary]
|
| 860 |
-
p_run.click(
|
|
|
|
| 861 |
None, None, None, js=_SCROLL_TOP)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 862 |
for btn, oc in [(b_clean, "success"), (b_sag, "failed_sag"), (b_string, "failed_stringing")]:
|
| 863 |
btn.click(record_outcome, [gr.State(oc), state], [real_log_msg, ledger_panel, node_cards])
|
| 864 |
|
|
|
|
| 18 |
|
| 19 |
import os
|
| 20 |
import random
|
| 21 |
+
import time
|
| 22 |
import uuid
|
| 23 |
+
from pathlib import Path
|
| 24 |
|
| 25 |
import gradio as gr
|
| 26 |
|
|
|
|
| 50 |
from core.nodes import render_node_cards
|
| 51 |
from core.reflect import reflect_on_job
|
| 52 |
from core.spine import SpineValidator
|
| 53 |
+
from learn.loop import run_iteration, SessionResult
|
| 54 |
from learn.policy import LearnedPolicy, cell_key
|
| 55 |
from core.viewer import (
|
| 56 |
GEO_READS,
|
|
|
|
| 176 |
|
| 177 |
def reset_learnings():
|
| 178 |
"""Reset the live ledger + learned policy to the curated baseline (seed + ingested),
|
| 179 |
+
clearing this session's accumulated runs. Also clears the loaded part and hides the
|
| 180 |
+
result groups so the next run starts from a clean LOAD tab."""
|
| 181 |
removed = LEDGER.reset_to_baseline()
|
| 182 |
POLICY.reset()
|
| 183 |
gr.Info(f"Reset to baseline β cleared {removed} runtime lesson(s) + the learned policy.")
|
|
|
|
| 191 |
gr.update(visible=False), # print results_group (re-hide)
|
| 192 |
"", # real_log_msg
|
| 193 |
studio_log_html(), # studio_log
|
| 194 |
+
{"geometry": None, "mesh": None, "label": None, "read": None}, # part state
|
| 195 |
+
gr.update(value=None, visible=False), # model3d
|
| 196 |
+
gr.update(visible=True), # part_placeholder
|
| 197 |
+
"", # part_status
|
| 198 |
+
gr.update(visible=False), # build_results
|
| 199 |
+
gr.update(visible=False), # read_results
|
| 200 |
+
gr.Tabs(selected="studio"), # tabs back to LOAD
|
| 201 |
)
|
| 202 |
|
| 203 |
|
|
|
|
| 229 |
|
| 230 |
|
| 231 |
def build_start(part):
|
| 232 |
+
"""Instant feedback when BUILD is clicked: jump to Build, reveal the slicer +
|
| 233 |
+
virtual print preview immediately, and show the read panel in a loading state
|
| 234 |
+
while the model runs. No part β stay put."""
|
| 235 |
if not (part and part.get("geometry")):
|
| 236 |
gr.Warning("Load a part on the BUILD tab first β quick-load Benchy, generate a primitive, or drop a mesh.")
|
| 237 |
+
return (gr.update(),) * 13
|
| 238 |
+
mesh = part.get("mesh")
|
| 239 |
+
label = part.get("label") or "PART"
|
| 240 |
+
vp_html = virtual_printer_html(mesh, caption=f"{label} Β· virtual print preview")
|
| 241 |
+
layer_img = layer_image(mesh, 1)
|
| 242 |
return (
|
| 243 |
gr.Tabs(selected="build"), # tabs
|
| 244 |
+
gr.update(visible=True), # build_results (reveal)
|
| 245 |
+
gr.update(visible=True), # read_results (reveal, loading state)
|
| 246 |
+
loader("READING THE PLAN Β· O'Brien is checking precedent and proposing settings"),
|
| 247 |
+
# read_loader
|
| 248 |
+
vp_html, # vprint
|
| 249 |
+
gr.update(value=1), # vp_slider reset
|
| 250 |
+
layer_img, # vp_layer
|
| 251 |
gr.update(interactive=True), # to_print_btn (un-gate)
|
| 252 |
gr.update(visible=False), # override_btn (hide)
|
| 253 |
+
"", # second_opinion_panel (clear stale)
|
| 254 |
gr.update(value="Engineer's Read"), # read_toggle (reset to the read)
|
| 255 |
+
gr.update(visible=True), # eng_read_group
|
| 256 |
+
gr.update(visible=False), # second_op_group
|
| 257 |
)
|
| 258 |
|
| 259 |
|
|
|
|
| 291 |
|
| 292 |
|
| 293 |
def warm_up_pending() -> str:
|
| 294 |
+
"""Header icon stays; the warm-up intent is signaled by a toast."""
|
| 295 |
+
gr.Info("Warming up the model (first load can take ~30s on ZeroGPU)β¦")
|
| 296 |
+
return _model_icon()
|
| 297 |
|
| 298 |
|
| 299 |
def warm_up_cb() -> str:
|
| 300 |
+
"""After warm-up, keep the info icon in the header."""
|
| 301 |
+
return _model_icon()
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def _model_icon() -> str:
|
| 305 |
+
"""Small info-icon callout for the header row. No text β the dropdown + warm-up
|
| 306 |
+
button already give enough context; the tooltip explains the backends."""
|
| 307 |
+
return (
|
| 308 |
+
"<span class='ce-callout' tabindex='0'>" + icon("info") + ""
|
| 309 |
+
"<span class='ce-tip'>The <b>LoRA v2/v3</b> adapters serve via ZeroGPU on the Space; "
|
| 310 |
+
"<b>Base</b> is the stock Gemma 4 E4B; <b>Modal API</b> is the remote endpoint. "
|
| 311 |
+
"Local users: pull the LoRA from HF Hub "
|
| 312 |
+
"(kylebrodeur/microfactory-node-lora-v2 / -v3-qat) or via ollama.</span></span>"
|
| 313 |
+
)
|
| 314 |
|
| 315 |
|
| 316 |
def _model_callout() -> str:
|
| 317 |
+
"""Legacy full-width model callout (used when more explanatory text is wanted)."""
|
| 318 |
return (
|
| 319 |
"<span class='ce-callout' tabindex='0'>" + icon("info") + " MODEL INFO"
|
| 320 |
"<span class='ce-tip'>The <b>LoRA v2/v3</b> adapters serve via ZeroGPU on the Space; "
|
|
|
|
| 325 |
|
| 326 |
|
| 327 |
def select_model(choice: str) -> str:
|
| 328 |
+
"""Model switcher: apply the chosen backend, then return the info icon so the
|
| 329 |
+
header stays compact."""
|
|
|
|
|
|
|
| 330 |
choice = choice or MODEL_OPTIONS[0]
|
| 331 |
try:
|
| 332 |
_apply_model_choice(choice)
|
| 333 |
except Exception:
|
| 334 |
pass
|
| 335 |
+
return _model_icon()
|
| 336 |
|
| 337 |
|
| 338 |
# ββ simulated environment (this is a sim lab β conditions are generated, overridable) ββ
|
|
|
|
| 389 |
rec = advise(job, env, retrieved, references, policy_note)
|
| 390 |
spine = SPINE.check(rec.advice.settings, material)
|
| 391 |
hint = steepest_overhang_hint(mesh) if geometry_type in ("overhang", "bridge") else None
|
| 392 |
+
vp_html = virtual_printer_html(mesh, settings=spine.settings,
|
| 393 |
+
caption=f"{material} Β· {geometry_type}") # init on BUILD
|
| 394 |
|
| 395 |
precedent = precedent_eval_html(retrieved, env)
|
| 396 |
if retrieved:
|
|
|
|
| 407 |
confirm_vis = gr.update(visible=spine.requires_approval)
|
| 408 |
approval_md = ("**HITL gate:** the Spine clamped a boundary setting β review, then **Confirm & Print**."
|
| 409 |
if spine.requires_approval else "Within safe envelope β ready when you are.")
|
| 410 |
+
spine_notes_text = f"{spine_md}\n\n{approval_md}" if spine_md else approval_md
|
| 411 |
session_id = uuid.uuid4().hex
|
| 412 |
state = {"job": job.model_dump(), "env": env.model_dump(), "settings": spine.settings.model_dump(),
|
| 413 |
+
"advice": rec.advice.model_dump(), "label": part.get("label"), "session_id": session_id,
|
| 414 |
+
"spine_notes": spine_notes_text}
|
| 415 |
|
| 416 |
# ββ field log (Space only β gated on HF_TOKEN; local/offline no-ops) ββ
|
| 417 |
field_log.log_build(
|
|
|
|
| 436 |
f"**Chief Engineer O'Brien:** {rec.advice.reasoning}", # reasoning
|
| 437 |
risk_callouts_html(rec.advice.risks, hint) + placement_callout(material, bed_position), # risks
|
| 438 |
settings_panel_html(spine.settings, material), # settings (LCARS panel)
|
| 439 |
+
spine_notes_text, # spine notes
|
| 440 |
gcode_panel_html(spine.settings, material), # g-code (LCARS panel)
|
| 441 |
confirm_vis, # confirm visibility
|
| 442 |
render_node_cards(env, working=True), # node cards
|
| 443 |
state, # state
|
| 444 |
+
virtual_printer_html(mesh, settings=spine.settings,
|
| 445 |
+
caption=f"{material} Β· {geometry_type}"), # vprint (refined caption)
|
| 446 |
gr.update(value=1), # reset layer scrubber
|
| 447 |
layer_image(mesh, 1), # initial scrubbed layer
|
| 448 |
+
"", # read_loader (clear)
|
|
|
|
| 449 |
)
|
| 450 |
|
| 451 |
|
| 452 |
+
def _compute_second_opinion(state):
|
| 453 |
+
"""Run the Inspector (La Forge) critique once. Returns the raw tuple used by
|
| 454 |
+
the UI: (panel_html, to_print_update, override_update)."""
|
| 455 |
if not state or "advice" not in state:
|
| 456 |
return ("<div class='ce-sub'>Build a job first β then I'll give the plan a second look.</div>",
|
| 457 |
gr.update(interactive=True), gr.update(visible=False))
|
|
|
|
| 477 |
return panel, gr.update(interactive=True), gr.update(visible=False)
|
| 478 |
|
| 479 |
|
| 480 |
+
def second_opinion(state):
|
| 481 |
+
"""Idempotent wrapper: if La Forge already weighed in on this build, return the
|
| 482 |
+
cached verdict; otherwise compute it once and store it in state."""
|
| 483 |
+
if state and state.get("second_opinion"):
|
| 484 |
+
return state["second_opinion"]
|
| 485 |
+
result = _compute_second_opinion(state)
|
| 486 |
+
if state:
|
| 487 |
+
state["second_opinion"] = result
|
| 488 |
+
return result
|
| 489 |
+
|
| 490 |
+
|
| 491 |
def toggle_read(choice, state):
|
| 492 |
"""Segmented toggle on Build: flip between Engineer's Read and Second Opinion,
|
| 493 |
+
showing one panel at a time. The opinion is computed lazily on first reveal and
|
| 494 |
+
then cached for the rest of the build. While it computes, the Second Opinion panel
|
| 495 |
+
shows a mini-loader so the user knows La Forge is reviewing the plan."""
|
| 496 |
if str(choice).lower().startswith("engineer"):
|
| 497 |
+
yield (gr.update(visible=True), gr.update(visible=False),
|
| 498 |
+
"", gr.update(), gr.update(), state)
|
| 499 |
+
return
|
| 500 |
+
cached = state.get("second_opinion") if state else None
|
| 501 |
+
if cached:
|
| 502 |
+
panel, to_print, override = cached
|
| 503 |
+
yield (gr.update(visible=False), gr.update(visible=True), panel, to_print, override, state)
|
| 504 |
+
return
|
| 505 |
+
# show in-place loader while the Inspector runs
|
| 506 |
+
yield (gr.update(visible=False), gr.update(visible=True),
|
| 507 |
+
loader("CONSULTING LA FORGE", stages=[
|
| 508 |
+
"reading the engineer's plan",
|
| 509 |
+
"checking precedent and risks",
|
| 510 |
+
"comparing settings to the safe envelope",
|
| 511 |
+
"drafting the pre-print verdict",
|
| 512 |
+
]), gr.update(interactive=True), gr.update(visible=False), state)
|
| 513 |
+
panel, to_print, override = _compute_second_opinion(state)
|
| 514 |
+
state = (state or {}) | {"second_opinion": (panel, to_print, override)}
|
| 515 |
+
yield (gr.update(visible=False), gr.update(visible=True), panel, to_print, override, state)
|
| 516 |
|
| 517 |
|
| 518 |
def ack_override(state):
|
|
|
|
| 538 |
f"{icon('printer')} {PRINTER}</div>")
|
| 539 |
|
| 540 |
|
| 541 |
+
def plan_card_html(state):
|
| 542 |
+
"""THE PLAN card: Spine-validated proposed settings + Spine notes for the current job."""
|
| 543 |
+
if not state or "settings" not in state:
|
| 544 |
+
return ("<div class='ce-sub'>No plan built yet. Go to <b>BUILD</b> β define a part β "
|
| 545 |
+
"<b>SLICE</b>, then return here to print it.</div>")
|
| 546 |
+
settings = PrintSettings(**state["settings"])
|
| 547 |
+
material = state["job"]["material"]
|
| 548 |
+
html = settings_panel_html(settings, material)
|
| 549 |
+
notes = state.get("spine_notes") or ""
|
| 550 |
+
if notes:
|
| 551 |
+
html += ("<div class='ce-sub' style='margin-top:6px;'>" +
|
| 552 |
+
notes.replace("\n", "<br>") + "</div>")
|
| 553 |
+
return html
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
def plan_verdict_html(state):
|
| 557 |
+
"""La Forge's pre-print stance if already computed on the SLICE tab."""
|
| 558 |
+
if not state or "settings" not in state:
|
| 559 |
+
return ("<div class='ce-sub'>Build a job first to get La Forge's pre-print stance.</div>")
|
| 560 |
+
cached = state.get("second_opinion")
|
| 561 |
+
if cached:
|
| 562 |
+
return cached[0]
|
| 563 |
+
return ("<div class='ce-sub'>" + icon("search") +
|
| 564 |
+
" Run the Inspector (<b>Second Opinion</b>) on the SLICE tab for a pre-print stance.</div>")
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
def _print_plan_values(state):
|
| 568 |
+
"""Return the Engineer's proposed settings values (or safe defaults) for VARY sliders."""
|
| 569 |
+
defaults = {"nozzle_temp": 200, "bed_temp": 60, "fan_pct": 80, "retraction_mm": 4.5}
|
| 570 |
+
if not state or "settings" not in state:
|
| 571 |
+
return defaults
|
| 572 |
+
s = state["settings"]
|
| 573 |
+
return {k: s.get(k, defaults[k]) for k in defaults}
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
def apply_overrides(state, nozzle, bed, fan, retract):
|
| 577 |
+
"""Compare VARY slider values to the Engineer's plan. If anything changed, build an
|
| 578 |
+
override PrintSettings and log that the operator defied the plan. Returns the override
|
| 579 |
+
dict (or None when the plan is unchanged)."""
|
| 580 |
+
if not state or "settings" not in state:
|
| 581 |
+
return None
|
| 582 |
+
plan = PrintSettings(**state["settings"])
|
| 583 |
+
diffs = {}
|
| 584 |
+
if abs(float(nozzle) - plan.nozzle_temp) > 1e-6:
|
| 585 |
+
diffs["nozzle_temp"] = float(nozzle)
|
| 586 |
+
if abs(float(bed) - plan.bed_temp) > 1e-6:
|
| 587 |
+
diffs["bed_temp"] = float(bed)
|
| 588 |
+
if abs(float(fan) - plan.fan_pct) > 1e-6:
|
| 589 |
+
diffs["fan_pct"] = float(fan)
|
| 590 |
+
if abs(float(retract) - plan.retraction_mm) > 1e-6:
|
| 591 |
+
diffs["retraction_mm"] = float(retract)
|
| 592 |
+
if not diffs:
|
| 593 |
+
state.pop("print_overrides", None)
|
| 594 |
+
return None
|
| 595 |
+
overrides = plan.model_copy(update=diffs)
|
| 596 |
+
state["print_overrides"] = overrides.model_dump()
|
| 597 |
+
job = Job(**state["job"])
|
| 598 |
+
env = Environment(**state["env"])
|
| 599 |
+
field_log.log_event("print_override", {
|
| 600 |
+
"material": job.material, "geometry": job.geometry_type,
|
| 601 |
+
"env_temp": env.temp, "env_humidity": env.humidity,
|
| 602 |
+
"settings": overrides.model_dump(),
|
| 603 |
+
})
|
| 604 |
+
if state.get("session_id"):
|
| 605 |
+
deliberation_log.log_turns(state["session_id"], "preflight", [
|
| 606 |
+
{"agent": "Operator", "act": "override",
|
| 607 |
+
"content": (f"Overrode the Engineer: nozzle {overrides.nozzle_temp:.0f}Β°C, "
|
| 608 |
+
f"bed {overrides.bed_temp:.0f}Β°C, fan {overrides.fan_pct:.0f}%, "
|
| 609 |
+
f"retraction {overrides.retraction_mm:.1f}mm.")},
|
| 610 |
+
], _delib_ctx(job, env))
|
| 611 |
+
return overrides.model_dump()
|
| 612 |
+
|
| 613 |
+
|
| 614 |
def _simulated_result_panel(sess, run_summary, material, geometry_type, env, label) -> str:
|
| 615 |
"""Two-zone outcome β the dominant SIMULATED RESULT zone (the compact LOG A REAL
|
| 616 |
PRINT zone is static UI below). Shows the final outcome, the climb, whether the
|
|
|
|
| 639 |
)
|
| 640 |
|
| 641 |
|
| 642 |
+
def run_print(state, iterations, nozzle, bed, fan, retract, progress=gr.Progress()):
|
| 643 |
+
"""PRINT: run THIS job (inherited from Build) through the closed loop.
|
| 644 |
+
Now a generator so the iteration log + quality chart + policy cell fill in
|
| 645 |
+
live as each iteration completes. Per-iteration timing is shown next to each row."""
|
|
|
|
| 646 |
if not state or "job" not in state:
|
| 647 |
gr.Warning("Build a job first (BUILD β SLICE), then print it here.")
|
| 648 |
+
yield (gr.update(),) * 9
|
| 649 |
+
return
|
| 650 |
job = Job(**state["job"])
|
| 651 |
env = Environment(**state["env"])
|
| 652 |
material, geometry_type = job.material, job.geometry_type
|
| 653 |
key = cell_key(material, geometry_type, env)
|
| 654 |
before_html = policy_cell_html(POLICY.cell_stats(material, geometry_type, env), key)
|
| 655 |
|
| 656 |
+
overrides_dict = apply_overrides(state, nozzle, bed, fan, retract)
|
| 657 |
+
overrides = PrintSettings(**overrides_dict) if overrides_dict else None
|
| 658 |
+
|
| 659 |
+
records, verdicts, timings = [], [], []
|
| 660 |
+
n_iters = int(iterations)
|
| 661 |
+
start_iter = time.perf_counter()
|
| 662 |
+
for i in range(1, n_iters + 1):
|
| 663 |
+
progress(i / n_iters, desc=f"Iteration {i}/{n_iters}")
|
| 664 |
+
record = run_iteration(job, env, POLICY, LEDGER, i, overrides=overrides)
|
| 665 |
+
verdict = inspector.grade_iteration(geometry_type, record.result)
|
| 666 |
+
elapsed = time.perf_counter() - start_iter
|
| 667 |
+
start_iter = time.perf_counter()
|
| 668 |
+
records.append(record)
|
| 669 |
+
verdicts.append(verdict)
|
| 670 |
+
timings.append(elapsed)
|
| 671 |
+
yield (
|
| 672 |
+
gr.update(visible=True), # results_group
|
| 673 |
+
"", # outcome_panel
|
| 674 |
+
"", # p_headline
|
| 675 |
+
quality_curve_html([r.result.quality for r in records]), # p_curve
|
| 676 |
+
iteration_log_html(records, verdicts, timings), # p_log
|
| 677 |
+
policy_cell_html(POLICY.cell_stats(material, geometry_type, env), key), # p_policy
|
| 678 |
+
gr.update(), # ledger_panel
|
| 679 |
+
gr.update(), # node_cards
|
| 680 |
+
"", # review_summary
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
after = POLICY.cell_stats(material, geometry_type, env)
|
| 684 |
+
run_summary = inspector.summarize_run(records, material=material, geometry=geometry_type)
|
|
|
|
| 685 |
|
| 686 |
+
traj = [r.result.quality for r in records]
|
| 687 |
+
first = next((r.n for r in records if r.result.outcome == "success"), None)
|
| 688 |
field_log.log_event("print_run", {"material": material, "geometry": geometry_type,
|
| 689 |
"env_temp": env.temp, "env_humidity": env.humidity,
|
| 690 |
+
"iterations": len(records), "q_start": round(traj[0], 3),
|
| 691 |
"q_end": round(traj[-1], 3), "first_clean": first,
|
| 692 |
+
"inspector_stance": run_summary.stance,
|
| 693 |
+
"used_override": overrides is not None,
|
| 694 |
+
"override_settings": overrides.model_dump() if overrides else None})
|
| 695 |
# ββ deliberation log: World simulates -> La Forge grades, per iteration; then verdict ββ
|
| 696 |
ctx = _delib_ctx(job, env)
|
| 697 |
loop_turns = []
|
| 698 |
+
for r, g in zip(records, verdicts):
|
| 699 |
clamp = " (Spine clamped a setting)" if r.clamped else ""
|
| 700 |
loop_turns.append({"agent": "World", "act": "simulate", "stance": r.result.outcome,
|
| 701 |
"content": f"Iteration {r.n}: {r.result.detail}.{clamp} Policy: {r.learned}."})
|
|
|
|
| 708 |
], ctx)
|
| 709 |
headline = (
|
| 710 |
f"**{state.get('label') or geometry_type} Β· {material} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β "
|
| 711 |
+
f"started at quality **{traj[0]:.2f}** ({records[0].result.outcome}); "
|
| 712 |
+ (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."
|
| 713 |
if first else f"still improving β best **{max(traj):.2f}** after {len(traj)} runs.")
|
| 714 |
+ " The Engineer proposed; a separate simulated world reported the outcome; the **Inspector** "
|
|
|
|
| 716 |
)
|
| 717 |
policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);font-size:11px;"
|
| 718 |
f"letter-spacing:2px;'>{icon('arrow')} LEARNED</div>{policy_cell_html(after, key)}")
|
| 719 |
+
outcome = _simulated_result_panel(
|
| 720 |
+
SessionResult(job=job, env=env, records=records),
|
| 721 |
+
run_summary, material, geometry_type, env, state.get("label"))
|
| 722 |
+
yield (
|
| 723 |
+
gr.update(visible=True), # results_group
|
| 724 |
+
outcome, # outcome_panel
|
| 725 |
+
headline, # p_headline
|
| 726 |
+
quality_curve_html(traj), # p_curve
|
| 727 |
+
iteration_log_html(records, verdicts, timings), # p_log
|
| 728 |
+
policy_html, # p_policy
|
| 729 |
+
ledger_html(), # ledger_panel
|
| 730 |
+
render_node_cards(env, working=False), # node_cards
|
| 731 |
inspector_panel(run_summary, label="LA FORGE Β· RUN VERDICT"), # review_summary
|
| 732 |
)
|
| 733 |
|
|
|
|
| 756 |
|
| 757 |
|
| 758 |
def _action_bar(reset_btn_label="RESET", primary_label=None, primary_variant="primary",
|
| 759 |
+
primary_id=None, primary_arrow=True):
|
| 760 |
"""Build the consistent top-right action bar (small primary + persistent Reset).
|
| 761 |
+
Both buttons are scale=0 so they DON'T stretch full-width. Primary buttons get
|
| 762 |
+
a right-side arrow icon by default to signal "proceed to next stage"."""
|
| 763 |
with gr.Row(elem_classes=["ce-actionbar"]):
|
| 764 |
reset = gr.Button(reset_btn_label, elem_classes=["ce-pillbtn", "ce-act"], scale=0)
|
| 765 |
primary = None
|
| 766 |
if primary_label:
|
| 767 |
+
primary_classes = ["ce-act"]
|
| 768 |
+
if primary_arrow:
|
| 769 |
+
primary_classes.append("ce-icon-arrow-after")
|
| 770 |
primary = gr.Button(primary_label, variant=primary_variant,
|
| 771 |
+
elem_classes=primary_classes, elem_id=primary_id, scale=0)
|
| 772 |
return reset, primary
|
| 773 |
|
| 774 |
|
| 775 |
+
def refresh_print_tab(state):
|
| 776 |
+
"""Refresh the PRINT tab's plan + verdict readouts whenever the tab is visited."""
|
| 777 |
+
return job_readout(state), plan_card_html(state), plan_verdict_html(state)
|
| 778 |
+
|
| 779 |
+
|
| 780 |
+
def open_vary_panel(state):
|
| 781 |
+
"""Open the VARY override panel and seed the sliders with the Engineer's plan."""
|
| 782 |
+
vals = _print_plan_values(state)
|
| 783 |
+
return (gr.update(visible=True),
|
| 784 |
+
gr.update(value=vals["nozzle_temp"]),
|
| 785 |
+
gr.update(value=vals["bed_temp"]),
|
| 786 |
+
gr.update(value=vals["fan_pct"]),
|
| 787 |
+
gr.update(value=vals["retraction_mm"]))
|
| 788 |
+
|
| 789 |
+
|
| 790 |
# outputs touched by Reset (shared by the persistent Reset on every tab)
|
| 791 |
def _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy, p_log,
|
| 792 |
+
p_headline, outcome_panel, results_group, real_log_msg, studio_log,
|
| 793 |
+
part, model3d, part_placeholder, part_status, build_results,
|
| 794 |
+
read_results, tabs):
|
| 795 |
return [ledger_panel, node_cards, review_summary, p_curve, p_policy, p_log,
|
| 796 |
+
p_headline, outcome_panel, results_group, real_log_msg, studio_log,
|
| 797 |
+
part, model3d, part_placeholder, part_status, build_results, read_results, tabs]
|
| 798 |
|
| 799 |
|
| 800 |
def build() -> gr.Blocks:
|
| 801 |
with gr.Blocks(title="Microfactory Node: 3D Printer") as demo:
|
| 802 |
gr.HTML(command_bar(llm.backend_status()))
|
| 803 |
+
# header row: dropdown + warm button + info icon only (NO background tints, NO stretching)
|
| 804 |
with gr.Row(elem_id="ce-modelswitch"):
|
| 805 |
model_select = gr.Dropdown(MODEL_OPTIONS, value=MODEL_OPTIONS[0],
|
| 806 |
show_label=False, container=False,
|
| 807 |
elem_classes=["ce-modeldd"], scale=0)
|
| 808 |
warm_btn = gr.Button("WARM UP", elem_classes=["ce-pillbtn", "ce-icon-bolt"], scale=0)
|
| 809 |
+
model_status = gr.HTML(_model_icon(), elem_classes=["ce-status-inline"])
|
| 810 |
|
| 811 |
state = gr.State()
|
| 812 |
part = gr.State({"geometry": None, "mesh": None, "label": None, "read": None})
|
|
|
|
| 814 |
with gr.Tabs() as tabs:
|
| 815 |
# βββββββββββββββββββββββββ BUILD Β· define the job ββββββββββββββββββββββββ
|
| 816 |
with gr.Tab("LOAD", id="studio"):
|
| 817 |
+
# Top row: ENVIRONMENT + POSITION + MATERIAL readouts on the left,
|
| 818 |
+
# RANDOMIZE / OVERRIDE / RESET / SLICE on the right.
|
| 819 |
with gr.Row(elem_classes=["ce-actionbar", "ce-envbar"]):
|
| 820 |
sensors_readout = gr.HTML(elem_classes=["ce-envbar-readout"])
|
| 821 |
+
with gr.Column(elem_classes=["ce-inline-group"]):
|
| 822 |
+
gr.HTML("<div class='ce-inline-label'>POSITION"
|
| 823 |
+
"<span class='ce-callout' tabindex='0'>" + icon("info", size=10) +
|
| 824 |
+
"<span class='ce-tip'>edges/corners run cooler β warp/adhesion risk</span>"
|
| 825 |
+
"</span></div>")
|
| 826 |
+
bed_position = gr.Radio(BED_POSITIONS, value="center", show_label=False,
|
| 827 |
+
elem_classes=["ce-pills"])
|
| 828 |
+
with gr.Column(elem_classes=["ce-inline-group"]):
|
| 829 |
+
gr.HTML("<div class='ce-inline-label'>MATERIAL</div>")
|
| 830 |
+
material = gr.Radio(MATERIALS, value="PLA", show_label=False, elem_classes=["ce-pills"])
|
| 831 |
+
# spacer pushes the action buttons to the right edge
|
| 832 |
+
gr.HTML("<div style='flex:1;'></div>")
|
| 833 |
roll_btn = gr.Button("RANDOMIZE",
|
| 834 |
elem_classes=["ce-pillbtn", "ce-icon-shuffle"], scale=0)
|
| 835 |
gr.HTML("<button class='ce-pillbtn ce-icon-sliders' "
|
|
|
|
| 839 |
"text-transform:uppercase;letter-spacing:.5px;font-size:12px;"
|
| 840 |
"font-weight:700;cursor:pointer;display:inline-flex;"
|
| 841 |
"align-items:center;gap:6px;'>OVERRIDE</button>")
|
|
|
|
|
|
|
| 842 |
reset_s = gr.Button("RESET", elem_classes=["ce-pillbtn", "ce-act"], scale=0)
|
| 843 |
run_btn = gr.Button("SLICE", variant="primary",
|
| 844 |
elem_classes=["ce-act"], elem_id="ce-run", scale=0)
|
| 845 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
# OVERRIDE ENVIRONMENT popup (hidden by default; toggled via JS)
|
| 847 |
gr.HTML("<div class='ce-popup-backdrop' data-popup-backdrop='override'></div>")
|
| 848 |
with gr.Group(elem_classes=["ce-popup"], elem_id="ce-popup-override"):
|
|
|
|
| 863 |
with gr.Column(scale=3, elem_classes=["ce-part-viewer-col"]):
|
| 864 |
part_placeholder = gr.HTML(_viewer_placeholder())
|
| 865 |
model3d = gr.Model3D(value=None, label="", height=360, visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
# intro + job log moved DOWN into the part area
|
| 867 |
gr.HTML(tab_intro("Load the part, set the material and the room, then "
|
| 868 |
"<b>SLICE</b> to read the engineer's pre-flight check. "
|
| 869 |
"You give it the part, the material, and the room; it "
|
| 870 |
"infers what kind of part this is."))
|
| 871 |
studio_log = gr.HTML(studio_log_html())
|
| 872 |
+
# RIGHT: dropzone + mesh-source buttons + NOTES
|
| 873 |
with gr.Column(scale=1, elem_classes=["ce-part-actions-col"]):
|
| 874 |
mesh_in = gr.File(file_types=[".stl", ".glb", ".obj"],
|
| 875 |
label="UPLOAD MESH", show_label=False,
|
|
|
|
| 884 |
show_label=False, elem_classes=["ce-pills"])
|
| 885 |
gen_size = gr.Number(value=30, label="SIZE (mm)", elem_classes=["ce-num"])
|
| 886 |
gen_btn = gr.Button("GENERATE", elem_classes=["ce-pillbtn"], scale=0)
|
| 887 |
+
description = gr.Textbox(label="NOTES (OPTIONAL)",
|
| 888 |
+
placeholder="e.g. 45Β° bracket, 60mm tall",
|
| 889 |
+
elem_classes=["ce-notes-inline"])
|
| 890 |
|
| 891 |
# βββββββββββββββββ SLICE Β· slice + analyze + pre-flight check βββββββββββββ
|
| 892 |
with gr.Tab("SLICE", id="build"):
|
| 893 |
reset_b, to_print_btn = _action_bar(primary_label="PRINT (RUN ITERATIONS)")
|
| 894 |
gr.HTML(tab_intro("The pre-flight check, <b>before it prints</b>: slice the part, read "
|
| 895 |
"precedent, flag failures, and get a second opinion. Then β PRINT."))
|
|
|
|
| 896 |
with gr.Group(visible=False, elem_classes=["ce-part-card"]) as build_results:
|
| 897 |
# slice + motion preview side by side β grouped like LOAD's PART card
|
| 898 |
gr.HTML(rule("SLICE Β· CROSS-SECTION"))
|
| 899 |
with gr.Row(equal_height=False, elem_classes=["ce-part-row"]):
|
| 900 |
with gr.Column(scale=3):
|
| 901 |
vp_layer = gr.Image(label="", height=360, show_label=False)
|
| 902 |
+
with gr.Column(scale=1, elem_classes=["ce-vp"]):
|
|
|
|
|
|
|
| 903 |
gr.HTML(rule("MOTION PREVIEW"))
|
| 904 |
vprint = gr.HTML()
|
| 905 |
+
with gr.Column(elem_classes=["ce-hslider"]):
|
| 906 |
+
vp_slider = gr.Slider(1, SCRUB_LAYERS, value=1, step=1,
|
| 907 |
+
show_label=False, container=False)
|
| 908 |
+
gr.HTML("<div class='ce-sub' style='padding:0 12px;'>Scrub through real cross-sections of "
|
| 909 |
+
"<i>this</i> part at full mesh fidelity. The preview animates the layer rise "
|
| 910 |
+
"while the read loads below.</div>")
|
| 911 |
|
| 912 |
# Engineer's Read β Second Opinion (one panel at a time)
|
| 913 |
+
with gr.Group(visible=False, elem_classes=["ce-card"]) as read_results:
|
| 914 |
+
backend = gr.Markdown()
|
| 915 |
+
gr.HTML(rule("THE READ"))
|
| 916 |
+
read_loader = gr.HTML()
|
| 917 |
+
read_toggle = gr.Radio(["Engineer's Read", "Second Opinion"],
|
| 918 |
+
value="Engineer's Read", show_label=False,
|
| 919 |
+
elem_classes=["ce-seg"])
|
| 920 |
+
with gr.Group(visible=True, elem_classes=["ce-card"]) as eng_read_group:
|
| 921 |
+
precedent = gr.HTML(elem_id="ce-precedent")
|
| 922 |
+
reasoning = gr.Markdown(elem_id="ce-reasoning")
|
| 923 |
+
risks = gr.HTML()
|
| 924 |
+
gr.HTML(rule("VALIDATION + G-CODE"))
|
| 925 |
+
spine_notes = gr.Markdown()
|
| 926 |
+
with gr.Row(equal_height=False):
|
| 927 |
+
settings_html = gr.HTML()
|
| 928 |
+
gcode_html = gr.HTML()
|
| 929 |
+
confirm_btn = gr.Button("CONFIRM & PRINT", elem_id="ce-confirm", visible=False)
|
| 930 |
+
with gr.Group(visible=False, elem_classes=["ce-card"]) as second_op_group:
|
| 931 |
+
gr.HTML("<div class='ce-sub' style='padding:0 12px;'>A separate inspector β <b>La Forge</b> β reviews "
|
| 932 |
"the plan before it prints: O'Brien is an optimist, La Forge is not.</div>")
|
| 933 |
+
second_opinion_panel = gr.HTML()
|
| 934 |
+
override_btn = gr.Button("PRINT ANYWAY (I'VE REVIEWED THE OBJECTION)",
|
| 935 |
+
visible=False, elem_classes=["ce-pillbtn"])
|
| 936 |
|
| 937 |
# ββββββββββββββββββββ PRINT Β· run it, iterate, grade βββββββββββββββββββββ
|
| 938 |
with gr.Tab("PRINT", id="print"):
|
|
|
|
| 940 |
gr.HTML(tab_intro("Print <b>this job</b> (inherited from Build). The Engineer proposes β "
|
| 941 |
"the Spine vetoes β a <b>simulated world</b> prints β the <b>Inspector "
|
| 942 |
"grades</b> β policy + ledger learn. Quality compounds failβclean."))
|
| 943 |
+
|
| 944 |
+
# ENVBAR: job readout | iteration chip | VARY
|
| 945 |
+
with gr.Row(elem_classes=["ce-actionbar", "ce-envbar"]):
|
| 946 |
+
p_job = gr.HTML(job_readout(None), elem_classes=["ce-envbar-readout"])
|
| 947 |
+
p_iter_readout = gr.HTML(
|
| 948 |
+
"<div class='ce-sub' style='text-align:center;'><b>8</b> ITERATIONS</div>")
|
| 949 |
+
gr.HTML("<div style='flex:1;'></div>")
|
| 950 |
+
vary_btn = gr.Button("VARY", elem_classes=["ce-pillbtn"], scale=0)
|
| 951 |
+
|
| 952 |
+
# THE PLAN card
|
| 953 |
+
with gr.Group(elem_classes=["ce-card"]) as plan_card:
|
| 954 |
+
gr.HTML(rule("THE PLAN"))
|
| 955 |
+
plan_settings = gr.HTML()
|
| 956 |
+
plan_verdict = gr.HTML()
|
| 957 |
+
|
| 958 |
+
# ITERATIONS hslider + explainer
|
| 959 |
+
with gr.Column(elem_classes=["ce-hslider"]):
|
| 960 |
+
p_iters = gr.Slider(1, 16, value=8, step=1, show_label=False, container=False)
|
| 961 |
+
gr.HTML("<div class='ce-sub' style='margin:-8px 0 10px;'>"
|
| 962 |
+
"1 = single print Β· 4 = quick convergence Β· 8 = balanced climb Β· 16 = full convergence run"
|
| 963 |
+
"</div>")
|
| 964 |
+
|
| 965 |
+
# VARY override panel
|
| 966 |
+
with gr.Group(visible=False, elem_classes=["ce-card"]) as vary_group:
|
| 967 |
+
gr.HTML(rule("OPERATOR OVERRIDE"))
|
| 968 |
+
gr.HTML("<div class='ce-sub' style='border-left:3px solid var(--ao-red);padding:6px 10px;'>"
|
| 969 |
+
+ icon("alert") + " You are overriding the Engineer's plan. The run will use "
|
| 970 |
+
"these settings instead of the Spine-validated proposal.</div>")
|
| 971 |
+
with gr.Row():
|
| 972 |
+
p_nozzle = gr.Slider(150, 300, value=200, step=1,
|
| 973 |
+
label="NOZZLE Β°C", elem_classes=["ce-num"])
|
| 974 |
+
p_bed = gr.Slider(40, 120, value=60, step=1,
|
| 975 |
+
label="BED Β°C", elem_classes=["ce-num"])
|
| 976 |
+
p_fan = gr.Slider(0, 100, value=80, step=1,
|
| 977 |
+
label="FAN %", elem_classes=["ce-num"])
|
| 978 |
+
p_retract = gr.Slider(0, 10, value=4.5, step=0.1,
|
| 979 |
+
label="RETRACTION mm", elem_classes=["ce-num"])
|
| 980 |
+
reset_plan_btn = gr.Button("RESET TO ENGINEER'S PLAN", elem_classes=["ce-pillbtn"])
|
| 981 |
|
| 982 |
with gr.Group(visible=False) as results_group:
|
| 983 |
+
gr.HTML(rule("OUTCOME"))
|
| 984 |
+
outcome_panel = gr.HTML()
|
| 985 |
p_headline = gr.Markdown()
|
| 986 |
+
gr.HTML(rule("QUALITY PER ITERATION"))
|
| 987 |
+
p_curve = gr.HTML()
|
| 988 |
+
gr.HTML(rule("ITERATION LOG"))
|
| 989 |
+
p_log = gr.HTML()
|
|
|
|
|
|
|
|
|
|
| 990 |
gr.HTML(rule("LEARNED POLICY CELL"))
|
| 991 |
p_policy = gr.HTML()
|
|
|
|
|
|
|
| 992 |
# compact secondary zone (~20%): log a REAL print back into the ledger
|
| 993 |
with gr.Row(elem_classes=["ce-card"]):
|
| 994 |
with gr.Column(scale=1):
|
|
|
|
| 1024 |
# ββ wiring ββ
|
| 1025 |
reset_outs = _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy,
|
| 1026 |
p_log, p_headline, outcome_panel, results_group, real_log_msg,
|
| 1027 |
+
studio_log, part, model3d, part_placeholder, part_status,
|
| 1028 |
+
build_results, read_results, tabs)
|
| 1029 |
preview_outs = [part, model3d, part_status, part_placeholder]
|
| 1030 |
foot_in = [part, material, temp, humidity, bed_position]
|
| 1031 |
benchy_btn.click(load_benchy, None, preview_outs).then(status_footer, foot_in, [footer])
|
|
|
|
| 1051 |
|
| 1052 |
# Two-step BUILD: instant loader + tab-switch, then the heavy model call (reveals
|
| 1053 |
# the results), then refresh the inherited-job readout on the Print tab.
|
| 1054 |
+
build_start_outs = [tabs, build_results, read_results, read_loader, vprint, vp_slider,
|
| 1055 |
+
vp_layer, to_print_btn, override_btn, second_opinion_panel,
|
| 1056 |
+
read_toggle, eng_read_group, second_op_group]
|
| 1057 |
build_outs = [backend, precedent, reasoning, risks, settings_html, spine_notes,
|
| 1058 |
gcode_html, confirm_btn, node_cards, state, vprint, vp_slider, vp_layer,
|
| 1059 |
+
read_loader]
|
| 1060 |
run_btn.click(build_start, [part], build_start_outs).then(
|
| 1061 |
build_job, [part, material, description, temp, humidity, bed_position, model_select],
|
| 1062 |
build_outs).then(job_readout, [state], [p_job])
|
|
|
|
| 1064 |
vp_slider.change(scrub_layer, [vp_slider, part], [vp_layer])
|
| 1065 |
read_toggle.change(toggle_read, [read_toggle, state],
|
| 1066 |
[eng_read_group, second_op_group, second_opinion_panel,
|
| 1067 |
+
to_print_btn, override_btn, state])
|
| 1068 |
override_btn.click(ack_override, state, [to_print_btn, override_btn])
|
| 1069 |
to_print_btn.click(lambda: gr.Tabs(selected="print"), None, [tabs]).then(
|
| 1070 |
None, None, None, js=_SCROLL_TOP)
|
| 1071 |
+
tabs.select(refresh_print_tab, [state], [p_job, plan_settings, plan_verdict])
|
| 1072 |
|
| 1073 |
# PRINT: run the loop on THIS job (slider 1 = single print), or log a real outcome.
|
| 1074 |
+
print_outs = [results_group, outcome_panel, p_headline, p_curve, p_log, p_policy,
|
| 1075 |
ledger_panel, node_cards, review_summary]
|
| 1076 |
+
p_run.click(lambda: gr.update(visible=False), None, [vary_group]).then(
|
| 1077 |
+
run_print, [state, p_iters, p_nozzle, p_bed, p_fan, p_retract], print_outs).then(
|
| 1078 |
None, None, None, js=_SCROLL_TOP)
|
| 1079 |
+
p_iters.change(lambda v: f"<div class='ce-sub' style='text-align:center;'><b>{int(v)}</b> ITERATIONS</div>",
|
| 1080 |
+
[p_iters], [p_iter_readout])
|
| 1081 |
+
vary_btn.click(open_vary_panel, [state], [vary_group, p_nozzle, p_bed, p_fan, p_retract])
|
| 1082 |
+
reset_plan_btn.click(open_vary_panel, [state], [vary_group, p_nozzle, p_bed, p_fan, p_retract])
|
| 1083 |
for btn, oc in [(b_clean, "success"), (b_sag, "failed_sag"), (b_string, "failed_stringing")]:
|
| 1084 |
btn.click(record_outcome, [gr.State(oc), state], [real_log_msg, ledger_panel, node_cards])
|
| 1085 |
|
core/chief_engineer.py
CHANGED
|
@@ -16,10 +16,10 @@ from .prompts import build_system_prompt
|
|
| 16 |
|
| 17 |
# Conservative per-material starting points for the offline/parse-fail fallback.
|
| 18 |
_FALLBACK_SETTINGS: dict[str, dict[str, float]] = {
|
| 19 |
-
"PLA": {"nozzle_temp": 205, "bed_temp": 60, "retraction_mm": 5, "fan_pct": 100, "first_layer_fan_pct": 0},
|
| 20 |
-
"PETG": {"nozzle_temp": 235, "bed_temp": 80, "retraction_mm": 4, "fan_pct": 40, "first_layer_fan_pct": 0},
|
| 21 |
-
"ABS": {"nozzle_temp": 245, "bed_temp": 100, "retraction_mm": 4, "fan_pct": 20, "first_layer_fan_pct": 0},
|
| 22 |
-
"TPU": {"nozzle_temp": 228, "bed_temp": 50, "retraction_mm": 2, "fan_pct": 40, "first_layer_fan_pct": 0},
|
| 23 |
}
|
| 24 |
_GEOMETRY_RISK = {
|
| 25 |
"overhang": ("underside of the overhang", "sag", "steep overhangs droop before the layer freezes", "overhang"),
|
|
|
|
| 16 |
|
| 17 |
# Conservative per-material starting points for the offline/parse-fail fallback.
|
| 18 |
_FALLBACK_SETTINGS: dict[str, dict[str, float]] = {
|
| 19 |
+
"PLA": {"nozzle_temp": 205, "bed_temp": 60, "retraction_mm": 5, "fan_pct": 100, "first_layer_fan_pct": 0, "layer_height": 0.20},
|
| 20 |
+
"PETG": {"nozzle_temp": 235, "bed_temp": 80, "retraction_mm": 4, "fan_pct": 40, "first_layer_fan_pct": 0, "layer_height": 0.20},
|
| 21 |
+
"ABS": {"nozzle_temp": 245, "bed_temp": 100, "retraction_mm": 4, "fan_pct": 20, "first_layer_fan_pct": 0, "layer_height": 0.20},
|
| 22 |
+
"TPU": {"nozzle_temp": 228, "bed_temp": 50, "retraction_mm": 2, "fan_pct": 40, "first_layer_fan_pct": 0, "layer_height": 0.24},
|
| 23 |
}
|
| 24 |
_GEOMETRY_RISK = {
|
| 25 |
"overhang": ("underside of the overhang", "sag", "steep overhangs droop before the layer freezes", "overhang"),
|
core/models.py
CHANGED
|
@@ -37,6 +37,7 @@ class PrintSettings(BaseModel):
|
|
| 37 |
retraction_mm: float
|
| 38 |
fan_pct: float
|
| 39 |
first_layer_fan_pct: float
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
class RiskRegion(BaseModel):
|
|
|
|
| 37 |
retraction_mm: float
|
| 38 |
fan_pct: float
|
| 39 |
first_layer_fan_pct: float
|
| 40 |
+
layer_height: float = 0.2 # mm β drives virtual-print preview cross-sections
|
| 41 |
|
| 42 |
|
| 43 |
class RiskRegion(BaseModel):
|
core/prompts.py
CHANGED
|
@@ -27,7 +27,7 @@ OUTPUT_CONTRACT = """Respond ONLY with valid JSON, no prose outside it, in exact
|
|
| 27 |
"reasoning": "2-4 sentences. START with your evaluation of the prior jobs: what transfers, what doesn't, and why. Then the decision.",
|
| 28 |
"settings": {
|
| 29 |
"nozzle_temp": <C>, "bed_temp": <C>, "retraction_mm": <mm>,
|
| 30 |
-
"fan_pct": <0-100>, "first_layer_fan_pct": <0-100>
|
| 31 |
},
|
| 32 |
"risks": [
|
| 33 |
{"location": "where on the part", "risk": "sag|stringing|adhesion|warping|delamination",
|
|
|
|
| 27 |
"reasoning": "2-4 sentences. START with your evaluation of the prior jobs: what transfers, what doesn't, and why. Then the decision.",
|
| 28 |
"settings": {
|
| 29 |
"nozzle_temp": <C>, "bed_temp": <C>, "retraction_mm": <mm>,
|
| 30 |
+
"fan_pct": <0-100>, "first_layer_fan_pct": <0-100>, "layer_height": <mm, e.g. 0.12-0.28>
|
| 31 |
},
|
| 32 |
"risks": [
|
| 33 |
{"location": "where on the part", "risk": "sag|stringing|adhesion|warping|delamination",
|
core/spine.py
CHANGED
|
@@ -23,6 +23,7 @@ COMMON_BOUNDS: dict[str, tuple[float, float]] = {
|
|
| 23 |
"retraction_mm": (0.5, 8.0),
|
| 24 |
"fan_pct": (0.0, 100.0),
|
| 25 |
"first_layer_fan_pct": (0.0, 30.0),
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
|
|
|
|
| 23 |
"retraction_mm": (0.5, 8.0),
|
| 24 |
"fan_pct": (0.0, 100.0),
|
| 25 |
"first_layer_fan_pct": (0.0, 30.0),
|
| 26 |
+
"layer_height": (0.08, 0.60),
|
| 27 |
}
|
| 28 |
|
| 29 |
|
core/theme.py
CHANGED
|
@@ -202,31 +202,20 @@ textarea:focus, input:focus {{ border-color:var(--ao-orange) !important; box-sha
|
|
| 202 |
.ce-icon-arrow::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>"); mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>"); }}
|
| 203 |
|
| 204 |
/* Arrow AFTER text (right-side icon β indicates "next step"). For buttons whose
|
| 205 |
-
label means "proceed to next stage" (SLICE β, PRINT β, REFRESH β).
|
| 206 |
-
.
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
-
|
| 213 |
-
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
|
| 214 |
-
}}
|
| 215 |
-
/* Inside a Gradio button the label is nested in a <span> β attach to button so
|
| 216 |
-
the ::after lands at the end of the visible label, not after the wrapper. */
|
| 217 |
-
.ce-icon-arrow-after button::after, button.ce-icon-arrow-after::after {{
|
| 218 |
-
content:""; display:inline-block; width:14px; height:14px;
|
| 219 |
-
margin-left:8px; vertical-align:-2px;
|
| 220 |
background:currentColor; -webkit-mask-size:contain; mask-size:contain;
|
| 221 |
-webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
|
| 222 |
-webkit-mask-position:center; mask-position:center;
|
| 223 |
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
|
| 224 |
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
|
| 225 |
}}
|
| 226 |
-
/* But ALSO put one inside the inner span so it appears flush with the label text */
|
| 227 |
-
.ce-icon-arrow-after button span::after {{
|
| 228 |
-
content:"\2192"; margin-left:6px; font-weight:900; opacity:.95;
|
| 229 |
-
}}
|
| 230 |
|
| 231 |
/* ββ TAB ICONS (CSS-only via :nth-child on the four workflow tabs) ββ */
|
| 232 |
/* Tab bar order is fixed: LOAD Β· SLICE Β· PRINT Β· REVIEW. Each gets its own icon
|
|
@@ -585,6 +574,48 @@ CSS += """
|
|
| 585 |
/* model switcher pills sit in the header row β KEEP concise, don't conflict with main rule */
|
| 586 |
#ce-modelswitch .ce-seg label {{ font-size:11px !important; padding:5px 12px !important; }}
|
| 587 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
/* hover callout (info icon next to the model switcher) */
|
| 589 |
.ce-callout { position:relative; display:inline-flex; align-items:center; gap:4px; cursor:help;
|
| 590 |
color:var(--ao-orange); font-size:11px; letter-spacing:1px; text-transform:uppercase; }
|
|
@@ -716,6 +747,8 @@ _ICONS: dict[str, tuple[str, bool]] = {
|
|
| 716 |
"thermo": ("M14 14V5a2 2 0 10-4 0v9a4 4 0 104 0z", False),
|
| 717 |
"droplet": ("M12 2.5C12 2.5 5 10 5 14a7 7 0 0014 0c0-4-7-11.5-7-11.5z", False),
|
| 718 |
"target": ("M12 2v3 M12 19v3 M2 12h3 M19 12h3 M12 7a5 5 0 100 10 5 5 0 000-10z", False),
|
|
|
|
|
|
|
| 719 |
"printer": ("M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 "
|
| 720 |
"M6 14h12v8H6z", False),
|
| 721 |
"play": ("M6 4l14 8-14 8V4z", True),
|
|
|
|
| 202 |
.ce-icon-arrow::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>"); mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>"); }}
|
| 203 |
|
| 204 |
/* Arrow AFTER text (right-side icon β indicates "next step"). For buttons whose
|
| 205 |
+
label means "proceed to next stage" (SLICE β, PRINT β, REFRESH β).
|
| 206 |
+
Gradio wraps each button in a .wrap; elem_classes lands on that wrapper, so we
|
| 207 |
+
target the label <span> inside the <button> and append the arrow as an SVG
|
| 208 |
+
mask-image pseudo-element. */
|
| 209 |
+
.ce-icon-arrow-after button {{ display:inline-flex !important; align-items:center !important; gap:4px !important; }}
|
| 210 |
+
.ce-icon-arrow-after button span::after {{
|
| 211 |
+
content:""; display:inline-block; width:14px; height:14px; flex:0 0 14px;
|
| 212 |
+
margin-left:4px; vertical-align:-2px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
background:currentColor; -webkit-mask-size:contain; mask-size:contain;
|
| 214 |
-webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
|
| 215 |
-webkit-mask-position:center; mask-position:center;
|
| 216 |
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
|
| 217 |
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
|
| 218 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
/* ββ TAB ICONS (CSS-only via :nth-child on the four workflow tabs) ββ */
|
| 221 |
/* Tab bar order is fixed: LOAD Β· SLICE Β· PRINT Β· REVIEW. Each gets its own icon
|
|
|
|
| 574 |
/* model switcher pills sit in the header row β KEEP concise, don't conflict with main rule */
|
| 575 |
#ce-modelswitch .ce-seg label {{ font-size:11px !important; padding:5px 12px !important; }}
|
| 576 |
|
| 577 |
+
/* compact inline pill groups inside the envbar (BUILD-PLATE POSITION + MATERIAL) */
|
| 578 |
+
.ce-envbar .ce-inline-group { display:flex !important; flex-direction:column !important;
|
| 579 |
+
gap:3px !important; margin:0 10px !important; }
|
| 580 |
+
.ce-envbar .ce-inline-label { color:var(--ao-orange); font-size:10px !important;
|
| 581 |
+
letter-spacing:1.5px; text-transform:uppercase; font-weight:700;
|
| 582 |
+
display:flex !important; align-items:center !important; gap:4px; }
|
| 583 |
+
.ce-envbar .ce-inline-label .ce-callout { font-size:10px !important; }
|
| 584 |
+
.ce-envbar .ce-inline-label .ce-tip { width:280px; top:120%; }
|
| 585 |
+
/* inline pills are smaller than the full-width ones */
|
| 586 |
+
.ce-envbar .ce-pills label { padding:4px 10px !important; font-size:11px !important; }
|
| 587 |
+
.ce-envbar .ce-pills label span { display:inline-flex !important; align-items:center !important;
|
| 588 |
+
gap:5px !important; }
|
| 589 |
+
/* pill icons by value (small, before the label text) */
|
| 590 |
+
.ce-pills label:has(input[value="center"]) span::before,
|
| 591 |
+
.ce-pills label:has(input[value="edge"]) span::before,
|
| 592 |
+
.ce-pills label:has(input[value="corner"]) span::before,
|
| 593 |
+
.ce-pills label:has(input[value="PLA"]) span::before,
|
| 594 |
+
.ce-pills label:has(input[value="PETG"]) span::before,
|
| 595 |
+
.ce-pills label:has(input[value="ABS"]) span::before,
|
| 596 |
+
.ce-pills label:has(input[value="TPU"]) span::before {
|
| 597 |
+
content:""; display:inline-block; width:11px; height:11px; flex:0 0 11px;
|
| 598 |
+
background:currentColor; -webkit-mask-size:contain; mask-size:contain;
|
| 599 |
+
-webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
|
| 600 |
+
-webkit-mask-position:center; mask-position:center; }
|
| 601 |
+
.ce-pills label:has(input[value="center"]) span::before {
|
| 602 |
+
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2v3 M12 19v3 M2 12h3 M19 12h3 M12 7a5 5 0 100 10 5 5 0 000-10z'/></svg>");
|
| 603 |
+
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2v3 M12 19v3 M2 12h3 M19 12h3 M12 7a5 5 0 100 10 5 5 0 000-10z'/></svg>"); }
|
| 604 |
+
.ce-pills label:has(input[value="edge"]) span::before {
|
| 605 |
+
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 4h16v16H4z M4 4v16'/></svg>");
|
| 606 |
+
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 4h16v16H4z M4 4v16'/></svg>"); }
|
| 607 |
+
.ce-pills label:has(input[value="corner"]) span::before {
|
| 608 |
+
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 4h16v16H4z M4 4v7 M4 4h7'/></svg>");
|
| 609 |
+
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 4h16v16H4z M4 4v7 M4 4h7'/></svg>"); }
|
| 610 |
+
.ce-pills label:has(input[value="PLA"]) span::before,
|
| 611 |
+
.ce-pills label:has(input[value="PETG"]) span::before,
|
| 612 |
+
.ce-pills label:has(input[value="ABS"]) span::before,
|
| 613 |
+
.ce-pills label:has(input[value="TPU"]) span::before {
|
| 614 |
+
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2.5C12 2.5 5 10 5 14a7 7 0 0014 0c0-4-7-11.5-7-11.5z'/></svg>");
|
| 615 |
+
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2.5C12 2.5 5 10 5 14a7 7 0 0014 0c0-4-7-11.5-7-11.5z'/></svg>"); }
|
| 616 |
+
/* tighten space below the envbar so the PART card sits directly under it */
|
| 617 |
+
.ce-envbar { margin-bottom:2px !important; }
|
| 618 |
+
|
| 619 |
/* hover callout (info icon next to the model switcher) */
|
| 620 |
.ce-callout { position:relative; display:inline-flex; align-items:center; gap:4px; cursor:help;
|
| 621 |
color:var(--ao-orange); font-size:11px; letter-spacing:1px; text-transform:uppercase; }
|
|
|
|
| 747 |
"thermo": ("M14 14V5a2 2 0 10-4 0v9a4 4 0 104 0z", False),
|
| 748 |
"droplet": ("M12 2.5C12 2.5 5 10 5 14a7 7 0 0014 0c0-4-7-11.5-7-11.5z", False),
|
| 749 |
"target": ("M12 2v3 M12 19v3 M2 12h3 M19 12h3 M12 7a5 5 0 100 10 5 5 0 000-10z", False),
|
| 750 |
+
"edge": ("M4 4h16v16H4z M4 4v16", False),
|
| 751 |
+
"corner": ("M4 4h16v16H4z M4 4v7 M4 4h7", False),
|
| 752 |
"printer": ("M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 "
|
| 753 |
"M6 14h12v8H6z", False),
|
| 754 |
"play": ("M6 4l14 8-14 8V4z", True),
|
core/viewer.py
CHANGED
|
@@ -288,6 +288,7 @@ def gcode_readout(settings: PrintSettings, material: str) -> str:
|
|
| 288 |
"""Short snippet whose header lines come from the proposed settings."""
|
| 289 |
return "\n".join([
|
| 290 |
f"; Chief Engineer β start g-code for {material} (header tied to recommendation)",
|
|
|
|
| 291 |
f"M140 S{settings.bed_temp:.0f} ; set bed",
|
| 292 |
f"M104 S{settings.nozzle_temp:.0f} ; set nozzle",
|
| 293 |
f"M190 S{settings.bed_temp:.0f} ; wait for bed",
|
|
@@ -328,9 +329,10 @@ def quality_curve_html(trajectory: list[float], threshold: float = 0.7) -> str:
|
|
| 328 |
)
|
| 329 |
|
| 330 |
|
| 331 |
-
def iteration_log_html(records, verdicts=None) -> str:
|
| 332 |
"""Per-iteration log. `verdicts` (optional, aligned to records) adds the QA
|
| 333 |
-
Inspector's terse grade on each run β the second voice in the loop.
|
|
|
|
| 334 |
rows = []
|
| 335 |
for i, r in enumerate(records):
|
| 336 |
clean = r.result.outcome == "success"
|
|
@@ -339,6 +341,10 @@ def iteration_log_html(records, verdicts=None) -> str:
|
|
| 339 |
if verdicts and i < len(verdicts) and verdicts[i] is not None:
|
| 340 |
v = verdicts[i]
|
| 341 |
insp = (f"<br><span style='color:{v.color};'>{icon('search')} inspector [{v.stance}]: {v.headline}</span>")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
rows.append(
|
| 343 |
f"<div style='font-family:ui-monospace,monospace;font-size:11px;border-left:3px solid {col};"
|
| 344 |
f"background:var(--ao-surface);padding:5px 10px;margin:3px 0;color:var(--ao-text);'>"
|
|
@@ -346,7 +352,7 @@ def iteration_log_html(records, verdicts=None) -> str:
|
|
| 346 |
f"noz {r.settings.nozzle_temp:.0f}Β°C bed {r.settings.bed_temp:.0f}Β°C fan {r.settings.fan_pct:.0f}% "
|
| 347 |
f"ret {r.settings.retraction_mm:.1f}mm"
|
| 348 |
+ (" Β· <span style='color:var(--ao-orange);'>spine-clamped</span>" if r.clamped else "")
|
| 349 |
-
+ f"<br><span style='color:var(--ao-outline);'>β³ {r.learned}</span>{insp}</div>"
|
| 350 |
)
|
| 351 |
return "".join(rows)
|
| 352 |
|
|
|
|
| 288 |
"""Short snippet whose header lines come from the proposed settings."""
|
| 289 |
return "\n".join([
|
| 290 |
f"; Chief Engineer β start g-code for {material} (header tied to recommendation)",
|
| 291 |
+
f"; layer height {settings.layer_height:.2f} mm",
|
| 292 |
f"M140 S{settings.bed_temp:.0f} ; set bed",
|
| 293 |
f"M104 S{settings.nozzle_temp:.0f} ; set nozzle",
|
| 294 |
f"M190 S{settings.bed_temp:.0f} ; wait for bed",
|
|
|
|
| 329 |
)
|
| 330 |
|
| 331 |
|
| 332 |
+
def iteration_log_html(records, verdicts=None, timings=None) -> str:
|
| 333 |
"""Per-iteration log. `verdicts` (optional, aligned to records) adds the QA
|
| 334 |
+
Inspector's terse grade on each run β the second voice in the loop.
|
| 335 |
+
`timings` (optional, aligned to records) adds per-iteration elapsed ms."""
|
| 336 |
rows = []
|
| 337 |
for i, r in enumerate(records):
|
| 338 |
clean = r.result.outcome == "success"
|
|
|
|
| 341 |
if verdicts and i < len(verdicts) and verdicts[i] is not None:
|
| 342 |
v = verdicts[i]
|
| 343 |
insp = (f"<br><span style='color:{v.color};'>{icon('search')} inspector [{v.stance}]: {v.headline}</span>")
|
| 344 |
+
timing = ""
|
| 345 |
+
if timings and i < len(timings):
|
| 346 |
+
timing = (f"<span style='float:right;color:var(--ao-outline);'>"
|
| 347 |
+
f"+{timings[i]*1000:.0f}ms</span>")
|
| 348 |
rows.append(
|
| 349 |
f"<div style='font-family:ui-monospace,monospace;font-size:11px;border-left:3px solid {col};"
|
| 350 |
f"background:var(--ao-surface);padding:5px 10px;margin:3px 0;color:var(--ao-text);'>"
|
|
|
|
| 352 |
f"noz {r.settings.nozzle_temp:.0f}Β°C bed {r.settings.bed_temp:.0f}Β°C fan {r.settings.fan_pct:.0f}% "
|
| 353 |
f"ret {r.settings.retraction_mm:.1f}mm"
|
| 354 |
+ (" Β· <span style='color:var(--ao-orange);'>spine-clamped</span>" if r.clamped else "")
|
| 355 |
+
+ f"{timing}<br><span style='color:var(--ao-outline);'>β³ {r.learned}</span>{insp}</div>"
|
| 356 |
)
|
| 357 |
return "".join(rows)
|
| 358 |
|
core/widgets.py
CHANGED
|
@@ -20,7 +20,7 @@ from pathlib import Path
|
|
| 20 |
_ISO = 0.55 # oblique projection skew, matches sim/virtual_printer
|
| 21 |
|
| 22 |
|
| 23 |
-
def _projected_layers(mesh_path: str | Path, layer_height_mm: float = 0.
|
| 24 |
max_layers: int = 44, max_segs_per_layer: int = 520) -> dict:
|
| 25 |
"""Slice the mesh and return projected 2D polylines per layer + bounds.
|
| 26 |
|
|
@@ -134,22 +134,30 @@ def layer_image(mesh_path: str | Path | None, idx: int, n: int = SCRUB_LAYERS,
|
|
| 134 |
return np.asarray(img)
|
| 135 |
|
| 136 |
|
| 137 |
-
def virtual_printer_html(mesh_path: str | Path | None,
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
if not mesh_path or not Path(str(mesh_path)).exists():
|
| 140 |
return ("<div class='ce-rule'>VIRTUAL PRINT</div>"
|
| 141 |
"<div class='ce-sub'>run a recommendation to slice the part β</div>")
|
| 142 |
-
|
|
|
|
| 143 |
if not data:
|
| 144 |
return ("<div class='ce-rule'>VIRTUAL PRINT</div>"
|
| 145 |
"<div class='ce-sub'>mesh too thin to slice.</div>")
|
| 146 |
payload = json.dumps(data).replace("'", "'")
|
|
|
|
| 147 |
return (
|
| 148 |
"<div class='ce-rule'>VIRTUAL PRINT</div>"
|
| 149 |
"<div style='border:1px solid var(--ao-outline-dim);background:#0a0c14;'>"
|
| 150 |
f"<canvas class='ce-vp' width='520' height='300' data-vp='{payload}' "
|
| 151 |
"style='width:100%;height:auto;display:block;'></canvas></div>"
|
| 152 |
-
f"<div class='ce-sub' style='margin-top:2px;'>{caption} Β· real cross-sections of "
|
| 153 |
"this part, layer by layer (motion preview β not a slicer)</div>"
|
| 154 |
)
|
| 155 |
|
|
|
|
| 20 |
_ISO = 0.55 # oblique projection skew, matches sim/virtual_printer
|
| 21 |
|
| 22 |
|
| 23 |
+
def _projected_layers(mesh_path: str | Path, layer_height_mm: float = 0.2,
|
| 24 |
max_layers: int = 44, max_segs_per_layer: int = 520) -> dict:
|
| 25 |
"""Slice the mesh and return projected 2D polylines per layer + bounds.
|
| 26 |
|
|
|
|
| 134 |
return np.asarray(img)
|
| 135 |
|
| 136 |
|
| 137 |
+
def virtual_printer_html(mesh_path: str | Path | None,
|
| 138 |
+
settings: "PrintSettings | None" = None,
|
| 139 |
+
caption: str = "") -> str:
|
| 140 |
+
"""A live virtual-printer canvas for the cockpit (animated by VP_JS).
|
| 141 |
+
|
| 142 |
+
Uses the layer height from the proposed PrintSettings so the preview is tied
|
| 143 |
+
to the actual recommendation rather than a hard-coded default.
|
| 144 |
+
"""
|
| 145 |
if not mesh_path or not Path(str(mesh_path)).exists():
|
| 146 |
return ("<div class='ce-rule'>VIRTUAL PRINT</div>"
|
| 147 |
"<div class='ce-sub'>run a recommendation to slice the part β</div>")
|
| 148 |
+
layer_height = settings.layer_height if settings else 0.2
|
| 149 |
+
data = _projected_layers(mesh_path, layer_height_mm=layer_height)
|
| 150 |
if not data:
|
| 151 |
return ("<div class='ce-rule'>VIRTUAL PRINT</div>"
|
| 152 |
"<div class='ce-sub'>mesh too thin to slice.</div>")
|
| 153 |
payload = json.dumps(data).replace("'", "'")
|
| 154 |
+
lh_note = f"{layer_height:.2f} mm layers"
|
| 155 |
return (
|
| 156 |
"<div class='ce-rule'>VIRTUAL PRINT</div>"
|
| 157 |
"<div style='border:1px solid var(--ao-outline-dim);background:#0a0c14;'>"
|
| 158 |
f"<canvas class='ce-vp' width='520' height='300' data-vp='{payload}' "
|
| 159 |
"style='width:100%;height:auto;display:block;'></canvas></div>"
|
| 160 |
+
f"<div class='ce-sub' style='margin-top:2px;'>{caption} Β· {lh_note} Β· real cross-sections of "
|
| 161 |
"this part, layer by layer (motion preview β not a slicer)</div>"
|
| 162 |
)
|
| 163 |
|
dist/deliberation/deliberations.jsonl
CHANGED
|
@@ -1,29 +1,29 @@
|
|
| 1 |
-
{"session_id": "abs-overhang-edge", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "No close precedent exists
|
| 2 |
{"session_id": "abs-overhang-edge", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β no clamp.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:14+00:00"}
|
| 3 |
-
{"session_id": "abs-overhang-edge", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "
|
| 4 |
-
{"session_id": "abs-overhang-edge", "track": "
|
| 5 |
-
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 5, "agent": "
|
| 6 |
-
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 6, "agent": "
|
| 7 |
-
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 7, "agent": "
|
| 8 |
-
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 8, "agent": "
|
| 9 |
-
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 9, "agent": "
|
| 10 |
-
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 10, "agent": "
|
| 11 |
-
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 11, "agent": "
|
| 12 |
-
{"session_id": "abs-overhang-edge", "track": "
|
| 13 |
-
{"session_id": "
|
| 14 |
-
{"session_id": "petg-overhang-center", "track": "preflight", "turn":
|
| 15 |
-
{"session_id": "petg-overhang-center", "track": "preflight", "turn":
|
| 16 |
-
{"session_id": "petg-overhang-center", "track": "preflight", "turn":
|
| 17 |
-
{"session_id": "petg-overhang-center", "track": "print-loop", "turn":
|
| 18 |
-
{"session_id": "petg-overhang-center", "track": "print-loop", "turn":
|
| 19 |
-
{"session_id": "petg-overhang-center", "track": "print-loop", "turn":
|
| 20 |
-
{"session_id": "petg-overhang-center", "track": "print-loop", "turn":
|
| 21 |
-
{"session_id": "petg-overhang-center", "track": "print-loop", "turn":
|
| 22 |
-
{"session_id": "petg-overhang-center", "track": "print-loop", "turn":
|
| 23 |
-
{"session_id": "petg-overhang-center", "track": "review", "turn":
|
| 24 |
-
{"session_id": "pla-adhesion-center", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "The
|
| 25 |
{"session_id": "pla-adhesion-center", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β no clamp.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:02:55+00:00"}
|
| 26 |
-
{"session_id": "pla-adhesion-center", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "caution", "content": "
|
| 27 |
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 4, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:09+00:00"}
|
| 28 |
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 5, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=1.00). β Engineer flagged adhesion; the settings covered it. Credit the call β or it was conservative.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:16+00:00"}
|
| 29 |
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 6, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:23+00:00"}
|
|
@@ -31,14 +31,13 @@
|
|
| 31 |
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 8, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:37+00:00"}
|
| 32 |
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 9, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=1.00). β Engineer flagged adhesion; the settings covered it. Credit the call β or it was conservative.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:44+00:00"}
|
| 33 |
{"session_id": "pla-adhesion-center", "track": "review", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β PLA/adhesion climbed 1.00 β 1.00. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:51+00:00"}
|
| 34 |
-
{"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "No close precedent exists. TPU is flexible and prone to stringing
|
| 35 |
{"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β no clamp.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:05+00:00"}
|
| 36 |
-
{"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "
|
| 37 |
-
{"session_id": "tpu-stringing-edge", "track": "
|
| 38 |
-
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 5, "agent": "
|
| 39 |
-
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 6, "agent": "
|
| 40 |
-
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 7, "agent": "
|
| 41 |
-
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 8, "agent": "
|
| 42 |
-
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 9, "agent": "
|
| 43 |
-
{"session_id": "tpu-stringing-edge", "track": "
|
| 44 |
-
{"session_id": "tpu-stringing-edge", "track": "review", "turn": 11, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β TPU/stringing climbed 0.86 β 0.86. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:05:08+00:00"}
|
|
|
|
| 1 |
+
{"session_id": "abs-overhang-edge", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "No close precedent exists. ABS is high warp material, and overhangs amplify this risk significantly. The room temp (26Β°C) is moderate but requires active thermal management to prevent warping during cooling. We must maintain a stable thermal gradient from the bed up through the print area. I will use established ABS parameters for adhesion and minimize forced air.\nProposed: nozzle 255Β°C, bed 100Β°C, fan 30%, first-layer fan 0%, retraction 4.0mm.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:07+00:00"}
|
| 2 |
{"session_id": "abs-overhang-edge", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β no clamp.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:14+00:00"}
|
| 3 |
+
{"session_id": "abs-overhang-edge", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "caution", "content": "Thermal management remains a guess, not a plan. β The reliance on ambient room temp and moderate bed temps doesn't guarantee stability. Warping risk is unflagged until it happens.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:21+00:00"}
|
| 4 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 4, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 1: sag (quality 0.19, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.19) β learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:28+00:00"}
|
| 5 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 5, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β exactly as called. β Quality 0.19. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:35+00:00"}
|
| 6 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 6, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 2: sag (quality 0.27, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.27) β learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:42+00:00"}
|
| 7 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 7, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β exactly as called. β Quality 0.27. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:49+00:00"}
|
| 8 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 8, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 3: sag (quality 0.33, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.33) β learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:56+00:00"}
|
| 9 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 9, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β exactly as called. β Quality 0.33. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:03+00:00"}
|
| 10 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 10, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 4: warp (quality 0.34, dominant cost: warp). Policy: ABS/overhang/mid/humid: warp (q=0.34) β learned: fan_pct -10.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:10+00:00"}
|
| 11 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 11, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "missed", "content": "Failed on warping β and it wasn't flagged. β Quality 0.34. The Engineer didn't predict this mode. That gap is what the next iteration has to close.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:17+00:00"}
|
| 12 |
+
{"session_id": "abs-overhang-edge", "track": "review", "turn": 12, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "caution", "content": "Improving but not yet clean (best 0.34). β Quality rose 0.19 β 0.34 over 4 runs but never crossed the bar. More iterations or a different lever needed β the loop is learning, slowly.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:24+00:00"}
|
| 13 |
+
{"session_id": "petg-overhang-center", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "The prior job [1] is highly relevant, showing PETG overhang sag at 23Β°C. Since the current environment (24Β°C, 55% RH) is only slightly warmer and drier, the lesson about cooling compromise remains critical. I must maintain the fan setting high enough to stabilize the plastic but low enough not to induce thermal stress or excessive warping on PETG. The overhang geometry dictates careful management of cooling rates.\nProposed: nozzle 245Β°C, bed 85Β°C, fan 60%, first-layer fan 0%, retraction 5.0mm.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:31+00:00"}
|
| 14 |
+
{"session_id": "petg-overhang-center", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β no clamp.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:38+00:00"}
|
| 15 |
+
{"session_id": "petg-overhang-center", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "dispute", "content": "Overhang sag and environment are not solved by minor temperature tweaks. β The proposed fan settings fail to account for PETG's inherent thermal instability. Adhesion risk remains high regardless of the slight environmental shift.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:45+00:00"}
|
| 16 |
+
{"session_id": "petg-overhang-center", "track": "preflight", "turn": 4, "agent": "Operator", "role": "Operator", "act": "override", "stance": "override", "content": "Acknowledged La Forge's objection. Proceeding to print on the operator's call.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:52+00:00"}
|
| 17 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 5, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:59+00:00"}
|
| 18 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 6, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β Engineer flagged sag; the settings covered it. Credit the call β or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:06+00:00"}
|
| 19 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 7, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:13+00:00"}
|
| 20 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 8, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β Engineer flagged sag; the settings covered it. Credit the call β or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:20+00:00"}
|
| 21 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 9, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:27+00:00"}
|
| 22 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β Engineer flagged sag; the settings covered it. Credit the call β or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:34+00:00"}
|
| 23 |
+
{"session_id": "petg-overhang-center", "track": "review", "turn": 11, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β PETG/overhang climbed 0.73 β 0.73. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:41+00:00"}
|
| 24 |
+
{"session_id": "pla-adhesion-center", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "The prior job [1] (PLA/adhesion @ 19Β°C, 38% RH) is highly relevant because the current ambient temperature (21Β°C) is still cool relative to optimal printing conditions, and adhesion failure was the primary issue. The lesson that cold ambient air compromises first-layer bonding remains valid. Therefore, we must maintain a high bed temp and minimize cooling effects on the first few layers.\nProposed: nozzle 215Β°C, bed 60Β°C, fan 30%, first-layer fan 0%, retraction 5.0mm.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:02:48+00:00"}
|
| 25 |
{"session_id": "pla-adhesion-center", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β no clamp.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:02:55+00:00"}
|
| 26 |
+
{"session_id": "pla-adhesion-center", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "caution", "content": "The ambient conditions are not a guarantee. β Relying on past environmental data to predict adhesion success is thin. Watch for drafts or humidity spikes.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:02+00:00"}
|
| 27 |
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 4, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:09+00:00"}
|
| 28 |
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 5, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=1.00). β Engineer flagged adhesion; the settings covered it. Credit the call β or it was conservative.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:16+00:00"}
|
| 29 |
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 6, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:23+00:00"}
|
|
|
|
| 31 |
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 8, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:37+00:00"}
|
| 32 |
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 9, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=1.00). β Engineer flagged adhesion; the settings covered it. Credit the call β or it was conservative.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:44+00:00"}
|
| 33 |
{"session_id": "pla-adhesion-center", "track": "review", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β PLA/adhesion climbed 1.00 β 1.00. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:51+00:00"}
|
| 34 |
+
{"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "No close precedent exists (TPU/stringing). TPU is flexible and prone to stringing due to its viscoelastic nature, especially when printed at ambient temperatures. The high humidity (65% RH) will exacerbate this by increasing surface tackiness. We must minimize the time the plastic spends in the air after extrusion while maintaining flow. I'll keep the nozzle temp low but stable, increase retraction slightly for better cleanup, and use a moderate fan to help cool the stringing areas quickly.\nProposed: nozzle 230Β°C, bed 45Β°C, fan 60%, first-layer fan 0%, retraction 3.0mm.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:03:58+00:00"}
|
| 35 |
{"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β no clamp.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:05+00:00"}
|
| 36 |
+
{"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "caution", "content": "Humidity risk outweighs temperature adjustments. β 65% RH and TPU are a poor pairing. Focus on enclosure or drying, not just retraction settings.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:12+00:00"}
|
| 37 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 4, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:19+00:00"}
|
| 38 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 5, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β Engineer flagged stringing; the settings covered it. Credit the call β or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:26+00:00"}
|
| 39 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 6, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:33+00:00"}
|
| 40 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 7, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β Engineer flagged stringing; the settings covered it. Credit the call β or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:40+00:00"}
|
| 41 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 8, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:47+00:00"}
|
| 42 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 9, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β Engineer flagged stringing; the settings covered it. Credit the call β or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:54+00:00"}
|
| 43 |
+
{"session_id": "tpu-stringing-edge", "track": "review", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β TPU/stringing climbed 0.86 β 0.86. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:05:01+00:00"}
|
|
|
learn/loop.py
CHANGED
|
@@ -67,8 +67,8 @@ def _record_lesson(job: Job, env: Environment, s: PrintSettings, result: SimResu
|
|
| 67 |
|
| 68 |
|
| 69 |
def run_iteration(job: Job, env: Environment, policy: LearnedPolicy, ledger: LedgerManager,
|
| 70 |
-
n: int, record: bool = True) -> IterationRecord:
|
| 71 |
-
proposed = policy.propose(job.material, job.geometry_type, env)
|
| 72 |
checked = _SPINE.check(proposed, job.material)
|
| 73 |
settings = checked.settings
|
| 74 |
result = simulate(settings, job, env)
|
|
|
|
| 67 |
|
| 68 |
|
| 69 |
def run_iteration(job: Job, env: Environment, policy: LearnedPolicy, ledger: LedgerManager,
|
| 70 |
+
n: int, record: bool = True, overrides: PrintSettings | None = None) -> IterationRecord:
|
| 71 |
+
proposed = overrides if overrides is not None else policy.propose(job.material, job.geometry_type, env)
|
| 72 |
checked = _SPINE.check(proposed, job.material)
|
| 73 |
settings = checked.settings
|
| 74 |
result = simulate(settings, job, env)
|
test_core.py
CHANGED
|
@@ -86,9 +86,27 @@ def test_gcode_readout_ties_to_settings():
|
|
| 86 |
s = PrintSettings(nozzle_temp=205, bed_temp=60, retraction_mm=5, fan_pct=100, first_layer_fan_pct=0)
|
| 87 |
g = gcode_readout(s, "PLA")
|
| 88 |
assert "M104 S205" in g and "M140 S60" in g, g
|
|
|
|
| 89 |
print("β g-code header is populated from proposed settings")
|
| 90 |
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
def test_ingest_distiller():
|
| 93 |
from pathlib import Path as _P
|
| 94 |
from ingest.distill import parse_prusa_ini, parse_klipper_cfg, parse_marlin_config
|
|
@@ -163,6 +181,7 @@ if __name__ == "__main__":
|
|
| 163 |
test_reflect_appends()
|
| 164 |
test_retrieval_orders_by_env_distance()
|
| 165 |
test_gcode_readout_ties_to_settings()
|
|
|
|
| 166 |
test_ingest_distiller()
|
| 167 |
test_precedent_eval_narration()
|
| 168 |
test_simulator_physical_and_deterministic()
|
|
|
|
| 86 |
s = PrintSettings(nozzle_temp=205, bed_temp=60, retraction_mm=5, fan_pct=100, first_layer_fan_pct=0)
|
| 87 |
g = gcode_readout(s, "PLA")
|
| 88 |
assert "M104 S205" in g and "M140 S60" in g, g
|
| 89 |
+
assert "layer height 0.20 mm" in g, g
|
| 90 |
print("β g-code header is populated from proposed settings")
|
| 91 |
|
| 92 |
|
| 93 |
+
# GIF export button removed; keep motion preview tests via UI smoke if needed.
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def test_virtual_printer_html_ties_to_settings():
|
| 97 |
+
from core.widgets import virtual_printer_html
|
| 98 |
+
from core.viewer import generate_primitive
|
| 99 |
+
from core.models import PrintSettings
|
| 100 |
+
mesh, _geo = generate_primitive("box", 20)
|
| 101 |
+
default_html = virtual_printer_html(mesh)
|
| 102 |
+
assert "0.20 mm layers" in default_html, default_html
|
| 103 |
+
fine = PrintSettings(nozzle_temp=200, bed_temp=60, retraction_mm=4.5, fan_pct=80,
|
| 104 |
+
first_layer_fan_pct=0, layer_height=0.12)
|
| 105 |
+
fine_html = virtual_printer_html(mesh, settings=fine)
|
| 106 |
+
assert "0.12 mm layers" in fine_html, fine_html
|
| 107 |
+
print("β virtual-print preview layer height follows PrintSettings")
|
| 108 |
+
|
| 109 |
+
|
| 110 |
def test_ingest_distiller():
|
| 111 |
from pathlib import Path as _P
|
| 112 |
from ingest.distill import parse_prusa_ini, parse_klipper_cfg, parse_marlin_config
|
|
|
|
| 181 |
test_reflect_appends()
|
| 182 |
test_retrieval_orders_by_env_distance()
|
| 183 |
test_gcode_readout_ties_to_settings()
|
| 184 |
+
test_virtual_printer_html_ties_to_settings()
|
| 185 |
test_ingest_distiller()
|
| 186 |
test_precedent_eval_narration()
|
| 187 |
test_simulator_physical_and_deterministic()
|