kylebrodeur commited on
Commit
e2c3323
·
verified ·
1 Parent(s): a49c22d

deploy: update Space from deploy_preflight --push

Browse files
.codeboarding/logs/wrapper-server.log CHANGED
@@ -246,3 +246,27 @@ INFO: 127.0.0.1:44498 - "GET /health HTTP/1.1" 200 OK
246
  [stderr] 2026-06-14 06:21:26 INFO [watchfiles.main:308] 3 changes detected
247
  [stderr] 2026-06-14 06:21:26 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
248
  [stderr] 2026-06-14 06:21:26 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  [stderr] 2026-06-14 06:21:26 INFO [watchfiles.main:308] 3 changes detected
247
  [stderr] 2026-06-14 06:21:26 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
248
  [stderr] 2026-06-14 06:21:26 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
249
+ [stderr] 2026-06-14 06:22:00 INFO [watchfiles.main:308] 1 change detected
250
+ [stderr] 2026-06-14 06:22:00 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
251
+ [stderr] 2026-06-14 06:22:00 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
252
+ [stderr] 2026-06-14 06:22:28 INFO [watchfiles.main:308] 1 change detected
253
+ [stderr] 2026-06-14 06:22:29 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
254
+ [stderr] 2026-06-14 06:22:29 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
255
+ [stderr] 2026-06-14 06:22:37 INFO [watchfiles.main:308] 1 change detected
256
+ [stderr] 2026-06-14 06:22:38 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
257
+ [stderr] 2026-06-14 06:22:38 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
258
+ [stderr] 2026-06-14 06:24:42 INFO [watchfiles.main:308] 1 change detected
259
+ [stderr] 2026-06-14 06:24:43 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
260
+ [stderr] 2026-06-14 06:24:43 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
261
+ [stderr] 2026-06-14 06:25:02 INFO [watchfiles.main:308] 1 change detected
262
+ [stderr] 2026-06-14 06:25:02 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
263
+ [stderr] 2026-06-14 06:25:02 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
264
+ [stderr] 2026-06-14 06:25:17 INFO [watchfiles.main:308] 1 change detected
265
+ [stderr] 2026-06-14 06:25:18 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
266
+ [stderr] 2026-06-14 06:25:18 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
267
+ [stderr] 2026-06-14 06:25:46 INFO [watchfiles.main:308] 1 change detected
268
+ [stderr] 2026-06-14 06:25:46 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
269
+ [stderr] 2026-06-14 06:25:46 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
270
+ [stderr] 2026-06-14 06:27:45 INFO [watchfiles.main:308] 1 change detected
271
+ [stderr] 2026-06-14 06:27:46 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
272
+ [stderr] 2026-06-14 06:27:46 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
app.py CHANGED
@@ -37,6 +37,7 @@ from core import llm
37
  from core import seed_lessons
38
  from core.theme import (
39
  THEME, CSS, rule, command_bar, footer_bar, inspector_panel, icon, loader, tab_intro,
 
40
  )
41
  from core.widgets import virtual_printer_html, layer_image, SCRUB_LAYERS, VP_HEAD
42
  from core.chief_engineer import advise
@@ -215,7 +216,7 @@ def build_start(part):
215
  return (gr.update(),) * 9
216
  return (
217
  gr.Tabs(selected="build"), # tabs
218
- loader("BUILDING THE JOB · evaluating precedent and proposing settings"), # build_loader
219
  gr.update(visible=False), # build_results (hide until ready)
220
  gr.update(interactive=True), # to_print_btn (un-gate)
221
  gr.update(visible=False), # override_btn (hide)
@@ -581,7 +582,7 @@ def build() -> gr.Blocks:
581
  with gr.Tabs() as tabs:
582
  # ───────────────────────── BUILD · define the job ────────────────────────
583
  with gr.Tab("BUILD", id="studio"):
584
- reset_s, run_btn = _action_bar(primary_label="BUILD JOB", primary_id="ce-run")
585
 
586
  # 3-column setup: ENVIRONMENT | BUILD-PLATE+NOTES | MATERIAL
587
  with gr.Row(equal_height=False):
@@ -774,6 +775,8 @@ def build() -> gr.Blocks:
774
  # Simulated environment: roll on load, re-roll on demand, keep readout + footer in sync.
775
  sensor_outs = [temp, humidity, bed_position, sensors_readout]
776
  demo.load(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])
 
 
777
  demo.load(lambda: field_log.privacy_notice() if field_log.is_active() else "",
778
  None, [privacy_line])
779
  roll_btn.click(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])
 
37
  from core import seed_lessons
38
  from core.theme import (
39
  THEME, CSS, rule, command_bar, footer_bar, inspector_panel, icon, loader, tab_intro,
40
+ CLOCK_JS,
41
  )
