kylebrodeur commited on
Commit
6b09b49
Β·
verified Β·
1 Parent(s): 8627426

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
.agents/skills/hf-cli/.hf-skill-manifest.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "installed_revision": "7bf59b7f85b79c74207b10d5e425934514e8b089",
3
+ "schema_version": 1
4
+ }
.agents/skills/hf-cli/SKILL.md ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: hf-cli
3
+ description: "Hugging Face Hub CLI (`hf`) for downloading, uploading, and managing models, datasets, spaces, buckets, repos, papers, jobs, and more on the Hugging Face Hub. Use when: handling authentication; managing local cache; managing Hugging Face Buckets; running or scheduling jobs on Hugging Face infrastructure; managing Hugging Face repos; discussions and pull requests; browsing models, datasets and spaces; reading, searching, or browsing academic papers; managing collections; querying datasets; configuring spaces; setting up webhooks; or deploying and managing HF Inference Endpoints. Make sure to use this skill whenever the user mentions 'hf', 'huggingface', 'Hugging Face', 'huggingface-cli', or 'hugging face cli', or wants to do anything related to the Hugging Face ecosystem and to AI and ML in general. Also use for cloud storage needs like training checkpoints, data pipelines, or agent traces. Use even if the user doesn't explicitly ask for a CLI command. Replaces the deprecated `huggingface-cli`."
4
+ ---
5
+
6
+ Install: `curl -LsSf https://hf.co/cli/install.sh | bash -s`.
7
+
8
+ The Hugging Face Hub CLI tool `hf` is available. IMPORTANT: The `hf` command replaces the deprecated `huggingface-cli` command.
9
+
10
+ Use `hf --help` to view available functions. Note that auth commands are now all under `hf auth` e.g. `hf auth whoami`.
11
+
12
+ Generated with `huggingface_hub v1.19.0`. Run `hf skills add --force` to regenerate.
13
+
14
+ ## Commands
15
+
16
+ - `hf cp SRC` β€” Copy files between local paths, repositories, and buckets. `[--format [auto|human|agent|json|quiet]]`
17
+ - `hf download REPO_ID` β€” Download files from the Hub. `[--type [model|dataset|space] --revision TEXT --include TEXT --exclude TEXT --cache-dir TEXT --local-dir TEXT --force-download --dry-run --max-workers INTEGER --format [auto|human|agent|json|quiet]]`
18
+ - `hf env` β€” Print information about the environment. `[--format [auto|human|agent|json|quiet]]`
19
+ - `hf sync` β€” Sync files between local directory and a bucket. `[--delete --ignore-times --ignore-sizes --plan TEXT --apply TEXT --dry-run --include TEXT --exclude TEXT --filter-from TEXT --existing --ignore-existing --verbose --format [auto|human|agent|json|quiet]]`
20
+ - `hf update` β€” Update the `hf` CLI to the latest version. `[--format [auto|human|agent|json|quiet]]`
21
+ - `hf upload REPO_ID` β€” Upload a file or a folder to the Hub. Recommended for single-commit uploads. `[--type [model|dataset|space] --revision TEXT --private --include TEXT --exclude TEXT --delete TEXT --commit-message TEXT --commit-description TEXT --create-pr --every FLOAT --format [auto|human|agent|json|quiet]]`
22
+ - `hf upload-large-folder REPO_ID LOCAL_PATH` β€” Upload a large folder to the Hub. Recommended for resumable uploads. `[--type [model|dataset|space] --revision TEXT --private --include TEXT --exclude TEXT --num-workers INTEGER --no-report --no-bars --format [auto|human|agent|json|quiet]]`
23
+ - `hf version` β€” Print information about the hf version. `[--format [auto|human|agent|json|quiet]]`
24
+
25
+ ### `hf auth` β€” Manage authentication (login, logout, etc.).
26
+
27
+ - `hf auth list` β€” List all stored access tokens. `[--format [auto|human|agent|json|quiet]]`
28
+ - `hf auth login` β€” Login using a token from huggingface.co/settings/tokens. `[--add-to-git-credential --force --format [auto|human|agent|json|quiet]]`
29
+ - `hf auth logout` β€” Logout from a specific token. `[--token-name TEXT --format [auto|human|agent|json|quiet]]`
30
+ - `hf auth switch` β€” Switch between access tokens. `[--token-name TEXT --add-to-git-credential --format [auto|human|agent|json|quiet]]`
31
+ - `hf auth token` β€” Print the current access token to stdout. `[--format [auto|human|agent|json|quiet]]`
32
+ - `hf auth whoami` β€” Find out which huggingface.co account you are logged in as. `[--format [auto|human|agent|json|quiet]]`
33
+
34
+ ### `hf buckets` β€” Commands to interact with buckets.
35
+
36
+ - `hf buckets cp SRC` β€” Copy files between local paths, repositories, and buckets. `[--format [auto|human|agent|json|quiet]]`
37
+ - `hf buckets create BUCKET_ID` β€” Create a new bucket. `[--private --region [us|eu] --exist-ok --format [auto|human|agent|json|quiet]]`
38
+ - `hf buckets delete BUCKET_ID` β€” Delete a bucket. `[--yes --missing-ok --format [auto|human|agent|json|quiet]]`
39
+ - `hf buckets info BUCKET_ID` β€” Get info about a bucket. `[--format [auto|human|agent|json|quiet]]`
40
+ - `hf buckets list` β€” List buckets or files in a bucket. `[--human-readable --tree --recursive --search TEXT --format [auto|human|agent|json|quiet]]`
41
+ - `hf buckets move FROM_ID TO_ID` β€” Move (rename) a bucket to a new name or namespace. `[--format [auto|human|agent|json|quiet]]`
42
+ - `hf buckets remove ARGUMENT` β€” Remove files from a bucket. `[--recursive --yes --dry-run --include TEXT --exclude TEXT --format [auto|human|agent|json|quiet]]`
43
+ - `hf buckets sync` β€” Sync files between local directory and a bucket. `[--delete --ignore-times --ignore-sizes --plan TEXT --apply TEXT --dry-run --include TEXT --exclude TEXT --filter-from TEXT --existing --ignore-existing --verbose --format [auto|human|agent|json|quiet]]`
44
+
45
+ ### `hf cache` β€” Manage local cache directory.
46
+
47
+ - `hf cache list` β€” List cached repositories or revisions. `[--cache-dir TEXT --revisions --filter TEXT --sort [accessed|accessed:asc|accessed:desc|modified|modified:asc|modified:desc|name|name:asc|name:desc|size|size:asc|size:desc] --limit INTEGER --format [auto|human|agent|json|quiet]]`
48
+ - `hf cache prune` β€” Remove detached revisions from the cache. `[--cache-dir TEXT --yes --dry-run --format [auto|human|agent|json|quiet]]`
49
+ - `hf cache rm TARGETS` β€” Remove cached repositories or revisions. `[--cache-dir TEXT --yes --dry-run --format [auto|human|agent|json|quiet]]`
50
+ - `hf cache verify REPO_ID` β€” Verify checksums for a single repo revision from cache or a local directory. `[--type [model|dataset|space] --revision TEXT --cache-dir TEXT --local-dir TEXT --fail-on-missing-files --fail-on-extra-files --format [auto|human|agent|json|quiet]]`
51
+
52
+ ### `hf collections` β€” Interact with collections on the Hub.
53
+
54
+ - `hf collections add-item COLLECTION_SLUG ITEM_ID ITEM_TYPE` β€” Add an item to a collection. `[--note TEXT --exists-ok --format [auto|human|agent|json|quiet]]`
55
+ - `hf collections create TITLE` β€” Create a new collection on the Hub. `[--namespace TEXT --description TEXT --private --exists-ok --format [auto|human|agent|json|quiet]]`
56
+ - `hf collections delete COLLECTION_SLUG` β€” Delete a collection from the Hub. `[--missing-ok --format [auto|human|agent|json|quiet]]`
57
+ - `hf collections delete-item COLLECTION_SLUG ITEM_OBJECT_ID` β€” Delete an item from a collection. `[--missing-ok --format [auto|human|agent|json|quiet]]`
58
+ - `hf collections info COLLECTION_SLUG` β€” Get info about a collection on the Hub. `[--format [auto|human|agent|json|quiet]]`
59
+ - `hf collections list` β€” List collections on the Hub. `[--owner TEXT --item TEXT --sort [lastModified|trending|upvotes] --limit INTEGER --format [auto|human|agent|json|quiet]]`
60
+ - `hf collections update COLLECTION_SLUG` β€” Update a collection's metadata on the Hub. `[--title TEXT --description TEXT --position INTEGER --private --theme TEXT --format [auto|human|agent|json|quiet]]`
61
+ - `hf collections update-item COLLECTION_SLUG ITEM_OBJECT_ID` β€” Update an item in a collection. `[--note TEXT --position INTEGER --format [auto|human|agent|json|quiet]]`
62
+
63
+ ### `hf datasets` β€” Interact with datasets on the Hub.
64
+
65
+ - `hf datasets card DATASET_ID` β€” Get the dataset card (README) for a dataset on the Hub. `[--metadata --text --format [auto|human|agent|json|quiet]]`
66
+ - `hf datasets info DATASET_ID` β€” Get info about a dataset on the Hub. `[--revision TEXT --expand TEXT --format [auto|human|agent|json|quiet]]`
67
+ - `hf datasets leaderboard DATASET_ID` β€” List model scores from a dataset leaderboard. This command helps find the best models for a task or compare models by benchmark scores. Use 'hf datasets ls --filter benchmark:official' to list available leaderboards. `[--limit INTEGER --format [auto|human|agent|json|quiet]]`
68
+ - `hf datasets list` β€” List datasets on the Hub, or files in a dataset repo. `[--search TEXT --author TEXT --filter TEXT --sort [created_at|downloads|last_modified|likes|trending_score] --limit INTEGER --expand TEXT --human-readable --tree --recursive --revision TEXT --format [auto|human|agent|json|quiet]]`
69
+ - `hf datasets parquet DATASET_ID` β€” List parquet file URLs available for a dataset. `[--subset TEXT --split TEXT --format [auto|human|agent|json|quiet]]`
70
+ - `hf datasets sql SQL` β€” Execute a raw SQL query with DuckDB against dataset parquet URLs. `[--format [auto|human|agent|json|quiet]]`
71
+
72
+ ### `hf discussions` β€” Manage discussions and pull requests on the Hub.
73
+
74
+ - `hf discussions close REPO_ID NUM` β€” Close a discussion or pull request. `[--comment TEXT --yes --type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
75
+ - `hf discussions comment REPO_ID NUM` β€” Comment on a discussion or pull request. `[--body TEXT --body-file PATH --type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
76
+ - `hf discussions create REPO_ID --title TEXT` β€” Create a new discussion or pull request on a repo. `[--body TEXT --body-file PATH --pull-request --type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
77
+ - `hf discussions diff REPO_ID NUM` β€” Show the diff of a pull request. `[--type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
78
+ - `hf discussions info REPO_ID NUM` β€” Get info about a discussion or pull request. `[--type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
79
+ - `hf discussions list REPO_ID` β€” List discussions and pull requests on a repo. `[--status [open|closed|merged|draft|all] --kind [all|discussion|pull_request] --author TEXT --limit INTEGER --type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
80
+ - `hf discussions merge REPO_ID NUM` β€” Merge a pull request. `[--comment TEXT --yes --type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
81
+ - `hf discussions rename REPO_ID NUM NEW_TITLE` β€” Rename a discussion or pull request. `[--type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
82
+ - `hf discussions reopen REPO_ID NUM` β€” Reopen a closed discussion or pull request. `[--comment TEXT --yes --type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
83
+
84
+ ### `hf endpoints` β€” Manage Hugging Face Inference Endpoints.
85
+
86
+ - `hf endpoints catalog deploy --repo TEXT` β€” Deploy an Inference Endpoint from the Model Catalog. `[--name TEXT --accelerator TEXT --namespace TEXT --format [auto|human|agent|json|quiet]]`
87
+ - `hf endpoints catalog list` β€” List available Catalog models. `[--format [auto|human|agent|json|quiet]]`
88
+ - `hf endpoints delete NAME` β€” Delete an Inference Endpoint permanently. `[--namespace TEXT --yes --format [auto|human|agent|json|quiet]]`
89
+ - `hf endpoints deploy NAME --repo TEXT --framework TEXT --accelerator TEXT --instance-size TEXT --instance-type TEXT --region TEXT --vendor TEXT` β€” Deploy an Inference Endpoint from a Hub repository. `[--namespace TEXT --task TEXT --min-replica INTEGER --max-replica INTEGER --scale-to-zero-timeout INTEGER --scaling-metric [pendingRequests|hardwareUsage] --scaling-threshold FLOAT --format [auto|human|agent|json|quiet]]`
90
+ - `hf endpoints describe NAME` β€” Get information about an existing endpoint. `[--namespace TEXT --format [auto|human|agent|json|quiet]]`
91
+ - `hf endpoints list` β€” Lists all Inference Endpoints for the given namespace. `[--namespace TEXT --format [auto|human|agent|json|quiet]]`
92
+ - `hf endpoints pause NAME` β€” Pause an Inference Endpoint. `[--namespace TEXT --format [auto|human|agent|json|quiet]]`
93
+ - `hf endpoints resume NAME` β€” Resume an Inference Endpoint. `[--namespace TEXT --fail-if-already-running --format [auto|human|agent|json|quiet]]`
94
+ - `hf endpoints scale-to-zero NAME` β€” Scale an Inference Endpoint to zero. `[--namespace TEXT --format [auto|human|agent|json|quiet]]`
95
+ - `hf endpoints update NAME` β€” Update an existing endpoint. `[--namespace TEXT --repo TEXT --accelerator TEXT --instance-size TEXT --instance-type TEXT --framework TEXT --revision TEXT --task TEXT --min-replica INTEGER --max-replica INTEGER --scale-to-zero-timeout INTEGER --scaling-metric [pendingRequests|hardwareUsage] --scaling-threshold FLOAT --format [auto|human|agent|json|quiet]]`
96
+
97
+ ### `hf extensions` β€” Manage hf CLI extensions.
98
+
99
+ - `hf extensions exec NAME` β€” Execute an installed extension.
100
+ - `hf extensions install REPO_ID` β€” Install an extension from a public GitHub repository. `[--force --format [auto|human|agent|json|quiet]]`
101
+ - `hf extensions list` β€” List installed extension commands. `[--format [auto|human|agent|json|quiet]]`
102
+ - `hf extensions remove NAME` β€” Remove an installed extension. `[--format [auto|human|agent|json|quiet]]`
103
+ - `hf extensions search` β€” Search extensions available on GitHub (tagged with 'hf-extension' topic). `[--format [auto|human|agent|json|quiet]]`
104
+
105
+ ### `hf jobs` β€” Run and manage Jobs on the Hub.
106
+
107
+ - `hf jobs cancel JOB_ID` β€” Cancel a Job `[--namespace TEXT --format [auto|human|agent|json|quiet]]`
108
+ - `hf jobs hardware` β€” List available hardware options for Jobs `[--format [auto|human|agent|json|quiet]]`
109
+ - `hf jobs inspect JOB_IDS` β€” Display detailed information on one or more Jobs `[--namespace TEXT --format [auto|human|agent|json|quiet]]`
110
+ - `hf jobs labels JOB_ID` β€” Update labels on a Job. Replaces all existing labels. `[--label TEXT --clear --namespace TEXT --format [auto|human|agent|json|quiet]]`
111
+ - `hf jobs logs JOB_ID` β€” Fetch the logs of a Job. `[--follow --tail INTEGER --namespace TEXT --format [auto|human|agent|json|quiet]]`
112
+ - `hf jobs ps` β€” List Jobs. `[--all --namespace TEXT --filter TEXT --format [auto|human|agent|json|quiet]]`
113
+ - `hf jobs run IMAGE COMMAND` β€” Run a Job. `[--env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --flavor [cpu-basic|cpu-upgrade|cpu-performance|cpu-xl|t4-small|t4-medium|l4x1|l4x4|l40sx1|l40sx4|l40sx8|a10g-small|a10g-large|a10g-largex2|a10g-largex4|a100-large|a100x4|a100x8|h200|h200x2|h200x4|h200x8|rtx-pro-6000|rtx-pro-6000x2|rtx-pro-6000x4|rtx-pro-6000x8] --timeout TEXT --detach --expose INTEGER --namespace TEXT]`
114
+ - `hf jobs scheduled delete SCHEDULED_JOB_ID` β€” Delete a scheduled Job. `[--namespace TEXT --format [auto|human|agent|json|quiet]]`
115
+ - `hf jobs scheduled inspect SCHEDULED_JOB_IDS` β€” Display detailed information on one or more scheduled Jobs `[--namespace TEXT --format [auto|human|agent|json|quiet]]`
116
+ - `hf jobs scheduled labels SCHEDULED_JOB_ID` β€” Update labels on a scheduled Job. Replaces all existing labels. `[--label TEXT --clear --namespace TEXT --format [auto|human|agent|json|quiet]]`
117
+ - `hf jobs scheduled ps` β€” List scheduled Jobs `[--all --namespace TEXT --filter TEXT --format [auto|human|agent|json|quiet]]`
118
+ - `hf jobs scheduled resume SCHEDULED_JOB_ID` β€” Resume (unpause) a scheduled Job. `[--namespace TEXT --format [auto|human|agent|json|quiet]]`
119
+ - `hf jobs scheduled run SCHEDULE IMAGE COMMAND` β€” Schedule a Job. `[--suspend --concurrency --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --flavor [cpu-basic|cpu-upgrade|cpu-performance|cpu-xl|t4-small|t4-medium|l4x1|l4x4|l40sx1|l40sx4|l40sx8|a10g-small|a10g-large|a10g-largex2|a10g-largex4|a100-large|a100x4|a100x8|h200|h200x2|h200x4|h200x8|rtx-pro-6000|rtx-pro-6000x2|rtx-pro-6000x4|rtx-pro-6000x8] --timeout TEXT --expose INTEGER --namespace TEXT]`
120
+ - `hf jobs scheduled suspend SCHEDULED_JOB_ID` β€” Suspend (pause) a scheduled Job. `[--namespace TEXT --format [auto|human|agent|json|quiet]]`
121
+ - `hf jobs scheduled uv run SCHEDULE SCRIPT` β€” Run a UV script (local file or URL) on HF infrastructure `[--suspend --concurrency --image TEXT --flavor [cpu-basic|cpu-upgrade|cpu-performance|cpu-xl|t4-small|t4-medium|l4x1|l4x4|l40sx1|l40sx4|l40sx8|a10g-small|a10g-large|a10g-largex2|a10g-largex4|a100-large|a100x4|a100x8|h200|h200x2|h200x4|h200x8|rtx-pro-6000|rtx-pro-6000x2|rtx-pro-6000x4|rtx-pro-6000x8] --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --timeout TEXT --expose INTEGER --namespace TEXT --with TEXT --python TEXT]`
122
+ - `hf jobs stats` β€” Fetch the resource usage statistics and metrics of Jobs `[--namespace TEXT --format [auto|human|agent|json|quiet]]`
123
+ - `hf jobs uv run SCRIPT` β€” Run a UV script (local file or URL) on HF infrastructure `[--image TEXT --flavor [cpu-basic|cpu-upgrade|cpu-performance|cpu-xl|t4-small|t4-medium|l4x1|l4x4|l40sx1|l40sx4|l40sx8|a10g-small|a10g-large|a10g-largex2|a10g-largex4|a100-large|a100x4|a100x8|h200|h200x2|h200x4|h200x8|rtx-pro-6000|rtx-pro-6000x2|rtx-pro-6000x4|rtx-pro-6000x8] --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --timeout TEXT --detach --expose INTEGER --namespace TEXT --with TEXT --python TEXT]`
124
+
125
+ ### `hf models` β€” Interact with models on the Hub.
126
+
127
+ - `hf models card MODEL_ID` β€” Get the model card (README) for a model on the Hub. `[--metadata --text --format [auto|human|agent|json|quiet]]`
128
+ - `hf models info MODEL_ID` β€” Get info about a model on the Hub. `[--revision TEXT --expand TEXT --format [auto|human|agent|json|quiet]]`
129
+ - `hf models list` β€” List models on the Hub, or files in a model repo. `[--search TEXT --author TEXT --filter TEXT --num-parameters TEXT --sort [created_at|downloads|last_modified|likes|trending_score] --limit INTEGER --expand TEXT --human-readable --tree --recursive --revision TEXT --format [auto|human|agent|json|quiet]]`
130
+
131
+ ### `hf papers` β€” Interact with papers on the Hub.
132
+
133
+ - `hf papers info PAPER_ID` β€” Get info about a paper on the Hub. `[--format [auto|human|agent|json|quiet]]`
134
+ - `hf papers list` β€” List daily papers on the Hub. `[--date TEXT --week TEXT --month TEXT --submitter TEXT --sort [publishedAt|trending] --limit INTEGER --format [auto|human|agent|json|quiet]]`
135
+ - `hf papers read PAPER_ID` β€” Read a paper as markdown. `[--format [auto|human|agent|json|quiet]]`
136
+ - `hf papers search QUERY` β€” Search papers on the Hub. `[--limit INTEGER --format [auto|human|agent|json|quiet]]`
137
+
138
+ ### `hf repos` β€” Manage repos on the Hub.
139
+
140
+ - `hf repos branch create REPO_ID BRANCH` β€” Create a new branch for a repo on the Hub. `[--revision TEXT --type [model|dataset|space] --exist-ok --format [auto|human|agent|json|quiet]]`
141
+ - `hf repos branch delete REPO_ID BRANCH` β€” Delete a branch from a repo on the Hub. `[--type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
142
+ - `hf repos cp SRC` β€” Copy files between local paths, repositories, and buckets. `[--format [auto|human|agent|json|quiet]]`
143
+ - `hf repos create REPO_ID` β€” Create a new repo on the Hub. `[--type [model|dataset|space] --space-sdk TEXT --private --public --protected --exist-ok --resource-group-id TEXT --region [us|eu] --flavor [cpu-basic|cpu-upgrade|zero-a10g|t4-small|t4-medium|l4x1|l4x4|l40sx1|l40sx4|l40sx8|a10g-small|a10g-large|a10g-largex2|a10g-largex4|a100-large|a100x4|a100x8] --storage [small|medium|large] --sleep-time INTEGER --secrets TEXT --secrets-file TEXT --env TEXT --env-file TEXT --volume TEXT --format [auto|human|agent|json|quiet]]`
144
+ - `hf repos delete REPO_ID` β€” Delete a repo from the Hub. This is an irreversible operation. `[--type [model|dataset|space] --missing-ok --yes --format [auto|human|agent|json|quiet]]`
145
+ - `hf repos delete-files REPO_ID PATTERNS` β€” Delete files from a repo on the Hub. `[--type [model|dataset|space] --revision TEXT --commit-message TEXT --commit-description TEXT --create-pr --format [auto|human|agent|json|quiet]]`
146
+ - `hf repos duplicate FROM_ID` β€” Duplicate a repo on the Hub (model, dataset, or Space). `[--type [model|dataset|space] --private --public --protected --exist-ok --flavor [cpu-basic|cpu-upgrade|zero-a10g|t4-small|t4-medium|l4x1|l4x4|l40sx1|l40sx4|l40sx8|a10g-small|a10g-large|a10g-largex2|a10g-largex4|a100-large|a100x4|a100x8] --storage [small|medium|large] --sleep-time INTEGER --secrets TEXT --secrets-file TEXT --env TEXT --env-file TEXT --volume TEXT --format [auto|human|agent|json|quiet]]`
147
+ - `hf repos list` β€” List all repos (models, datasets, spaces, buckets) with storage info. `[--namespace TEXT --type [model|dataset|space|bucket] --search TEXT --limit INTEGER --explore --format [auto|human|agent|json|quiet]]`
148
+ - `hf repos move FROM_ID TO_ID` β€” Move a repository from a namespace to another namespace. `[--type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
149
+ - `hf repos settings REPO_ID` β€” Update the settings of a repository. `[--gated [auto|manual|false] --private --public --protected --type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
150
+ - `hf repos tag create REPO_ID TAG` β€” Create a tag for a repo. `[--message TEXT --revision TEXT --type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
151
+ - `hf repos tag delete REPO_ID TAG` β€” Delete a tag for a repo. `[--yes --type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
152
+ - `hf repos tag list REPO_ID` β€” List tags for a repo. `[--type [model|dataset|space] --format [auto|human|agent|json|quiet]]`
153
+
154
+ ### `hf skills` β€” Manage skills for AI assistants.
155
+
156
+ - `hf skills add` β€” Download a Hugging Face skill and install it for an AI assistant. `[--claude --global --dest PATH --force --format [auto|human|agent|json|quiet]]`
157
+ - `hf skills list` β€” List available skills from the Hugging Face marketplace. `[--format [auto|human|agent|json|quiet]]`
158
+ - `hf skills preview` β€” Print the generated `hf-cli` SKILL.md to stdout. `[--format [auto|human|agent|json|quiet]]`
159
+ - `hf skills update` β€” Update installed Hugging Face marketplace skills. `[--claude --global --dest PATH --format [auto|human|agent|json|quiet]]`
160
+
161
+ ### `hf spaces` β€” Interact with spaces on the Hub.
162
+
163
+ - `hf spaces card SPACE_ID` β€” Get the Space card (README) for a Space on the Hub. `[--metadata --text --format [auto|human|agent|json|quiet]]`
164
+ - `hf spaces dev-mode SPACE_ID` β€” Enable or disable dev mode on a Space. `[--stop --format [auto|human|agent|json|quiet]]`
165
+ - `hf spaces hardware` β€” List available hardware options for Spaces. `[--format [auto|human|agent|json|quiet]]`
166
+ - `hf spaces hot-reload SPACE_ID` β€” Hot-reload any Python file of a Space without a full rebuild + restart. `[--local-file PATH --skip-checks --skip-summary --format [auto|human|agent|json|quiet]]`
167
+ - `hf spaces info SPACE_ID` β€” Get info about a space on the Hub. `[--revision TEXT --expand TEXT --format [auto|human|agent|json|quiet]]`
168
+ - `hf spaces list` β€” List spaces on the Hub, or files in a space repo. `[--search TEXT --author TEXT --filter TEXT --sort [created_at|last_modified|likes|trending_score] --limit INTEGER --expand TEXT --human-readable --tree --recursive --revision TEXT --format [auto|human|agent|json|quiet]]`
169
+ - `hf spaces logs SPACE_ID` β€” Fetch the run or build logs of a Space. `[--build --follow --tail INTEGER --format [auto|human|agent|json|quiet]]`
170
+ - `hf spaces pause SPACE_ID` β€” Pause a Space. `[--format [auto|human|agent|json|quiet]]`
171
+ - `hf spaces restart SPACE_ID` β€” Restart a Space. `[--factory-reboot --format [auto|human|agent|json|quiet]]`
172
+ - `hf spaces search QUERY` β€” Search spaces on the Hub using semantic search. `[--filter TEXT --sdk TEXT --include-non-running --description --limit INTEGER --format [auto|human|agent|json|quiet]]`
173
+ - `hf spaces secrets add SPACE_ID` β€” Add or update secrets for a Space. `[--secrets TEXT --secrets-file TEXT --format [auto|human|agent|json|quiet]]`
174
+ - `hf spaces secrets delete SPACE_ID KEY` β€” Remove a secret from a Space. `[--yes --format [auto|human|agent|json|quiet]]`
175
+ - `hf spaces secrets list SPACE_ID` β€” List secrets for a Space. Secret values are write-only and not returned. `[--format [auto|human|agent|json|quiet]]`
176
+ - `hf spaces settings SPACE_ID` β€” Update the settings of a Space. `[--sleep-time INTEGER --hardware [cpu-basic|cpu-upgrade|zero-a10g|t4-small|t4-medium|l4x1|l4x4|l40sx1|l40sx4|l40sx8|a10g-small|a10g-large|a10g-largex2|a10g-largex4|a100-large|a100x4|a100x8] --format [auto|human|agent|json|quiet]]`
177
+ - `hf spaces ssh SPACE_ID` β€” SSH into a Space's Dev Mode container. `[--identity-file PATH --dry-run --auto --format [auto|human|agent|json|quiet]]`
178
+ - `hf spaces variables add SPACE_ID` β€” Add or update environment variables for a Space. `[--env TEXT --env-file TEXT --format [auto|human|agent|json|quiet]]`
179
+ - `hf spaces variables delete SPACE_ID KEY` β€” Remove an environment variable from a Space. `[--yes --format [auto|human|agent|json|quiet]]`
180
+ - `hf spaces variables list SPACE_ID` β€” List environment variables for a Space. `[--format [auto|human|agent|json|quiet]]`
181
+ - `hf spaces volumes delete SPACE_ID` β€” Remove all volumes from a Space. `[--yes --format [auto|human|agent|json|quiet]]`
182
+ - `hf spaces volumes list SPACE_ID` β€” List volumes mounted in a Space. `[--format [auto|human|agent|json|quiet]]`
183
+ - `hf spaces volumes set SPACE_ID` β€” Set (replace) volumes for a Space. `[--volume TEXT --format [auto|human|agent|json|quiet]]`
184
+
185
+ ### `hf webhooks` β€” Manage webhooks on the Hub.
186
+
187
+ - `hf webhooks create --watch TEXT` β€” Create a new webhook. `[--url TEXT --job-id TEXT --domain [repo|discussions] --secret TEXT --format [auto|human|agent|json|quiet]]`
188
+ - `hf webhooks delete WEBHOOK_ID` β€” Delete a webhook permanently. `[--yes --format [auto|human|agent|json|quiet]]`
189
+ - `hf webhooks disable WEBHOOK_ID` β€” Disable an active webhook. `[--format [auto|human|agent|json|quiet]]`
190
+ - `hf webhooks enable WEBHOOK_ID` β€” Enable a disabled webhook. `[--format [auto|human|agent|json|quiet]]`
191
+ - `hf webhooks info WEBHOOK_ID` β€” Show full details for a single webhook. `[--format [auto|human|agent|json|quiet]]`
192
+ - `hf webhooks list` β€” List all webhooks for the current user. `[--format [auto|human|agent|json|quiet]]`
193
+ - `hf webhooks update WEBHOOK_ID` β€” Update an existing webhook. Only provided options are changed. `[--url TEXT --watch TEXT --domain [repo|discussions] --secret TEXT --format [auto|human|agent|json|quiet]]`
194
+
195
+ ## Common options
196
+
197
+ - `--format` β€” Output format: `--format json` (or `--json`) or `--format table` (default).
198
+ - `-q / --quiet` β€” Quiet output (one ID per line).
199
+ - `--revision` β€” Git revision id which can be a branch name, a tag, or a commit hash.
200
+ - `--token` β€” Use a User Access Token. Prefer setting `HF_TOKEN` env var instead of passing `--token`.
201
+ - `--type` β€” The type of repository (model, dataset, or space).
202
+
203
+ ## Mounting repos as local filesystems
204
+
205
+ To mount Hub repositories or buckets as local filesystems β€” no download, no copy, no waiting β€” use `hf-mount`. Files are fetched on demand. GitHub: https://github.com/huggingface/hf-mount
206
+
207
+ Install: `curl -fsSL https://raw.githubusercontent.com/huggingface/hf-mount/main/install.sh | sh`
208
+
209
+ Some command examples:
210
+ - `hf-mount start repo openai-community/gpt2 /tmp/gpt2` β€” mount a repo (read-only)
211
+ - `hf-mount start --hf-token $HF_TOKEN bucket myuser/my-bucket /tmp/data` β€” mount a bucket (read-write)
212
+ - `hf-mount status` / `hf-mount stop /tmp/data` β€” list or unmount
213
+
214
+ ## Tips
215
+
216
+ - Use `hf <command> --help` for full options, descriptions, usage, and real-world examples
217
+ - Authenticate with `HF_TOKEN` env var (recommended) or with `--token`
218
+ - Update the CLI with `hf update` (uses the correct command for the detected install method)
.env.example ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chief Engineer β€” config template. Copy to `.env` and fill in as needed.
2
+ # `.env` is LOADED AUTOMATICALLY at import (core/__init__.py); real shell env vars
3
+ # always win over the file. `.env` is gitignored β€” never committed, never uploaded
4
+ # to the Space (keep your HF_TOKEN here safely).
5
+ #
6
+ # TWO CONTEXTS:
7
+ # β€’ LOCAL (this .env) β†’ dev + recording via Ollama. Use the LOCAL block.
8
+ # β€’ SPACE (HF Space) β†’ do NOT push a .env. Set these same KEY=VALUE pairs
9
+ # as Space *Variables* (and HF_TOKEN as a *Secret*)
10
+ # in Settings, or via the huggingface_hub API
11
+ # (see docs/SPACE-DEPLOY.md). Use the ZEROGPU block.
12
+
13
+ # ── LOCAL (Ollama) ────────────────────────────────────────────────────────
14
+ # Primary model tag. gemma4:e4b = gemma4:latest (~9.6GB). NOTE: gemma4:4b does NOT exist.
15
+ CHIEF_ENGINEER_MODEL=gemma4:e4b # Kyle's pick (6/11). gemma4:e2b is ~2Γ— faster if you want snappier.
16
+ # Force the deterministic fallback (skip Ollama) β€” handy for fast offline B-roll:
17
+ # CHIEF_ENGINEER_OFFLINE=1
18
+
19
+ # ── ZEROGPU / SPACE (live transformers inference) ──────────────────────────
20
+ # Set these on the ZeroGPU Space (Variables), not locally, unless you have a local GPU.
21
+ # CHIEF_ENGINEER_BACKEND=zerogpu # default 'ollama'; 'zerogpu' = live HF model
22
+ # CHIEF_ENGINEER_HF_MODEL=google/gemma-4-E2B-it # transformers-native (resolved 6/10)
23
+ # CHIEF_ENGINEER_GPU_SECONDS=90 # 1st call loads the model
24
+ # CHIEF_ENGINEER_MAX_NEW_TOKENS=512
25
+ # HF_TOKEN=hf_xxx # ONLY if the model is gated (Space SECRET; keep out of git)
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/benchy.glb filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Runtime ledger (seed corpus lives in seed_lessons.jsonl, which IS tracked)
2
+ data/lessons.jsonl
3
+ # Generated by `python -m ingest.run` from your configs
4
+ data/references.jsonl
5
+ # Learned policy β€” accumulates at runtime from the simulated loop
6
+ data/policy.json
7
+ # Transient UI artifacts (parametric-generated mesh, on-the-fly GIFs)
8
+ data/_generated.glb
9
+ data/_vprint.gif
10
+ data/_modal_candidate_lessons.jsonl # Modal Lane-B review file (pre-honesty-gate)
11
+ dist/
12
+
13
+ # Sample meshes ARE committed β€” the HF Space doesn't run make_assets, and
14
+ # benchy.glb can't be regenerated there (needs the STL + a decimation tool).
15
+ # Regenerate the primitives locally with `make assets` if needed.
16
+
17
+ __pycache__/
18
+ *.pyc
19
+ .venv/
20
+ .env # local config + secrets (HF_TOKEN). NEVER commit or upload to the Space.
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
Makefile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: setup setup-zerogpu assets run test demo bench trace preflight
2
+
3
+ # Local dev uses uv (fast, locked). The HF Space still installs via pip+requirements.txt.
4
+ # Entrypoints: app.py + test_core.py at root; helper scripts live in scripts/ and run
5
+ # as modules (-m scripts.<name>) so the repo root is importable for `from core...`.
6
+ setup: ## create/sync the uv env (locked) + generate sample meshes
7
+ uv sync
8
+ uv run python -m scripts.make_assets
9
+
10
+ setup-zerogpu: ## sync incl. the ZeroGPU live-inference extra (deploy/testing only)
11
+ uv sync --extra zerogpu
12
+ uv run python -m scripts.make_assets
13
+
14
+ assets: ## (re)generate sample meshes for the 3D preview
15
+ uv run python -m scripts.make_assets
16
+
17
+ run: ## launch the Gradio app (needs `ollama serve` for live inference)
18
+ uv run python app.py
19
+
20
+ test: ## headless core tests (no Ollama required)
21
+ uv run python test_core.py
22
+
23
+ preflight: ## GO/NO-GO gate on the real stack (see RUNBOOK)
24
+ uv run python -m scripts.preflight
25
+
26
+ demo: ## scripted integration run / video-beat dry run
27
+ uv run python -m scripts.scripted_demo
28
+
29
+ bench: ## measure model latency on this hardware (needs Ollama)
30
+ uv run python -m scripts.bench_latency
31
+
32
+ trace: ## export the lesson ledger as a HF Datasets-ready trace
33
+ uv run python -m scripts.export_trace
README.md CHANGED
@@ -1,14 +1,123 @@
1
  ---
2
- title: Microfactory Lab
3
- emoji: πŸ‘
4
- colorFrom: indigo
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.17.3
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
 
 
 
 
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: The Chief Engineer
3
+ emoji: πŸ–¨οΈ
4
+ colorFrom: gray
5
+ colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.17.3
8
+ python_version: "3.11"
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
+ short_description: Local Gemma that predicts 3D-print failures before they run
13
+ tags:
14
+ - build-small-hackathon
15
+ - backyard-ai
16
+ - off-the-grid
17
+ - llama-champion
18
+ - off-brand
19
  ---
20
 
21
+ # The Chief Engineer
22
+
23
+ > My grandfather had a shop β€” an electrical engineer who tinkered, a home RadioShack with a
24
+ > machine shop attached. He spent a lifetime accumulating skill that lived in his hands. Then
25
+ > Alzheimer's took him, the shop was sold, and all of it just… went. The Chief Engineer is the
26
+ > opposite of that: a small AI that runs on my own machine and learns the craft of 3D printing
27
+ > the way a shop veteran would β€” job by job, remembering what worked in which conditions β€” and
28
+ > tells me where a print will fail *before* it runs.
29
+
30
+ **Build Small hackathon Β· Backyard AI track.** A small local **Gemma** model that has accumulated
31
+ expert 3D-printing knowledge across prior jobs and applies it **proactively** to a new one.
32
+ Knowledge **compounds** β€” job N+1 is better-informed than job N. The proactive part is the
33
+ *judgment*: the model evaluates what prior jobs transfer to this one, applies precedent when it
34
+ fits, and says *"no close precedent"* when it doesn't.
35
+
36
+ ## The loop
37
+
38
+ ```
39
+ Job (geometry + material) + Environment (temp, humidity)
40
+ β†’ retrieve nearest prior jobs (exact material+geometry, normalized [temp,humidity] distance, top 2-3)
41
+ β†’ Chief Engineer EVALUATES what transfers β†’ proposes settings + risk regions (Brain)
42
+ β†’ Spine validates against material bounds, clamps/vetoes unsafe values (Spine)
43
+ β†’ HITL "Confirm & Print" β†’ you print β†’ you report the real outcome
44
+ β†’ reflect β†’ distill an environment-keyed lesson β†’ ledger grows β†’ next job is smarter
45
+ ```
46
+
47
+ The model **never grades its own outcome** β€” you click *Printed clean / Sagged / Stringing*, or
48
+ run the **simulated world** (a deterministic stand-in for the printer + sensors). Either way the
49
+ outcome comes from outside the model. The honest signal is what makes the compounding real.
50
+
51
+ **This is more than a lookup.** Two knowledge sources feed every recommendation β€” *RAG* (retrieved
52
+ prior jobs the LLM reasons over) and a **learned parametric policy** (`learn/policy.py`) that stores
53
+ setting offsets per (material, geometry, *environment-bucket*) and updates from each outcome. Because
54
+ cells are bucketed, a lesson from one humid PETG bridge **generalizes** to the next, not-identical
55
+ one. The **Learning Loop** tab runs the closed loop live: pick a hard job and watch print quality
56
+ climb from failure to clean over a handful of iterations. The one simulated boundary (print
57
+ outcomes) is spelled out in *What's real vs frontier* below.
58
+
59
+ ## Run it (fully local, no cloud, no API key)
60
+
61
+ ```bash
62
+ ollama serve &
63
+ ollama pull gemma4:e4b # = gemma4:latest, 9.6GB. (gemma4:4b does NOT exist.)
64
+ make setup # uv sync (locked env) + generate sample meshes
65
+ make run # β†’ http://localhost:7860 (or: uv run python app.py)
66
+ ```
67
+
68
+ Local dev uses [uv](https://docs.astral.sh/uv); the HF Space installs via `requirements.txt`.
69
+ **Layout:** `app.py` (Space entrypoint) + `test_core.py` at the root; the core library lives in
70
+ `core/`, runnable helpers in `scripts/` (run via `make <target>` or `uv run python -m scripts.<name>`),
71
+ and the simulator / learning-loop / ingestion packages in `sim/`, `learn/`, `ingest/`. See
72
+ `docs/RUNBOOK.md` for the full run-and-test order of operations.
73
+
74
+ Falls back to a deterministic advisor if Ollama is unreachable, so the demo never crashes; the
75
+ UI always shows which model ran. Set `CHIEF_ENGINEER_MODEL=gemma4:e2b` if CPU latency is high.
76
+
77
+ **Resetting demo state.** The Learning Loop and the πŸ§ͺ *Simulate outcome* button accumulate runtime
78
+ state β€” earned/sim lessons in `data/lessons.jsonl` and the learned cells in `data/policy.json` (both
79
+ gitignored). For a clean slate between demos:
80
+
81
+ ```bash
82
+ rm -f data/lessons.jsonl data/policy.json # seed corpus reloads automatically on next launch
83
+ ```
84
+
85
+ ## Workspaces
86
+
87
+ 1. **Studio** β€” define the job: pick a reference part type, quick-load 3DBenchy, generate a
88
+ primitive, or upload a mesh; choose material; set run conditions. Live 3D preview + a
89
+ layer-by-layer *virtual print* of the actual part.
90
+ 2. **Analysis** β€” the load-bearing moment, on its own page: the model's precedent evaluation and
91
+ reasoning, predicted failure regions, the Spine-validated settings + G-code as instrument
92
+ readouts, then close the loop (πŸ§ͺ *Simulate outcome* or record a real one).
93
+ 3. **Learning Loop** β€” the compounding made literal: run the closed simulated loop and watch the
94
+ quality curve climb while the learned policy cell fills in.
95
+ 4. **Knowledge + Mesh** β€” the node mesh (one node executes, others as context) and the live ledger
96
+ where lessons accumulate (seed β†’ earned β†’ sim).
97
+
98
+ ## Badges
99
+
100
+ Off the Grid (local Ollama/Gemma) Β· Llama Champion (Ollama runs on llama.cpp) Β·
101
+ Sharing is Caring (ledger/trace to the Hub) Β· Field Notes (build writeup). Off-Brand if the
102
+ Astrometrics UI lands (see `../DESIGN.md`). **Not** Well-Tuned β€” fine-tuning is named as frontier.
103
+
104
+ ## What's real vs frontier (honest claims)
105
+
106
+ - **Built:** retrieval-based compounding knowledge, environment-keyed lessons, a learned parametric
107
+ policy that generalizes across similar conditions, the closed learning loop, the Brain/Spine veto,
108
+ proactive geometric risk flags, fully local Gemma inference, human-reported + simulated outcomes,
109
+ knowledge ingestion from slicer/firmware configs.
110
+ - **Simulated (the one boundary):** print outcomes, via a deterministic physics-lite stand-in for the
111
+ printer + sensors (`sim/outcome.py`) β€” the model never grades its own work.
112
+ - **Frontier (not built):** weight-level fine-tuning on the accumulated ledger, real distributed
113
+ multi-node execution, the physical interfaces (g-code streaming, env sensors, camera defect CV).
114
+
115
+ ## License
116
+
117
+ MIT. **No OrcaSlicer/PrusaSlicer code** (AGPL-3.0) β€” trimesh/PySLM/manifold3d/pyclipr only.
118
+
119
+ ## Source
120
+
121
+ Built in the open. The full build plan, run-and-test runbook, the small-Gemma steering notes,
122
+ and the write-up live in the project's source repository (curated docs are added to this Space
123
+ after the demo video + write-up are finalized).
app.py CHANGED
@@ -1,7 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
1
+ """The Chief Engineer β€” Gradio app (three workspaces).
2
+
3
+ WORKBENCH: a staged single-job flow β€” β‘  define the job β†’ β‘‘ part + virtual print β†’
4
+ β‘’ engineer's analysis (precedent eval + reasoning) β†’ β‘£ validation (Spine veto +
5
+ g-code) β†’ β‘€ close the loop (simulate / record outcome β†’ reflect β†’ ledger grows).
6
+ Stages reveal top-to-bottom as the job runs.
7
+ LEARNING LOOP: the simulated compounding loop — quality climbs fail→clean.
8
+ KNOWLEDGE + MESH: the node mesh + the live ledger (seed β†’ earned β†’ sim).
9
+
10
+ Local-only. Real Ollama calls (gemma4:e4b), deterministic fallback so the demo
11
+ never crashes. Run: `ollama serve` + `make run` (= `uv run python app.py`).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import random
17
+
18
  import gradio as gr
19
 
20
+ from core import llm
21
+ from core import seed_lessons
22
+ from core.theme import THEME, CSS, rule, command_bar, footer_bar
23
+ from core.widgets import virtual_printer_html, layer_image, SCRUB_LAYERS, VP_HEAD
24
+ from core.chief_engineer import advise
25
+ from core.ledger import LedgerManager
26
+ from core.models import BED_POSITIONS, Environment, GEOMETRY_TYPES, Job, MATERIALS, PrintSettings
27
+ from core.nodes import render_node_cards
28
+ from core.reflect import reflect_on_job
29
+ from core.spine import SpineValidator
30
+ from learn.loop import run_iteration, run_session
31
+ from learn.policy import LearnedPolicy, cell_key
32
+ from sim.outcome import simulate
33
+ from core.viewer import (
34
+ PART_LABEL,
35
+ benchy_mesh,
36
+ gcode_panel_html,
37
+ generate_primitive,
38
+ iteration_log_html,
39
+ placement_callout,
40
+ policy_cell_html,
41
+ precedent_eval_html,
42
+ quality_curve_html,
43
+ risk_callouts_html,
44
+ sample_mesh,
45
+ settings_panel_html,
46
+ steepest_overhang_hint,
47
+ )
48
+
49
+ try:
50
+ from ingest.distill import reference_block
51
+ except Exception: # ingestion is optional / removable
52
+ def reference_block(material): # type: ignore
53
+ return []
54
+
55
+ LEDGER = LedgerManager()
56
+ SPINE = SpineValidator()
57
+ POLICY = LearnedPolicy()
58
+ _loaded = seed_lessons.ensure_seeded(LEDGER)
59
+
60
+ # Astrometrics OS visual layer lives in core/theme.py (THEME + CSS + helpers) so
61
+ # the Off-Brand skin stays a single removable module. See ../DESIGN.md.
62
+
63
+
64
+ def ledger_html() -> str:
65
+ c = LEDGER.count()
66
+ entries = LEDGER.all()[-8:][::-1]
67
+ rows = []
68
+ for e in entries:
69
+ tag = "SEED" if e.source == "seed" else "EARNED"
70
+ col = "var(--ao-outline)" if e.source == "seed" else "var(--ao-green)"
71
+ rows.append(
72
+ f"<div style='border-left:3px solid {col};background:var(--ao-surface);padding:6px 10px;"
73
+ f"margin:4px 0;font-family:ui-monospace,monospace;font-size:11px;'>"
74
+ f"<span style='color:{col};font-weight:700;'>{tag}</span> "
75
+ f"<span style='color:var(--ao-orange);'>{e.material}/{e.geometry_type}</span> "
76
+ f"<span style='color:var(--ao-outline);'>@ {e.env_temp:.0f}Β°C/{e.env_humidity:.0f}% β†’ {e.outcome}</span>"
77
+ f"<div style='color:var(--ao-text);'>{e.lesson}</div></div>"
78
+ )
79
+ head = (
80
+ f"<div style='color:var(--ao-orange);font-family:ui-monospace,monospace;'>"
81
+ f"LEDGER Β· {c['total']} lessons ({c['seed']} seed Β· {c['earned']} earned)</div>"
82
+ )
83
+ return head + "".join(rows)
84
+
85
+
86
+ _DEFAULT_PART = {"geometry": "overhang", "mesh": sample_mesh("overhang"), "label": "OVERHANG TEST"}
87
+
88
+
89
+ def _set_part(geometry: str, mesh, label: str):
90
+ """Shared STUDIO preview update β†’ (part_state, model3d, status). The virtual
91
+ print is NOT rendered here β€” it initializes on ANALYZE (see get_recommendation),
92
+ so it doesn't loop perpetually on the Studio preview."""
93
+ return (
94
+ {"geometry": geometry, "mesh": mesh, "label": label},
95
+ gr.update(value=mesh),
96
+ f"β–£ ACTIVE PART Β· **{label}** Β· class `{geometry}`",
97
+ )
98
+
99
+
100
+ def analyze_start(part):
101
+ """Immediate feedback the instant ANALYZE is clicked: jump to the Analysis tab
102
+ and show spinners while the model runs (e4b warm ~15-20s on CPU). The heavy
103
+ get_recommendation runs right after via .then(). No part selected β†’ stay put."""
104
+ if not (part and part.get("geometry")):
105
+ gr.Warning("Select a part in Studio first β€” pick a type, load Benchy, generate, or upload.")
106
+ return (gr.update(),) * 7
107
+ return (
108
+ gr.Tabs(selected="analysis"),
109
+ "⏳ **Analyzing the job** β€” evaluating precedent and proposing settings…", # reasoning
110
+ "<div class='ce-sub'>⏳ retrieving precedent…</div>", # precedent
111
+ "", # risks
112
+ "", # settings_html
113
+ "", # gcode_html
114
+ "<div class='ce-sub'>⏳ slicing the part…</div>", # vprint
115
+ )
116
+
117
+
118
+ def select_type(geo): # reference part-type pill chosen
119
+ return _set_part(geo, sample_mesh(geo), PART_LABEL.get(geo, geo.upper()))
120
+
121
+
122
+ def load_benchy():
123
+ mesh = benchy_mesh()
124
+ label = "3DBENCHY (CC0)" if mesh else "BENCHY MISSING β€” add assets/benchy.glb"
125
+ return _set_part("overhang", mesh, label)
126
+
127
+
128
+ def generate_part(kind, size):
129
+ mesh, geo = generate_primitive(kind, size or 30)
130
+ return _set_part(geo, mesh, f"GENERATED {str(kind).upper()} Β· {float(size or 30):.0f}MM")
131
+
132
+
133
+ def upload_part(f):
134
+ if not f:
135
+ return (gr.update(), gr.update(), gr.update(), gr.update())
136
+ from pathlib import Path as _P
137
+ return _set_part("overhang", f.name, f"UPLOAD Β· {_P(f.name).name[:26]}")
138
+
139
+
140
+ def scrub_layer(idx, part):
141
+ """Render one cross-section layer at full fidelity for the scrubber slider."""
142
+ mesh = (part or {}).get("mesh")
143
+ return layer_image(mesh, idx)
144
+
145
+
146
+ # ── simulated sensors (this is a sim lab β€” conditions are generated, overridable) ──
147
+ def _sensor_readout(t, h, pos) -> str:
148
+ return ("<div class='ce-sub' style='font-size:13px;'>SIMULATED SENSORS Β· "
149
+ f"🌑 <b style='color:var(--ao-blue);'>{float(t):.0f}°C</b> · "
150
+ f"πŸ’§ <b style='color:var(--ao-blue);'>{float(h):.0f}%RH</b> Β· "
151
+ f"βŒ– <b style='color:var(--ao-blue);'>{pos}</b> "
152
+ "<span style='opacity:.6;'>(🎲 re-roll or βš™ override)</span></div>")
153
+
154
+
155
+ def randomize_sensors():
156
+ """Roll a plausible ambient + plate position β€” the lab's simulated sensor feed."""
157
+ t = random.choice([18, 20, 22, 24, 26, 28, 30, 32])
158
+ h = random.choice([30, 38, 45, 52, 60, 68])
159
+ pos = random.choices(BED_POSITIONS, weights=[3, 2, 1])[0] # center most common
160
+ return gr.update(value=t), gr.update(value=h), gr.update(value=pos), _sensor_readout(t, h, pos)
161
+
162
+
163
+ def sync_readout(t, h, pos):
164
+ return _sensor_readout(t, h, pos)
165
+
166
+
167
+ def get_recommendation(part, material, description, temp, humidity, bed_position):
168
+ if not (part and part.get("geometry")): # guard: empty start, no part chosen
169
+ return ("", "", "⚠ **Select a part in Studio** (pick a type, load Benchy, generate, "
170
+ "or upload) before analyzing.", "", "", "", "", gr.update(visible=False),
171
+ gr.update(), {}, "", gr.update(), gr.update())
172
+ geometry_type, mesh = part["geometry"], part.get("mesh")
173
+ job = Job(geometry_type=geometry_type, material=material, description=description or "",
174
+ bed_position=bed_position or "center")
175
+ env = Environment(temp=float(temp), humidity=float(humidity))
176
+
177
+ retrieved = LEDGER.retrieve(material, geometry_type, env.temp, env.humidity)
178
+ references = reference_block(material)
179
+ policy_note = POLICY.policy_note(material, geometry_type, env)
180
+ rec = advise(job, env, retrieved, references, policy_note)
181
+ spine = SPINE.check(rec.advice.settings, material)
182
+ hint = steepest_overhang_hint(mesh) if geometry_type in ("overhang", "bridge") else None
183
+ vp_html = virtual_printer_html(mesh, f"{material} Β· {geometry_type}") # init on ANALYZE
184
+
185
+ precedent = precedent_eval_html(retrieved, env)
186
+ if retrieved:
187
+ rows = "".join(
188
+ f"<div style='font-family:ui-monospace,monospace;font-size:11px;color:var(--ao-outline);'>"
189
+ f"β€’ {e.job_id} ({e.source}) {e.outcome} @ {e.env_temp:.0f}Β°C/{e.env_humidity:.0f}% "
190
+ f"(dist {dist:.2f})</div>"
191
+ for e, dist in retrieved
192
+ )
193
+ precedent += f"<div style='margin-top:4px;'>{rows}</div>"
194
+
195
+ fb = " ⚠ deterministic fallback" if rec.used_fallback else ""
196
+ spine_md = ("**πŸ›‘ Spine veto:** " + " \n".join(spine.vetoes)) if spine.vetoes else ""
197
+ confirm_vis = gr.update(visible=spine.requires_approval)
198
+ approval_md = ("⏸ **HITL gate:** the Spine clamped a boundary setting β€” review, then **Confirm & Print**."
199
+ if spine.requires_approval else "βœ… within safe envelope β€” ready when you are.")
200
+ state = {"job": job.model_dump(), "env": env.model_dump(), "settings": spine.settings.model_dump()}
201
+
202
+ return (
203
+ f"{rec.backend}{fb}", # backend status
204
+ precedent, # precedent
205
+ f"**Chief Engineer:** {rec.advice.reasoning}", # reasoning
206
+ risk_callouts_html(rec.advice.risks, hint) + placement_callout(material, bed_position), # risks + placement
207
+ settings_panel_html(spine.settings, material), # settings (LCARS panel)
208
+ f"{spine_md}\n\n{approval_md}" if spine_md else approval_md, # spine notes
209
+ gcode_panel_html(spine.settings, material), # g-code (LCARS panel)
210
+ confirm_vis, # confirm visibility
211
+ render_node_cards(env, working=True), # node cards
212
+ state, # state
213
+ vp_html, # virtual print (animates once)
214
+ gr.update(value=1), # reset layer scrubber
215
+ layer_image(mesh, 1), # initial scrubbed layer
216
+ )
217
+
218
+
219
+ def run_learning(material, geometry_type, temp, humidity, iterations):
220
+ """Primary demo: run the closed simulated loop and watch quality compound."""
221
+ job = Job(geometry_type=geometry_type, material=material, description="")
222
+ env = Environment(temp=float(temp), humidity=float(humidity))
223
+ key = cell_key(material, geometry_type, env)
224
+ before = POLICY.cell_stats(material, geometry_type, env)
225
+ before_html = policy_cell_html(before, key)
226
+
227
+ sess = run_session(job, env, int(iterations), POLICY, LEDGER)
228
+ after = POLICY.cell_stats(material, geometry_type, env)
229
+
230
+ traj = sess.trajectory
231
+ first = sess.first_success
232
+ headline = (
233
+ f"**{material}/{geometry_type} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β€” "
234
+ f"started at quality **{traj[0]:.2f}** ({sess.records[0].result.outcome}); "
235
+ + (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."
236
+ if first else f"still improving β€” best **{max(traj):.2f}** after {len(traj)} runs.")
237
+ + " The model proposed; a separate simulated world reported the outcome; "
238
+ "the policy and ledger learned from it. *(Simulated β€” see SIMULATION.md.)*"
239
+ )
240
+ policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);'>β–Ό learned β–Ό</div>"
241
+ f"{policy_cell_html(after, key)}")
242
+ return (
243
+ headline, # l_headline
244
+ quality_curve_html(traj), # l_curve
245
+ policy_html, # l_policy
246
+ iteration_log_html(sess.records), # l_log
247
+ ledger_html(), # ledger_panel
248
+ render_node_cards(env, working=False), # node_cards
249
+ )
250
+
251
+
252
+ def simulate_outcome(state):
253
+ """Analysis: close the loop in the world (deterministic), then learn from it."""
254
+ if not state:
255
+ return "Run a recommendation first.", ledger_html()
256
+ job = Job(**state["job"])
257
+ env = Environment(**state["env"])
258
+ settings = PrintSettings(**state["settings"])
259
+ result = simulate(settings, job, env)
260
+ learned = POLICY.update(job.material, job.geometry_type, env, result)
261
+ POLICY.save()
262
+ # record the simulated outcome to the ledger as precedent
263
+ from learn.loop import _record_lesson
264
+ _record_lesson(job, env, settings, result, LEDGER)
265
+ icon = "βœ“" if result.outcome == "success" else "βœ—"
266
+ return (
267
+ f"πŸ§ͺ **Simulated world** (stand-in for printer + sensors): {icon} **{result.detail}**. \n"
268
+ f"↳ policy update: *{learned}* \n*(The model never saw this outcome in advance β€” SIMULATION.md.)*",
269
+ ledger_html(),
270
+ )
271
+
272
+
273
+ def record_outcome(outcome, state):
274
+ if not state:
275
+ return "Run a recommendation first.", ledger_html(), render_node_cards(Environment(temp=22, humidity=45))
276
+ job = Job(**state["job"])
277
+ env = Environment(**state["env"])
278
+ settings = PrintSettings(**state["settings"])
279
+ entry = reflect_on_job(job, env, settings, outcome, LEDGER)
280
+ msg = f"πŸ“’ Lesson recorded (earned): *{entry.lesson}*"
281
+ return msg, ledger_html(), render_node_cards(env, working=False)
282
+
283
+
284
+ def launch(**kw):
285
+ """Single launch entrypoint so the Astrometrics theme/CSS/clock apply
286
+ everywhere the app is started (Gradio 6 takes these on launch, not Blocks)."""
287
+ return build().queue().launch(theme=THEME, css=CSS, head=VP_HEAD, **kw)
288
+
289
+
290
+ def build() -> gr.Blocks:
291
+ with gr.Blocks(title="The Chief Engineer") as demo:
292
+ gr.HTML(command_bar(llm.backend_status()))
293
+ gr.Markdown(
294
+ "<div class='ce-sub'>a small local model that learns 3D printing job by job β€” "
295
+ "and tells you where a print fails <b>before</b> it runs Β· gemma4 Β· fully local</div>"
296
+ )
297
+ state = gr.State()
298
+ part = gr.State({"geometry": None, "mesh": None, "label": None})
299
+
300
+ with gr.Tabs() as tabs:
301
+ # ─────────────────────────── STUDIO (define) ───────────────────────────
302
+ with gr.Tab("⬑ STUDIO", id="studio"):
303
+ gr.HTML("<div class='ce-sub'>DEFINE THE JOB Β· β‘  part β†’ β‘‘ material β†’ "
304
+ "β‘’ conditions β†’ <b>ANALYZE</b></div>")
305
+ with gr.Row():
306
+ with gr.Column(scale=2, elem_classes=["ce-console"]):
307
+ gr.HTML(rule("β‘  SELECT PART"))
308
+ gr.HTML("<div class='ce-sub'>REFERENCE PART TYPE β€” what the engineer reasons about</div>")
309
+ type_radio = gr.Radio(GEOMETRY_TYPES, value=None, show_label=False,
310
+ elem_classes=["ce-pills"])
311
+ gr.HTML("<div class='ce-sub' style='margin-top:6px;'>OR LOAD A MODEL</div>")
312
+ with gr.Row():
313
+ benchy_btn = gr.Button("β›΄ QUICK-LOAD BENCHY", elem_classes=["ce-pillbtn"])
314
+ with gr.Accordion("βš™ GENERATE A PRIMITIVE", open=False):
315
+ gen_kind = gr.Radio(["box", "cylinder", "cone", "sphere"], value="box",
316
+ show_label=False, elem_classes=["ce-pills"])
317
+ gen_size = gr.Number(value=30, label="SIZE (mm)", elem_classes=["ce-num"])
318
+ gen_btn = gr.Button("GENERATE", elem_classes=["ce-pillbtn"])
319
+ mesh_in = gr.File(file_types=[".stl", ".glb", ".obj"],
320
+ label="UPLOAD YOUR OWN MESH", elem_classes=["ce-drop"])
321
+ part_status = gr.Markdown("β–£ **no part selected** β€” pick a type, load Benchy, "
322
+ "generate, or upload to begin")
323
+
324
+ gr.HTML(rule("β‘‘ MATERIAL"))
325
+ material = gr.Radio(MATERIALS, value="PLA", show_label=False, elem_classes=["ce-pills"])
326
+
327
+ gr.HTML(rule("β‘’ SIMULATED SENSORS"))
328
+ sensors_readout = gr.HTML()
329
+ roll_btn = gr.Button("🎲 RANDOMIZE SENSORS", elem_classes=["ce-pillbtn"])
330
+ with gr.Accordion("βš™ OVERRIDE SENSORS", open=False):
331
+ with gr.Row():
332
+ temp = gr.Number(value=22, label="AMBIENT Β°C", elem_classes=["ce-num"])
333
+ humidity = gr.Number(value=45, label="HUMIDITY %RH", elem_classes=["ce-num"])
334
+ gr.HTML("<div class='ce-sub'>BUILD-PLATE POSITION β€” edges/corners run "
335
+ "cooler β†’ warp/adhesion risk</div>")
336
+ bed_position = gr.Radio(BED_POSITIONS, value="center", show_label=False,
337
+ elem_classes=["ce-pills"])
338
+ description = gr.Textbox(label="NOTES (OPTIONAL)",
339
+ placeholder="e.g. 45Β° bracket, 60mm tall")
340
+ run_btn = gr.Button("β–Ά ANALYZE JOB", variant="primary")
341
+ with gr.Column(scale=3):
342
+ gr.HTML(rule("PART PREVIEW"))
343
+ model3d = gr.Model3D(value=None, label="", height=320)
344
+ gr.HTML("<div class='ce-sub'>Rotate/zoom the part. The layer-by-layer "
345
+ "<b>virtual print</b> renders on the Analysis page when you ANALYZE.</div>")
346
+
347
+ # ─────────────────────────── ANALYSIS (results) ────────────────────────
348
+ with gr.Tab("β—‡ ANALYSIS", id="analysis"):
349
+ backend = gr.Markdown()
350
+ gr.HTML("<div class='ce-sub'>β‘  the engineer's read β†’ β‘‘ validated settings + "
351
+ "g-code β†’ β‘’ virtual print β†’ β‘£ close the loop</div>")
352
+ with gr.Row():
353
+ with gr.Column():
354
+ gr.HTML(rule("β‘  PRECEDENT EVALUATION"))
355
+ precedent = gr.HTML(elem_id="ce-precedent")
356
+ gr.HTML(rule("ENGINEER'S REASONING"))
357
+ reasoning = gr.Markdown(elem_id="ce-reasoning")
358
+ gr.HTML(rule("PREDICTED FAILURE REGIONS"))
359
+ risks = gr.HTML()
360
+ with gr.Column():
361
+ gr.HTML(rule("β‘‘ VALIDATION + G-CODE"))
362
+ spine_notes = gr.Markdown()
363
+ settings_html = gr.HTML()
364
+ gcode_html = gr.HTML()
365
+ confirm_btn = gr.Button("βœ… Confirm & Print", elem_id="ce-confirm", visible=False)
366
+ gr.HTML(rule("β‘’ VIRTUAL PRINT"))
367
+ vprint = gr.HTML()
368
+ gr.HTML("<div class='ce-sub'>Slide through the layers β€” each is a real "
369
+ "cross-section of <i>this</i> part at full mesh fidelity.</div>")
370
+ vp_slider = gr.Slider(1, SCRUB_LAYERS, value=1, step=1, label="LAYER")
371
+ vp_layer = gr.Image(label="", height=380, show_label=False)
372
+ gr.HTML(rule("β‘£ CLOSE THE LOOP"))
373
+ gr.HTML("<div class='ce-sub'>Simulate the print (deterministic stand-in for printer + "
374
+ "sensors), or record a real outcome. Either way the model never grades itself.</div>")
375
+ with gr.Row(elem_id="ce-sim"):
376
+ sim_btn = gr.Button("πŸ§ͺ Simulate outcome (deterministic world)")
377
+ with gr.Row(elem_id="ce-outcomes"):
378
+ b_clean = gr.Button("βœ“ Printed clean")
379
+ b_sag = gr.Button("βœ— Sagged")
380
+ b_string = gr.Button("βœ— Stringing")
381
+ outcome_msg = gr.Markdown()
382
+
383
+ # ─────────────────────────── LEARNING LOOP ─────────────────────────────
384
+ with gr.Tab("β—³ LEARNING LOOP", id="loop"):
385
+ gr.HTML(
386
+ "<div class='ce-sub'>THE COMPOUNDING LOOP Β· the model proposes β†’ a <b>simulated world</b> "
387
+ "reports the outcome β†’ the <b>learned policy</b> and <b>ledger</b> update β†’ the next run is "
388
+ "better. Pick a hard job and watch quality climb from failure to clean. "
389
+ "<i>Simulated; physical interfaces framed in SIMULATION.md.</i></div>"
390
+ )
391
+ with gr.Row():
392
+ with gr.Column(scale=2, elem_classes=["ce-console"]):
393
+ gr.HTML(rule("β‘  SIMULATION CONTROLS"))
394
+ l_geometry = gr.Radio(GEOMETRY_TYPES, value="bridge", show_label=False,
395
+ elem_classes=["ce-pills"])
396
+ l_material = gr.Radio(MATERIALS, value="PETG", show_label=False, elem_classes=["ce-pills"])
397
+ with gr.Row():
398
+ l_temp = gr.Number(value=29, label="AMBIENT Β°C", elem_classes=["ce-num"])
399
+ l_humidity = gr.Number(value=62, label="HUMIDITY %RH", elem_classes=["ce-num"])
400
+ l_iters = gr.Slider(1, 16, value=8, step=1, label="ITERATIONS")
401
+ l_run = gr.Button("β–Ά RUN SIMULATED LEARNING", variant="primary")
402
+ l_headline = gr.Markdown()
403
+ with gr.Column(scale=3):
404
+ gr.HTML(rule("β‘‘ QUALITY PER ITERATION"))
405
+ l_curve = gr.HTML()
406
+ gr.HTML(rule("β‘’ LEARNED POLICY CELL"))
407
+ l_policy = gr.HTML()
408
+ gr.HTML(rule("β‘£ ITERATION LOG"))
409
+ l_log = gr.HTML()
410
+
411
+ # ─────────────────────────── KNOWLEDGE + MESH ──────────────────────────
412
+ with gr.Tab("β–€ KNOWLEDGE + MESH", id="ledger"):
413
+ gr.HTML("<div class='ce-sub'>The capability mesh (one node executes, the others "
414
+ "stand by as context) and the live ledger where knowledge compounds "
415
+ "(seed β†’ earned β†’ sim).</div>")
416
+ with gr.Row():
417
+ with gr.Column():
418
+ gr.HTML(rule("CAPABILITY MESH"))
419
+ node_cards = gr.HTML(render_node_cards(Environment(temp=22, humidity=45)))
420
+ with gr.Column():
421
+ gr.HTML(rule("LESSON LEDGER"))
422
+ ledger_panel = gr.HTML(ledger_html())
423
+ refresh = gr.Button("⟳ REFRESH LEDGER", elem_classes=["ce-pillbtn"])
424
+
425
+ gr.HTML(footer_bar())
426
+
427
+ # ── wiring ──
428
+ preview_outs = [part, model3d, part_status]
429
+ type_radio.change(select_type, [type_radio], preview_outs)
430
+ benchy_btn.click(load_benchy, None, preview_outs)
431
+ gen_btn.click(generate_part, [gen_kind, gen_size], preview_outs)
432
+ mesh_in.upload(upload_part, [mesh_in], preview_outs)
433
+
434
+ # Simulated sensors: roll on load, re-roll on demand, keep the readout in sync.
435
+ sensor_outs = [temp, humidity, bed_position, sensors_readout]
436
+ demo.load(randomize_sensors, None, sensor_outs)
437
+ roll_btn.click(randomize_sensors, None, sensor_outs)
438
+ for c in (temp, humidity, bed_position):
439
+ c.change(sync_readout, [temp, humidity, bed_position], [sensors_readout])
440
+
441
+ # Two-step ANALYZE: instant tab-switch + spinners, then the heavy model call.
442
+ analyze_outs = [backend, precedent, reasoning, risks, settings_html, spine_notes,
443
+ gcode_html, confirm_btn, node_cards, state, vprint, vp_slider, vp_layer]
444
+ run_btn.click(analyze_start, [part],
445
+ [tabs, reasoning, precedent, risks, settings_html, gcode_html, vprint]).then(
446
+ get_recommendation, [part, material, description, temp, humidity, bed_position],
447
+ analyze_outs)
448
+ vp_slider.change(scrub_layer, [vp_slider, part], [vp_layer])
449
+
450
+ for btn, oc in [(b_clean, "success"), (b_sag, "failed_sag"), (b_string, "failed_stringing")]:
451
+ btn.click(record_outcome, [gr.State(oc), state], [outcome_msg, ledger_panel, node_cards])
452
+ sim_btn.click(simulate_outcome, [state], [outcome_msg, ledger_panel])
453
+ refresh.click(lambda: ledger_html(), outputs=[ledger_panel])
454
+
455
+ l_run.click(run_learning, [l_material, l_geometry, l_temp, l_humidity, l_iters],
456
+ [l_headline, l_curve, l_policy, l_log, ledger_panel, node_cards])
457
+
458
+ return demo
459
+
460
 
461
+ if __name__ == "__main__":
462
+ print(f"[chief-engineer] {llm.backend_status()} | seeded {_loaded} lessons this run")
463
+ launch(server_name="0.0.0.0", server_port=7860)
assets/benchy.glb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:37474bde1a081da6cda0a2d1ebc3c384a30f280fe335678aecb6873cc9da2ccc
3
+ size 1077356
assets/bridge.glb ADDED
Binary file (1.43 kB). View file
 
assets/cube.glb ADDED
Binary file (968 Bytes). View file
 
assets/overhang.glb ADDED
Binary file (1.18 kB). View file
 
assets/vase.glb ADDED
Binary file (5.38 kB). View file
 
core/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Core library for the Chief Engineer (models, LLM, ledger, spine, advisor).
2
+ Imported as a package; entrypoints live at the repo root (app.py) and in scripts/."""
3
+
4
+ import os
5
+ from pathlib import Path
6
+
7
+
8
+ def _load_dotenv() -> None:
9
+ """Tiny stdlib .env loader (no python-dotenv dep): reads KEY=VALUE lines from
10
+ the repo root .env. Real environment always wins (setdefault), so
11
+ `CHIEF_ENGINEER_MODEL=x make run` still overrides the file."""
12
+ env = Path(__file__).resolve().parent.parent / ".env"
13
+ if not env.is_file():
14
+ return
15
+ for line in env.read_text().splitlines():
16
+ line = line.strip()
17
+ if not line or line.startswith("#") or "=" not in line:
18
+ continue
19
+ k, _, v = line.partition("=")
20
+ k, v = k.strip(), v.strip().strip('"').strip("'")
21
+ if k:
22
+ os.environ.setdefault(k, v)
23
+
24
+
25
+ _load_dotenv()
core/chief_engineer.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The Chief Engineer β€” Brain.
2
+
3
+ Reimplements hubAgent.ts's call shape (pattern-review.md Β§1): assemble system
4
+ prompt β†’ real Ollama call β†’ parse to Pydantic, with a conservative fallback so
5
+ a parse failure or an offline Space never crashes the demo. The reasoning text
6
+ carries the model's EVALUATION of precedent β€” the load-bearing moment.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from . import llm
14
+ from .models import Advice, Environment, Job, LessonEntry, PrintSettings, RiskRegion
15
+ 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"),
26
+ "bridge": ("the unsupported span", "sag", "long bridges sag without enough cooling", "bridge"),
27
+ "stringing": ("travel moves across gaps", "stringing", "molten plastic oozes during travel", None),
28
+ "adhesion": ("first layer / corners", "adhesion", "poor first-layer bond lifts the part", "first_layer"),
29
+ "vase": ("thin single-wall sections", "warping", "tall thin walls can wobble or warp", None),
30
+ }
31
+
32
+
33
+ @dataclass
34
+ class Recommendation:
35
+ advice: Advice
36
+ used_fallback: bool
37
+ backend: str
38
+
39
+
40
+ def _fallback_advice(job: Job, env: Environment, retrieved: list[tuple[LessonEntry, float]]) -> Advice:
41
+ base = _FALLBACK_SETTINGS.get(job.material.upper(), _FALLBACK_SETTINGS["PLA"])
42
+ loc, risk, why, hint = _GEOMETRY_RISK.get(
43
+ job.geometry_type, ("the part", "warping", "general risk", None)
44
+ )
45
+ if retrieved:
46
+ e, dist = retrieved[0]
47
+ reasoning = (
48
+ f"[fallback] Nearest precedent: Job {e.job_id} ({e.material}/{e.geometry_type} "
49
+ f"@ {e.env_temp:.0f}Β°C/{e.env_humidity:.0f}% β†’ {e.outcome}, env-dist {dist:.2f}). "
50
+ f"Applying its lesson with conservative {job.material} defaults."
51
+ )
52
+ else:
53
+ reasoning = (
54
+ f"[fallback] No close precedent for {job.material}/{job.geometry_type}. "
55
+ f"Reasoning from material properties with conservative defaults."
56
+ )
57
+ return Advice(
58
+ reasoning=reasoning,
59
+ settings=PrintSettings(**base),
60
+ risks=[RiskRegion(location=loc, risk=risk, why=why, anchor_hint=hint)],
61
+ )
62
+
63
+
64
+ def advise(
65
+ job: Job,
66
+ env: Environment,
67
+ retrieved: list[tuple[LessonEntry, float]],
68
+ references: list[str] | None = None,
69
+ policy_note: str | None = None,
70
+ ) -> Recommendation:
71
+ system = build_system_prompt(job, env, retrieved, references, policy_note)
72
+ raw = llm.chat_json(system, "Give your recommendation for THIS job now.")
73
+ backend = llm.backend_status()
74
+ if raw is None:
75
+ return Recommendation(_fallback_advice(job, env, retrieved), used_fallback=True, backend=backend)
76
+ try:
77
+ advice = Advice(**raw)
78
+ return Recommendation(advice, used_fallback=False, backend=backend)
79
+ except Exception:
80
+ return Recommendation(_fallback_advice(job, env, retrieved), used_fallback=True, backend=backend)
core/ledger.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Append-only lesson ledger + environment-keyed retrieval.
2
+
3
+ Modeled on pi-qmd-ledger's `append_ledger` (pattern-review.md Β§3) but with the
4
+ qmd embeddings / vector search deliberately left behind. Retrieval is the
5
+ locked design: exact match on material AND geometry_type, then rank by
6
+ Euclidean distance on NORMALIZED [temp, humidity], top 2-3. No vector DB.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import math
13
+ from pathlib import Path
14
+
15
+ from .models import LessonEntry
16
+
17
+ DATA_DIR = Path(__file__).resolve().parent.parent / "data"
18
+ LEDGER_PATH = DATA_DIR / "lessons.jsonl"
19
+
20
+ # Normalization ranges (02-ARCHITECTURE.md / 04-BUILD-PROMPT.md): map both env
21
+ # axes to 0-1 so humidity's 0-100 span doesn't swamp temperature in the metric.
22
+ TEMP_MIN, TEMP_MAX = 15.0, 35.0
23
+ HUM_MIN, HUM_MAX = 20.0, 80.0
24
+
25
+
26
+ def _norm(temp: float, humidity: float) -> tuple[float, float]:
27
+ t = (temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN)
28
+ h = (humidity - HUM_MIN) / (HUM_MAX - HUM_MIN)
29
+ return t, h
30
+
31
+
32
+ class LedgerManager:
33
+ def __init__(self, path: Path = LEDGER_PATH) -> None:
34
+ self.path = path
35
+ self.path.parent.mkdir(parents=True, exist_ok=True)
36
+ self.path.touch(exist_ok=True)
37
+
38
+ # --- storage -----------------------------------------------------------
39
+ def all(self) -> list[LessonEntry]:
40
+ out: list[LessonEntry] = []
41
+ for line in self.path.read_text(encoding="utf-8").splitlines():
42
+ line = line.strip()
43
+ if not line:
44
+ continue
45
+ try:
46
+ out.append(LessonEntry(**json.loads(line)))
47
+ except Exception:
48
+ continue # never let one bad line crash the demo
49
+ return out
50
+
51
+ def append(self, entry: LessonEntry) -> None:
52
+ with self.path.open("a", encoding="utf-8") as f:
53
+ f.write(entry.model_dump_json() + "\n")
54
+
55
+ def count(self) -> dict[str, int]:
56
+ buckets = {"seed": 0, "earned": 0, "ingested": 0, "sim": 0}
57
+ for e in self.all():
58
+ buckets[e.source if e.source in buckets else "earned"] += 1
59
+ buckets["total"] = sum(buckets.values())
60
+ return buckets
61
+
62
+ # --- retrieval (the thesis-critical query) -----------------------------
63
+ def retrieve(
64
+ self, material: str, geometry_type: str, temp: float, humidity: float, k: int = 3
65
+ ) -> list[tuple[LessonEntry, float]]:
66
+ """Return up to k (lesson, env_distance) sorted nearest-first.
67
+
68
+ Exact match on material AND geometry_type; ranked by normalized
69
+ Euclidean env-distance. Returns [] when no precedent matches β€” which
70
+ is a *valid, strong* outcome (the model reasons from material
71
+ properties instead).
72
+ """
73
+ tn, hn = _norm(temp, humidity)
74
+ scored: list[tuple[LessonEntry, float]] = []
75
+ for e in self.all():
76
+ if e.material != material or e.geometry_type != geometry_type:
77
+ continue
78
+ etn, ehn = _norm(e.env_temp, e.env_humidity)
79
+ dist = math.hypot(tn - etn, hn - ehn)
80
+ scored.append((e, dist))
81
+ scored.sort(key=lambda x: x[1])
82
+ return scored[:k]
core/llm.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local Ollama client (real calls, not mocks).
2
+
3
+ Fully local Gemma inference β†’ earns Off the Grid + Llama Champion by
4
+ construction (Ollama runs on llama.cpp). The UI surfaces which model ran and
5
+ whether it was a real call or the deterministic fallback (never crash the demo).
6
+
7
+ `gemma4:e4b` is the default (= gemma4:latest, 9.6GB). `gemma4:e2b` if CPU
8
+ latency demands. NOTE: `gemma4:4b` does NOT exist β€” never use that tag.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+
16
+ MODEL = os.environ.get("CHIEF_ENGINEER_MODEL", "gemma4:e4b")
17
+
18
+ # Backend select. Default "ollama" keeps local/recording behavior IDENTICAL.
19
+ # Set CHIEF_ENGINEER_BACKEND=zerogpu ONLY on the published Space (see
20
+ # llm_zerogpu.py + docs/plan/08-ZEROGPU-DEPLOY.md). Unknown/import-fail β†’ ollama.
21
+ BACKEND = os.environ.get("CHIEF_ENGINEER_BACKEND", "ollama").lower()
22
+
23
+ try:
24
+ import ollama # type: ignore
25
+ except Exception: # pragma: no cover
26
+ ollama = None # type: ignore
27
+
28
+
29
+ def _zerogpu():
30
+ """Lazily import the ZeroGPU backend; None unless selected and importable."""
31
+ if BACKEND != "zerogpu":
32
+ return None
33
+ try:
34
+ from . import llm_zerogpu # heavy deps are import-guarded inside
35
+ return llm_zerogpu
36
+ except Exception:
37
+ return None
38
+
39
+
40
+ def _forced_offline() -> bool:
41
+ """Force the deterministic fallback path regardless of any daemon/backend.
42
+ Read dynamically (not cached) so tests can toggle it. Used by the offline
43
+ core suite so `make test` never touches Ollama, even when `ollama serve` is up."""
44
+ return os.environ.get("CHIEF_ENGINEER_OFFLINE", "").lower() in ("1", "true", "yes")
45
+
46
+
47
+ def is_available() -> bool:
48
+ """True if the active backend can serve a real call."""
49
+ if _forced_offline():
50
+ return False
51
+ zg = _zerogpu()
52
+ if zg is not None:
53
+ return zg.is_available()
54
+ if ollama is None:
55
+ return False
56
+ try:
57
+ ollama.list()
58
+ return True
59
+ except Exception:
60
+ return False
61
+
62
+
63
+ def backend_status() -> str:
64
+ zg = _zerogpu()
65
+ if zg is not None:
66
+ return zg.backend_status()
67
+ return f"🟒 live · {MODEL} (local Ollama)" if is_available() else \
68
+ f"🟑 offline fallback · {MODEL} unreachable (deterministic)"
69
+
70
+
71
+ def chat_json(system: str, user: str, temperature: float = 0.4) -> dict | None:
72
+ """One JSON-mode chat turn. Returns parsed dict, or None to signal fallback."""
73
+ if _forced_offline():
74
+ return None
75
+ zg = _zerogpu()
76
+ if zg is not None:
77
+ return zg.chat_json(system, user, temperature)
78
+ if not is_available():
79
+ return None
80
+ try:
81
+ resp = ollama.chat(
82
+ model=MODEL,
83
+ messages=[
84
+ {"role": "system", "content": system},
85
+ {"role": "user", "content": user},
86
+ ],
87
+ format="json",
88
+ options={"temperature": temperature},
89
+ )
90
+ content = resp["message"]["content"].strip()
91
+ # Fence-strip safety net (GEMMA-STEERING Technique 2): small Gemmas can
92
+ # wrap JSON in ```json fences even in JSON mode. Strip before parsing.
93
+ if content.startswith("```"):
94
+ content = content.strip("`").lstrip()
95
+ if content[:4].lower() == "json":
96
+ content = content[4:]
97
+ return json.loads(content)
98
+ except Exception:
99
+ return None
core/llm_zerogpu.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ZeroGPU inference backend β€” STAGED for the published HF Space, OFF by default.
2
+
3
+ Why this exists: `docs/writeup/05-PROJECTED-OUTCOME.md` Β§3 flags that a judge who
4
+ opens our Space sees the deterministic FALLBACK (Ollama can't run on a standard
5
+ CPU Space), while only the video shows the real model. This module lets the LIVE
6
+ Space run the real model on **ZeroGPU** with a **QAT Gemma** checkpoint β€” closing
7
+ that gap β€” WITHOUT changing anything locally or in the current demo path.
8
+
9
+ Activation is opt-in and total:
10
+ - Selected ONLY when `CHIEF_ENGINEER_BACKEND=zerogpu`. Otherwise `llm.py`
11
+ behaves exactly as before (Ollama β†’ deterministic fallback). Nothing about
12
+ the local dev / recording flow changes.
13
+ - Deploy-only deps (torch/transformers/accelerate/spaces) live in
14
+ `requirements-zerogpu.txt`, NOT base `requirements.txt`, and are import-
15
+ guarded here β€” importing this on a box without them is a safe no-op that
16
+ reports unavailable.
17
+
18
+ It preserves the Off-the-Grid story: the model CAN run fully local (proven in the
19
+ video); ZeroGPU is just the hosted convenience so judges can try it live.
20
+
21
+ Model ids RESOLVED 6/10 β€” see docs/plan/08-ZEROGPU-DEPLOY.md (primary:
22
+ google/gemma-4-E2B-it, bf16 transformers-native). Original checklist:
23
+ - Set `CHIEF_ENGINEER_HF_MODEL` to the QAT Gemma checkpoint you pick. Google's
24
+ Gemma QAT int4 releases are the target. CONFIRM the exact repo id AND that it
25
+ loads via `transformers` β€” some QAT releases ship **GGUF** (for llama.cpp);
26
+ for those, use a llama-cpp-python path, not this transformers one.
27
+ - Confirm the model is ≀32B (hackathon rule) and note its params for Tiny Titan.
28
+ See `docs/plan/08-ZEROGPU-DEPLOY.md`.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import os
35
+ import re
36
+
37
+ HF_MODEL = os.environ.get("CHIEF_ENGINEER_HF_MODEL", "google/gemma-4-E2B-it") # resolved 6/10
38
+ _GPU_SECONDS = int(os.environ.get("CHIEF_ENGINEER_GPU_SECONDS", "90")) # 1st call loads the model
39
+ _MAX_NEW = int(os.environ.get("CHIEF_ENGINEER_MAX_NEW_TOKENS", "512"))
40
+
41
+ # Import-guarded heavy deps. Absent locally β†’ module reports unavailable, no crash.
42
+ try:
43
+ import torch # type: ignore
44
+ from transformers import AutoModelForCausalLM, AutoTokenizer # type: ignore
45
+ _HAVE_HF = True
46
+ except Exception: # pragma: no cover
47
+ torch = None # type: ignore
48
+ _HAVE_HF = False
49
+
50
+ try:
51
+ import spaces # type: ignore (the ZeroGPU package; only present on the Space)
52
+ _HAVE_SPACES = True
53
+ except Exception: # pragma: no cover
54
+ _HAVE_SPACES = False
55
+
56
+
57
+ def _gpu(fn):
58
+ """Decorate with @spaces.GPU when available; identity off-Space (e.g. local GPU)."""
59
+ if _HAVE_SPACES:
60
+ return spaces.GPU(duration=_GPU_SECONDS)(fn)
61
+ return fn
62
+
63
+
64
+ _tok = None
65
+ _model = None
66
+
67
+
68
+ def _ensure_loaded() -> bool:
69
+ """Load tokenizer + model once. MUST be called inside the @spaces.GPU context on
70
+ ZeroGPU (GPU is only allocated there) β€” we load to CPU then move to CUDA. No
71
+ device_map='auto' (discouraged on ZeroGPU; it grabs devices outside the GPU window)."""
72
+ global _tok, _model
73
+ if not _HAVE_HF:
74
+ return False
75
+ if _model is not None:
76
+ return True
77
+ try:
78
+ _tok = AutoTokenizer.from_pretrained(HF_MODEL)
79
+ _model = AutoModelForCausalLM.from_pretrained(
80
+ HF_MODEL,
81
+ torch_dtype=getattr(torch, "bfloat16", None),
82
+ low_cpu_mem_usage=True,
83
+ )
84
+ if torch is not None and torch.cuda.is_available():
85
+ _model = _model.to("cuda")
86
+ return True
87
+ except Exception:
88
+ _tok = _model = None
89
+ return False
90
+
91
+
92
+ def is_available() -> bool:
93
+ """Whether this backend CAN serve β€” i.e. the heavy deps imported. The actual
94
+ model load is lazy (first chat, inside the GPU window), so we don't block app
95
+ startup on a multi-GB load that would also run outside the GPU allocation."""
96
+ return _HAVE_HF
97
+
98
+
99
+ def backend_status() -> str:
100
+ where = "ZeroGPU" if _HAVE_SPACES else "local GPU/CPU"
101
+ if not _HAVE_HF:
102
+ return f"🟑 offline fallback · transformers/torch absent (deterministic)"
103
+ loaded = " (loaded)" if _model is not None else " (loads on first analyze)"
104
+ return f"🟒 live · {HF_MODEL} (transformers on {where}){loaded}"
105
+
106
+
107
+ def _build_prompt(system: str, user: str) -> str:
108
+ # Gemma's chat template has no separate system role β€” fold system into the
109
+ # first user turn (the standard Gemma pattern).
110
+ messages = [{"role": "user", "content": f"{system}\n\n{user}"}]
111
+ return _tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
112
+
113
+
114
+ @_gpu
115
+ def _generate(system: str, user: str, temperature: float) -> str | None:
116
+ if not _ensure_loaded():
117
+ return None
118
+ prompt = _build_prompt(system, user)
119
+ if torch is not None and torch.cuda.is_available() and _model.device.type != "cuda":
120
+ _model.to("cuda") # ZeroGPU: ensure on-GPU during the call
121
+ inputs = _tok(prompt, return_tensors="pt").to(_model.device)
122
+ out = _model.generate(
123
+ **inputs,
124
+ max_new_tokens=_MAX_NEW,
125
+ do_sample=temperature > 0,
126
+ temperature=max(temperature, 1e-4),
127
+ )
128
+ text = _tok.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
129
+ return text
130
+
131
+
132
+ _JSON = re.compile(r"\{.*\}", re.DOTALL)
133
+
134
+
135
+ def chat_json(system: str, user: str, temperature: float = 0.4) -> dict | None:
136
+ """Mirror of llm.chat_json's contract: parsed dict, or None to trigger fallback."""
137
+ try:
138
+ text = _generate(system, user, temperature)
139
+ except Exception:
140
+ return None
141
+ if not text:
142
+ return None
143
+ # Strip code fences, then grab the outermost JSON object.
144
+ text = text.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
145
+ m = _JSON.search(text)
146
+ if not m:
147
+ return None
148
+ try:
149
+ return json.loads(m.group(0))
150
+ except Exception:
151
+ return None
core/models.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pydantic domain types for the Chief Engineer.
2
+
3
+ Adapted from microfactory-lab `core/src/types.ts` β€” stripped of routing,
4
+ economic, energy, and tick concepts (over-scope per pattern-review.md Β§7).
5
+ Only what the compounding-knowledge loop needs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ MATERIALS = ["PLA", "PETG", "ABS", "TPU"]
13
+ # geometry_type doubles as a retrieval key (pattern-review.md Β§3/Β§9)
14
+ GEOMETRY_TYPES = ["overhang", "bridge", "stringing", "adhesion", "vase"]
15
+ OUTCOMES = ["success", "failed_sag", "failed_stringing"]
16
+ # where the part sits on the heated bed β€” drives edge-cooling warp + adhesion risk
17
+ BED_POSITIONS = ["center", "edge", "corner"]
18
+
19
+
20
+ class Job(BaseModel):
21
+ geometry_type: str
22
+ material: str
23
+ description: str = ""
24
+ mesh_path: str | None = None
25
+ bed_position: str = "center" # center|edge|corner β€” affects warp/adhesion
26
+
27
+
28
+ class Environment(BaseModel):
29
+ temp: float = Field(description="Ambient temperature Β°C")
30
+ humidity: float = Field(description="Ambient humidity %")
31
+
32
+
33
+ class PrintSettings(BaseModel):
34
+ nozzle_temp: float
35
+ bed_temp: float
36
+ retraction_mm: float
37
+ fan_pct: float
38
+ first_layer_fan_pct: float
39
+
40
+
41
+ class RiskRegion(BaseModel):
42
+ location: str # human description, e.g. "underside of the overhang"
43
+ risk: str # "sag" | "stringing" | "adhesion" | "warping" | ...
44
+ why: str # one-line rationale
45
+ anchor_hint: str | None = None # optional geometry hint for the 3D annotation
46
+
47
+
48
+ class Advice(BaseModel):
49
+ """The Chief Engineer's structured proposal (the LLM's output)."""
50
+
51
+ reasoning: str # the EVALUATION of precedent β€” the load-bearing text
52
+ settings: PrintSettings
53
+ risks: list[RiskRegion] = []
54
+
55
+
56
+ class LessonEntry(BaseModel):
57
+ """Append-only ledger record. Schema is locked (02-ARCHITECTURE.md)."""
58
+
59
+ job_id: str
60
+ material: str
61
+ geometry_type: str
62
+ env_temp: float
63
+ env_humidity: float
64
+ outcome: str
65
+ lesson: str
66
+ source: str = "earned" # "seed" | "earned"
67
+ timestamp: str
core/nodes.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Node / swarm mesh β€” faithful VISUALIZATION of the lab, not a distributed system.
2
+
3
+ Mirrors microfactory-lab's `zellij-layout.kdl`: a Chief Engineer hub coordinating
4
+ capability nodes. Only the 3D Print node executes real work (the core loop);
5
+ the others render as mesh context / available capacity. No FileBridge IPC, no
6
+ pi-link, no six live processes (that was the Kaggle failure surface).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .models import Environment
12
+
13
+ # (name, capability, active?) β€” active node runs the real core loop.
14
+ NODES = [
15
+ ("Chief Engineer", "hub / router", True),
16
+ ("3D Print", "fdm", True),
17
+ ("CNC Mill", "cnc", False),
18
+ ("Laser Cutter", "laser", False),
19
+ ("Sinter Press", "sinter", False),
20
+ ("Metal 3D Print", "metal3d", False),
21
+ ("Hub Router", "routing", False),
22
+ ]
23
+
24
+
25
+ def render_node_cards(env: Environment, working: bool = False) -> str:
26
+ cards = []
27
+ for name, cap, active in NODES:
28
+ if name == "3D Print":
29
+ state = "🟠 WORKING" if working else "🟒 ONLINE"
30
+ sub = f"{env.temp:.0f}Β°C Β· {env.humidity:.0f}% RH"
31
+ border = "var(--ao-orange)"
32
+ elif active:
33
+ state = "🟒 ONLINE"
34
+ sub = "coordinating"
35
+ border = "var(--ao-orange)"
36
+ else:
37
+ state = "βšͺ IDLE"
38
+ sub = "mesh context Β· available capacity"
39
+ border = "var(--ao-outline-dim)"
40
+ cards.append(
41
+ f"<div style='border-left:3px solid {border};background:var(--ao-surface);"
42
+ f"padding:8px 12px;margin:6px 0;font-family:ui-monospace,monospace;'>"
43
+ f"<div style='color:var(--ao-orange);font-weight:600;letter-spacing:1px;'>{name.upper()}"
44
+ f"<span style='float:right;color:var(--ao-text);font-weight:400;'>{state}</span></div>"
45
+ f"<div style='color:var(--ao-outline);font-size:11px;'>{cap} Β· {sub}</div></div>"
46
+ )
47
+ return (
48
+ "<div><div style='color:var(--ao-outline);font-family:ui-monospace,monospace;"
49
+ "font-size:11px;margin-bottom:4px;'>CAPABILITY MESH β€” 1 node executing, "
50
+ "others shown as network context</div>" + "".join(cards) + "</div>"
51
+ )
core/prompts.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """System-prompt assembly (versioned instruction steering, not activation steering).
2
+
3
+ `build_system_prompt()` concatenates: persona + job/env + a Historical
4
+ Precedent block of 2-3 retrieved prior jobs. CRITICAL: the prompt asks the
5
+ model to EVALUATE applicability β€” apply, adapt, or set precedent aside, and
6
+ say "no close precedent" when nothing fits. It is NOT told to always cite.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .models import Environment, Job, LessonEntry
12
+
13
+ PERSONA = """You are the Chief Engineer: a veteran print-shop master who has run \
14
+ thousands of FDM jobs. You are terse and physical. You think in feeds, temps, \
15
+ cooling, and how the room affects the plastic. You do not hype. You proposed \
16
+ settings; a deterministic Spine will veto anything unsafe, so propose what is \
17
+ *right*, not what is merely safe.
18
+
19
+ You reason about PRECEDENT before you decide. You are given similar prior jobs \
20
+ with their conditions and outcomes. Weigh what transfers to THIS job and what \
21
+ does not. If a prior job is close, apply or adapt its lesson and say so. If \
22
+ nothing close applies, say "no close precedent" and reason from material \
23
+ properties. Knowing what you don't know is a strength, not a weakness."""
24
+
25
+ OUTPUT_CONTRACT = """Respond ONLY with valid JSON, no prose outside it, in exactly this shape:
26
+ {
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",
34
+ "why": "one line", "anchor_hint": "overhang|bridge|first_layer|corner|null"}
35
+ ]
36
+ }"""
37
+
38
+
39
+ def _precedent_block(lessons: list[tuple[LessonEntry, float]]) -> str:
40
+ if not lessons:
41
+ return (
42
+ "HISTORICAL PRECEDENT:\n"
43
+ " (none) β€” no prior job matches this material + geometry. "
44
+ "Reason from material properties and say so plainly.\n"
45
+ )
46
+ lines = ["HISTORICAL PRECEDENT (nearest prior jobs by environment):"]
47
+ for i, (e, dist) in enumerate(lessons, 1):
48
+ lines.append(
49
+ f" [{i}] Job {e.job_id} ({e.source}) β€” {e.material}/{e.geometry_type} "
50
+ f"@ {e.env_temp:.0f}Β°C, {e.env_humidity:.0f}% RH β†’ {e.outcome} "
51
+ f"(env-distance {dist:.2f})\n lesson: {e.lesson}"
52
+ )
53
+ return "\n".join(lines) + "\n"
54
+
55
+
56
+ def _reference_block(references: list[str]) -> str:
57
+ if not references:
58
+ return ""
59
+ lines = "\n".join(f" - {r}" for r in references)
60
+ return (
61
+ "MATERIAL REFERENCE (hard parameters distilled from your slicer/firmware configs):\n"
62
+ f"{lines}\nTreat these as bounds/baselines, not precedent.\n\n"
63
+ )
64
+
65
+
66
+ def build_system_prompt(
67
+ job: Job,
68
+ env: Environment,
69
+ retrieved: list[tuple[LessonEntry, float]],
70
+ references: list[str] | None = None,
71
+ policy_note: str | None = None,
72
+ ) -> str:
73
+ policy_block = f"{policy_note}\n\n" if policy_note else ""
74
+ return (
75
+ f"{PERSONA}\n\n"
76
+ f"CURRENT JOB:\n"
77
+ f" material: {job.material}\n"
78
+ f" geometry: {job.geometry_type}\n"
79
+ f" description: {job.description or '(none given)'}\n\n"
80
+ f"ENVIRONMENT (right now in the room):\n"
81
+ f" temperature: {env.temp:.0f}Β°C\n"
82
+ f" humidity: {env.humidity:.0f}% RH\n\n"
83
+ f"{_reference_block(references or [])}"
84
+ f"{_precedent_block(retrieved)}\n"
85
+ f"{policy_block}"
86
+ f"{OUTPUT_CONTRACT}"
87
+ )
88
+
89
+
90
+ # --- reflection prompt (post-job compression) ------------------------------
91
+ REFLECT_SYSTEM = """You are the Chief Engineer distilling a finished job into ONE \
92
+ durable, reusable lesson for your future self. Be specific about material, the \
93
+ conditions, and the lever that mattered. One or two sentences. No fluff.
94
+
95
+ Respond ONLY with valid JSON: {"lesson": "<one or two sentence lesson>"}"""
96
+
97
+
98
+ def build_reflect_prompt(job: Job, env: Environment, settings_summary: str, outcome: str) -> str:
99
+ return (
100
+ f"JOB: {job.material}/{job.geometry_type} β€” {job.description or '(no description)'}\n"
101
+ f"ROOM: {env.temp:.0f}Β°C, {env.humidity:.0f}% RH\n"
102
+ f"SETTINGS USED: {settings_summary}\n"
103
+ f"REAL OUTCOME (human-reported): {outcome}\n\n"
104
+ f"Write the lesson."
105
+ )
core/reflect.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Post-job reflection β€” the compression step (the thesis).
2
+
3
+ Reimplements hubAgent.ts `reviewOutcome()` (pattern-review.md Β§4), re-keyed
4
+ from routing-confidence to environment + geometry. A finished job + a
5
+ HUMAN-REPORTED outcome becomes one durable, env-keyed LessonEntry. The model
6
+ NEVER judges its own outcome β€” the outcome is always passed in from the manual
7
+ button.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from datetime import datetime, timezone
13
+
14
+ from . import llm
15
+ from .ledger import LedgerManager
16
+ from .models import Environment, Job, LessonEntry, PrintSettings
17
+ from .prompts import REFLECT_SYSTEM, build_reflect_prompt
18
+
19
+
20
+ def _settings_summary(s: PrintSettings) -> str:
21
+ return (
22
+ f"nozzle {s.nozzle_temp:.0f}Β°C, bed {s.bed_temp:.0f}Β°C, "
23
+ f"retraction {s.retraction_mm:.1f}mm, fan {s.fan_pct:.0f}%, "
24
+ f"first-layer fan {s.first_layer_fan_pct:.0f}%"
25
+ )
26
+
27
+
28
+ def _fallback_lesson(job: Job, env: Environment, s: PrintSettings, outcome: str) -> str:
29
+ verdict = {
30
+ "success": "held up",
31
+ "failed_sag": "sagged",
32
+ "failed_stringing": "strung",
33
+ }.get(outcome, outcome)
34
+ return (
35
+ f"{job.material} {job.geometry_type} at {env.temp:.0f}Β°C/{env.humidity:.0f}% RH "
36
+ f"{verdict} with {_settings_summary(s)}."
37
+ )
38
+
39
+
40
+ def reflect_on_job(
41
+ job: Job,
42
+ env: Environment,
43
+ settings: PrintSettings,
44
+ outcome: str,
45
+ ledger: LedgerManager,
46
+ job_id: str | None = None,
47
+ ) -> LessonEntry:
48
+ """Distill outcome β†’ lesson, append as an 'earned' entry, return it."""
49
+ raw = llm.chat_json(REFLECT_SYSTEM, build_reflect_prompt(job, env, _settings_summary(settings), outcome))
50
+ lesson = (raw or {}).get("lesson") if isinstance(raw, dict) else None
51
+ if not lesson:
52
+ lesson = _fallback_lesson(job, env, settings, outcome)
53
+
54
+ entry = LessonEntry(
55
+ job_id=job_id or f"job-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}",
56
+ material=job.material,
57
+ geometry_type=job.geometry_type,
58
+ env_temp=env.temp,
59
+ env_humidity=env.humidity,
60
+ outcome=outcome,
61
+ lesson=lesson,
62
+ source="earned",
63
+ timestamp=datetime.now(timezone.utc).isoformat(),
64
+ )
65
+ ledger.append(entry)
66
+ return entry
core/seed_lessons.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Seed the ledger on first run.
2
+
3
+ If `data/lessons.jsonl` is empty, load the 12 curated seed lessons
4
+ (`data/seed_lessons.jsonl`, source="seed") so the demo starts with a real
5
+ capability corpus and the ledger visibly grows seed β†’ earned.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+ from .ledger import LedgerManager
14
+ from .models import LessonEntry
15
+
16
+ SEED_PATH = Path(__file__).resolve().parent.parent / "data" / "seed_lessons.jsonl"
17
+
18
+
19
+ def ensure_seeded(ledger: LedgerManager) -> int:
20
+ """Load seeds if the ledger has no entries. Returns count loaded."""
21
+ if ledger.all():
22
+ return 0
23
+ if not SEED_PATH.exists():
24
+ return 0
25
+ loaded = 0
26
+ for line in SEED_PATH.read_text(encoding="utf-8").splitlines():
27
+ line = line.strip()
28
+ if not line:
29
+ continue
30
+ try:
31
+ entry = LessonEntry(**json.loads(line))
32
+ except Exception:
33
+ continue
34
+ entry.source = "seed"
35
+ ledger.append(entry)
36
+ loaded += 1
37
+ return loaded
core/spine.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The Spine β€” deterministic veto over the model's proposed settings.
2
+
3
+ Reimplements the lab's "LLM proposes, code decides" paradigm (nodeAgent.ts
4
+ canDoJob() / 15W breaker; pattern-review.md Β§2) as hardcoded material bounds.
5
+ No power/Joule tracking. The model never overrides these clamps.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from .models import PrintSettings
13
+
14
+ # Hardcoded hardware/material envelope (Ender-class FDM).
15
+ # nozzle/bed in Β°C; retraction in mm; fan in %.
16
+ MATERIAL_BOUNDS: dict[str, dict[str, tuple[float, float]]] = {
17
+ "PLA": {"nozzle_temp": (190, 220), "bed_temp": (45, 65)},
18
+ "PETG": {"nozzle_temp": (220, 245), "bed_temp": (70, 90)},
19
+ "ABS": {"nozzle_temp": (230, 260), "bed_temp": (95, 110)},
20
+ "TPU": {"nozzle_temp": (220, 235), "bed_temp": (35, 60)},
21
+ }
22
+ 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
+
29
+ @dataclass
30
+ class SpineResult:
31
+ settings: PrintSettings # clamped, safe-to-run
32
+ vetoes: list[str] # human-readable clamp notes
33
+ requires_approval: bool # trips the HITL gate
34
+
35
+
36
+ class SpineValidator:
37
+ def check(self, settings: PrintSettings, material: str) -> SpineResult:
38
+ vetoes: list[str] = []
39
+ data = settings.model_dump()
40
+
41
+ bounds = dict(COMMON_BOUNDS)
42
+ mat = MATERIAL_BOUNDS.get(material.upper())
43
+ if mat:
44
+ bounds.update(mat)
45
+ else:
46
+ vetoes.append(f"Unknown material '{material}' β€” applying generic limits only.")
47
+
48
+ for field, (lo, hi) in bounds.items():
49
+ val = float(data[field])
50
+ if val < lo:
51
+ vetoes.append(f"{field} {val:g} below safe {lo:g} for {material} β†’ clamped to {lo:g}.")
52
+ data[field] = lo
53
+ elif val > hi:
54
+ vetoes.append(f"{field} {val:g} above safe {hi:g} for {material} β†’ clamped to {hi:g}.")
55
+ data[field] = hi
56
+
57
+ clamped = PrintSettings(**data)
58
+ # Trip the human gate if the model tried to push a real boundary.
59
+ requires_approval = any("clamped" in v for v in vetoes)
60
+ return SpineResult(settings=clamped, vetoes=vetoes, requires_approval=requires_approval)
core/theme.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Astrometrics OS β€” the Off-Brand visual layer (badge: 🎨 Off-Brand).
2
+
3
+ Single source of truth for the custom look, kept OUT of app.py so it stays a
4
+ removable layer: `from core.theme import THEME, CSS, rule, command_bar`. Every
5
+ token traces to ../DESIGN.md Β§1 β€” no ad-hoc colors. Aesthetic: Futuristic
6
+ Brutalism / LCARS terminal β€” Command Orange on Void Black, monospace, square
7
+ corners, signal-colored panes (orange=active, purple=cognition, yellow=human
8
+ gate, red=veto, green=pass).
9
+
10
+ Two parts:
11
+ THEME β€” a gr.themes.Base that recolors Gradio's OWN component variables (so
12
+ buttons/inputs/blocks obey the palette, not just our CSS overrides).
13
+ CSS β€” the structural chrome CSS does on top can't: the LCARS command bar,
14
+ ribbon tabs, rule headers, elbow corners, focused-pane borders.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import gradio as gr
20
+
21
+ # --- canonical palette (DESIGN.md Β§1) ----------------------------------------
22
+ VOID = "#131313"
23
+ SURFACE = "#1f1f1f"
24
+ SURFACE_HI = "#2a2a2a"
25
+ ORANGE = "#ff9c00"
26
+ ORANGE_SOFT = "#ffc384"
27
+ PURPLE = "#c2c1ff"
28
+ BLUE = "#5fafff"
29
+ YELLOW = "#ffff66"
30
+ TEXT = "#e2e2e2"
31
+ OUTLINE = "#a28d79"
32
+ OUTLINE_DIM = "#544433"
33
+ GREEN = "#82c88c"
34
+ RED = "#ffb4ab"
35
+
36
+ _MONO = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
37
+
38
+
39
+ def astrometrics_theme() -> gr.themes.Base:
40
+ """Recolor Gradio's component variables to the Astrometrics palette. Both the
41
+ base and *_dark variants are set so the deck looks identical regardless of the
42
+ browser's light/dark preference (a Space defaults to system)."""
43
+ t = gr.themes.Base(
44
+ primary_hue=gr.themes.colors.orange,
45
+ secondary_hue=gr.themes.colors.blue,
46
+ neutral_hue=gr.themes.colors.stone,
47
+ font=[_MONO], font_mono=[_MONO],
48
+ radius_size=gr.themes.sizes.radius_none,
49
+ )
50
+ pairs = dict(
51
+ body_background_fill=VOID, body_background_fill_dark=VOID,
52
+ body_text_color=TEXT, body_text_color_dark=TEXT,
53
+ body_text_color_subdued=OUTLINE, body_text_color_subdued_dark=OUTLINE,
54
+ background_fill_primary=VOID, background_fill_primary_dark=VOID,
55
+ background_fill_secondary=SURFACE, background_fill_secondary_dark=SURFACE,
56
+ block_background_fill=SURFACE, block_background_fill_dark=SURFACE,
57
+ block_border_color=OUTLINE_DIM, block_border_color_dark=OUTLINE_DIM,
58
+ block_label_background_fill=VOID, block_label_background_fill_dark=VOID,
59
+ block_label_text_color=ORANGE, block_label_text_color_dark=ORANGE,
60
+ block_title_text_color=ORANGE, block_title_text_color_dark=ORANGE,
61
+ border_color_primary=OUTLINE_DIM, border_color_primary_dark=OUTLINE_DIM,
62
+ color_accent_soft=SURFACE_HI, color_accent_soft_dark=SURFACE_HI,
63
+ input_background_fill=VOID, input_background_fill_dark=VOID,
64
+ input_border_color=OUTLINE_DIM, input_border_color_dark=OUTLINE_DIM,
65
+ input_border_color_focus=ORANGE, input_border_color_focus_dark=ORANGE,
66
+ button_primary_background_fill=ORANGE, button_primary_background_fill_dark=ORANGE,
67
+ button_primary_text_color=VOID, button_primary_text_color_dark=VOID,
68
+ button_secondary_background_fill=SURFACE, button_secondary_background_fill_dark=SURFACE,
69
+ button_secondary_text_color=ORANGE, button_secondary_text_color_dark=ORANGE,
70
+ button_secondary_border_color=OUTLINE_DIM, button_secondary_border_color_dark=OUTLINE_DIM,
71
+ slider_color=ORANGE, slider_color_dark=ORANGE,
72
+ link_text_color=BLUE, link_text_color_dark=BLUE,
73
+ )
74
+ return t.set(**pairs)
75
+
76
+
77
+ THEME = astrometrics_theme()
78
+
79
+ # --- structural chrome the theme can't express -------------------------------
80
+ CSS = f"""
81
+ /* kill the stock Gradio tells */
82
+ footer, .gradio-container footer {{ display:none !important; }}
83
+ .gradio-container {{ padding-top:4px; }}
84
+
85
+ /* deck frame around the whole console */
86
+ #ce-deck {{ border:1px solid var(--ao-outline-dim); border-top:2px solid var(--ao-orange);
87
+ padding:14px 16px 4px; background:linear-gradient(180deg, rgba(255,156,0,0.03), transparent 120px); }}
88
+
89
+ /* flatten Gradio's boxed labels into bare instrument labels */
90
+ .block {{ box-shadow:none !important; }}
91
+ .block > .label-wrap, span[data-testid="block-info"] {{ background:transparent !important;
92
+ border:none !important; padding:0 0 2px !important; }}
93
+ .block .label-wrap span, label > span:first-child {{ background:transparent !important; }}
94
+
95
+ /* a "console" column: thin orange spine down the left */
96
+ .ce-console {{ border-left:2px solid var(--ao-orange) !important; padding-left:12px !important;
97
+ background:linear-gradient(90deg, rgba(255,156,0,0.04), transparent 40px) !important; }}
98
+ """
99
+
100
+ CSS += f"""
101
+ :root, .gradio-container {{
102
+ --ao-void:{VOID}; --ao-surface:{SURFACE}; --ao-surface-hi:{SURFACE_HI};
103
+ --ao-orange:{ORANGE}; --ao-orange-soft:{ORANGE_SOFT}; --ao-purple:{PURPLE};
104
+ --ao-blue:{BLUE}; --ao-yellow:{YELLOW}; --ao-text:{TEXT};
105
+ --ao-outline:{OUTLINE}; --ao-outline-dim:{OUTLINE_DIM}; --ao-green:{GREEN}; --ao-red:{RED};
106
+ }}
107
+ .gradio-container {{ background:var(--ao-void) !important; max-width:1180px !important; }}
108
+ * {{ font-family:{_MONO} !important; }}
109
+ .gradio-container .prose, .gradio-container p, .gradio-container span,
110
+ .gradio-container li {{ color:var(--ao-text); }}
111
+
112
+ /* ── LCARS COMMAND BAR ─────────────────────────────────────────────── */
113
+ #ce-cmdbar {{ display:flex; align-items:stretch; gap:0; margin-bottom:14px; }}
114
+ #ce-cmdbar .ce-elbow {{ background:var(--ao-orange); color:var(--ao-void);
115
+ font-weight:800; letter-spacing:3px; text-transform:uppercase; font-size:15px;
116
+ padding:10px 22px 10px 16px; border-top-right-radius:18px; white-space:nowrap;
117
+ display:flex; align-items:center; }}
118
+ #ce-cmdbar .ce-bar {{ flex:1; background:var(--ao-surface-hi);
119
+ border-bottom:2px solid var(--ao-orange); display:flex; align-items:center;
120
+ justify-content:flex-end; gap:18px; padding:0 16px; font-size:11px;
121
+ letter-spacing:1px; text-transform:uppercase; }}
122
+ #ce-cmdbar .ce-bar b {{ color:var(--ao-orange); }}
123
+ #ce-cmdbar #ce-clock {{ color:var(--ao-blue); }}
124
+ .ce-sub {{ color:var(--ao-outline); letter-spacing:.5px; font-size:12px; line-height:1.5; }}
125
+ .ce-sub b {{ color:var(--ao-orange-soft); }}
126
+
127
+ /* ── RULE HEADERS ── LABEL ──────────── (flex line fills the width) */
128
+ .ce-rule {{ display:flex; align-items:center; gap:10px; color:var(--ao-orange);
129
+ text-transform:uppercase; letter-spacing:3px; font-size:12px; font-weight:700; margin:10px 0 4px; }}
130
+ .ce-rule::after {{ content:""; flex:1; height:0; border-top:1px solid var(--ao-outline-dim); }}
131
+ .ce-rule::before {{ content:"β–Έ"; color:var(--ao-orange); }}
132
+
133
+ /* labels as orange uppercase */
134
+ .block .label-wrap span, label > span, span[data-testid="block-info"] {{
135
+ color:var(--ao-orange) !important; text-transform:uppercase; letter-spacing:1.4px;
136
+ font-size:11px !important; font-weight:700; }}
137
+
138
+ /* square brutalist blocks, dim borders */
139
+ .block, .form, .gr-box, .gr-panel {{ border-radius:0 !important;
140
+ border:1px solid var(--ao-outline-dim) !important; background:var(--ao-surface) !important; }}
141
+ textarea, input, select, .gr-input {{ background:var(--ao-void) !important;
142
+ color:var(--ao-text) !important; border-radius:0 !important; }}
143
+ textarea:focus, input:focus {{ border-color:var(--ao-orange) !important; box-shadow:none !important; }}
144
+
145
+ /* ── RIBBON TABS (LCARS elbows) ── each tab a rounded ribbon; selected = solid orange */
146
+ .tab-nav, .tab-container {{ border-bottom:2px solid var(--ao-orange) !important; gap:6px !important;
147
+ padding-bottom:0 !important; }}
148
+ .tab-nav button, .tab-container button, button[role="tab"] {{
149
+ color:var(--ao-orange-soft) !important; background:var(--ao-outline-dim) !important;
150
+ text-transform:uppercase; letter-spacing:2px; font-size:12px !important; font-weight:800;
151
+ border:none !important; border-radius:14px 14px 0 0 !important; padding:8px 22px !important;
152
+ margin:0 !important; }}
153
+ .tab-nav button:hover, button[role="tab"]:hover {{ color:var(--ao-void) !important;
154
+ background:var(--ao-orange-soft) !important; }}
155
+ .tab-nav button.selected, button[role="tab"][aria-selected="true"],
156
+ .tab-container button.selected {{ background:var(--ao-orange) !important; color:#131313 !important; }}
157
+
158
+ /* ── file dropzone as an LCARS bay ── */
159
+ .ce-drop, .ce-drop .center, .ce-drop [data-testid="block-label"] {{ background:var(--ao-void) !important; }}
160
+ .ce-drop {{ border:1px dashed var(--ao-outline) !important; border-radius:0 !important; }}
161
+ .ce-drop:hover {{ border-color:var(--ao-orange) !important; }}
162
+ .ce-drop .wrap, .ce-drop .or, .ce-drop button {{ color:var(--ao-orange-soft) !important;
163
+ text-transform:uppercase; letter-spacing:1px; font-size:12px; }}
164
+
165
+ /* sliders + ranges */
166
+ input[type=range] {{ accent-color:var(--ao-orange) !important; }}
167
+
168
+ /* buttons: square, uppercase, letter-spaced */
169
+ button.primary, .gr-button-primary, button[variant="primary"] {{
170
+ background:var(--ao-orange) !important; color:var(--ao-void) !important;
171
+ font-weight:800 !important; border:none !important; border-radius:0 !important;
172
+ letter-spacing:1.5px !important; text-transform:uppercase; }}
173
+ button.secondary {{ background:var(--ao-surface) !important; color:var(--ao-orange) !important;
174
+ border:1px solid var(--ao-outline-dim) !important; border-radius:0 !important;
175
+ text-transform:uppercase; letter-spacing:1px !important; }}
176
+ button.secondary:hover {{ border-color:var(--ao-orange) !important; }}
177
+
178
+ /* ── SIGNAL-CODED PANELS (left accent = meaning) ── */
179
+ #ce-reasoning {{ border-left:3px solid var(--ao-orange) !important; padding:10px 14px !important; }}
180
+ #ce-precedent .ce-panel, .ce-cognition {{ border-left:3px solid var(--ao-purple); }}
181
+ #ce-confirm {{ background:var(--ao-orange) !important; color:var(--ao-void) !important;
182
+ font-weight:800 !important; border:none !important; border-radius:0 !important;
183
+ text-transform:uppercase; letter-spacing:2px !important; }}
184
+ /* outcome / sim buttons get a quieter deck look */
185
+ #ce-sim button, #ce-outcomes button {{ background:var(--ao-surface) !important;
186
+ border:1px solid var(--ao-outline-dim) !important; color:var(--ao-text) !important; }}
187
+
188
+ /* JSON + code blocks on the void */
189
+ .gr-json, .cm-editor, .code-wrap {{ background:var(--ao-void) !important; }}
190
+ footer {{ color:var(--ao-outline-dim) !important; }}
191
+
192
+ /* ── LCARS pill radio groups (part type, material) ── */
193
+ .ce-pills fieldset, .ce-pills .wrap {{ display:flex !important; flex-wrap:wrap; gap:6px;
194
+ border:none !important; background:transparent !important; }}
195
+ .ce-pills label {{ border:1px solid var(--ao-outline) !important; border-radius:999px !important;
196
+ padding:5px 15px !important; cursor:pointer; background:var(--ao-surface) !important;
197
+ color:var(--ao-text) !important; text-transform:uppercase; letter-spacing:.5px;
198
+ font-size:12px !important; font-weight:600; transition:all .12s; margin:0 !important; }}
199
+ .ce-pills label span {{ color:var(--ao-text) !important; }} /* unselected: light, legible */
200
+ .ce-pills label:hover {{ border-color:var(--ao-orange) !important; color:var(--ao-orange-soft) !important; }}
201
+ .ce-pills label:hover span {{ color:var(--ao-orange-soft) !important; }}
202
+ .ce-pills input[type=radio] {{ position:absolute !important; opacity:0 !important; width:0 !important; height:0 !important; }}
203
+ .ce-pills label:has(input:checked) {{ background:var(--ao-orange) !important;
204
+ border-color:var(--ao-orange) !important; font-weight:800; }}
205
+ .ce-pills label:has(input:checked), .ce-pills label:has(input:checked) span {{
206
+ color:#131313 !important; }} /* selected: dark on orange, readable */
207
+
208
+ /* ── LCARS pill buttons (mesh sources) ── */
209
+ .ce-pillbtn button {{ border-radius:999px !important; border:1px solid var(--ao-orange) !important;
210
+ background:var(--ao-surface) !important; color:var(--ao-orange-soft) !important; box-shadow:none !important;
211
+ text-transform:uppercase; letter-spacing:.5px; font-size:12px !important; font-weight:700; }}
212
+ .ce-pillbtn button:hover {{ border-color:var(--ao-orange) !important;
213
+ background:var(--ao-orange) !important; color:#131313 !important; }}
214
+
215
+ /* number inputs as instrument fields */
216
+ .ce-num input {{ background:var(--ao-void) !important; border:1px solid var(--ao-outline-dim) !important;
217
+ border-radius:0 !important; color:var(--ao-blue) !important; font-size:15px !important; font-weight:700; }}
218
+
219
+ /* accordion headers as LCARS stage bars */
220
+ .gradio-container .label-wrap, button.label-wrap {{ color:var(--ao-orange) !important; }}
221
+ details > summary, .accordion .label-wrap {{ text-transform:uppercase; letter-spacing:2px; }}
222
+
223
+ /* optional CRT scanline β€” off by default; add class 'ce-crt' to body to enable */
224
+ body.ce-crt .gradio-container::before {{ content:""; position:fixed; inset:0; z-index:9999;
225
+ pointer-events:none; background:repeating-linear-gradient(0deg,
226
+ rgba(0,0,0,0.10) 0px, rgba(0,0,0,0.10) 1px, transparent 1px, transparent 3px); }}
227
+ """
228
+
229
+ # tiny LCARS clock β€” updates the command-bar time; no-op if the element is absent
230
+ CLOCK_JS = """
231
+ () => {
232
+ const tick = () => {
233
+ const el = document.getElementById('ce-clock');
234
+ if (el) {
235
+ const d = new Date();
236
+ el.textContent = d.toISOString().slice(11,19) + ' UTC';
237
+ }
238
+ };
239
+ tick(); setInterval(tick, 1000);
240
+ }
241
+ """
242
+
243
+
244
+ def rule(label: str) -> str:
245
+ """An LCARS rule header: ``β–Έ LABEL ─────────────`` (line fills via CSS)."""
246
+ return f"<div class='ce-rule'>{label}</div>"
247
+
248
+
249
+ def footer_bar() -> str:
250
+ """Replaces the hidden Gradio footer with an Astrometrics status strip."""
251
+ return (
252
+ "<div style='display:flex;justify-content:space-between;gap:16px;margin-top:14px;"
253
+ "padding:6px 14px;border-top:2px solid var(--ao-orange);background:var(--ao-surface-hi);"
254
+ "font-size:10px;letter-spacing:1px;text-transform:uppercase;color:var(--ao-outline);'>"
255
+ "<span>ASTROMETRICS OS Β· CHIEF ENGINEER</span>"
256
+ "<span>LLM PROPOSES Β· <b style='color:var(--ao-orange);'>SPINE DISPOSES</b> Β· HUMAN CONFIRMS</span>"
257
+ "<span>FULLY LOCAL Β· GEMMA4</span></div>"
258
+ )
259
+
260
+
261
+ def command_bar(backend_status: str) -> str:
262
+ """The top LCARS command strip: orange elbow title + status/clock bar."""
263
+ return (
264
+ "<div id='ce-cmdbar'>"
265
+ "<div class='ce-elbow'>β–š THE CHIEF ENGINEER</div>"
266
+ "<div class='ce-bar'>"
267
+ f"<span>{backend_status}</span>"
268
+ "<span>BUILD&nbsp;SMALL Β· BACKYARD&nbsp;AI</span>"
269
+ "<span id='ce-clock'>--:--:-- UTC</span>"
270
+ "</div></div>"
271
+ )
core/viewer.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """3D preview helpers + G-code readout.
2
+
3
+ gr.Model3D gives orbit/zoom/pan for free. We keep trimesh use minimal: pick a
4
+ sample mesh for the geometry_type (or use an uploaded one) and, where it helps,
5
+ locate the steepest overhang so a risk callout anchors to something real. Risk
6
+ regions render as labeled callouts beside the interactive model β€” robust, and
7
+ the model stays interactive. No slicing, no simulation.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ from .models import PrintSettings, RiskRegion
15
+
16
+ ASSETS = Path(__file__).resolve().parent.parent / "assets"
17
+ DATA = Path(__file__).resolve().parent.parent / "data"
18
+ _SAMPLE = {
19
+ "overhang": "overhang.glb",
20
+ "bridge": "bridge.glb",
21
+ "vase": "vase.glb",
22
+ "stringing": "cube.glb",
23
+ "adhesion": "cube.glb",
24
+ }
25
+
26
+
27
+ def sample_mesh(geometry_type: str) -> str | None:
28
+ path = ASSETS / _SAMPLE.get(geometry_type, "cube.glb")
29
+ return str(path) if path.exists() else None
30
+
31
+
32
+ PART_LABEL = {
33
+ "overhang": "OVERHANG TEST", "bridge": "BRIDGE TEST", "vase": "VASE (THIN WALL)",
34
+ "stringing": "STRINGING TOWER", "adhesion": "ADHESION CUBE",
35
+ }
36
+
37
+
38
+ def benchy_mesh() -> str | None:
39
+ """The CC0 3DBenchy, IF dropped into assets/benchy.glb (can't fetch on a
40
+ locked Space; Kyle adds it locally). None β†’ caller shows a 'add the file' hint."""
41
+ p = ASSETS / "benchy.glb"
42
+ return str(p) if p.exists() else None
43
+
44
+
45
+ _PRIMITIVES = ("box", "cylinder", "cone", "sphere")
46
+ # which geometry_type (the model's reasoning class) each primitive maps to
47
+ _PRIM_GEO = {"box": "adhesion", "cylinder": "vase", "cone": "overhang", "sphere": "overhang"}
48
+
49
+
50
+ def generate_primitive(kind: str, size_mm: float = 30.0) -> tuple[str, str]:
51
+ """Generate a parametric primitive with trimesh β†’ (mesh_path, geometry_type).
52
+ Offline, zero new deps. Saved to data/_generated.glb for the preview/slicer."""
53
+ import trimesh
54
+
55
+ s = max(5.0, float(size_mm))
56
+ kind = kind if kind in _PRIMITIVES else "box"
57
+ if kind == "box":
58
+ m = trimesh.creation.box(extents=(s, s, s))
59
+ elif kind == "cylinder":
60
+ m = trimesh.creation.cylinder(radius=s / 2, height=s)
61
+ elif kind == "cone":
62
+ m = trimesh.creation.cone(radius=s / 2, height=s)
63
+ else:
64
+ m = trimesh.creation.icosphere(radius=s / 2)
65
+ m.apply_translation(-m.bounds[0]) # sit on the bed (z β‰₯ 0)
66
+ DATA.mkdir(exist_ok=True)
67
+ out = DATA / "_generated.glb"
68
+ m.export(out)
69
+ return str(out), _PRIM_GEO[kind]
70
+
71
+
72
+ def settings_panel_html(settings: PrintSettings, material: str) -> str:
73
+ """Render proposed settings as an LCARS instrument readout (not raw JSON)."""
74
+ rows = [
75
+ ("NOZZLE", f"{settings.nozzle_temp:.0f}", "Β°C"),
76
+ ("BED", f"{settings.bed_temp:.0f}", "Β°C"),
77
+ ("RETRACTION", f"{settings.retraction_mm:.1f}", "mm"),
78
+ ("FAN", f"{settings.fan_pct:.0f}", "%"),
79
+ ("FIRST-LAYER FAN", f"{settings.first_layer_fan_pct:.0f}", "%"),
80
+ ]
81
+ cells = "".join(
82
+ "<div style='display:flex;justify-content:space-between;align-items:baseline;"
83
+ "border-bottom:1px solid var(--ao-outline-dim);padding:5px 2px;'>"
84
+ f"<span style='color:var(--ao-orange);letter-spacing:1.5px;font-size:10px;'>{name}</span>"
85
+ f"<span style='color:var(--ao-text);font-size:15px;font-weight:700;'>{val}"
86
+ f"<span style='color:var(--ao-outline);font-size:10px;'> {unit}</span></span></div>"
87
+ for name, val, unit in rows
88
+ )
89
+ return (
90
+ "<div style='font-family:ui-monospace,monospace;background:var(--ao-void);"
91
+ "border:1px solid var(--ao-outline-dim);border-left:3px solid var(--ao-orange);padding:8px 12px;'>"
92
+ f"<div style='color:var(--ao-orange);font-weight:700;letter-spacing:2px;font-size:11px;"
93
+ f"margin-bottom:4px;'>PROPOSED SETTINGS Β· {material} <span style='color:var(--ao-outline);"
94
+ "font-weight:400;'>(SPINE-VALIDATED)</span></div>" + cells + "</div>"
95
+ )
96
+
97
+
98
+ def gcode_panel_html(settings: PrintSettings, material: str) -> str:
99
+ """The g-code readout as a styled terminal panel (not gr.Code)."""
100
+ body = gcode_readout(settings, material).replace("<", "&lt;")
101
+ return (
102
+ "<div style='font-family:ui-monospace,monospace;background:var(--ao-void);"
103
+ "border:1px solid var(--ao-outline-dim);padding:8px 12px;'>"
104
+ "<div style='color:var(--ao-orange);font-weight:700;letter-spacing:2px;font-size:11px;'>"
105
+ "START G-CODE <span style='color:var(--ao-outline);font-weight:400;'>(HEADER TIED TO SETTINGS)</span></div>"
106
+ f"<pre style='color:var(--ao-blue);font-size:11px;margin:6px 0 0;white-space:pre-wrap;'>{body}</pre></div>"
107
+ )
108
+
109
+
110
+ def steepest_overhang_hint(mesh_path: str | None) -> str | None:
111
+ """Optional: report where the steepest downward-facing face sits (minimal trimesh)."""
112
+ if not mesh_path or not Path(mesh_path).exists():
113
+ return None
114
+ try:
115
+ import numpy as np
116
+ import trimesh
117
+
118
+ mesh = trimesh.load(mesh_path, force="mesh")
119
+ import math
120
+ normals = mesh.face_normals
121
+ downward = normals[:, 2] # -1 = fully downward-facing
122
+ idx = int(downward.argmin())
123
+ steep = -float(downward[idx]) # 0..1; 1 = horizontal ceiling
124
+ if steep > 0.30: # meaningfully overhanging
125
+ angle = math.degrees(math.asin(min(1.0, steep))) # overhang angle from vertical
126
+ c = mesh.triangles_center[idx]
127
+ note = f"steepest overhang ~{angle:.0f}Β° near (x={c[0]:.0f}, y={c[1]:.0f}, z={c[2]:.0f}) mm"
128
+ if angle >= 50: # past the usual support threshold
129
+ note += " β€” likely needs supports (or reorient to reduce it)"
130
+ return note
131
+ except Exception:
132
+ return None
133
+ return None
134
+
135
+
136
+ def risk_callouts_html(risks: list[RiskRegion], geo_hint: str | None = None) -> str:
137
+ if not risks:
138
+ body = "<div style='color:var(--ao-green);'>No failure regions flagged.</div>"
139
+ else:
140
+ rows = []
141
+ for r in risks:
142
+ anchor = f" Β· {r.anchor_hint}" if r.anchor_hint else ""
143
+ rows.append(
144
+ f"<div style='border-left:3px solid var(--ao-red);background:var(--ao-surface);"
145
+ f"padding:6px 10px;margin:5px 0;font-family:ui-monospace,monospace;font-size:12px;'>"
146
+ f"<span style='color:var(--ao-red);font-weight:700;'>⚠ {r.risk.upper()}</span> "
147
+ f"<span style='color:var(--ao-text);'>@ {r.location}{anchor}</span>"
148
+ f"<div style='color:var(--ao-outline);'>{r.why}</div></div>"
149
+ )
150
+ body = "".join(rows)
151
+ if geo_hint:
152
+ body += f"<div style='color:var(--ao-outline);font-size:11px;font-family:ui-monospace,monospace;'>↳ {geo_hint}</div>"
153
+ return f"<div><div style='color:var(--ao-orange);font-family:ui-monospace,monospace;font-size:11px;'>PREDICTED FAILURE REGIONS</div>{body}</div>"
154
+
155
+
156
+ _VERDICT = {"failed_sag": "sagged", "failed_stringing": "strung", "success": "printed clean"}
157
+
158
+
159
+ def precedent_eval_html(retrieved, env) -> str:
160
+ """The load-bearing moment, narrated deterministically from the env delta.
161
+
162
+ Makes the "humidity is higher than the job that sagged" framing reliable on
163
+ screen even before the model's prose β€” the model's reasoning then adds to it.
164
+ """
165
+ if not retrieved:
166
+ return (
167
+ "<div style='border-left:3px solid var(--ao-purple);background:var(--ao-surface);"
168
+ "padding:10px 14px;font-family:ui-monospace,monospace;'>"
169
+ "<div style='color:var(--ao-purple);font-weight:700;letter-spacing:1px;'>NO CLOSE PRECEDENT</div>"
170
+ "<div style='color:var(--ao-text);'>Nothing in the ledger matches this material + geometry. "
171
+ "Reasoning from material properties β€” and saying so. Knowing what it doesn't know is the point.</div></div>"
172
+ )
173
+ e, dist = retrieved[0]
174
+ dt = env.temp - e.env_temp
175
+ dh = env.humidity - e.env_humidity
176
+
177
+ def phrase(delta, unit, hi, lo):
178
+ if abs(delta) < 1:
179
+ return f"about the same {unit}"
180
+ return f"{abs(delta):.0f}{unit} {hi if delta > 0 else lo}"
181
+
182
+ t_ph = phrase(dt, "Β°C", "warmer", "cooler")
183
+ h_ph = phrase(dh, " pts", "more humid", "drier")
184
+ verdict = _VERDICT.get(e.outcome, e.outcome)
185
+
186
+ failed = e.outcome.startswith("failed")
187
+ worse = (e.geometry_type in ("overhang", "bridge") and dt > 1) or ("string" in e.geometry_type and dh > 1)
188
+ if failed and worse:
189
+ impl = "Conditions are <b>worse</b> than that failure β€” expect the same risk and adjusting to prevent it."
190
+ col = "var(--ao-red)"
191
+ elif failed and not worse:
192
+ impl = "Conditions are <b>better</b> than that failure β€” the original cause is less likely now."
193
+ col = "var(--ao-orange)"
194
+ else:
195
+ impl = "That job succeeded under similar conditions β€” leaning on what worked."
196
+ col = "var(--ao-green)"
197
+
198
+ return (
199
+ f"<div style='border-left:3px solid {col};background:var(--ao-surface);"
200
+ f"padding:10px 14px;font-family:ui-monospace,monospace;'>"
201
+ f"<div style='color:var(--ao-orange);font-weight:700;letter-spacing:1px;'>PRECEDENT EVALUATION</div>"
202
+ f"<div style='color:var(--ao-text);'>Nearest prior job <b>{e.job_id}</b> ({e.source}) β€” "
203
+ f"{e.material}/{e.geometry_type} <b>{verdict}</b> at {e.env_temp:.0f}Β°C / {e.env_humidity:.0f}% RH.<br>"
204
+ f"Right now it's <b>{t_ph}</b> and <b>{h_ph}</b> (env-distance {dist:.2f}).<br>{impl}</div></div>"
205
+ )
206
+
207
+
208
+ _POS_SHRINK = {"ABS": "high", "PETG": "moderate", "PLA": "low", "TPU": "low"}
209
+
210
+
211
+ def placement_callout(material: str, bed_position: str) -> str:
212
+ """Deterministic build-plate placement risk + suggested alignment. Bed edges/
213
+ corners run cooler + draftier β†’ warp/adhesion risk, worst for high-shrink
214
+ materials. Returns an HTML block (or '' when centered/low-risk)."""
215
+ pos = (bed_position or "center").lower()
216
+ if pos == "center":
217
+ return ""
218
+ shrink = _POS_SHRINK.get(material.upper(), "moderate")
219
+ risky = shrink in ("high", "moderate")
220
+ col = "var(--ao-red)" if (pos == "corner" and risky) else (
221
+ "var(--ao-orange)" if risky else "var(--ao-outline)")
222
+ sev = "corner" if pos == "corner" else "edge"
223
+ body = (f"{material} has <b>{shrink}</b> shrink; a {sev} of the heated bed runs cooler and "
224
+ f"draftier, so the first layer can lift and the part can warp.")
225
+ fix = ("Center the part on the bed" + (
226
+ ", add a brim, and an enclosure if you have one." if shrink == "high"
227
+ else " and add a brim." if risky else "; minor risk for this material."))
228
+ return (
229
+ f"<div style='border-left:3px solid {col};background:var(--ao-surface);padding:6px 10px;"
230
+ f"margin:5px 0;font-family:ui-monospace,monospace;font-size:12px;'>"
231
+ f"<span style='color:{col};font-weight:700;'>βŒ– PLACEMENT Β· {sev.upper()}</span> "
232
+ f"<span style='color:var(--ao-text);'>{body}</span>"
233
+ f"<div style='color:var(--ao-orange-soft);'>↳ suggested: {fix}</div></div>"
234
+ )
235
+
236
+
237
+ def gcode_readout(settings: PrintSettings, material: str) -> str:
238
+ """Short snippet whose header lines come from the proposed settings."""
239
+ return "\n".join([
240
+ f"; Chief Engineer β€” start g-code for {material} (header tied to recommendation)",
241
+ f"M140 S{settings.bed_temp:.0f} ; set bed",
242
+ f"M104 S{settings.nozzle_temp:.0f} ; set nozzle",
243
+ f"M190 S{settings.bed_temp:.0f} ; wait for bed",
244
+ f"M109 S{settings.nozzle_temp:.0f} ; wait for nozzle",
245
+ "G28 ; home all axes",
246
+ "G92 E0 ; reset extruder",
247
+ f"M106 S{settings.first_layer_fan_pct * 2.55:.0f} ; first-layer fan {settings.first_layer_fan_pct:.0f}%",
248
+ f"; retraction {settings.retraction_mm:.1f} mm ; cruise fan {settings.fan_pct:.0f}%",
249
+ "; … (toolpath generated by your slicer, never by the model)",
250
+ ])
251
+
252
+
253
+ # --- learning-loop renderers (the primary demo surface) --------------------
254
+
255
+ def quality_curve_html(trajectory: list[float], threshold: float = 0.7) -> str:
256
+ """Astrometrics bar chart of quality per iteration β€” the 'it gets better' shot."""
257
+ if not trajectory:
258
+ return "<div style='color:var(--ao-outline);font-family:ui-monospace,monospace;'>run the loop β†’</div>"
259
+ bars = []
260
+ for i, q in enumerate(trajectory, 1):
261
+ h = max(4, int(q * 120))
262
+ col = "var(--ao-green)" if q >= threshold else ("var(--ao-orange)" if q >= 0.5 else "var(--ao-red)")
263
+ bars.append(
264
+ f"<div style='display:flex;flex-direction:column;justify-content:flex-end;align-items:center;'>"
265
+ f"<div style='color:var(--ao-text);font-size:9px;'>{q:.2f}</div>"
266
+ f"<div style='width:22px;height:{h}px;background:{col};'></div>"
267
+ f"<div style='color:var(--ao-outline);font-size:9px;'>{i}</div></div>"
268
+ )
269
+ line_top = int((1 - threshold) * 120) + 14
270
+ return (
271
+ "<div style='font-family:ui-monospace,monospace;'>"
272
+ "<div style='color:var(--ao-orange);font-weight:700;letter-spacing:1px;'>PRINT QUALITY PER ITERATION "
273
+ f"<span style='color:var(--ao-outline);font-weight:400;'>(green = clean β‰₯ {threshold:.2f})</span></div>"
274
+ "<div style='position:relative;display:flex;gap:6px;align-items:flex-end;padding:16px 4px 0;"
275
+ "background:var(--ao-surface);border-left:3px solid var(--ao-orange);'>"
276
+ f"<div style='position:absolute;left:0;right:0;top:{line_top}px;border-top:1px dashed var(--ao-outline-dim);'></div>"
277
+ + "".join(bars) + "</div></div>"
278
+ )
279
+
280
+
281
+ def iteration_log_html(records) -> str:
282
+ rows = []
283
+ for r in records:
284
+ clean = r.result.outcome == "success"
285
+ col = "var(--ao-green)" if clean else "var(--ao-red)"
286
+ rows.append(
287
+ f"<div style='font-family:ui-monospace,monospace;font-size:11px;border-left:3px solid {col};"
288
+ f"background:var(--ao-surface);padding:5px 10px;margin:3px 0;color:var(--ao-text);'>"
289
+ f"<b style='color:{col};'>#{r.n} {r.result.outcome}</b> Β· q={r.result.quality:.2f} Β· "
290
+ f"noz {r.settings.nozzle_temp:.0f}Β°C bed {r.settings.bed_temp:.0f}Β°C fan {r.settings.fan_pct:.0f}% "
291
+ f"ret {r.settings.retraction_mm:.1f}mm"
292
+ + (" Β· <span style='color:var(--ao-orange);'>spine-clamped</span>" if r.clamped else "")
293
+ + f"<br><span style='color:var(--ao-outline);'>↳ {r.learned}</span></div>"
294
+ )
295
+ return "".join(rows)
296
+
297
+
298
+ def policy_cell_html(cell, key: str) -> str:
299
+ if cell is None or not getattr(cell, "offsets", None):
300
+ return ("<div style='color:var(--ao-outline);font-family:ui-monospace,monospace;font-size:11px;'>"
301
+ f"POLICY CELL {key}: untrained (baseline only).</div>")
302
+ deltas = " ".join(f"{k} {v:+g}" for k, v in cell.offsets.items())
303
+ return (
304
+ f"<div style='border-left:3px solid var(--ao-purple);background:var(--ao-surface);"
305
+ f"padding:8px 12px;font-family:ui-monospace,monospace;font-size:11px;'>"
306
+ f"<div style='color:var(--ao-purple);font-weight:700;'>LEARNED POLICY CELL Β· {key}</div>"
307
+ f"<div style='color:var(--ao-text);'>offsets vs baseline: <b>{deltas}</b></div>"
308
+ f"<div style='color:var(--ao-outline);'>{cell.trials} runs Β· {cell.success_rate*100:.0f}% clean</div></div>"
309
+ )
core/widgets.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Custom visual-beat widgets β€” live in-browser instruments rendered into gr.HTML.
2
+
3
+ The marquee one is the VIRTUAL PRINTER: the real mesh sliced into cross-sections
4
+ (reusing sim.virtual_printer) and animated *rising* on a <canvas>, client-side, so
5
+ it's a live instrument in the cockpit rather than a pre-rendered GIF. Astrometrics
6
+ palette; zero new deps; fully offline/Space-safe.
7
+
8
+ Gradio gotcha handled here: a <script> injected via gr.HTML does NOT execute. So
9
+ the animation logic lives once in VP_JS (loaded via the Blocks `js=`), exposing a
10
+ global that scans for `canvas.ce-vp` elements and animates any it hasn't started.
11
+ The HTML payload is just a <canvas> with the layer data in a data-attribute β€” no
12
+ inline script β€” and the global picks it up on the next tick.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from pathlib import Path
19
+
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.8,
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
+
27
+ {bounds:[xmin,xmax,ymin,ymax], layers:[[[x0,y0,x1,y1],...], ...]} β€” projected
28
+ oblique screen space, rounded to keep the data-attribute payload small. Segments
29
+ per layer are capped (down-sampled) so a high-res mesh doesn't bloat the page β€”
30
+ the rising animation only needs enough segments to read; the layer SCRUBBER
31
+ (layer_image) renders full fidelity server-side.
32
+ """
33
+ from sim.virtual_printer import slice_segments
34
+
35
+ layers = slice_segments(mesh_path, layer_height_mm, max_layers=max_layers)
36
+ out_layers, xs, ys = [], [], []
37
+ for _z, segs in layers:
38
+ if len(segs) > max_segs_per_layer: # down-sample for payload
39
+ step = len(segs) // max_segs_per_layer + 1
40
+ segs = segs[::step]
41
+ L = []
42
+ for seg in segs:
43
+ (x0, y0, z0), (x1, y1, z1) = seg[0], seg[1]
44
+ px0, py0 = x0 + _ISO * y0, z0 + _ISO * y0
45
+ px1, py1 = x1 + _ISO * y1, z1 + _ISO * y1
46
+ L.append([round(px0, 1), round(py0, 1), round(px1, 1), round(py1, 1)])
47
+ xs += [px0, px1]; ys += [py0, py1]
48
+ if L:
49
+ out_layers.append(L)
50
+ if not out_layers:
51
+ return {}
52
+ return {"bounds": [min(xs), max(xs), min(ys), max(ys)], "layers": out_layers}
53
+
54
+
55
+ # ── layer scrubber: full-fidelity single-layer cross-section, rendered server-side ──
56
+ _TRIS_CACHE: dict = {}
57
+
58
+
59
+ def _tris_for(mesh_path: str):
60
+ """Load + cache (triangles, bounds) for a mesh so scrubbing doesn't reload it."""
61
+ p = str(mesh_path)
62
+ if p not in _TRIS_CACHE:
63
+ import trimesh
64
+ m = trimesh.load(p, force="mesh")
65
+ _TRIS_CACHE[p] = (m.vertices[m.faces], m.bounds.copy())
66
+ return _TRIS_CACHE[p]
67
+
68
+
69
+ SCRUB_LAYERS = 40 # fixed layer count for the scrubber slider (independent of the animation)
70
+
71
+
72
+ def _fill_or_outline(d, segs, px) -> str:
73
+ """Draw a layer as FILLED solid regions (shapely polygonize on snap-rounded
74
+ segments β€” like a real slicer) with bright perimeters and holes cut out. Falls
75
+ back to plain outlines if shapely is absent or the rings don't close. Returns a
76
+ short label describing what was drawn."""
77
+ try:
78
+ from shapely.geometry import LineString
79
+ from shapely.ops import polygonize, unary_union
80
+ lines = [LineString([(round(float(sg[0][0]), 2), round(float(sg[0][1]), 2)),
81
+ (round(float(sg[1][0]), 2), round(float(sg[1][1]), 2))]) for sg in segs]
82
+ polys = list(polygonize(unary_union(lines)))
83
+ if polys:
84
+ for p in polys: # solid body
85
+ d.polygon([px(x, y) for x, y in p.exterior.coords], fill=(110, 70, 20))
86
+ for p in polys: # holes β†’ background
87
+ for r in p.interiors:
88
+ d.polygon([px(x, y) for x, y in r.coords], fill=(10, 12, 20))
89
+ for p in polys: # bright perimeters
90
+ d.line([px(x, y) for x, y in p.exterior.coords], fill=(255, 150, 40), width=2)
91
+ for r in p.interiors:
92
+ d.line([px(x, y) for x, y in r.coords], fill=(255, 150, 40), width=1)
93
+ return f"{len(polys)} filled regions"
94
+ except Exception:
95
+ pass
96
+ for sg in segs: # fallback: outline
97
+ d.line([px(sg[0][0], sg[0][1]), px(sg[1][0], sg[1][1])], fill=(255, 150, 40), width=1)
98
+ return f"{len(segs)} perimeter segments"
99
+
100
+
101
+ def layer_image(mesh_path: str | Path | None, idx: int, n: int = SCRUB_LAYERS,
102
+ size: tuple[int, int] = (560, 380)):
103
+ """Top-down cross-section of layer `idx` at FULL mesh fidelity (no payload cap β€”
104
+ one layer at a time), drawn as a filled solid layer like a real slicer. Stable XY
105
+ bounds so the part doesn't jump while scrubbing. Returns an RGB numpy array."""
106
+ import numpy as np
107
+ from PIL import Image, ImageDraw
108
+ from sim.virtual_printer import _slice_at
109
+
110
+ W, H, pad = size[0], size[1], 26
111
+ img = Image.new("RGB", (W, H), (10, 12, 20))
112
+ d = ImageDraw.Draw(img)
113
+ if not mesh_path or not Path(str(mesh_path)).exists():
114
+ d.text((pad, H // 2), "run ANALYZE to slice the part", fill=(150, 160, 180))
115
+ return np.asarray(img)
116
+
117
+ tris, bounds = _tris_for(mesh_path)
118
+ zmin, zmax = float(bounds[0, 2]), float(bounds[1, 2])
119
+ h = max(zmax - zmin, 1e-6)
120
+ zs = np.linspace(zmin + h * 0.01, zmax - h * 0.01, n)
121
+ idx = max(1, min(n, int(idx)))
122
+ z = float(zs[idx - 1])
123
+ segs = _slice_at(tris, z)
124
+
125
+ xmin, ymin = float(bounds[0, 0]), float(bounds[0, 1])
126
+ xmax, ymax = float(bounds[1, 0]), float(bounds[1, 1])
127
+ s = min((W - 2 * pad) / max(xmax - xmin, 1e-6), (H - 2 * pad) / max(ymax - ymin, 1e-6))
128
+
129
+ def px(x, y):
130
+ return (pad + (x - xmin) * s, H - (pad + (y - ymin) * s))
131
+
132
+ drawn = _fill_or_outline(d, segs, px)
133
+ d.text((pad, 8), f"LAYER {idx}/{n} Β· z={z:.1f} mm Β· {drawn}", fill=(210, 220, 235))
134
+ return np.asarray(img)
135
+
136
+
137
+ def virtual_printer_html(mesh_path: str | Path | None, caption: str = "") -> str:
138
+ """A live virtual-printer canvas for the cockpit (animated by VP_JS)."""
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
+ data = _projected_layers(mesh_path)
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("'", "&#39;")
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
+
156
+
157
+ # One-time client animator, injected as a real <script> via launch(head=...).
158
+ # (launch(js=...) proved unreliable for setting up a persistent scan loop; a head
159
+ # script runs on load deterministically and is CSP-friendly on a Space.)
160
+ VP_HEAD = r"""
161
+ <script>
162
+ (function(){
163
+ function start(){
164
+ // --- LCARS clock ---
165
+ const tickClock = () => {
166
+ const el = document.getElementById('ce-clock');
167
+ if (el) el.textContent = new Date().toISOString().slice(11,19) + ' UTC';
168
+ };
169
+ tickClock(); setInterval(tickClock, 1000);
170
+
171
+ // --- virtual-printer animator ---
172
+ const DONE='#46627f', CUR='#ff9c00', NOZ='#ffe6b4', BG='#0a0c14';
173
+ function animate(cv){
174
+ let data; try { data = JSON.parse(cv.getAttribute('data-vp')); } catch(e){ return; }
175
+ if(!data || !data.layers || !data.layers.length) return;
176
+ const ctx = cv.getContext('2d'); const W=cv.width, H=cv.height, pad=22;
177
+ const [xmin,xmax,ymin,ymax] = data.bounds;
178
+ const sx = (W-2*pad)/Math.max(xmax-xmin,1e-6), sy=(H-2*pad)/Math.max(ymax-ymin,1e-6);
179
+ const s = Math.min(sx,sy);
180
+ const X = x => pad + (x-xmin)*s, Y = y => H - (pad + (y-ymin)*s);
181
+ const N = data.layers.length;
182
+ let upto = 0, hold = 0;
183
+ function frame(){
184
+ ctx.fillStyle=BG; ctx.fillRect(0,0,W,H);
185
+ for(let li=0; li<=upto && li<N; li++){
186
+ const segs = data.layers[li];
187
+ ctx.strokeStyle = (li===upto)?CUR:DONE; ctx.lineWidth=(li===upto)?1.6:1;
188
+ ctx.beginPath();
189
+ for(const [x0,y0,x1,y1] of segs){ ctx.moveTo(X(x0),Y(y0)); ctx.lineTo(X(x1),Y(y1)); }
190
+ ctx.stroke();
191
+ }
192
+ // nozzle: centroid of current layer
193
+ const cur = data.layers[Math.min(upto,N-1)];
194
+ let cx=0,cy=0,n=0; for(const s2 of cur){ cx+=s2[0]+s2[2]; cy+=s2[1]+s2[3]; n+=2; }
195
+ if(n){ ctx.fillStyle=NOZ; ctx.beginPath(); ctx.arc(X(cx/n),Y(cy/n),3.2,0,7); ctx.fill(); }
196
+ // HUD
197
+ ctx.fillStyle='#9fb0c8'; ctx.font='10px ui-monospace,monospace';
198
+ ctx.fillText('LAYER '+(upto+1)+'/'+N+' Β· '+Math.round(100*(upto+1)/N)+'%', pad, 14);
199
+ if(upto < N-1){ if(++hold>=3){ hold=0; upto++; } cv._raf = requestAnimationFrame(frame); }
200
+ // else: the full part is drawn β€” hold the final frame and STOP (no perpetual loop)
201
+ }
202
+ frame();
203
+ }
204
+ function scan(){ document.querySelectorAll('canvas.ce-vp:not([data-init])').forEach(cv=>{
205
+ cv.setAttribute('data-init','1'); animate(cv); }); }
206
+ setInterval(scan, 400); scan();
207
+ }
208
+ if (document.readyState !== 'loading') start();
209
+ else document.addEventListener('DOMContentLoaded', start);
210
+ })();
211
+ </script>
212
+ """
data/seed_lessons.jsonl ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"job_id": "seed-001", "material": "PLA", "geometry_type": "stringing", "env_temp": 22.0, "env_humidity": 45.0, "outcome": "failed_stringing", "lesson": "PLA strung at 215C in moderate humidity. Dropping nozzle to ~200C and nudging retraction up cleared it. With PLA, temperature is usually the bigger lever than retraction distance.", "source": "seed", "timestamp": "2026-05-20T10:00:00Z"}
2
+ {"job_id": "seed-002", "material": "PLA", "geometry_type": "stringing", "env_temp": 24.0, "env_humidity": 70.0, "outcome": "failed_stringing", "lesson": "High ambient humidity made PLA string even at a sane 205C. Settings tweaks did little; the real fix was drying the filament. Above ~60% humidity, suspect moisture before chasing retraction.", "source": "seed", "timestamp": "2026-05-20T10:05:00Z"}
3
+ {"job_id": "seed-003", "material": "PLA", "geometry_type": "overhang", "env_temp": 23.0, "env_humidity": 40.0, "outcome": "success", "lesson": "Steep PLA overhang printed clean with cooling fan at 100%. PLA likes maximum cooling on overhangs to freeze the layer before it droops.", "source": "seed", "timestamp": "2026-05-20T10:10:00Z"}
4
+ {"job_id": "seed-004", "material": "PLA", "geometry_type": "overhang", "env_temp": 28.0, "env_humidity": 50.0, "outcome": "failed_sag", "lesson": "PLA overhang sagged in a warm room even at full fan. Warm ambient air weakens part cooling; on hot days drop a few degrees and ensure unobstructed airflow, or add support past ~50 degrees of overhang.", "source": "seed", "timestamp": "2026-05-20T10:15:00Z"}
5
+ {"job_id": "seed-005", "material": "PLA", "geometry_type": "bridge", "env_temp": 22.0, "env_humidity": 42.0, "outcome": "success", "lesson": "Short PLA bridge under ~50mm printed flat with full cooling and slightly increased bridge flow. Max fan plus brisk bridge speed is the reliable combo for PLA spans.", "source": "seed", "timestamp": "2026-05-20T10:20:00Z"}
6
+ {"job_id": "seed-006", "material": "PLA", "geometry_type": "adhesion", "env_temp": 19.0, "env_humidity": 38.0, "outcome": "failed_sag", "lesson": "First layer wouldn't stick in a cold room. Bumping bed to ~60C, slowing the first layer, and turning the fan off for the opening layers fixed adhesion. Cold ambient hurts first-layer bonding most.", "source": "seed", "timestamp": "2026-05-20T10:25:00Z"}
7
+ {"job_id": "seed-007", "material": "PETG", "geometry_type": "stringing", "env_temp": 23.0, "env_humidity": 48.0, "outcome": "failed_stringing", "lesson": "PETG strung badly at 245C. Lowering to ~232C, raising travel speed, and enabling wipe-before-travel cleaned it up. PETG strings more than PLA by nature, so live at the low end of its temp range.", "source": "seed", "timestamp": "2026-05-20T10:30:00Z"}
8
+ {"job_id": "seed-008", "material": "PETG", "geometry_type": "stringing", "env_temp": 25.0, "env_humidity": 65.0, "outcome": "failed_stringing", "lesson": "PETG is hygroscopic and strung heavily in humid air despite good settings. Drying the spool was the only durable fix. Above ~60% humidity, dry PETG first and treat settings as secondary.", "source": "seed", "timestamp": "2026-05-20T10:35:00Z"}
9
+ {"job_id": "seed-009", "material": "PETG", "geometry_type": "overhang", "env_temp": 23.0, "env_humidity": 45.0, "outcome": "failed_sag", "lesson": "PETG overhang drooped when I ran the fan low for strength. PETG needs a cooling compromise: ~30-50% fan generally, bumped to 60-70% specifically over overhangs and bridges.", "source": "seed", "timestamp": "2026-05-20T10:40:00Z"}
10
+ {"job_id": "seed-010", "material": "PETG", "geometry_type": "adhesion", "env_temp": 22.0, "env_humidity": 47.0, "outcome": "failed_sag", "lesson": "PETG layers came out brittle with the fan too high. Dropping cooling to ~30% and raising temp 5C restored layer bonding. Over-cooling PETG causes delamination; it is the opposite of PLA.", "source": "seed", "timestamp": "2026-05-20T10:45:00Z"}
11
+ {"job_id": "seed-011", "material": "PETG", "geometry_type": "bridge", "env_temp": 24.0, "env_humidity": 44.0, "outcome": "success", "lesson": "PETG bridge held by temporarily raising fan to ~70% for the span only, then returning to 40%. Targeted cooling bursts beat globally high fan, which would weaken the rest of the part.", "source": "seed", "timestamp": "2026-05-20T10:50:00Z"}
12
+ {"job_id": "seed-012", "material": "PETG", "geometry_type": "stringing", "env_temp": 21.0, "env_humidity": 40.0, "outcome": "success", "lesson": "Clean PETG print at 230C with gentle retraction (moderate distance, ~40mm/s) in dry conditions. PETG stays molten longer than PLA, so gentle retraction plus fast travel beats aggressive retraction, which clogs.", "source": "seed", "timestamp": "2026-05-20T10:55:00Z"}
ingest/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Knowledge Ingestion
2
+
3
+ Turns the sources in `../docs/KNOWLEDGE-SOURCES.md` into Chief Engineer knowledge.
4
+ Two kinds, matching how each source actually carries information:
5
+
6
+ | Output | From | Goes to | Used as |
7
+ |--------|------|---------|---------|
8
+ | **Reference facts** | Prusa `.ini`, Klipper `.cfg`, Marlin `.h` (data only) | `data/references.jsonl` | injected into the prompt as a *Material Reference* block |
9
+ | **Candidate lessons** | research distilled into env-keyed JSONL rows | the ledger (`source="ingested"`) | retrieved exactly like seed/earned lessons |
10
+
11
+ ## Local distiller (deterministic, runs offline)
12
+
13
+ ```bash
14
+ uv run python -m ingest.run # uses ingest/samples/
15
+ uv run python -m ingest.run --dir /path/to/your/marlin_klipper_prusa_files
16
+ ```
17
+
18
+ Replace `ingest/samples/*` with your real configs and a `*lessons*.jsonl` of
19
+ research-distilled rows. The sample files are illustrative and clearly marked.
20
+
21
+ > **License-safe:** we ingest profile *data* (INI/cfg/#define values). We never
22
+ > import or link OrcaSlicer/PrusaSlicer code (AGPL-3.0).
23
+
24
+ ## Heavy datasets + fine-tune β†’ Modal (`modal_app.py`)
25
+
26
+ `ablam/gcode` (>6GB) and `3DTimeDataset/3DTime` are too big for the Space; they
27
+ run on Modal. `modal_app.py` is a **stub to be replaced by Kyle's MCP-hackathon
28
+ ingestion code** (which already parses training data for multi-agent use).
29
+ Off the critical demo path; this is where the Modal bonus + the frontier
30
+ fine-tune live.
31
+
32
+ ```bash
33
+ uv pip install modal datasets # not in the Space requirements
34
+ modal token set
35
+ modal run ingest/modal_app.py::sample_gcode
36
+ ```
ingest/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Knowledge ingestion β€” distill structured 3D-printing sources into Chief
2
+ Engineer references + candidate lessons. See ingest/README.md."""
ingest/distill.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Knowledge distiller β€” turn structured sources into Chief Engineer knowledge.
2
+
3
+ Two outputs, matching how the sources actually carry information:
4
+
5
+ 1. **Reference facts** (material baselines from slicer/firmware configs) β†’
6
+ `data/references.jsonl`. Injected into the prompt as a "Material Reference"
7
+ block. These are NOT env-keyed precedent β€” they're hard parameters.
8
+ 2. **Candidate lessons** (research distilled into env-keyed rows) β†’ appended to
9
+ the ledger as `source="ingested"`, retrieved exactly like seed/earned lessons.
10
+
11
+ Deterministic + stdlib (configparser/regex). License-safe: we ingest *profile
12
+ data*, never slicer code. No OrcaSlicer/PrusaSlicer imports.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import configparser
18
+ import json
19
+ import re
20
+ from pathlib import Path
21
+
22
+ from pydantic import BaseModel
23
+
24
+ from core.ledger import LedgerManager
25
+ from core.models import LessonEntry, MATERIALS
26
+
27
+ DATA = Path(__file__).resolve().parent.parent / "data"
28
+ REFERENCES_PATH = DATA / "references.jsonl"
29
+
30
+
31
+ class ReferenceFact(BaseModel):
32
+ material: str
33
+ param: str # "nozzle_temp" | "bed_temp" | "retraction_mm" | "max_temp" | ...
34
+ value: float
35
+ source: str # where it came from (filename / profile)
36
+
37
+
38
+ def _material_of(text: str) -> str | None:
39
+ up = text.upper()
40
+ for m in MATERIALS:
41
+ if m in up:
42
+ return m
43
+ if "ACETAL" in up or "DELRIN" in up:
44
+ return None
45
+ return None
46
+
47
+
48
+ # --- 3D-ADAM defect taxonomy (encoded from the paper's defect classes) ------
49
+ # RAG-ready risk knowledge keyed to the geometry/condition that triggers it.
50
+ DEFECT_TAXONOMY = {
51
+ "warping": {"risk": "warping", "trigger": "high-shrinkage material on a cool/draughty bed",
52
+ "note": "corners lift as lower layers cool and contract; raise bed temp, enclose, slow first layer."},
53
+ "under_extrusion": {"risk": "under_extrusion", "trigger": "too-low temp / too-fast flow / partial clog",
54
+ "note": "gaps and weak walls; raise temp or slow down, check for moisture and clogs."},
55
+ "stringing": {"risk": "stringing", "trigger": "wet filament or hot travel",
56
+ "note": "fine whiskers across gaps; dry filament first, then lower temp / tune retraction."},
57
+ "cracking": {"risk": "delamination", "trigger": "over-cooling on tall prints / poor layer bond",
58
+ "note": "layers split under stress; reduce fan, raise temp, enclose for ABS."},
59
+ }
60
+
61
+
62
+ def parse_prusa_ini(path: Path) -> list[ReferenceFact]:
63
+ """Extract per-filament baselines from a PrusaSlicer-style INI (data only)."""
64
+ facts: list[ReferenceFact] = []
65
+ cp = configparser.ConfigParser(strict=False, interpolation=None)
66
+ try:
67
+ cp.read(path, encoding="utf-8")
68
+ except Exception:
69
+ return facts
70
+ for section in cp.sections():
71
+ mat = _material_of(section)
72
+ if not mat:
73
+ continue
74
+ src = f"{path.name}:[{section}]"
75
+ kv = cp[section]
76
+
77
+ def first_num(raw: str | None) -> float | None:
78
+ if not raw:
79
+ return None
80
+ m = re.search(r"-?\d+(?:\.\d+)?", raw.split(",")[0])
81
+ return float(m.group()) if m else None
82
+
83
+ for key, param in (
84
+ ("temperature", "nozzle_temp"),
85
+ ("first_layer_temperature", "nozzle_temp"),
86
+ ("bed_temperature", "bed_temp"),
87
+ ("retract_length", "retraction_mm"),
88
+ ):
89
+ v = first_num(kv.get(key))
90
+ if v is not None:
91
+ facts.append(ReferenceFact(material=mat, param=param, value=v, source=src))
92
+ return facts
93
+
94
+
95
+ def parse_klipper_cfg(path: Path) -> list[ReferenceFact]:
96
+ """Pull safety-relevant limits from a Klipper printer.cfg ([extruder] max_temp)."""
97
+ facts: list[ReferenceFact] = []
98
+ try:
99
+ text = path.read_text(encoding="utf-8", errors="ignore")
100
+ except OSError:
101
+ return facts
102
+ for section, param in (("extruder", "max_temp"), ("heater_bed", "bed_max_temp")):
103
+ m = re.search(rf"\[{section}\][^\[]*?max_temp\s*[:=]\s*(\d+(?:\.\d+)?)", text, re.S | re.I)
104
+ if m:
105
+ facts.append(ReferenceFact(material="*", param=param, value=float(m.group(1)),
106
+ source=f"{path.name}:[{section}]"))
107
+ return facts
108
+
109
+
110
+ def parse_marlin_config(path: Path) -> list[ReferenceFact]:
111
+ """Pull hotend/bed max temps from a Marlin Configuration.h (#define ... MAXTEMP)."""
112
+ facts: list[ReferenceFact] = []
113
+ try:
114
+ text = path.read_text(encoding="utf-8", errors="ignore")
115
+ except OSError:
116
+ return facts
117
+ for define, param in (("HEATER_0_MAXTEMP", "max_temp"), ("BED_MAXTEMP", "bed_max_temp")):
118
+ m = re.search(rf"#define\s+{define}\s+(\d+)", text)
119
+ if m:
120
+ facts.append(ReferenceFact(material="*", param=param, value=float(m.group(1)),
121
+ source=f"{path.name}:{define}"))
122
+ return facts
123
+
124
+
125
+ def write_references(facts: list[ReferenceFact]) -> int:
126
+ DATA.mkdir(parents=True, exist_ok=True)
127
+ with REFERENCES_PATH.open("w", encoding="utf-8") as f:
128
+ for fact in facts:
129
+ f.write(fact.model_dump_json() + "\n")
130
+ return len(facts)
131
+
132
+
133
+ def load_references() -> list[ReferenceFact]:
134
+ if not REFERENCES_PATH.exists():
135
+ return []
136
+ out = []
137
+ for line in REFERENCES_PATH.read_text(encoding="utf-8").splitlines():
138
+ if line.strip():
139
+ try:
140
+ out.append(ReferenceFact(**json.loads(line)))
141
+ except Exception:
142
+ continue
143
+ return out
144
+
145
+
146
+ def reference_block(material: str, facts: list[ReferenceFact] | None = None) -> list[str]:
147
+ """Compact material-reference lines for the prompt (this material + global limits)."""
148
+ facts = facts if facts is not None else load_references()
149
+ rel = [f for f in facts if f.material in (material, "*")]
150
+ return [f"{f.param}={f.value:g} ({f.source})" for f in rel]
151
+
152
+
153
+ def ingest_candidate_lessons(path: Path, ledger: LedgerManager) -> int:
154
+ """Append research-distilled env-keyed rows to the ledger as source='ingested'."""
155
+ if not path.exists():
156
+ return 0
157
+ n = 0
158
+ for line in path.read_text(encoding="utf-8").splitlines():
159
+ if not line.strip():
160
+ continue
161
+ try:
162
+ row = json.loads(line)
163
+ row["source"] = "ingested"
164
+ ledger.append(LessonEntry(**row))
165
+ n += 1
166
+ except Exception:
167
+ continue
168
+ return n
ingest/modal_app.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Modal app β€” heavy-dataset ingestion at scale (the Modal-bonus track).
2
+
3
+ SCAFFOLD: the structure + the output CONTRACT are done; the per-dataset *mapping*
4
+ (raw row β†’ Chief Engineer record) is where Kyle's MCP-hackathon parser plugs in β€”
5
+ see the MAPPERS registry below. Isolated from the demo path: `modal` is
6
+ import-guarded, not in the Space requirements, and nothing in app.py imports this.
7
+
8
+ Why Modal: the source datasets (slicer g-code corpora, 3D-ADAM defect sets, etc.)
9
+ are too big to pull/parse on a laptop or in the Space. Modal does the heavy
10
+ download+parse; the OUTPUT is the same three lanes the local pipeline already
11
+ consumes, so the artifacts drop straight into the app.
12
+
13
+ THE CONTRACT β€” three artifact shapes (one JSON object per line):
14
+ A) reference fact -> data/references.jsonl
15
+ {"material","param","value","source"} (hard params)
16
+ B) candidate lesson -> data/_modal_candidate_lessons.jsonl (REVIEW before ledger)
17
+ {"job_id","material","geometry_type","env_temp","env_humidity",
18
+ "outcome","lesson","source":"ingested","timestamp"}
19
+ C) calibration obs -> sim/calibration/observations.modal.jsonl
20
+ {"material","geometry_type","env_temp","env_humidity","nozzle_temp",
21
+ "bed_temp","retraction_mm","fan_pct","first_layer_fan_pct","outcome","quality"?}
22
+ (enums: see core/models.py MATERIALS / GEOMETRY_TYPES / OUTCOMES.)
23
+
24
+ Run (after `uv pip install modal datasets` + `modal token set`):
25
+ modal run ingest/modal_app.py # all registered datasets
26
+ modal run ingest/modal_app.py --dataset 3d-adam --limit 2000
27
+ Lane B is written to a REVIEW file, never straight to the ledger (honesty gate).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ from pathlib import Path
34
+
35
+ # Import-guarded so the rest of the app/tests never depend on modal being present.
36
+ try:
37
+ import modal
38
+ except Exception: # pragma: no cover
39
+ modal = None # type: ignore
40
+
41
+ # ── per-dataset mappers: raw HF/GH row -> list of (lane, record dict) ──────────
42
+ # TODO(kyle): drop your MCP-hackathon parsing here. Each mapper takes one dataset
43
+ # row and returns zero or more ("A"|"B"|"C", record) tuples in the shapes above.
44
+ # Honesty rule: only emit Lane-B lessons that are true + directional; emit Lane-C
45
+ # only when the row carries the actual settings used.
46
+
47
+ def _map_3d_adam(row: dict) -> list[tuple[str, dict]]:
48
+ """3D-ADAM-style defect dataset β†’ mostly Lane C (settings + defect outcome)."""
49
+ # TODO(kyle): map your columns. Example shape (pseudocode β€” adjust field names):
50
+ # return [("C", {"material": row["material"], "geometry_type": "overhang",
51
+ # "env_temp": row.get("ambient_c", 22), "env_humidity": row.get("rh", 45),
52
+ # "nozzle_temp": row["nozzle"], "bed_temp": row["bed"], "retraction_mm": row["retract"],
53
+ # "fan_pct": row["fan"], "first_layer_fan_pct": 0,
54
+ # "outcome": _defect_to_outcome(row["label"]), "quality": row.get("quality")})]
55
+ return []
56
+
57
+
58
+ def _map_gcode(row: dict) -> list[tuple[str, dict]]:
59
+ """Slicer g-code corpus β†’ Lane A material baselines (temps/retraction)."""
60
+ # TODO(kyle): extract M104/M140/retraction etc. into reference facts:
61
+ # return [("A", {"material": m, "param": "nozzle_temp", "value": v, "source": "ablam/gcode"})]
62
+ return []
63
+
64
+
65
+ # dataset key β†’ (HF dataset id or loader, mapper). Add yours here.
66
+ MAPPERS = {
67
+ "3d-adam": ("<hf-org/3d-adam-id>", _map_3d_adam), # TODO(kyle): real id
68
+ "gcode": ("ablam/gcode", _map_gcode),
69
+ }
70
+
71
+ _VALID_LANES = {"A", "B", "C"}
72
+
73
+
74
+ if modal is not None:
75
+ app = modal.App("chief-engineer-ingest")
76
+ image = modal.Image.debian_slim().pip_install(
77
+ "datasets", "pyarrow", "huggingface_hub", "pydantic>=2.7")
78
+
79
+ @app.function(image=image, timeout=3600)
80
+ def distill(dataset: str, limit: int = 1000, split: str = "train") -> dict:
81
+ """Heavy step on Modal: load a dataset, map rows β†’ lane records, return them.
82
+ Returns {"A":[...], "B":[...], "C":[...], "stats":{...}} for the local
83
+ entrypoint to write into the repo's artifact files."""
84
+ from datasets import load_dataset
85
+
86
+ if dataset not in MAPPERS:
87
+ return {"error": f"unknown dataset '{dataset}'; known: {list(MAPPERS)}"}
88
+ hf_id, mapper = MAPPERS[dataset]
89
+ ds = load_dataset(hf_id, split=f"{split}[:{limit}]")
90
+ out: dict[str, list] = {"A": [], "B": [], "C": []}
91
+ for row in ds:
92
+ for lane, rec in mapper(dict(row)):
93
+ if lane in _VALID_LANES:
94
+ out[lane].append(rec)
95
+ out["stats"] = {"dataset": dataset, "hf_id": hf_id, "rows": len(ds),
96
+ "A": len(out["A"]), "B": len(out["B"]), "C": len(out["C"])}
97
+ return out
98
+
99
+ @app.local_entrypoint()
100
+ def main(dataset: str = "", limit: int = 1000):
101
+ """Run locally: fan out to Modal, then write artifacts into the repo in the
102
+ three lane shapes. Lane B goes to a REVIEW file (not the ledger)."""
103
+ root = Path(__file__).resolve().parent.parent
104
+ targets = [dataset] if dataset else list(MAPPERS)
105
+ agg: dict[str, list] = {"A": [], "B": [], "C": []}
106
+ for d in targets:
107
+ res = distill.remote(d, limit)
108
+ print(" ", res.get("stats", res))
109
+ for lane in ("A", "B", "C"):
110
+ agg[lane].extend(res.get(lane, []))
111
+
112
+ def _write(path: Path, rows: list, append: bool):
113
+ if not rows:
114
+ return
115
+ path.parent.mkdir(parents=True, exist_ok=True)
116
+ with path.open("a" if append else "w") as f:
117
+ for r in rows:
118
+ f.write(json.dumps(r) + "\n")
119
+ print(f" wrote {len(rows)} β†’ {path.relative_to(root)}")
120
+
121
+ _write(root / "data" / "references.jsonl", agg["A"], append=True) # Lane A (safe)
122
+ _write(root / "data" / "_modal_candidate_lessons.jsonl", agg["B"], append=False) # Lane B (REVIEW)
123
+ _write(root / "sim" / "calibration" / "observations.modal.jsonl", agg["C"], append=False) # Lane C
124
+ print("\nNext:")
125
+ print(" β€’ REVIEW data/_modal_candidate_lessons.jsonl (honesty gate), then fold into the")
126
+ print(" ledger via ingest_candidate_lessons / the ingestion work order.")
127
+ print(" β€’ Calibrate: uv run python -m sim.calibrate --data sim/calibration/observations.modal.jsonl")
128
+ print(" β€’ make test β†’ make run")
129
+
130
+ # FRONTIER (named in the writeup, NOT in-window): fine-tune a small Gemma on the
131
+ # accumulated ledger. Kyle's MCP-hackathon training pipeline would plug in here.
132
+ # @app.function(gpu="A10G", image=image, timeout=3600)
133
+ # def finetune_on_ledger(...): ...
134
+
135
+
136
+ if __name__ == "__main__":
137
+ print("Modal app scaffold. Install modal + datasets, `modal token set`, then:")
138
+ print(" modal run ingest/modal_app.py [--dataset 3d-adam --limit 2000]")
139
+ print("Plug your MCP-hackathon parsers into the MAPPERS registry (see docstring).")
ingest/run.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CLI: distill the sample sources into references + candidate lessons.
2
+
3
+ uv run python -m ingest.run # uses ingest/samples/
4
+ uv run python -m ingest.run --dir /path/to/your/configs
5
+
6
+ Replace ingest/samples/ with your real Marlin/Klipper/Prusa files and research
7
+ JSONL (or point --dir at them). Heavy datasets (ablam/gcode, 3DTime) go through
8
+ ingest/modal_app.py, not here.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ from pathlib import Path
15
+
16
+ from core.ledger import LedgerManager
17
+ from ingest.distill import (
18
+ ingest_candidate_lessons,
19
+ parse_klipper_cfg,
20
+ parse_marlin_config,
21
+ parse_prusa_ini,
22
+ write_references,
23
+ )
24
+
25
+ SAMPLES = Path(__file__).resolve().parent / "samples"
26
+
27
+
28
+ def main() -> None:
29
+ ap = argparse.ArgumentParser()
30
+ ap.add_argument("--dir", type=Path, default=SAMPLES, help="folder of source files")
31
+ args = ap.parse_args()
32
+ d = args.dir
33
+
34
+ facts = []
35
+ for p in d.glob("*.ini"):
36
+ facts += parse_prusa_ini(p)
37
+ for p in d.glob("*.cfg"):
38
+ facts += parse_klipper_cfg(p)
39
+ for p in list(d.glob("*.h")) + list(d.glob("*config*.txt")):
40
+ facts += parse_marlin_config(p)
41
+ n_ref = write_references(facts)
42
+ print(f"references: {n_ref} facts β†’ data/references.jsonl")
43
+ for f in facts:
44
+ print(f" Β· {f.material:4} {f.param:14} {f.value:g} ({f.source})")
45
+
46
+ ledger = LedgerManager()
47
+ total = 0
48
+ for p in d.glob("*lessons*.jsonl"):
49
+ k = ingest_candidate_lessons(p, ledger)
50
+ total += k
51
+ print(f"candidate lessons: +{k} from {p.name}")
52
+ print(f"ledger now: {ledger.count()}")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
ingest/samples/klipper_extruder.cfg ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SAMPLE (trimmed) Klipper printer.cfg β€” REPLACE with your real Voron/Ender cfg.
2
+ [extruder]
3
+ step_pin: PE2
4
+ microsteps: 16
5
+ nozzle_diameter: 0.400
6
+ max_temp: 300
7
+ pressure_advance: 0.040
8
+
9
+ [heater_bed]
10
+ heater_pin: PA1
11
+ max_temp: 120
ingest/samples/marlin_config.h ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // SAMPLE (trimmed) Marlin Configuration.h β€” REPLACE with your real firmware config.
2
+ #define TEMP_SENSOR_0 1
3
+ #define HEATER_0_MAXTEMP 275
4
+ #define BED_MAXTEMP 125
5
+ #define PIDTEMP
ingest/samples/prusa_filaments.ini ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ; SAMPLE (trimmed) PrusaSlicer-style filament profile data β€” REPLACE with your real Anycubic.ini etc.
2
+ ; We ingest profile DATA only; never slicer code. Values are illustrative.
3
+ [filament:Generic PLA]
4
+ temperature = 210
5
+ first_layer_temperature = 215
6
+ bed_temperature = 60
7
+ retract_length = 5
8
+
9
+ [filament:Generic PETG]
10
+ temperature = 240
11
+ first_layer_temperature = 240
12
+ bed_temperature = 85
13
+ retract_length = 4
14
+
15
+ [filament:Generic ABS]
16
+ temperature = 250
17
+ first_layer_temperature = 255
18
+ bed_temperature = 100
19
+ retract_length = 4
20
+
21
+ [filament:Generic TPU]
22
+ temperature = 230
23
+ first_layer_temperature = 230
24
+ bed_temperature = 50
25
+ retract_length = 2
ingest/samples/research_lessons.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {"job_id": "ingest-001", "material": "ABS", "geometry_type": "warping", "env_temp": 20.0, "env_humidity": 40.0, "outcome": "failed_sag", "lesson": "ABS corners lifted in an open, cool room. ABS needs an enclosure and a 100-110C bed; without it, warping is near-certain regardless of settings.", "source": "ingested", "timestamp": "2026-05-22T09:00:00Z"}
2
+ {"job_id": "ingest-002", "material": "ABS", "geometry_type": "adhesion", "env_temp": 24.0, "env_humidity": 45.0, "outcome": "success", "lesson": "ABS first layer held with bed at 105C, brim, and fan off for opening layers. Draught control matters more than glue for ABS adhesion.", "source": "ingested", "timestamp": "2026-05-22T09:05:00Z"}
3
+ {"job_id": "ingest-003", "material": "TPU", "geometry_type": "stringing", "env_temp": 23.0, "env_humidity": 55.0, "outcome": "failed_stringing", "lesson": "TPU strung badly; it is highly hygroscopic. Dry the spool and print slow (~25mm/s) with minimal retraction β€” TPU clogs with aggressive retraction.", "source": "ingested", "timestamp": "2026-05-22T09:10:00Z"}
learn/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Learning β€” the parametric policy (policy.py) and the closed loop (loop.py)
2
+ that improves it from simulated outcomes. See ../SIMULATION.md."""
learn/loop.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The closed learning loop β€” the primary thing we demo.
2
+
3
+ One iteration: the policy PROPOSES settings β†’ the Spine vetoes unsafe values β†’
4
+ the simulated world returns an outcome (the printer's role) β†’ the outcome is
5
+ written to the ledger as precedent AND folded into the policy. Run it N times
6
+ on the same job and watch quality climb from failure to clean: knowledge
7
+ compounding, made literal and self-driving.
8
+
9
+ Deterministic and offline by design (no LLM call in the loop) so the demo is
10
+ reproducible and fast. The cockpit's live path still runs the real LLM with
11
+ this same policy injected β€” see chief_engineer.advise / prompts.policy_note.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime, timezone
18
+
19
+ from core.ledger import LedgerManager
20
+ from learn.policy import LearnedPolicy
21
+ from core.models import Environment, Job, LessonEntry, PrintSettings
22
+ from sim.outcome import SimResult, simulate
23
+ from core.spine import SpineValidator
24
+
25
+ _SPINE = SpineValidator()
26
+
27
+
28
+ @dataclass
29
+ class IterationRecord:
30
+ n: int
31
+ settings: PrintSettings
32
+ result: SimResult
33
+ learned: str # what the policy changed
34
+ clamped: bool
35
+
36
+
37
+ @dataclass
38
+ class SessionResult:
39
+ job: Job
40
+ env: Environment
41
+ records: list[IterationRecord] = field(default_factory=list)
42
+
43
+ @property
44
+ def trajectory(self) -> list[float]:
45
+ return [r.result.quality for r in self.records]
46
+
47
+ @property
48
+ def first_success(self) -> int | None:
49
+ for r in self.records:
50
+ if r.result.outcome == "success":
51
+ return r.n
52
+ return None
53
+
54
+
55
+ def _record_lesson(job: Job, env: Environment, s: PrintSettings, result: SimResult,
56
+ ledger: LedgerManager) -> None:
57
+ verb = {"success": "printed clean", "failed_sag": "sagged",
58
+ "failed_stringing": "strung"}.get(result.outcome, result.outcome)
59
+ lesson = (f"[sim] {job.material} {job.geometry_type} at {env.temp:.0f}Β°C/{env.humidity:.0f}% RH "
60
+ f"{verb} (q={result.quality:.2f}) with nozzle {s.nozzle_temp:.0f}Β°C, fan {s.fan_pct:.0f}%, "
61
+ f"retraction {s.retraction_mm:.1f}mm.")
62
+ ledger.append(LessonEntry(
63
+ job_id=f"sim-{datetime.now(timezone.utc).strftime('%H%M%S%f')}",
64
+ material=job.material, geometry_type=job.geometry_type,
65
+ env_temp=env.temp, env_humidity=env.humidity, outcome=result.outcome,
66
+ lesson=lesson, source="sim", timestamp=datetime.now(timezone.utc).isoformat()))
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)
75
+ if record:
76
+ _record_lesson(job, env, settings, result, ledger)
77
+ learned = policy.update(job.material, job.geometry_type, env, result)
78
+ return IterationRecord(n=n, settings=settings, result=result, learned=learned,
79
+ clamped=bool(checked.vetoes))
80
+
81
+
82
+ def run_session(job: Job, env: Environment, iterations: int, policy: LearnedPolicy,
83
+ ledger: LedgerManager) -> SessionResult:
84
+ sess = SessionResult(job=job, env=env)
85
+ for i in range(1, iterations + 1):
86
+ sess.records.append(run_iteration(job, env, policy, ledger, i))
87
+ return sess
learn/policy.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Learned policy β€” the parametric layer that actually *improves*.
2
+
3
+ This is what makes the Chief Engineer more than a lookup. Each (material,
4
+ geometry, environment-BUCKET) cell holds learned offsets to the baseline
5
+ settings, updated from observed outcomes. Because cells are bucketed (not
6
+ exact env points), a lesson from one humid PETG bridge transfers to the *next*
7
+ humid PETG bridge β€” it generalizes, rather than recalling a single past job.
8
+
9
+ Two knowledge sources feed a recommendation, exactly as intended:
10
+ β€’ RAG β€” retrieved prior jobs reasoned over by the LLM (chief_engineer.py)
11
+ β€’ policy β€” these learned offsets, applied deterministically + shown to the LLM
12
+
13
+ Persisted to data/policy.json. Pure-Python, deterministic, no network.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+
22
+ from core.chief_engineer import _FALLBACK_SETTINGS
23
+ from core.models import Environment, Job, PrintSettings
24
+ from sim.outcome import SimResult
25
+
26
+ POLICY_PATH = Path(__file__).resolve().parent.parent / "data" / "policy.json"
27
+
28
+ # Corrective step per observed failure mode: which offsets to nudge, and by how
29
+ # much. Each move reduces the matching penalty in sim.outcome β€” so cells climb.
30
+ _CORRECTIONS = {
31
+ "sag": {"fan_pct": +12, "nozzle_temp": -3},
32
+ "stringing": {"retraction_mm": +0.5, "nozzle_temp": -4},
33
+ "adhesion": {"bed_temp": +6, "first_layer_fan_pct": -10},
34
+ "under_extrusion": {"nozzle_temp": +5},
35
+ "warp": {"fan_pct": -10},
36
+ }
37
+ # Keep learned offsets sane; the Spine still clamps the final settings.
38
+ _OFFSET_CLAMP = {
39
+ "nozzle_temp": 30, "bed_temp": 25, "retraction_mm": 3, "fan_pct": 80, "first_layer_fan_pct": 60,
40
+ }
41
+
42
+
43
+ def env_bucket(temp: float, humidity: float) -> tuple[str, str]:
44
+ tb = "cool" if temp < 20 else ("warm" if temp > 26 else "mid")
45
+ hb = "dry" if humidity < 35 else ("humid" if humidity > 55 else "mid")
46
+ return tb, hb
47
+
48
+
49
+ def cell_key(material: str, geometry: str, env: Environment) -> str:
50
+ tb, hb = env_bucket(env.temp, env.humidity)
51
+ return f"{material}/{geometry}/{tb}/{hb}"
52
+
53
+
54
+ @dataclass
55
+ class Cell:
56
+ offsets: dict[str, float]
57
+ trials: int = 0
58
+ successes: int = 0
59
+ quality_history: list[float] | None = None
60
+
61
+ @property
62
+ def success_rate(self) -> float:
63
+ return self.successes / self.trials if self.trials else 0.0
64
+
65
+
66
+ class LearnedPolicy:
67
+ def __init__(self, path: Path = POLICY_PATH) -> None:
68
+ self.path = path
69
+ self.cells: dict[str, Cell] = {}
70
+ self._load()
71
+
72
+ def _load(self) -> None:
73
+ if not self.path.exists():
74
+ return
75
+ try:
76
+ raw = json.loads(self.path.read_text(encoding="utf-8"))
77
+ except Exception:
78
+ return
79
+ for k, v in raw.items():
80
+ self.cells[k] = Cell(offsets=v.get("offsets", {}), trials=v.get("trials", 0),
81
+ successes=v.get("successes", 0),
82
+ quality_history=v.get("quality_history", []))
83
+
84
+ def save(self) -> None:
85
+ self.path.parent.mkdir(parents=True, exist_ok=True)
86
+ out = {k: {"offsets": c.offsets, "trials": c.trials, "successes": c.successes,
87
+ "quality_history": c.quality_history or []} for k, c in self.cells.items()}
88
+ self.path.write_text(json.dumps(out, indent=2), encoding="utf-8")
89
+
90
+ # --- read side ---------------------------------------------------------
91
+ def _baseline(self, material: str) -> dict[str, float]:
92
+ return dict(_FALLBACK_SETTINGS.get(material.upper(), _FALLBACK_SETTINGS["PLA"]))
93
+
94
+ def offsets_for(self, material: str, geometry: str, env: Environment) -> dict[str, float]:
95
+ c = self.cells.get(cell_key(material, geometry, env))
96
+ return dict(c.offsets) if c else {}
97
+
98
+ def propose(self, material: str, geometry: str, env: Environment) -> PrintSettings:
99
+ """Deterministic proposal = material baseline + learned offsets (clamped)."""
100
+ base = self._baseline(material)
101
+ for k, dv in self.offsets_for(material, geometry, env).items():
102
+ base[k] = base.get(k, 0.0) + dv
103
+ base["fan_pct"] = max(0.0, min(100.0, base["fan_pct"]))
104
+ base["first_layer_fan_pct"] = max(0.0, min(100.0, base["first_layer_fan_pct"]))
105
+ base["retraction_mm"] = max(0.0, base["retraction_mm"])
106
+ return PrintSettings(**base)
107
+
108
+ def cell_stats(self, material: str, geometry: str, env: Environment) -> Cell | None:
109
+ return self.cells.get(cell_key(material, geometry, env))
110
+
111
+ def policy_note(self, material: str, geometry: str, env: Environment) -> str:
112
+ """One line for the system prompt β€” steers the LLM with what's been learned."""
113
+ c = self.cell_stats(material, geometry, env)
114
+ if not c or not c.offsets:
115
+ return ""
116
+ tb, hb = env_bucket(env.temp, env.humidity)
117
+ deltas = ", ".join(f"{k} {v:+g}" for k, v in c.offsets.items())
118
+ return (f"LEARNED POLICY for {material}/{geometry} in {tb}/{hb} conditions "
119
+ f"(earned over {c.trials} runs, {c.success_rate*100:.0f}% clean): adjust baseline by {deltas}. "
120
+ f"Weigh this against the precedent above.")
121
+
122
+ # --- write side (learning) --------------------------------------------
123
+ def update(self, material: str, geometry: str, env: Environment, result: SimResult) -> str:
124
+ """Fold one observed outcome into the cell. Returns a human log line."""
125
+ key = cell_key(material, geometry, env)
126
+ c = self.cells.setdefault(key, Cell(offsets={}))
127
+ c.trials += 1
128
+ c.quality_history = (c.quality_history or []) + [round(result.quality, 3)]
129
+ if result.outcome == "success":
130
+ c.successes += 1
131
+ self.save()
132
+ return f"{key}: success (q={result.quality:.2f}) β€” holding policy"
133
+ moved = []
134
+ for field, step in _CORRECTIONS.get(result.failure_mode, {}).items():
135
+ cur = c.offsets.get(field, 0.0) + step
136
+ lim = _OFFSET_CLAMP.get(field, 1e9)
137
+ c.offsets[field] = max(-lim, min(lim, cur))
138
+ moved.append(f"{field} {step:+g}")
139
+ self.save()
140
+ return f"{key}: {result.failure_mode} (q={result.quality:.2f}) β€” learned: {', '.join(moved) or 'no-op'}"
pyproject.toml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "chief-engineer"
3
+ version = "0.1.0"
4
+ description = "A small local model that learns 3D printing job by job and predicts print failures before they happen."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ dependencies = [
9
+ "gradio>=6.17,<7",
10
+ "ollama>=0.4",
11
+ "pydantic>=2.7",
12
+ "trimesh>=4.4",
13
+ "shapely>=2.0",
14
+ ]
15
+
16
+ # Deploy-only extra for the ZeroGPU live path (mirrors requirements-zerogpu.txt).
17
+ # Install locally with: uv sync --extra zerogpu
18
+ [project.optional-dependencies]
19
+ zerogpu = [
20
+ "spaces>=0.30",
21
+ "torch>=2.4",
22
+ "transformers>=4.49",
23
+ "accelerate>=0.34",
24
+ ]
25
+
26
+ # pyproject is the source of truth for deps. requirements.txt is kept in sync by
27
+ # hand (only 4 base deps) because the HF Space installs via pip+requirements.txt,
28
+ # not uv. If they drift, regenerate: uv export --no-hashes --no-dev -o requirements.txt
29
+
30
+ # Permissive licenses only. Do NOT add OrcaSlicer/PrusaSlicer (AGPL-3.0).
31
+ [tool.chief-engineer]
32
+ model_default = "gemma4:e4b" # = gemma4:latest. gemma4:4b does NOT exist.
33
+ model_fast = "gemma4:e2b"
requirements-zerogpu.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploy-only deps for the ZeroGPU live-inference path. NOT in base requirements.txt
2
+ # and NOT needed for local dev / recording (that path uses Ollama).
3
+ #
4
+ # On the published HF Space (ZeroGPU hardware), install base + these, then set:
5
+ # CHIEF_ENGINEER_BACKEND=zerogpu
6
+ # CHIEF_ENGINEER_HF_MODEL=<your QAT Gemma checkpoint> # ⚠ verify on HF
7
+ # See docs/plan/08-ZEROGPU-DEPLOY.md. Leave this OFF until the Branch-A baseline
8
+ # (fallback Space + recorded video) is submitted.
9
+
10
+ spaces>=0.30 # ZeroGPU @spaces.GPU decorator (HF-provided on the Space)
11
+ torch>=2.4
12
+ transformers>=4.49 # Gemma 3 family support
13
+ accelerate>=0.34
14
+ # bitsandbytes # ONLY if loading a 4-bit (non-QAT) checkpoint on GPU
15
+ # llama-cpp-python # ALTERNATIVE: if you pick a QAT *GGUF* release instead of a
16
+ # # transformers checkpoint, run that via a llama.cpp path
17
+ # # (not llm_zerogpu.py, which is transformers-based).
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # The Chief Engineer β€” HF Space deps. Local-first; permissive licenses only.
2
+ # NEVER add OrcaSlicer/PrusaSlicer (AGPL-3.0).
3
+ gradio>=6.17,<7
4
+ ollama>=0.4 # local Gemma via Ollama (runs on llama.cpp)
5
+ pydantic>=2.7
6
+ trimesh>=4.4 # MIT β€” mesh load + overhang location (light use)
7
+ shapely>=2.0 # BSD β€” filled cross-section layers (GEOS bundled in the wheel; Space-safe)
8
+
9
+ # ── ZeroGPU live inference: ONE-STEP FLIP ──────────────────────────────────
10
+ # Uncomment the next line on the ZeroGPU Space (+ set hardware=ZeroGPU and the
11
+ # CHIEF_ENGINEER_BACKEND=zerogpu / CHIEF_ENGINEER_HF_MODEL env vars). Leave it
12
+ # commented for the light CPU fallback Space. See docs/plan/08-ZEROGPU-DEPLOY.md.
13
+ # -r requirements-zerogpu.txt
scripts/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Runnable entrypoints (run via `make <target>` or `uv run python -m scripts.<name>`)."""
scripts/bench_latency.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pre-window latency check β€” does the model respond fast enough for a live demo?
2
+
3
+ Run on the ACTUAL target hardware (your laptop / the Space). If a turn takes
4
+ ~40s, switch to a smaller quant now (gemma4:e2b), not on June 13.
5
+ Run: `make bench` (optionally CHIEF_ENGINEER_MODEL=gemma4:e2b)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ import time
12
+ from pathlib import Path
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) # repo root on path
15
+
16
+ from core import llm
17
+ from core.models import Environment, Job
18
+ from core.prompts import build_system_prompt
19
+
20
+ N = 3
21
+
22
+
23
+ def main() -> None:
24
+ if not llm.is_available():
25
+ print("⚠ Ollama not reachable β€” start `ollama serve` and pull the model, then re-run.")
26
+ print(f" target model: {llm.MODEL}")
27
+ return
28
+
29
+ job = Job(geometry_type="overhang", material="PLA", description="45Β° bracket")
30
+ env = Environment(temp=28, humidity=50)
31
+ system = build_system_prompt(job, env, [])
32
+
33
+ times = []
34
+ for i in range(N):
35
+ t0 = time.time()
36
+ out = llm.chat_json(system, "Give your recommendation for THIS job now.")
37
+ dt = time.time() - t0
38
+ times.append(dt)
39
+ ok = "ok" if out else "parse-fail"
40
+ print(f" run {i + 1}: {dt:5.1f}s ({ok})")
41
+
42
+ # Same cold/warm split + bands as preflight G2 (calibrated 6/10: warm <20s
43
+ # reads fine in a narrated demo).
44
+ cold, warm = times[0], (times[1:] or times)
45
+ warm_avg = sum(warm) / len(warm)
46
+ verdict = ("βœ… fine for a live narrated demo" if warm_avg < 20 else
47
+ ("🟑 long pauses β€” tighten prompt or use e2b/ZeroGPU" if warm_avg < 35 else
48
+ "πŸ”΄ too slow β€” use gemma4:e2b"))
49
+ print(f"\n{llm.MODEL}: warm avg {warm_avg:.1f}s (first call {cold:.1f}s) over {N} runs β†’ {verdict}")
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()
scripts/capture.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Capture spec β€” drive the live app through the demo beats deterministically,
2
+ so the Cap CLI (or any screen recorder) captures clean, repeatable clips.
3
+
4
+ This does NOT record β€” it walks the UI slowly with deliberate pauses while YOU
5
+ record the browser window with Cap. Beats are independent (record one, re-record
6
+ one) per the modular plan in RUNBOOK Phase 3.
7
+
8
+ One-time setup (local; not a Space/runtime dep):
9
+ uv pip install playwright && uv run playwright install chromium
10
+
11
+ Run the app first in another terminal:
12
+ make run # http://localhost:7860 (pre-warm one analyze!)
13
+
14
+ Then walk the beats (headed browser; record it with Cap):
15
+ uv run python -m scripts.capture --beat all # the full Studio->Analysis->Loop run
16
+ uv run python -m scripts.capture --beat 3 # just the load-bearing beat
17
+ uv run python -m scripts.capture --beat benchy # Studio + Benchy quick-load
18
+ uv run python -m scripts.capture --beat loop # the Learning Loop
19
+ # options: --url, --slowmo MS (default 350), --pause S between beats, --headless
20
+
21
+ Selectors are by visible text/role, so they track the current Astrometrics UI.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import sys
28
+ import time
29
+
30
+
31
+ def _pill(page, value):
32
+ """Click an LCARS pill. CSS uppercases the text, but the DOM keeps the raw
33
+ value (part types are lowercase: 'overhang'; materials are 'PLA'). Match the
34
+ <label> by its real text, scoped to pill groups."""
35
+ page.locator(".ce-pills label", has_text=value).first.click()
36
+
37
+
38
+ def _open_override(page):
39
+ """Sensors (temp/humidity/bed-position) live in the collapsed βš™ OVERRIDE
40
+ accordion now β€” open it before setting them. Idempotent: only opens if the
41
+ bed-position pill isn't already visible (so chained beats don't toggle it shut)."""
42
+ try:
43
+ if not page.locator(".ce-pills label", has_text="corner").first.is_visible():
44
+ page.get_by_text("OVERRIDE SENSORS", exact=False).first.click()
45
+ time.sleep(0.3)
46
+ except Exception:
47
+ pass
48
+
49
+
50
+ def _set_sensors(page, t, h):
51
+ """Set ambient Β°C + humidity %RH (the first two .ce-num fields = Studio's)."""
52
+ nums = page.locator(".ce-num input")
53
+ try:
54
+ nums.nth(0).fill(str(t)); nums.nth(0).dispatch_event("change")
55
+ nums.nth(1).fill(str(h)); nums.nth(1).dispatch_event("change")
56
+ except Exception:
57
+ pass
58
+
59
+
60
+ def beat_studio(page, slow):
61
+ """Beat 3a β€” define the job in STUDIO (empty start β†’ pick part + material;
62
+ set deterministic sensors for a repeatable, risk-relevant take)."""
63
+ page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
64
+ _pill(page, "overhang"); time.sleep(slow) # reference part type (DOM is lowercase)
65
+ _pill(page, "PLA"); time.sleep(slow) # material
66
+ _open_override(page); _set_sensors(page, 28, 60) # warm + humid β†’ precedent-rich
67
+ time.sleep(1.5) # part preview (3D model) settles
68
+
69
+
70
+ def beat_benchy(page, slow):
71
+ """Studio + the 3DBenchy quick-load (the recognizable hero part)."""
72
+ page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
73
+ page.get_by_role("button", name="QUICK-LOAD BENCHY").click()
74
+ time.sleep(3.0) # watch the hull slice live
75
+
76
+
77
+ def beat_analyze(page, slow):
78
+ """Beat 3b β€” ANALYZE -> auto-switch to ANALYSIS, let the reasoning land."""
79
+ page.get_by_role("button", name="ANALYZE JOB").click()
80
+ time.sleep(4.5) # reasoning + settings readout appear
81
+
82
+
83
+ def beat_scrub(page, slow):
84
+ """Slide through the filled cross-section layers (on the Analysis page)."""
85
+ sl = page.locator("input[type=range]").last
86
+ for v in (8, 18, 30, 40):
87
+ sl.fill(str(v)); sl.dispatch_event("input"); sl.dispatch_event("change")
88
+ time.sleep(0.9)
89
+
90
+
91
+ def beat_placement(page, slow):
92
+ """Build-plate beat β€” ABS in the corner: warp predicted + 'center it' suggestion."""
93
+ page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
94
+ _pill(page, "adhesion") # a flat part where bed adhesion/warp dominates
95
+ _pill(page, "ABS") # high-shrink material
96
+ _open_override(page) # bed-position now lives in the override panel
97
+ _pill(page, "corner") # worst plate position
98
+ time.sleep(slow)
99
+ page.get_by_role("button", name="ANALYZE JOB").click()
100
+ time.sleep(4.5) # PLACEMENT risk + suggested alignment appear
101
+
102
+
103
+ def beat_close_loop(page, slow):
104
+ """Beat 4a β€” simulate the outcome (deterministic), ledger grows."""
105
+ page.get_by_role("button", name="Simulate outcome").first.click()
106
+ time.sleep(3.0)
107
+
108
+
109
+ def beat_loop(page, slow):
110
+ """Beat 4b β€” the Learning Loop: quality climbs fail->clean."""
111
+ page.get_by_role("tab", name="LEARNING LOOP").click(); time.sleep(slow)
112
+ page.get_by_role("button", name="RUN SIMULATED LEARNING").click()
113
+ time.sleep(5.0) # the curve fills in
114
+
115
+
116
+ def beat_ledger(page, slow):
117
+ """Beat 4c β€” Knowledge + Mesh: the ledger where lessons accumulate."""
118
+ page.get_by_role("tab", name="KNOWLEDGE + MESH").click()
119
+ time.sleep(3.0)
120
+
121
+
122
+ BEATS = {
123
+ "studio": [beat_studio],
124
+ "benchy": [beat_studio, beat_benchy],
125
+ "3": [beat_studio, beat_analyze],
126
+ "scrub": [beat_studio, beat_analyze, beat_scrub],
127
+ "placement": [beat_placement, beat_scrub], # ABS-in-corner β†’ warp + "center it"
128
+ "4": [beat_close_loop, beat_loop, beat_ledger],
129
+ "loop": [beat_loop],
130
+ "ledger": [beat_ledger],
131
+ "all": [beat_studio, beat_analyze, beat_scrub, beat_close_loop,
132
+ beat_placement, beat_loop, beat_ledger],
133
+ }
134
+
135
+
136
+ def main() -> None:
137
+ ap = argparse.ArgumentParser(description="Walk the demo beats for screen capture.")
138
+ ap.add_argument("--beat", default="all", choices=sorted(BEATS), help="which beat(s) to walk")
139
+ ap.add_argument("--url", default="http://localhost:7860")
140
+ ap.add_argument("--slowmo", type=int, default=350, help="ms between Playwright actions")
141
+ ap.add_argument("--pause", type=float, default=1.5, help="seconds between beats")
142
+ ap.add_argument("--headless", action="store_true", help="hide the browser (no recording)")
143
+ args = ap.parse_args()
144
+
145
+ try:
146
+ from playwright.sync_api import sync_playwright
147
+ except ImportError:
148
+ sys.exit("playwright not installed β€” run: uv pip install playwright && uv run playwright install chromium")
149
+
150
+ print(f"Walking beat '{args.beat}' against {args.url} β€” start your Cap recording now.")
151
+ time.sleep(2.0)
152
+ slow = args.slowmo / 1000.0
153
+ with sync_playwright() as p:
154
+ browser = p.chromium.launch(headless=args.headless, slow_mo=args.slowmo)
155
+ page = browser.new_page(viewport={"width": 1280, "height": 900})
156
+ page.goto(args.url + "/?__theme=dark", wait_until="domcontentloaded")
157
+ time.sleep(2.5)
158
+ for step in BEATS[args.beat]:
159
+ print(f" β†’ {step.__name__}")
160
+ step(page, slow)
161
+ time.sleep(args.pause)
162
+ print("done β€” stop the recording. (browser stays open 5s)")
163
+ time.sleep(5.0)
164
+ browser.close()
165
+
166
+
167
+ if __name__ == "__main__":
168
+ main()
scripts/export_trace.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Export the lesson ledger as a Hugging Face Datasets-ready trace.
2
+
3
+ Earns the 'Sharing is Caring' badge: the agent's accumulated knowledge, posted
4
+ openly for others to learn from. Writes a clean JSONL + a dataset card.
5
+ Run: `make trace` (or `uv run python -m scripts.export_trace`) β†’ dist/
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) # repo root on path
14
+
15
+ from core.ledger import LedgerManager
16
+ from core.seed_lessons import ensure_seeded
17
+
18
+ DIST = Path(__file__).resolve().parent.parent / "dist"
19
+ CARD = """---
20
+ license: mit
21
+ task_categories: [tabular-regression]
22
+ tags: [3d-printing, additive-manufacturing, agent-trace, build-small-hackathon]
23
+ ---
24
+
25
+ # Chief Engineer β€” Lesson Ledger
26
+
27
+ Environment-keyed 3D-printing lessons accumulated by **The Chief Engineer**, a small local
28
+ Gemma agent built for the HF Build Small hackathon (Backyard AI). Each row is a durable lesson
29
+ keyed to material, geometry, and ambient conditions β€” `seed` rows bootstrap the corpus,
30
+ `earned` rows are written by the agent after a human-reported print outcome.
31
+
32
+ Schema: `job_id, material, geometry_type, env_temp, env_humidity, outcome, lesson, source, timestamp`.
33
+ """
34
+
35
+
36
+ def export() -> Path:
37
+ led = LedgerManager()
38
+ if not led.all():
39
+ ensure_seeded(led)
40
+ DIST.mkdir(parents=True, exist_ok=True)
41
+
42
+ out = DIST / "chief_engineer_ledger.jsonl"
43
+ with out.open("w", encoding="utf-8") as f:
44
+ for e in led.all():
45
+ f.write(e.model_dump_json() + "\n")
46
+ (DIST / "README.md").write_text(CARD, encoding="utf-8")
47
+
48
+ c = led.count()
49
+ print(f"exported {c['total']} lessons ({c['seed']} seed Β· {c['earned']} earned) β†’ {out}")
50
+ print(f"dataset card β†’ {DIST / 'README.md'}")
51
+ print("publish: huggingface-cli upload <user>/chief-engineer-ledger dist/ --repo-type dataset")
52
+ return out
53
+
54
+
55
+ if __name__ == "__main__":
56
+ export()
scripts/make_assets.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate small sample meshes for the gr.Model3D preview (build-time helper).
2
+
3
+ Permissive deps only (trimesh, MIT). Run once: `make assets` (= `uv run python -m scripts.make_assets`).
4
+ Produces a handful of .glb files keyed to geometry_type so the demo always has
5
+ something interactive to show without requiring an upload.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ ASSETS = Path(__file__).resolve().parent.parent / "assets"
13
+
14
+
15
+ def build() -> list[str]:
16
+ import numpy as np
17
+ import trimesh
18
+
19
+ ASSETS.mkdir(parents=True, exist_ok=True)
20
+ out: list[str] = []
21
+
22
+ def save(mesh: "trimesh.Trimesh", name: str) -> None:
23
+ path = ASSETS / name
24
+ mesh.export(path)
25
+ out.append(str(path))
26
+
27
+ # overhang: a base with a cantilever creating a steep unsupported underside
28
+ base = trimesh.creation.box(extents=[40, 40, 10])
29
+ base.apply_translation([0, 0, 5])
30
+ arm = trimesh.creation.box(extents=[40, 40, 10])
31
+ arm.apply_translation([30, 0, 35])
32
+ save(trimesh.util.concatenate([base, arm]), "overhang.glb")
33
+
34
+ # bridge: two pillars + a flat span across the gap
35
+ p1 = trimesh.creation.box(extents=[12, 40, 40]); p1.apply_translation([-24, 0, 20])
36
+ p2 = trimesh.creation.box(extents=[12, 40, 40]); p2.apply_translation([24, 0, 20])
37
+ span = trimesh.creation.box(extents=[60, 40, 6]); span.apply_translation([0, 0, 43])
38
+ save(trimesh.util.concatenate([p1, p2, span]), "bridge.glb")
39
+
40
+ # vase: a thin tall open cylinder shell
41
+ vase = trimesh.creation.annulus(r_min=18, r_max=20, height=80)
42
+ save(vase, "vase.glb")
43
+
44
+ # generic / adhesion / stringing: a simple calibration cube
45
+ save(trimesh.creation.box(extents=[20, 20, 20]), "cube.glb")
46
+ return out
47
+
48
+
49
+ if __name__ == "__main__":
50
+ paths = build()
51
+ print(f"wrote {len(paths)} meshes:")
52
+ for p in paths:
53
+ print(" ", p)
scripts/preflight.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Preflight β€” gated GO/NO-GO check for the real local stack. Run this FIRST
2
+ thing when you sit down locally, before touching anything else:
3
+
4
+ ollama serve & (if not already running)
5
+ make preflight [CHIEF_ENGINEER_MODEL=gemma4:e2b make preflight]
6
+ (or: uv run python -m scripts.preflight)
7
+
8
+ It exercises the REAL model path (the thing the sandbox could never verify) and
9
+ grades every gate the demo depends on. Each FAIL points at the matching section
10
+ of docs/plan/06-CONTINGENCY.md β€” so a failure costs minutes, not a night.
11
+
12
+ Never touches demo state: uses a temp ledger copy. Offline gates still run
13
+ without Ollama (reported as SKIP for the live ones). Exit code 1 if any
14
+ REQUIRED gate fails β€” safe to wire into a pre-record ritual.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import shutil
22
+ import sys
23
+ import tempfile
24
+ import time
25
+ from pathlib import Path
26
+
27
+ HERE = Path(__file__).resolve().parent.parent
28
+ sys.path.insert(0, str(HERE))
29
+
30
+ from core import llm # noqa: E402
31
+ from core.ledger import LedgerManager # noqa: E402
32
+ from core.models import Advice, Environment, Job # noqa: E402
33
+ from core.prompts import REFLECT_SYSTEM, build_reflect_prompt, build_system_prompt # noqa: E402
34
+ from core.spine import SpineValidator # noqa: E402
35
+ from core.models import PrintSettings # noqa: E402
36
+
37
+ RESULTS: list[tuple[str, str, str]] = [] # (gate, status, detail)
38
+ CONTINGENCY = "docs/plan/06-CONTINGENCY.md"
39
+
40
+
41
+ def record(gate: str, status: str, detail: str, section: str = "") -> None:
42
+ ptr = f" β†’ see {CONTINGENCY} Β§{section}" if (section and status == "FAIL") else ""
43
+ RESULTS.append((gate, status, detail))
44
+ icon = {"PASS": "βœ…", "WARN": "🟑", "FAIL": "πŸ”΄", "SKIP": "⏭"}[status]
45
+ print(f"{icon} {gate}: {status} β€” {detail}{ptr}")
46
+
47
+
48
+ def _temp_ledger() -> LedgerManager:
49
+ tmp = Path(tempfile.mkdtemp(prefix="preflight_")) / "lessons.jsonl"
50
+ seeds = HERE / "data" / "seed_lessons.jsonl"
51
+ if seeds.exists():
52
+ shutil.copy(seeds, tmp)
53
+ else:
54
+ tmp.touch()
55
+ return LedgerManager(path=tmp)
56
+
57
+
58
+ # --- G1: environment ---------------------------------------------------------
59
+ def g1_environment() -> bool:
60
+ if "4b" == llm.MODEL.split(":")[-1]:
61
+ record("G1 env", "FAIL", f"model tag '{llm.MODEL}' β€” gemma4:4b DOES NOT EXIST (Kaggle landmine)", "G1")
62
+ return False
63
+ if not llm.is_available():
64
+ record("G1 env", "FAIL", "Ollama daemon unreachable (is `ollama serve` running?)", "G1")
65
+ return False
66
+ try:
67
+ import ollama
68
+ tags = [m.get("model") or m.get("name") for m in ollama.list().get("models", [])]
69
+ except Exception as e:
70
+ tags = []
71
+ record("G1 env", "WARN", f"daemon up but list() odd: {e!r}")
72
+ if tags and not any(llm.MODEL in (t or "") or (t or "").startswith(llm.MODEL) for t in tags):
73
+ record("G1 env", "FAIL", f"'{llm.MODEL}' not pulled. Available: {tags}", "G1")
74
+ return False
75
+ record("G1 env", "PASS", f"daemon up, model '{llm.MODEL}' present ({len(tags)} tags local)")
76
+ _tiny_titan_check()
77
+ return True
78
+
79
+
80
+ def _tiny_titan_check() -> None:
81
+ """Report Tiny Titan ($1.5k ≀4B special award) eligibility from `ollama show`.
82
+ Informational β€” never blocks the demo. Verified 6/10: the field guide's 32B cap
83
+ counts TOTAL params ("not just active"); no ruling found for MatFormer E-models
84
+ (raw 5.1B/8.0B vs effective ~2B/~4B) on the ≀4B award β†’ treat as ambiguous and
85
+ ASK in the org discussions before tagging."""
86
+ try:
87
+ import ollama
88
+ info = ollama.show(llm.MODEL)
89
+ except Exception as e:
90
+ record("Tiny Titan", "SKIP", f"`ollama show` unavailable ({e!r:.60}) β€” run it by hand")
91
+ return
92
+
93
+ def _get(obj, *keys):
94
+ for k in keys:
95
+ if isinstance(obj, dict) and k in obj:
96
+ return obj[k]
97
+ if hasattr(obj, k):
98
+ return getattr(obj, k)
99
+ return None
100
+
101
+ details = _get(info, "details") or {}
102
+ modelinfo = _get(info, "modelinfo", "model_info") or {}
103
+ psize = _get(details, "parameter_size") # e.g. "4.3B"
104
+ b = None
105
+ if isinstance(modelinfo, dict):
106
+ for k, v in modelinfo.items():
107
+ if str(k).endswith("parameter_count") and isinstance(v, (int, float)):
108
+ b = float(v) / 1e9
109
+ if b is None and isinstance(psize, str):
110
+ try:
111
+ b = float(psize.strip().upper().rstrip("B"))
112
+ except Exception:
113
+ b = None
114
+
115
+ # Gemma 3n E-models report RAW params via ollama (E4B~8B) but are designed as
116
+ # EFFECTIVE 4B/2B (MatFormer + per-layer embeddings). The badge counts the
117
+ # effective size, so key off the model NAME, not the raw count.
118
+ import re
119
+ em = re.search(r"e(\d+)b", llm.MODEL.lower())
120
+ eff = float(em.group(1)) if em else None
121
+ raw = f"{b:.1f}B raw" if b is not None else "raw n/a"
122
+
123
+ if eff is not None:
124
+ if eff <= 4.0:
125
+ # Verified 6/10: the guide's 32B cap counts TOTAL params ("not just
126
+ # active") and no ruling exists for E-models on the <=4B award β€” so
127
+ # effective-params eligibility is genuinely AMBIGUOUS. Ask, don't tag.
128
+ record("Tiny Titan", "WARN",
129
+ f"{llm.MODEL}: effective ~{eff:.0f}B but {raw} β€” $1.5k award counts params "
130
+ f"ambiguously for E-models (32B cap counts TOTAL). ASK in the org "
131
+ f"discussions before tagging tiny-titan")
132
+ else:
133
+ record("Tiny Titan", "WARN",
134
+ f"{llm.MODEL}: effective ~{eff:.0f}B > 4B β€” outside Tiny Titan either way")
135
+ elif b is None:
136
+ record("Tiny Titan", "WARN", f"couldn't parse params (details={psize!r}); check `ollama show {llm.MODEL}` by hand")
137
+ elif b <= 4.0:
138
+ record("Tiny Titan", "PASS", f"{b:.2f}B ≀ 4B β†’ ELIGIBLE; add the tag")
139
+ else:
140
+ record("Tiny Titan", "WARN", f"{b:.2f}B > 4B β€” outside Tiny Titan; skip that badge")
141
+
142
+
143
+ # --- G2-G4: the load-bearing live calls ---------------------------------------
144
+ def g2_g4_live_calls() -> None:
145
+ lm = _temp_ledger()
146
+ # Case A: precedent-rich (humid PETG stringing β€” seeds 007/008/012 match)
147
+ job_a = Job(geometry_type="stringing", material="PETG", description="calibration tower, humid day")
148
+ env_a = Environment(temp=25, humidity=65)
149
+ retrieved = lm.retrieve("PETG", "stringing", 25, 65, k=3)
150
+ sys_a = build_system_prompt(job_a, env_a, retrieved)
151
+ # Case B: novel (TPU vase β€” no precedent in seeds)
152
+ job_b = Job(geometry_type="vase", material="TPU", description="flexible vase")
153
+ env_b = Environment(temp=22, humidity=45)
154
+ sys_b = build_system_prompt(job_b, env_b, lm.retrieve("TPU", "vase", 22, 45, k=3))
155
+
156
+ # Prompt-length budget (GEMMA-STEERING Technique 5): small-Gemma attention
157
+ # quality degrades past ~800 tokens. Informational β€” trim references/k if hot.
158
+ est = len(sys_a) // 4
159
+ flag = " ⚠ over the ~800-token small-Gemma budget β€” trim references / k" if est > 800 else ""
160
+ print(f" prompt size: ~{est} tokens (precedent-rich case){flag}")
161
+
162
+ times, parses, schemas = [], 0, 0
163
+ advice_a = None
164
+ N = 3
165
+ for i in range(N):
166
+ t0 = time.time()
167
+ raw = llm.chat_json(sys_a, "Give your recommendation for THIS job now.")
168
+ dt = time.time() - t0
169
+ times.append(dt)
170
+ print(f" live call {i+1}/{N}: {dt:5.1f}s {'(json ok)' if raw else '(parse FAIL)'}")
171
+ if raw is not None:
172
+ parses += 1
173
+ try:
174
+ advice_a = Advice(**raw)
175
+ schemas += 1
176
+ except Exception as e:
177
+ print(f" schema reject: {e!s:.120}")
178
+
179
+ # G2 latency β€” separate the one-time COLD model-load from WARM steady-state.
180
+ # The cold call (first) only happens once; you pre-warm before recording, so
181
+ # the demo experience is the warm number. Gate on warm, report cold as a tip.
182
+ cold = times[0]
183
+ warm = times[1:] if len(times) > 1 else times
184
+ warm_avg = sum(warm) / len(warm)
185
+ print(f" cold-start {cold:5.1f}s (one-time model load) Β· warm avg {warm_avg:.1f}s "
186
+ f"over {len(warm)} β€” pre-warm with one throwaway call before recording")
187
+ # Bands calibrated against real cockpit driving (Kyle, 6/10): warm ~18s on
188
+ # e4b reads fine in a narrated demo, so <20s is a PASS, not a warning.
189
+ if warm_avg < 20:
190
+ record("G2 latency", "PASS",
191
+ f"warm avg {warm_avg:.1f}s (cold {cold:.1f}s) β€” fine for a live narrated demo ({llm.MODEL}); pre-warm before recording")
192
+ elif warm_avg < 35:
193
+ record("G2 latency", "WARN",
194
+ f"warm avg {warm_avg:.1f}s (cold {cold:.1f}s) β€” long pauses; tighten prompt, or gemma4:e2b / ZeroGPU", "G2")
195
+ else:
196
+ record("G2 latency", "FAIL",
197
+ f"warm avg {warm_avg:.1f}s β€” too slow even warm; use gemma4:e2b or ZeroGPU", "G2")
198
+
199
+ # G3 contract
200
+ if schemas == N:
201
+ record("G3 contract", "PASS", f"{schemas}/{N} valid JSON + Advice schema")
202
+ elif schemas >= 1:
203
+ record("G3 contract", "WARN", f"only {schemas}/{N} schema-valid (fallback will cover, but video needs live)", "G3")
204
+ else:
205
+ record("G3 contract", "FAIL", f"0/{N} valid β€” live path unusable as-is", "G3")
206
+
207
+ # G4 reasoning quality β€” the load-bearing moment, heuristically graded
208
+ if advice_a is not None:
209
+ r = advice_a.reasoning.lower()
210
+ checks = {
211
+ "evaluates precedent (cites a job/precedent/prior)": any(w in r for w in ("precedent", "prior", "job", "seed-", "last time", "before")),
212
+ "reasons about the room (humidity/temp/moisture/dry)": any(w in r for w in ("humid", "moisture", "temp", "Β°c", " rh", "dry", "wet")),
213
+ "substantive (>120 chars)": len(advice_a.reasoning) > 120,
214
+ "flags at least one risk region": len(advice_a.risks) >= 1,
215
+ }
216
+ failed = [k for k, ok in checks.items() if not ok]
217
+ print(f" reasoning sample: \"{advice_a.reasoning[:180]}...\"")
218
+ if not failed:
219
+ record("G4 reasoning", "PASS", "precedent-evaluation text present and substantive")
220
+ else:
221
+ record("G4 reasoning", "WARN", f"weak on: {'; '.join(failed)} β€” prompt-tune before recording", "G4")
222
+ else:
223
+ record("G4 reasoning", "FAIL", "no schema-valid advice to grade", "G3")
224
+
225
+ # G4b novel case β€” must NOT hallucinate precedent
226
+ raw_b = llm.chat_json(sys_b, "Give your recommendation for THIS job now.")
227
+ if raw_b:
228
+ try:
229
+ adv_b = Advice(**raw_b)
230
+ rb = adv_b.reasoning.lower()
231
+ honest = any(w in rb for w in ("no close precedent", "no precedent", "no prior", "novel", "material properties", "first "))
232
+ cites_fake = "seed-" in rb
233
+ if honest and not cites_fake:
234
+ record("G4b novel-case", "PASS", "says no-precedent / reasons from material properties")
235
+ else:
236
+ record("G4b novel-case", "WARN", f"novel-job reasoning suspect (honest={honest}, cites_fake={cites_fake}) β€” check by eye", "G4")
237
+ print(f" novel sample: \"{adv_b.reasoning[:180]}...\"")
238
+ except Exception:
239
+ record("G4b novel-case", "WARN", "novel call returned but schema-invalid", "G3")
240
+ else:
241
+ record("G4b novel-case", "WARN", "novel call failed to parse", "G3")
242
+
243
+ # G5 reflection
244
+ raw_r = llm.chat_json(REFLECT_SYSTEM, build_reflect_prompt(
245
+ job_a, env_a, "nozzle 230Β°C, bed 80Β°C, retraction 4.5mm, fan 40%, first-layer fan 0%", "success"))
246
+ lesson = (raw_r or {}).get("lesson") if isinstance(raw_r, dict) else None
247
+ if lesson and len(lesson) > 30:
248
+ record("G5 reflection", "PASS", f"lesson distilled: \"{lesson[:100]}...\"")
249
+ elif lesson:
250
+ record("G5 reflection", "WARN", f"lesson thin: \"{lesson}\"", "G4")
251
+ else:
252
+ record("G5 reflection", "WARN", "reflect returned no lesson (deterministic fallback covers it)", "G3")
253
+
254
+
255
+ # --- G6: spine (offline, always) ----------------------------------------------
256
+ def g6_spine() -> None:
257
+ checked = SpineValidator().check(PrintSettings(
258
+ nozzle_temp=260, bed_temp=60, retraction_mm=5, fan_pct=100, first_layer_fan_pct=0), "PLA")
259
+ if checked.vetoes and checked.settings.nozzle_temp < 260:
260
+ record("G6 spine", "PASS", f"unsafe PLA 260Β°C clamped to {checked.settings.nozzle_temp:.0f}Β°C ({len(checked.vetoes)} veto)")
261
+ else:
262
+ record("G6 spine", "FAIL", "Spine did NOT clamp an unsafe setting β€” demo safety claim broken", "G6")
263
+
264
+
265
+ # --- G7: app serves (offline, always) -------------------------------------------
266
+ def g7_app() -> None:
267
+ try:
268
+ import urllib.request
269
+ import app as A
270
+ d = A.build()
271
+ d.launch(prevent_thread_lock=True, server_name="127.0.0.1", server_port=7991, quiet=True)
272
+ code = urllib.request.urlopen("http://127.0.0.1:7991/", timeout=15).status
273
+ d.close()
274
+ if code == 200:
275
+ record("G7 app", "PASS", "build() + launch + HTTP 200")
276
+ else:
277
+ record("G7 app", "FAIL", f"HTTP {code}", "G7")
278
+ except Exception as e:
279
+ record("G7 app", "FAIL", f"{e!r:.140}", "G7")
280
+
281
+
282
+ # --- G8: assets + data (offline, always) ---------------------------------------
283
+ def g8_assets() -> None:
284
+ missing = [n for n in ("overhang.glb", "bridge.glb", "vase.glb", "cube.glb")
285
+ if not (HERE / "assets" / n).exists()]
286
+ seeds = HERE / "data" / "seed_lessons.jsonl"
287
+ n_seeds = len([l for l in seeds.read_text().splitlines() if l.strip()]) if seeds.exists() else 0
288
+ if not missing and n_seeds == 12:
289
+ record("G8 assets", "PASS", "4 meshes present, 12 seed lessons")
290
+ elif missing:
291
+ record("G8 assets", "FAIL", f"missing meshes {missing} β€” run `make assets`", "G8")
292
+ else:
293
+ record("G8 assets", "WARN", f"seed count {n_seeds} != 12 β€” verify data/seed_lessons.jsonl", "G8")
294
+
295
+
296
+ def main() -> None:
297
+ print(f"Chief Engineer preflight β€” model={llm.MODEL} ({time.strftime('%Y-%m-%d %H:%M')})")
298
+ print("=" * 70)
299
+ live = g1_environment()
300
+ if live:
301
+ g2_g4_live_calls()
302
+ else:
303
+ for g in ("G2 latency", "G3 contract", "G4 reasoning", "G4b novel-case", "G5 reflection"):
304
+ record(g, "SKIP", "no live backend (offline gates still checked below)")
305
+ g6_spine()
306
+ g7_app()
307
+ g8_assets()
308
+
309
+ print("=" * 70)
310
+ fails = [g for g, s, _ in RESULTS if s == "FAIL"]
311
+ warns = [g for g, s, _ in RESULTS if s == "WARN"]
312
+ skips = [g for g, s, _ in RESULTS if s == "SKIP"]
313
+ if fails:
314
+ print(f"πŸ”΄ NO-GO: {len(fails)} gate(s) failed: {', '.join(fails)}")
315
+ print(f" Work {CONTINGENCY} top-to-bottom for each, then re-run.")
316
+ sys.exit(1)
317
+ if skips:
318
+ print("🟑 OFFLINE-ONLY PASS β€” fallback demo is safe, but DO NOT record the video")
319
+ print(" until the live gates run green. Start `ollama serve` and re-run.")
320
+ sys.exit(0)
321
+ if warns:
322
+ print(f"🟑 GO with warnings ({', '.join(warns)}) β€” read them before recording.")
323
+ sys.exit(0)
324
+ print("🟒 GO β€” all gates green. Record the demo today, not tomorrow.")
325
+
326
+
327
+ if __name__ == "__main__":
328
+ main()