42
  from core.widgets import virtual_printer_html, layer_image, SCRUB_LAYERS, VP_HEAD
43
  from core.chief_engineer import advise
 
216
  return (gr.update(),) * 9
217
  return (
218
  gr.Tabs(selected="build"), # tabs
219
+ loader("SLICING THE PART · reading precedent, proposing settings, running checks"), # build_loader
220
  gr.update(visible=False), # build_results (hide until ready)
221
  gr.update(interactive=True), # to_print_btn (un-gate)
222
  gr.update(visible=False), # override_btn (hide)
 
582
  with gr.Tabs() as tabs:
583
  # ───────────────────────── BUILD · define the job ────────────────────────
584
  with gr.Tab("BUILD", id="studio"):
585
+ reset_s, run_btn = _action_bar(primary_label="SLICE", primary_id="ce-run")
586
 
587
  # 3-column setup: ENVIRONMENT | BUILD-PLATE+NOTES | MATERIAL
588
  with gr.Row(equal_height=False):
 
775
  # Simulated environment: roll on load, re-roll on demand, keep readout + footer in sync.
776
  sensor_outs = [temp, humidity, bed_position, sensors_readout]
777
  demo.load(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])
778
+ # CLOCK_JS wires the LCARS clock, popup toggles, and loader stage cycling.
779
+ demo.load(None, None, None, js=CLOCK_JS)
780
  demo.load(lambda: field_log.privacy_notice() if field_log.is_active() else "",
781
  None, [privacy_line])
782
  roll_btn.click(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])
core/theme.py CHANGED
@@ -353,18 +353,36 @@ CSS += """
353
  opacity:1 !important; filter:none !important; animation:none !important; border-color:var(--ao-outline-dim) !important; }
354
  .gradio-container .generating::before, .gradio-container .generating::after { content:none !important; }
355
 
356
- /* one custom consolidated loader: a scanning bar + label, then content reveals */
357
- .ce-loader { display:flex; flex-direction:column; gap:8px; align-items:flex-start;
358
- padding:14px 4px; }
359
- .ce-loader-bar { position:relative; width:100%; height:6px; background:var(--ao-surface-hi);
 
 
 
360
  overflow:hidden; border:1px solid var(--ao-outline-dim); }
361
  .ce-loader-bar span { position:absolute; top:0; left:-40%; width:40%; height:100%;
362
- background:var(--ao-orange); animation:ce-scan 1.05s linear infinite; }
 
363
  @keyframes ce-scan { 0% { left:-40%; } 100% { left:100%; } }
364
- .ce-loader-text { color:var(--ao-orange); letter-spacing:2px; text-transform:uppercase;
365
- font-size:11px; font-weight:700; }
366
  .ce-loader-text::after { content:"…"; }
367
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  /* top-right action bar: small primary button, same spot every tab, reset persistent */
369
  .ce-actionbar { display:flex !important; justify-content:flex-end !important;
370
  align-items:center; gap:8px; flex-wrap:nowrap; margin:2px 0 8px; }
@@ -473,6 +491,35 @@ CLOCK_JS = """
473
  });
474
  };
475
  wirePopups(); setInterval(wirePopups, 1500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
  }
477
  """
478
 
@@ -525,13 +572,28 @@ def icon(name: str, size: int = 14, color: str | None = None) -> str:
525
  )
526
 
527
 
528
- def loader(text: str = "WORKING") -> str:
529
- """The single consolidated custom loader (no stock radio spinners). One scanning
530
- bar + label; content is revealed once the work completes (progressive reveal)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  return (
532
  "<div class='ce-loader'>"
 
533
  "<div class='ce-loader-bar'><span></span></div>"
534
- f"<div class='ce-loader-text'>{text}</div></div>"
 
535
  )
536
 
537
 
 
353
  opacity:1 !important; filter:none !important; animation:none !important; border-color:var(--ao-outline-dim) !important; }
354
  .gradio-container .generating::before, .gradio-container .generating::after { content:none !important; }
355
 
356
+ /* one custom consolidated loader: scanning bar + cycling stages + LCARS panels.
357
+ When inside the slice tab, fills more vertical space so the user sees something
358
+ is happening (was tiny before). */
359
+ .ce-loader { display:flex; flex-direction:column; gap:14px; align-items:stretch;
360
+ padding:32px 24px; background:linear-gradient(180deg, rgba(255,156,0,0.04), transparent 120px);
361
+ border-top:2px solid var(--ao-orange); min-height:220px; }
362
+ .ce-loader-bar { position:relative; width:100%; height:8px; background:var(--ao-surface-hi);
363
  overflow:hidden; border:1px solid var(--ao-outline-dim); }
364
  .ce-loader-bar span { position:absolute; top:0; left:-40%; width:40%; height:100%;
365
+ background:var(--ao-orange); animation:ce-scan 1.05s linear infinite;
366
+ box-shadow:0 0 12px var(--ao-orange); }
367
  @keyframes ce-scan { 0% { left:-40%; } 100% { left:100%; } }
368
+ .ce-loader-text { color:var(--ao-orange); letter-spacing:3px; text-transform:uppercase;
369
+ font-size:13px; font-weight:800; }
370
  .ce-loader-text::after { content:"…"; }
371
 
372
+ /* cycling stage list: each row dims/illuminates in turn so the user knows the
373
+ pipeline is working even though we can't stream true progress from the LLM. */
374
+ .ce-loader-stages { display:flex; flex-direction:column; gap:4px;
375
+ font-size:11px; letter-spacing:1.5px; text-transform:uppercase;
376
+ color:var(--ao-outline); }
377
+ .ce-loader-stages > div { display:flex; align-items:center; gap:8px;
378
+ opacity:0.35; transition:opacity .25s ease; }
379
+ .ce-loader-stages > div::before { content:"▸"; color:var(--ao-orange);
380
+ opacity:0.5; }
381
+ .ce-loader-stages > div.active { opacity:1; color:var(--ao-orange-soft); }
382
+ .ce-loader-stages > div.active::before { opacity:1; }
383
+ .ce-loader-stages > div.done { opacity:0.7; color:var(--ao-text); }
384
+ .ce-loader-stages > div.done::before { content:"✓"; color:var(--ao-green); opacity:1; }
385
+
386
  /* top-right action bar: small primary button, same spot every tab, reset persistent */
387
  .ce-actionbar { display:flex !important; justify-content:flex-end !important;
388
  align-items:center; gap:8px; flex-wrap:nowrap; margin:2px 0 8px; }
 
491
  });
492
  };
493
  wirePopups(); setInterval(wirePopups, 1500);
494
+
495
+ // Cycle loader stages: each [data-stages="cycle"] walks through its children
496
+ // marking one .active, prior ones .done. Tracks per-element so multiple loaders
497
+ // can run independently. Resets if the element is replaced (Gradio HTML update).
498
+ const cycleStages = () => {
499
+ document.querySelectorAll('[data-stages="cycle"]').forEach(el => {
500
+ const kids = el.children;
501
+ if (!kids.length) return;
502
+ if (el.dataset._idx === undefined) {
503
+ el.dataset._idx = '0';
504
+ el.dataset._tick = String(Date.now());
505
+ }
506
+ const now = Date.now();
507
+ const last = Number(el.dataset._tick);
508
+ // advance every 1200ms
509
+ if (now - last < 1200) return;
510
+ el.dataset._tick = String(now);
511
+ let i = Number(el.dataset._idx);
512
+ // mark prior as done, current as active
513
+ for (let k = 0; k < kids.length; k++) {
514
+ kids[k].classList.remove('active', 'done');
515
+ if (k < i) kids[k].classList.add('done');
516
+ else if (k === i) kids[k].classList.add('active');
517
+ }
518
+ i = (i + 1) % (kids.length + 1); // +1 lets all-done state breathe one tick
519
+ el.dataset._idx = String(i);
520
+ });
521
+ };
522
+ setInterval(cycleStages, 200);
523
  }
524
  """
525
 
 
572
  )
573
 
574
 
575
+ def loader(text: str = "WORKING", stages: list[str] | None = None) -> str:
576
+ """Consolidated custom loader: scanning bar + label + cycling stages list.
577
+ The stages animate via JS (CLOCK_JS wires it on load) to communicate that
578
+ work is happening through a multi-stage pipeline."""
579
+ stage_html = ""
580
+ if stages is None:
581
+ stages = [
582
+ "reading the part geometry",
583
+ "slicing cross-sections",
584
+ "querying precedent ledger",
585
+ "engineer proposing settings",
586
+ "qa inspector reviewing",
587
+ ]
588
+ if stages:
589
+ items = "".join(f"<div>{s}</div>" for s in stages)
590
+ stage_html = f"<div class='ce-loader-stages' data-stages='cycle'>{items}</div>"
591
  return (
592
  "<div class='ce-loader'>"
593
+ f"<div class='ce-loader-text'>{text}</div>"
594
  "<div class='ce-loader-bar'><span></span></div>"
595
+ f"{stage_html}"
596
+ "</div>"
597
  )
598
 
599
 
learn/finetune/gguf_pipeline_modal.py CHANGED
@@ -139,12 +139,52 @@ if modal is not None:
139
  print(f"GGUF saved: {final_path} ({size_mb:.0f}MB)")
140
  return final_path
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  @app.local_entrypoint()
143
  def main(base: str = BASE_MODEL, adapter: str = "",
144
- outtype: str = "q4_k_m", name: str = "microfactory-node"):
 
145
  if not adapter:
146
  print("ERROR: --adapter required. Example:")
147
- print(" modal run learn/finetune/gguf_pipeline_modal.py --adapter kylebrodeur/microfactory-node-lora-v2 --name microfactory-node-v2")
148
  return
149
 
150
  print(f"=== GGUF Pipeline: {adapter} ===")
@@ -158,6 +198,12 @@ if modal is not None:
158
  # Step 2: Convert to GGUF on CPU
159
  print("\n--- Step 2: Convert to GGUF (CPU) ---")
160
  gguf_path = convert_to_gguf.remote(merged_path, outtype, name)
 
 
 
 
 
 
161
  print(f"\n=== PIPELINE COMPLETE ===")
162
  print(f"GGUF file: {gguf_path}")
163
  print(f"\nDownload:")
 
139
  print(f"GGUF saved: {final_path} ({size_mb:.0f}MB)")
140
  return final_path
141
 
142
+ @app.function(image=cpu_image, timeout=3600,
143
+ volumes={"/out": vol},
144
+ secrets=[modal.Secret.from_name("chief-engineer-secrets"),
145
+ modal.Secret.from_name("HF_TOKEN")])
146
+ def upload_to_hub(gguf_path: str, repo_id: str, name: str = "microfactory-node") -> str:
147
+ """Upload the GGUF file to a HF Hub model repo. Creates the repo if missing."""
148
+ from huggingface_hub import HfApi, create_repo
149
+ token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
150
+ if not token:
151
+ raise RuntimeError("HF_TOKEN missing from chief-engineer-secrets")
152
+ token = token.strip() # secrets often contain trailing newlines
153
+ api = HfApi(token=token)
154
+ create_repo(repo_id, repo_type="model", exist_ok=True, token=token)
155
+ size_mb = os.path.getsize(gguf_path) / (1024 * 1024)
156
+ print(f"Uploading {gguf_path} ({size_mb:.0f}MB) → {repo_id}/{name}.gguf")
157
+ api.upload_file(
158
+ path_or_fileobj=gguf_path,
159
+ path_in_repo=f"{name}.gguf",
160
+ repo_id=repo_id,
161
+ repo_type="model",
162
+ commit_message=f"upload {name}.gguf ({size_mb:.0f}MB)",
163
+ )
164
+ url = f"https://huggingface.co/{repo_id}/blob/main/{name}.gguf"
165
+ print(f"✅ Uploaded: {url}")
166
+ return url
167
+
168
+ @app.local_entrypoint()
169
+ def upload_only(name: str = "", repo: str = ""):
170
+ """Upload an existing GGUF (already on the Modal volume) to HF Hub.
171
+ Useful for v1/v2 which were converted before the upload step existed.
172
+ Example: modal run gguf_pipeline_modal.py::upload_only --name microfactory-node --repo kylebrodeur/microfactory-node-gguf
173
+ """
174
+ if not name or not repo:
175
+ print("ERROR: --name and --repo required")
176
+ return
177
+ gguf_path = f"/out/gguf/{name}.gguf"
178
+ print(f"Uploading {gguf_path} → {repo}")
179
+ upload_to_hub.remote(gguf_path, repo, name)
180
+
181
  @app.local_entrypoint()
182
  def main(base: str = BASE_MODEL, adapter: str = "",
183
+ outtype: str = "q4_k_m", name: str = "microfactory-node",
184
+ upload: str = ""):
185
  if not adapter:
186
  print("ERROR: --adapter required. Example:")
187
+ print(" modal run learn/finetune/gguf_pipeline_modal.py --adapter kylebrodeur/microfactory-node-lora-v2 --name microfactory-node-v2 --upload kylebrodeur/microfactory-node-gguf")
188
  return
189
 
190
  print(f"=== GGUF Pipeline: {adapter} ===")
 
198
  # Step 2: Convert to GGUF on CPU
199
  print("\n--- Step 2: Convert to GGUF (CPU) ---")
200
  gguf_path = convert_to_gguf.remote(merged_path, outtype, name)
201
+
202
+ # Step 3 (optional): Upload to HF Hub
203
+ if upload:
204
+ print("\n--- Step 3: Upload to HF Hub ---")
205
+ upload_to_hub.remote(gguf_path, upload, name)
206
+
207
  print(f"\n=== PIPELINE COMPLETE ===")
208
  print(f"GGUF file: {gguf_path}")
209
  print(f"\nDownload:")