kylebrodeur commited on
Commit
afe3546
Β·
verified Β·
1 Parent(s): e6242f0

deploy: update Space from deploy_preflight --push

Browse files
.codeboarding/logs/wrapper-server.log CHANGED
@@ -540,3 +540,36 @@ INFO: 127.0.0.1:44498 - "GET /health HTTP/1.1" 200 OK
540
  [stderr] 2026-06-14 09:54:24 INFO [watchfiles.main:308] 1 change detected
541
  [stderr] 2026-06-14 09:54:24 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
542
  [stderr] 2026-06-14 09:54:24 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  [stderr] 2026-06-14 09:54:24 INFO [watchfiles.main:308] 1 change detected
541
  [stderr] 2026-06-14 09:54:24 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
542
  [stderr] 2026-06-14 09:54:24 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
543
+ [stderr] 2026-06-14 10:25:05 INFO [watchfiles.main:308] 1 change detected
544
+ [stderr] 2026-06-14 10:25:06 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
545
+ [stderr] 2026-06-14 10:25:06 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
546
+ [stderr] 2026-06-14 10:25:20 INFO [watchfiles.main:308] 1 change detected
547
+ [stderr] 2026-06-14 10:25:21 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
548
+ [stderr] 2026-06-14 10:25:21 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
549
+ [stderr] 2026-06-14 10:25:36 INFO [watchfiles.main:308] 1 change detected
550
+ [stderr] 2026-06-14 10:25:37 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
551
+ [stderr] 2026-06-14 10:25:37 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
552
+ [stderr] 2026-06-14 10:25:47 INFO [watchfiles.main:308] 1 change detected
553
+ [stderr] 2026-06-14 10:25:48 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
554
+ [stderr] 2026-06-14 10:25:48 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
555
+ [stderr] 2026-06-14 10:26:47 INFO [watchfiles.main:308] 1 change detected
556
+ [stderr] 2026-06-14 10:26:48 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
557
+ [stderr] 2026-06-14 10:26:48 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
558
+ [stderr] 2026-06-14 10:26:51 INFO [watchfiles.main:308] 1 change detected
559
+ [stderr] 2026-06-14 10:26:51 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
560
+ [stderr] 2026-06-14 10:26:51 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
561
+ [stderr] 2026-06-14 10:26:56 INFO [watchfiles.main:308] 1 change detected
562
+ [stderr] 2026-06-14 10:26:56 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
563
+ [stderr] 2026-06-14 10:26:56 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
564
+ [stderr] 2026-06-14 10:27:15 INFO [watchfiles.main:308] 1 change detected
565
+ [stderr] 2026-06-14 10:27:16 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
566
+ [stderr] 2026-06-14 10:27:16 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
567
+ [stderr] 2026-06-14 10:27:19 INFO [watchfiles.main:308] 1 change detected
568
+ [stderr] 2026-06-14 10:27:19 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
569
+ [stderr] 2026-06-14 10:27:19 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
570
+ [stderr] 2026-06-14 10:27:25 INFO [watchfiles.main:308] 1 change detected
571
+ [stderr] 2026-06-14 10:27:25 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
572
+ [stderr] 2026-06-14 10:27:25 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
573
+ [stderr] 2026-06-14 10:27:39 INFO [watchfiles.main:308] 1 change detected
574
+ [stderr] 2026-06-14 10:27:40 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
575
+ [stderr] 2026-06-14 10:27:40 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
app.py CHANGED
@@ -764,7 +764,8 @@ def _action_bar(reset_btn_label="RESET", primary_label=None, primary_variant="pr
764
  primary_id=None, primary_arrow=True):
765
  """Build the consistent top-right action bar (small primary + persistent Reset).
766
  Both buttons are scale=0 so they DON'T stretch full-width. Primary buttons get
767
- a right-side arrow icon by default to signal "proceed to next stage"."""
 
768
  with gr.Row(elem_classes=["ce-actionbar"]):
769
  reset = gr.Button(reset_btn_label, elem_classes=["ce-pillbtn", "ce-act"], scale=0)
770
  primary = None
@@ -772,6 +773,8 @@ def _action_bar(reset_btn_label="RESET", primary_label=None, primary_variant="pr
772
  primary_classes = ["ce-act"]
773
  if primary_arrow:
774
  primary_classes.append("ce-icon-arrow-after")
 
 
775
  primary = gr.Button(primary_label, variant=primary_variant,
776
  elem_classes=primary_classes, elem_id=primary_id, scale=0)
777
  return reset, primary
@@ -829,6 +832,7 @@ def build() -> gr.Blocks:
829
  sensors_readout = gr.HTML(elem_classes=["ce-envbar-readout"])
830
  gr.HTML("<button class='ce-pillbtn ce-icon-sliders' "
831
  "data-popup-trigger='override' type='button' "
 
832
  "style='background:var(--ao-surface);color:var(--ao-orange);"
833
  "border:none;padding:5px 15px;border-radius:999px;"
834
  "text-transform:uppercase;letter-spacing:.5px;font-size:12px;"
@@ -849,9 +853,10 @@ def build() -> gr.Blocks:
849
  with gr.Column(elem_classes=["ce-inline-group", "ce-envbar-actions"], scale=0):
850
  gr.HTML("<div class='ce-inline-label'>&#8203;</div>") # invisible label for alignment
851
  with gr.Row(elem_classes=["ce-inline-pills"]):
852
- roll_btn = gr.Button("RANDOMIZE",
853
  elem_classes=["ce-pillbtn", "ce-icon-shuffle"], scale=0)
854
- reset_s = gr.Button("RESET", elem_classes=["ce-pillbtn", "ce-act"], scale=0)
 
855
  run_btn = gr.Button("SLICE", variant="primary",
856
  elem_classes=["ce-act", "ce-icon-arrow-after"], elem_id="ce-run", scale=0)
857
 
@@ -886,7 +891,7 @@ def build() -> gr.Blocks:
886
  mesh_in = gr.File(file_types=[".stl", ".glb", ".obj"],
887
  label="UPLOAD MESH", show_label=False,
888
  elem_classes=["ce-drop"])
889
- benchy_btn = gr.Button("QUICK-LOAD BENCHY",
890
  elem_classes=["ce-pillbtn", "ce-icon-anchor",
891
  "ce-mesh-source"], scale=0)
892
  # GENERATE A PRIMITIVE: styled to match QUICK-LOAD BENCHY (ce-mesh-source)
@@ -895,7 +900,8 @@ def build() -> gr.Blocks:
895
  gen_kind = gr.Radio(["box", "cylinder", "cone", "sphere"], value="box",
896
  show_label=False, elem_classes=["ce-pills"])
897
  gen_size = gr.Number(value=30, label="SIZE (mm)", elem_classes=["ce-num"])
898
- gen_btn = gr.Button("GENERATE", elem_classes=["ce-pillbtn"], scale=0)
 
899
  description = gr.Textbox(label="NOTES (OPTIONAL)",
900
  placeholder="e.g. 45Β° bracket, 60mm tall",
901
  elem_classes=["ce-notes-inline"])
@@ -959,7 +965,8 @@ def build() -> gr.Blocks:
959
  p_iter_readout = gr.HTML(
960
  "<div class='ce-sub' style='text-align:center;'><b>8</b> ITERATIONS</div>")
961
  gr.HTML("<div style='flex:1;'></div>")
962
- vary_btn = gr.Button("VARY", elem_classes=["ce-pillbtn"], scale=0)
 
963
 
964
  # THE PLAN card
965
  with gr.Group(elem_classes=["ce-card"]) as plan_card:
 
764
  primary_id=None, primary_arrow=True):
765
  """Build the consistent top-right action bar (small primary + persistent Reset).
766
  Both buttons are scale=0 so they DON'T stretch full-width. Primary buttons get
767
+ a right-side arrow icon by default to signal "proceed to next stage".
768
+ If primary_id is omitted, a stable kebab-case id is derived from the label."""
769
  with gr.Row(elem_classes=["ce-actionbar"]):
770
  reset = gr.Button(reset_btn_label, elem_classes=["ce-pillbtn", "ce-act"], scale=0)
771
  primary = None
 
773
  primary_classes = ["ce-act"]
774
  if primary_arrow:
775
  primary_classes.append("ce-icon-arrow-after")
776
+ if primary_id is None:
777
+ primary_id = "ce-" + primary_label.lower().replace(" ", "-").replace("(", "").replace(")", "").replace("'", "")
778
  primary = gr.Button(primary_label, variant=primary_variant,
779
  elem_classes=primary_classes, elem_id=primary_id, scale=0)
780
  return reset, primary
 
832
  sensors_readout = gr.HTML(elem_classes=["ce-envbar-readout"])
833
  gr.HTML("<button class='ce-pillbtn ce-icon-sliders' "
834
  "data-popup-trigger='override' type='button' "
835
+ "id='ce-override' "
836
  "style='background:var(--ao-surface);color:var(--ao-orange);"
837
  "border:none;padding:5px 15px;border-radius:999px;"
838
  "text-transform:uppercase;letter-spacing:.5px;font-size:12px;"
 
853
  with gr.Column(elem_classes=["ce-inline-group", "ce-envbar-actions"], scale=0):
854
  gr.HTML("<div class='ce-inline-label'>&#8203;</div>") # invisible label for alignment
855
  with gr.Row(elem_classes=["ce-inline-pills"]):
856
+ roll_btn = gr.Button("RANDOMIZE", elem_id="ce-randomize",
857
  elem_classes=["ce-pillbtn", "ce-icon-shuffle"], scale=0)
858
+ reset_s = gr.Button("RESET", elem_id="ce-reset-load",
859
+ elem_classes=["ce-pillbtn", "ce-act"], scale=0)
860
  run_btn = gr.Button("SLICE", variant="primary",
861
  elem_classes=["ce-act", "ce-icon-arrow-after"], elem_id="ce-run", scale=0)
862
 
 
891
  mesh_in = gr.File(file_types=[".stl", ".glb", ".obj"],
892
  label="UPLOAD MESH", show_label=False,
893
  elem_classes=["ce-drop"])
894
+ benchy_btn = gr.Button("QUICK-LOAD BENCHY", elem_id="ce-benchy",
895
  elem_classes=["ce-pillbtn", "ce-icon-anchor",
896
  "ce-mesh-source"], scale=0)
897
  # GENERATE A PRIMITIVE: styled to match QUICK-LOAD BENCHY (ce-mesh-source)
 
900
  gen_kind = gr.Radio(["box", "cylinder", "cone", "sphere"], value="box",
901
  show_label=False, elem_classes=["ce-pills"])
902
  gen_size = gr.Number(value=30, label="SIZE (mm)", elem_classes=["ce-num"])
903
+ gen_btn = gr.Button("GENERATE", elem_id="ce-generate",
904
+ elem_classes=["ce-pillbtn"], scale=0)
905
  description = gr.Textbox(label="NOTES (OPTIONAL)",
906
  placeholder="e.g. 45Β° bracket, 60mm tall",
907
  elem_classes=["ce-notes-inline"])
 
965
  p_iter_readout = gr.HTML(
966
  "<div class='ce-sub' style='text-align:center;'><b>8</b> ITERATIONS</div>")
967
  gr.HTML("<div style='flex:1;'></div>")
968
+ vary_btn = gr.Button("VARY", elem_id="ce-vary",
969
+ elem_classes=["ce-pillbtn"], scale=0)
970
 
971
  # THE PLAN card
972
  with gr.Group(elem_classes=["ce-card"]) as plan_card:
scripts/capture.py CHANGED
@@ -1,9 +1,12 @@
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
@@ -12,13 +15,16 @@ 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
@@ -30,26 +36,37 @@ import time
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
- """Temp/humidity/bed-position live in the collapsed βš™ OVERRIDE ENVIRONMENT
40
- accordion β€” open it before setting them. Idempotent: only opens if the
41
- bed-position pill isn't already visible (so chained beats don't toggle it shut)."""
42
  try:
43
- if not page.locator(".ce-pills label", has_text="corner").first.is_visible():
44
- page.get_by_text("OVERRIDE ENVIRONMENT", exact=False).first.click()
 
45
  time.sleep(0.3)
46
  except Exception:
47
  pass
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")
@@ -57,37 +74,45 @@ def _set_sensors(page, t, h):
57
  pass
58
 
59
 
60
- def beat_studio(page, slow):
61
- """Beat 3a β€” define the job in STUDIO (empty start β†’ quick-load a part +
 
 
 
 
 
 
 
62
  material; set deterministic environment for a repeatable, risk-relevant take)."""
63
- page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
64
  _open_override(page); _set_sensors(page, 28, 60) # warm + humid β†’ precedent-rich
65
- _pill(page, "PLA"); time.sleep(slow) # material (engineer infers the part class)
66
- page.get_by_role("button", name="QUICK-LOAD BENCHY").click()
67
- time.sleep(1.5) # part preview (3D model) settles
 
68
 
69
 
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_build(page, slow):
78
- """Beat 3b β€” BUILD -> auto-switch to BUILD, let the engineer's read land."""
79
- page.get_by_role("button", name="BUILD JOB").click()
80
- time.sleep(4.5) # reasoning + settings readout appear
81
 
82
 
83
  def beat_second_opinion(page, slow):
84
- """The QA Inspector's pre-print critique (the second voice) on the BUILD page."""
85
- page.get_by_role("button", name="SECOND OPINION").first.click()
86
  time.sleep(3.0)
87
 
88
 
89
  def beat_scrub(page, slow):
90
- """Slide through the filled cross-section layers (on the Build page)."""
91
  sl = page.locator("input[type=range]").last
92
  for v in (8, 18, 30, 40):
93
  sl.fill(str(v)); sl.dispatch_event("input"); sl.dispatch_event("change")
@@ -95,56 +120,57 @@ def beat_scrub(page, slow):
95
 
96
 
97
  def beat_placement(page, slow):
98
- """Build-plate beat β€” ABS in the corner: warp predicted + 'center it' suggestion."""
99
- page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
100
  _pill(page, "ABS") # high-shrink material
101
- _open_override(page) # bed-position lives in the override panel
102
  _pill(page, "corner") # worst plate position
103
- page.get_by_role("button", name="QUICK-LOAD BENCHY").click()
 
104
  time.sleep(slow)
105
- page.get_by_role("button", name="BUILD JOB").click()
106
- time.sleep(4.5) # PLACEMENT risk + suggested alignment appear
107
 
108
 
109
- def beat_close_loop(page, slow):
110
- """Beat 4a β€” go to PRINT, simulate one print (deterministic) + inspector grade."""
111
  page.get_by_role("tab", name="PRINT").click(); time.sleep(slow)
112
- page.get_by_role("button", name="SIMULATE ONE PRINT").first.click()
113
  time.sleep(3.0)
114
 
115
 
116
- def beat_loop(page, slow):
117
  """Beat 4b β€” PRINT: run iterations, quality climbs fail->clean with inspector grades."""
118
  page.get_by_role("tab", name="PRINT").click(); time.sleep(slow)
119
- page.get_by_role("button", name="PRINT (RUN ITERATIONS)").click()
120
  time.sleep(5.0) # the curve fills in
121
 
122
 
123
- def beat_ledger(page, slow):
124
- """Beat 4c β€” Review: the ledger where lessons accumulate + the run verdict."""
125
  page.get_by_role("tab", name="REVIEW").click()
126
  time.sleep(3.0)
127
 
128
 
129
  BEATS = {
130
- "studio": [beat_studio],
131
- "benchy": [beat_studio, beat_benchy],
132
- "3": [beat_studio, beat_build],
133
- "scrub": [beat_studio, beat_build, beat_scrub],
134
- "second": [beat_studio, beat_build, beat_second_opinion], # the Inspector's pre-print read
135
- "placement": [beat_placement, beat_scrub], # ABS-in-corner β†’ warp + "center it"
136
- "4": [beat_close_loop, beat_loop, beat_ledger],
137
- "loop": [beat_loop],
138
- "ledger": [beat_ledger],
139
- "all": [beat_studio, beat_build, beat_second_opinion, beat_scrub, beat_close_loop,
140
- beat_placement, beat_loop, beat_ledger],
141
  }
142
 
143
 
144
  def main() -> None:
145
  ap = argparse.ArgumentParser(description="Walk the demo beats for screen capture.")
146
  ap.add_argument("--beat", default="all", choices=sorted(BEATS), help="which beat(s) to walk")
147
- ap.add_argument("--url", default="http://localhost:7860")
148
  ap.add_argument("--slowmo", type=int, default=350, help="ms between Playwright actions")
149
  ap.add_argument("--pause", type=float, default=1.5, help="seconds between beats")
150
  ap.add_argument("--headless", action="store_true", help="hide the browser (no recording)")
@@ -160,7 +186,7 @@ def main() -> None:
160
  slow = args.slowmo / 1000.0
161
  with sync_playwright() as p:
162
  browser = p.chromium.launch(headless=args.headless, slow_mo=args.slowmo)
163
- page = browser.new_page(viewport={"width": 1280, "height": 900})
164
  page.goto(args.url + "/?__theme=dark", wait_until="domcontentloaded")
165
  time.sleep(2.5)
166
  for step in BEATS[args.beat]:
 
1
+ """Capture beat driver β€” drive the live app through the demo beats deterministically.
 
2
 
3
+ This does NOT record video itself. Use it alongside Cap desktop, or point
4
+ scripts.record.py at an existing Chrome CDP endpoint. The script walks the UI
5
+ slowly with deliberate pauses while YOU (or cap-cli) capture the screen.
6
+
7
+ The UI tabs are LOAD Β· SLICE Β· PRINT Β· REVIEW. Selectors prefer stable IDs
8
+ over visible text wherever possible, so renames are less likely to break this
9
+ script than the previous text-only version.
10
 
11
  One-time setup (local; not a Space/runtime dep):
12
  uv pip install playwright && uv run playwright install chromium
 
15
  make run # http://localhost:7860 (pre-warm one analyze!)
16
 
17
  Then walk the beats (headed browser; record it with Cap):
18
+ uv run python -m scripts.capture --beat all
19
+ uv run python -m scripts.capture --beat 3 # load-bearing beat
20
+ uv run python -m scripts.capture --beat benchy # LOAD + Benchy quick-load
21
+ uv run python -m scripts.capture --beat loop # the learning loop
22
+
23
+ Options:
24
+ --url Space or local URL (default: https://node.microfactory.space)
25
+ --slowmo ms between Playwright actions (default 350)
26
+ --pause seconds between beats (default 1.5)
27
+ --headless hide the browser (no recording)
28
  """
29
 
30
  from __future__ import annotations
 
36
 
37
  def _pill(page, value):
38
  """Click an LCARS pill. CSS uppercases the text, but the DOM keeps the raw
39
+ value (part types are lowercase; materials are uppercase). Match the <label>
40
+ by its real text, scoped to pill groups."""
41
  page.locator(".ce-pills label", has_text=value).first.click()
42
 
43
 
44
  def _open_override(page):
45
+ """Open the OVERRIDE ENVIRONMENT popup if it is not already open.
46
+ Idempotent: checks the popup's visibility before toggling."""
 
47
  try:
48
+ popup = page.locator("#ce-popup-override")
49
+ if not popup.is_visible():
50
+ page.locator("#ce-override").first.click()
51
  time.sleep(0.3)
52
  except Exception:
53
  pass
54
 
55
 
56
+ def _close_override(page):
57
+ """Click the popup close button if it is open."""
58
+ try:
59
+ popup = page.locator("#ce-popup-override")
60
+ if popup.is_visible():
61
+ page.locator("#ce-popup-override .ce-popup-close").first.click()
62
+ time.sleep(0.2)
63
+ except Exception:
64
+ pass
65
+
66
+
67
  def _set_sensors(page, t, h):
68
+ """Set ambient Β°C + humidity %RH inside the override popup."""
69
+ nums = page.locator("#ce-popup-override .ce-num input")
70
  try:
71
  nums.nth(0).fill(str(t)); nums.nth(0).dispatch_event("change")
72
  nums.nth(1).fill(str(h)); nums.nth(1).dispatch_event("change")
 
74
  pass
75
 
76
 
77
+ def _load_benchy(page, slow):
78
+ """Shared helper: LOAD tab β†’ quick-load Benchy."""
79
+ page.get_by_role("tab", name="LOAD").click(); time.sleep(slow)
80
+ page.locator("#ce-benchy").first.click()
81
+ time.sleep(1.5)
82
+
83
+
84
+ def beat_load(page, slow):
85
+ """Beat 3a β€” define the job in LOAD (empty start β†’ quick-load a part +
86
  material; set deterministic environment for a repeatable, risk-relevant take)."""
87
+ page.get_by_role("tab", name="LOAD").click(); time.sleep(slow)
88
  _open_override(page); _set_sensors(page, 28, 60) # warm + humid β†’ precedent-rich
89
+ _close_override(page)
90
+ _pill(page, "PLA"); time.sleep(slow)
91
+ page.locator("#ce-benchy").first.click()
92
+ time.sleep(2.0) # part preview (3D model) settles
93
 
94
 
95
  def beat_benchy(page, slow):
96
+ """LOAD + the 3DBenchy quick-load (the recognizable hero part)."""
97
+ page.get_by_role("tab", name="LOAD").click(); time.sleep(slow)
98
+ page.locator("#ce-benchy").first.click()
99
+ time.sleep(3.0) # watch the hull preview settle
100
 
101
 
102
+ def beat_slice(page, slow):
103
+ """Beat 3b β€” SLICE: auto-switch to SLICE, let the engineer's read land."""
104
+ page.locator("#ce-run").first.click()
105
+ time.sleep(5.0) # reasoning + settings readout appear
106
 
107
 
108
  def beat_second_opinion(page, slow):
109
+ """The QA Inspector's pre-print critique on the SLICE page."""
110
+ page.locator("input[type=radio][value='Second Opinion']").first.check()
111
  time.sleep(3.0)
112
 
113
 
114
  def beat_scrub(page, slow):
115
+ """Slide through the filled cross-section layers (on the SLICE page)."""
116
  sl = page.locator("input[type=range]").last
117
  for v in (8, 18, 30, 40):
118
  sl.fill(str(v)); sl.dispatch_event("input"); sl.dispatch_event("change")
 
120
 
121
 
122
  def beat_placement(page, slow):
123
+ """Plate-position beat β€” ABS in the corner: warp predicted + 'center it' suggestion."""
124
+ page.get_by_role("tab", name="LOAD").click(); time.sleep(slow)
125
  _pill(page, "ABS") # high-shrink material
126
+ _open_override(page)
127
  _pill(page, "corner") # worst plate position
128
+ _close_override(page)
129
+ page.locator("#ce-benchy").first.click()
130
  time.sleep(slow)
131
+ page.locator("#ce-run").first.click()
132
+ time.sleep(5.0) # PLACEMENT risk + suggested alignment appear
133
 
134
 
135
+ def beat_print_single(page, slow):
136
+ """Beat 4a β€” go to PRINT, run one print (deterministic) + inspector grade."""
137
  page.get_by_role("tab", name="PRINT").click(); time.sleep(slow)
138
+ page.locator("#ce-print-run, #ce-print").first.click()
139
  time.sleep(3.0)
140
 
141
 
142
+ def beat_print_loop(page, slow):
143
  """Beat 4b β€” PRINT: run iterations, quality climbs fail->clean with inspector grades."""
144
  page.get_by_role("tab", name="PRINT").click(); time.sleep(slow)
145
+ page.locator("#ce-print-run, #ce-print").first.click()
146
  time.sleep(5.0) # the curve fills in
147
 
148
 
149
+ def beat_review(page, slow):
150
+ """Beat 4c β€” REVIEW: the ledger where lessons accumulate + the run verdict."""
151
  page.get_by_role("tab", name="REVIEW").click()
152
  time.sleep(3.0)
153
 
154
 
155
  BEATS = {
156
+ "load": [beat_load],
157
+ "benchy": [beat_benchy],
158
+ "3": [beat_load, beat_slice],
159
+ "scrub": [beat_load, beat_slice, beat_scrub],
160
+ "second": [beat_load, beat_slice, beat_second_opinion],
161
+ "placement": [beat_placement, beat_scrub],
162
+ "4": [beat_print_single, beat_print_loop, beat_review],
163
+ "loop": [beat_print_loop, beat_review],
164
+ "review": [beat_review],
165
+ "all": [beat_load, beat_slice, beat_second_opinion, beat_scrub,
166
+ beat_placement, beat_print_single, beat_print_loop, beat_review],
167
  }
168
 
169
 
170
  def main() -> None:
171
  ap = argparse.ArgumentParser(description="Walk the demo beats for screen capture.")
172
  ap.add_argument("--beat", default="all", choices=sorted(BEATS), help="which beat(s) to walk")
173
+ ap.add_argument("--url", default="https://node.microfactory.space")
174
  ap.add_argument("--slowmo", type=int, default=350, help="ms between Playwright actions")
175
  ap.add_argument("--pause", type=float, default=1.5, help="seconds between beats")
176
  ap.add_argument("--headless", action="store_true", help="hide the browser (no recording)")
 
186
  slow = args.slowmo / 1000.0
187
  with sync_playwright() as p:
188
  browser = p.chromium.launch(headless=args.headless, slow_mo=args.slowmo)
189
+ page = browser.new_page(viewport={"width": 1600, "height": 1000})
190
  page.goto(args.url + "/?__theme=dark", wait_until="domcontentloaded")
191
  time.sleep(2.5)
192
  for step in BEATS[args.beat]:
scripts/record.py CHANGED
@@ -4,6 +4,7 @@ One command from chief-engineer/:
4
  uv run python -m scripts.record # preflight β†’ pre-warm β†’ record β†’ export
5
  uv run python -m scripts.record --preflight-only
6
  uv run python -m scripts.record --beat 3 # single beat for re-takes
 
7
 
8
  Does the whole thing: checks cap-cli + Space + playwright, pre-warms the GPU,
9
  starts a detached Cap recording, drives the demo beats through a headed browser,
@@ -12,6 +13,10 @@ stops the recording, and exports to MP4.
12
  Prerequisites (one-time):
13
  source ~/projects/cap-cli-skill/setup.sh # defines cap() in your shell
14
  uv pip install playwright && uv run playwright install chromium
 
 
 
 
15
  """
16
 
17
  from __future__ import annotations
@@ -290,12 +295,12 @@ def prewarm(page, slow: float, url: str = SPACE_URL) -> None:
290
  _hide_hf_chrome(page)
291
  time.sleep(1.0)
292
 
293
- # Quick-load Benchy + PLA + BUILD
294
- page.get_by_role("tab", name="STUDIO").click()
295
  time.sleep(slow)
296
- page.get_by_role("button", name="QUICK-LOAD BENCHY").click()
297
  time.sleep(1.5)
298
- page.get_by_role("button", name="BUILD JOB").click()
299
  print(" waiting for cold-start inference (~30s)...")
300
  time.sleep(12.0) # generous β€” the first ZeroGPU call is slow
301
  print(" pre-warm complete β€” GPU is hot.\n")
@@ -310,16 +315,30 @@ def _pill(page, value):
310
 
311
 
312
  def _open_override(page):
 
313
  try:
314
- if not page.locator(".ce-pills label", has_text="corner").first.is_visible():
315
- page.get_by_text("OVERRIDE ENVIRONMENT", exact=False).first.click()
 
316
  time.sleep(0.3)
317
  except Exception:
318
  pass
319
 
320
 
 
 
 
 
 
 
 
 
 
 
 
321
  def _set_sensors(page, t, h):
322
- nums = page.locator(".ce-num input")
 
323
  try:
324
  nums.nth(0).fill(str(t)); nums.nth(0).dispatch_event("change")
325
  nums.nth(1).fill(str(h)); nums.nth(1).dispatch_event("change")
@@ -327,24 +346,24 @@ def _set_sensors(page, t, h):
327
  pass
328
 
329
 
330
- def beat_studio(page, slow):
331
- """Beat 3a β€” STUDIO: quick-load Benchy, set material + environment."""
332
- page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
333
- _open_override(page); _set_sensors(page, 28, 60)
334
  _pill(page, "PLA"); time.sleep(slow)
335
- page.get_by_role("button", name="QUICK-LOAD BENCHY").click()
336
  time.sleep(2.5) # let the 3D viewer fully load
337
 
338
 
339
- def beat_build(page, slow):
340
- """Beat 3b β€” BUILD: reasoning + settings readout."""
341
- page.get_by_role("button", name="BUILD JOB").click()
342
  time.sleep(7.0) # ZeroGPU reasoning + settings + Spine terminal
343
 
344
 
345
  def beat_second_opinion(page, slow):
346
  """Beat 3.5 β€” QA Inspector's pre-print critique."""
347
- page.get_by_role("button", name="SECOND OPINION").first.click()
348
  time.sleep(5.0) # let the Inspector card fully render
349
 
350
 
@@ -358,31 +377,32 @@ def beat_scrub(page, slow):
358
 
359
  def beat_placement(page, slow):
360
  """Beat 4b β€” ABS in corner: warp predicted + 'center it' suggestion."""
361
- page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
362
  _pill(page, "ABS")
363
  _open_override(page)
364
  _pill(page, "corner")
365
- page.get_by_role("button", name="QUICK-LOAD BENCHY").click()
 
366
  time.sleep(slow)
367
- page.get_by_role("button", name="BUILD JOB").click()
368
  time.sleep(7.0) # PLACEMENT risk + suggested alignment
369
 
370
 
371
  def beat_climbing_job(page, slow):
372
  """Beat 4a β€” PETG @ 30Β°C/65%: different conditions, different call."""
373
- page.get_by_role("tab", name="STUDIO").click(); time.sleep(slow)
374
- _open_override(page); _set_sensors(page, 30, 65)
375
  _pill(page, "PETG")
376
- page.get_by_role("button", name="QUICK-LOAD BENCHY").click()
377
  time.sleep(2.5)
378
- page.get_by_role("button", name="BUILD JOB").click()
379
  time.sleep(7.0) # reasoning with shifted conditions
380
 
381
 
382
  def beat_print_loop(page, slow):
383
  """Beat 4c β€” PRINT: run iterations, quality climbs with inspector grades."""
384
  page.get_by_role("tab", name="PRINT").click(); time.sleep(slow)
385
- page.get_by_role("button", name="PRINT (RUN ITERATIONS)").click()
386
  time.sleep(8.0) # let the curve fill in + inspector grades appear
387
 
388
 
@@ -394,14 +414,14 @@ def beat_review(page, slow):
394
 
395
  # Beat sets
396
  BEATS = {
397
- "3": [beat_studio, beat_build],
398
- "scrub": [beat_studio, beat_build, beat_scrub],
399
- "second": [beat_studio, beat_build, beat_second_opinion],
400
  "placement": [beat_placement],
401
  "climb": [beat_climbing_job, beat_print_loop, beat_review],
402
  "loop": [beat_print_loop, beat_review],
403
  "all": [
404
- beat_studio, beat_build, beat_second_opinion, beat_scrub,
405
  beat_placement, beat_climbing_job, beat_print_loop, beat_review,
406
  ],
407
  }
@@ -413,27 +433,27 @@ BEATS = {
413
 
414
  MANUAL_BEATS: dict[str, list[dict]] = {
415
  "3": [
416
- {"cue": "STUDIO tab β†’ QUICK-LOAD BENCHY β†’ material PLA β†’ BUILD JOB",
417
  "say": "It recalls the closest prior jobs and says what transfers β€” before anything prints.",
418
  "wait": 8},
419
  ],
420
  "second": [
421
- {"cue": "STUDIO β†’ QUICK-LOAD BENCHY β†’ PLA β†’ BUILD JOB β†’ πŸ” GET A SECOND OPINION",
422
  "say": "A separate, skeptical reviewer grades the plan β€” and can hold the print.",
423
  "wait": 10},
424
  ],
425
  "scrub": [
426
- {"cue": "STUDIO β†’ QUICK-LOAD BENCHY β†’ PLA β†’ BUILD JOB β†’ slide the LAYER scrubber",
427
  "say": "Real cross-sections of this part, layer by layer.",
428
  "wait": 8},
429
  ],
430
  "placement": [
431
- {"cue": "STUDIO β†’ ABS β†’ βš™ override β†’ corner β†’ QUICK-LOAD BENCHY β†’ BUILD JOB",
432
  "say": "It even knows where on the bed matters β€” corner ABS will warp; center it.",
433
  "wait": 10},
434
  ],
435
  "climb": [
436
- {"cue": "STUDIO β†’ PETG β†’ βš™ override 30Β°C / 65% β†’ QUICK-LOAD BENCHY β†’ BUILD JOB",
437
  "say": "Different conditions, different call β€” it's reasoning, not a lookup.",
438
  "wait": 8},
439
  {"cue": "PRINT tab β†’ PRINT (RUN ITERATIONS) β†’ watch quality climb",
@@ -444,22 +464,22 @@ MANUAL_BEATS: dict[str, list[dict]] = {
444
  "wait": 5},
445
  ],
446
  "all": [
447
- {"cue": "STUDIO β†’ QUICK-LOAD BENCHY β†’ PLA β†’ BUILD JOB",
448
  "say": "I tell it the part, the material, and the room β€” it figures out what kind of part on its own. It recalls the closest prior jobs and says what transfers β€” before anything prints.",
449
  "wait": 10},
450
  {"cue": "Point at PROPOSED SETTINGS + πŸ›‘ Spine line",
451
  "say": "The model proposes; deterministic code vetoes anything unsafe.",
452
  "wait": 4},
453
- {"cue": "πŸ” GET A SECOND OPINION β†’ read the Inspector card",
454
  "say": "A separate, skeptical reviewer grades the plan. The engineer never marks its own homework.",
455
  "wait": 6},
456
  {"cue": "Slide the LAYER scrubber through cross-sections",
457
  "say": "Real cross-sections of this part, layer by layer.",
458
  "wait": 6},
459
- {"cue": "STUDIO β†’ ABS β†’ βš™ override β†’ corner β†’ QUICK-LOAD BENCHY β†’ BUILD JOB",
460
  "say": "It even knows where on the bed matters β€” corner ABS will warp; center it.",
461
  "wait": 10},
462
- {"cue": "STUDIO β†’ PETG β†’ βš™ override 30Β°C / 65% β†’ QUICK-LOAD BENCHY β†’ BUILD JOB",
463
  "say": "Different conditions, different call β€” it's reasoning, not a lookup.",
464
  "wait": 8},
465
  {"cue": "PRINT tab β†’ PRINT (RUN ITERATIONS) β†’ watch the quality curve climb",
@@ -549,9 +569,12 @@ def record_manual(beat_name: str, url: str = SPACE_URL, no_cap: bool = False) ->
549
  return None
550
 
551
 
552
- def record_auto(beat_name: str, slowmo: int, pause: float, url: str = SPACE_URL) -> Path | None:
 
553
  """Auto mode: launch Chrome on Windows with CDP, connect Playwright from WSL,
554
- prewarm the GPU, THEN start recording, drive beats, stop, export."""
 
 
555
  print(f"\n=== RECORD (auto): beat '{beat_name}' ===\n")
556
 
557
  # 1) Launch Chrome + connect Playwright
@@ -561,27 +584,32 @@ def record_auto(beat_name: str, slowmo: int, pause: float, url: str = SPACE_URL)
561
  print(" βœ— playwright not installed")
562
  return None
563
 
564
- print(" launching Chrome (Windows) with remote debugging...")
565
- _launch_chrome()
566
- host_ip = _windows_host_ip()
567
- print(f" connecting to Chrome CDP at {host_ip}:{CDP_PORT}...")
568
-
569
  slow = slowmo / 1000.0
570
  with sync_playwright() as p:
571
- browser = p.chromium.connect_over_cdp(f"http://{host_ip}:{CDP_PORT}")
 
 
 
 
 
 
 
 
572
  if browser.contexts:
573
  page = browser.contexts[0].pages[0] if browser.contexts[0].pages else browser.contexts[0].new_page()
574
  else:
575
  page = browser.new_page()
576
  page.set_viewport_size({"width": 1707, "height": 1067}) # native screen res
577
 
578
- # Go fullscreen (F11) β€” hides taskbar + title bar for clean recording
579
- page.keyboard.press("F11")
580
- time.sleep(0.5)
 
581
 
582
  # Dismiss Chrome restore bubble + any initial popups
583
  time.sleep(1.0)
584
- _dismiss_popups(page)
 
585
 
586
  # 2) Pre-warm the GPU (NOT recording yet)
587
  prewarm(page, slow, url)
@@ -657,6 +685,8 @@ def main() -> None:
657
  help="ms between Playwright actions (auto mode only)")
658
  ap.add_argument("--pause", type=float, default=2.0,
659
  help="seconds between beats (auto mode only)")
 
 
660
  ap.add_argument("--url", default=None,
661
  help="Space URL (default: build-small-hackathon Space; use http://localhost:7860 for local)")
662
  args = ap.parse_args()
@@ -671,7 +701,7 @@ def main() -> None:
671
  return
672
 
673
  if args.mode == "auto":
674
- result = record_auto(args.beat, args.slowmo, args.pause, url)
675
  elif args.mode == "cues":
676
  result = record_manual(args.beat, url, no_cap=True)
677
  else:
 
4
  uv run python -m scripts.record # preflight β†’ pre-warm β†’ record β†’ export
5
  uv run python -m scripts.record --preflight-only
6
  uv run python -m scripts.record --beat 3 # single beat for re-takes
7
+ uv run python -m scripts.record --mode auto --cdp-url http://host:9222
8
 
9
  Does the whole thing: checks cap-cli + Space + playwright, pre-warms the GPU,
10
  starts a detached Cap recording, drives the demo beats through a headed browser,
 
13
  Prerequisites (one-time):
14
  source ~/projects/cap-cli-skill/setup.sh # defines cap() in your shell
15
  uv pip install playwright && uv run playwright install chromium
16
+
17
+ For a chromeless Gradio PWA window, install the Space in Chrome, then launch:
18
+ chrome --app=https://node.microfactory.space/?__theme=dark --remote-debugging-port=9222
19
+ and pass --cdp-url to attach.
20
  """
21
 
22
  from __future__ import annotations
 
295
  _hide_hf_chrome(page)
296
  time.sleep(1.0)
297
 
298
+ # Quick-load Benchy + PLA + SLICE
299
+ page.get_by_role("tab", name="LOAD").click()
300
  time.sleep(slow)
301
+ page.locator("#ce-benchy").first.click()
302
  time.sleep(1.5)
303
+ page.locator("#ce-run").first.click()
304
  print(" waiting for cold-start inference (~30s)...")
305
  time.sleep(12.0) # generous β€” the first ZeroGPU call is slow
306
  print(" pre-warm complete β€” GPU is hot.\n")
 
315
 
316
 
317
  def _open_override(page):
318
+ """Open the OVERRIDE ENVIRONMENT popup if it is not already open."""
319
  try:
320
+ popup = page.locator("#ce-popup-override")
321
+ if not popup.is_visible():
322
+ page.locator("#ce-override").first.click()
323
  time.sleep(0.3)
324
  except Exception:
325
  pass
326
 
327
 
328
+ def _close_override(page):
329
+ """Click the popup close button if it is open."""
330
+ try:
331
+ popup = page.locator("#ce-popup-override")
332
+ if popup.is_visible():
333
+ page.locator("#ce-popup-override .ce-popup-close").first.click()
334
+ time.sleep(0.2)
335
+ except Exception:
336
+ pass
337
+
338
+
339
  def _set_sensors(page, t, h):
340
+ """Set ambient Β°C + humidity %RH inside the override popup."""
341
+ nums = page.locator("#ce-popup-override .ce-num input")
342
  try:
343
  nums.nth(0).fill(str(t)); nums.nth(0).dispatch_event("change")
344
  nums.nth(1).fill(str(h)); nums.nth(1).dispatch_event("change")
 
346
  pass
347
 
348
 
349
+ def beat_load(page, slow):
350
+ """Beat 3a β€” LOAD: quick-load Benchy, set material + environment."""
351
+ page.get_by_role("tab", name="LOAD").click(); time.sleep(slow)
352
+ _open_override(page); _set_sensors(page, 28, 60); _close_override(page)
353
  _pill(page, "PLA"); time.sleep(slow)
354
+ page.locator("#ce-benchy").first.click()
355
  time.sleep(2.5) # let the 3D viewer fully load
356
 
357
 
358
+ def beat_slice(page, slow):
359
+ """Beat 3b β€” SLICE: reasoning + settings readout."""
360
+ page.locator("#ce-run").first.click()
361
  time.sleep(7.0) # ZeroGPU reasoning + settings + Spine terminal
362
 
363
 
364
  def beat_second_opinion(page, slow):
365
  """Beat 3.5 β€” QA Inspector's pre-print critique."""
366
+ page.locator("input[type=radio][value='Second Opinion']").first.check()
367
  time.sleep(5.0) # let the Inspector card fully render
368
 
369
 
 
377
 
378
  def beat_placement(page, slow):
379
  """Beat 4b β€” ABS in corner: warp predicted + 'center it' suggestion."""
380
+ page.get_by_role("tab", name="LOAD").click(); time.sleep(slow)
381
  _pill(page, "ABS")
382
  _open_override(page)
383
  _pill(page, "corner")
384
+ _close_override(page)
385
+ page.locator("#ce-benchy").first.click()
386
  time.sleep(slow)
387
+ page.locator("#ce-run").first.click()
388
  time.sleep(7.0) # PLACEMENT risk + suggested alignment
389
 
390
 
391
  def beat_climbing_job(page, slow):
392
  """Beat 4a β€” PETG @ 30Β°C/65%: different conditions, different call."""
393
+ page.get_by_role("tab", name="LOAD").click(); time.sleep(slow)
394
+ _open_override(page); _set_sensors(page, 30, 65); _close_override(page)
395
  _pill(page, "PETG")
396
+ page.locator("#ce-benchy").first.click()
397
  time.sleep(2.5)
398
+ page.locator("#ce-run").first.click()
399
  time.sleep(7.0) # reasoning with shifted conditions
400
 
401
 
402
  def beat_print_loop(page, slow):
403
  """Beat 4c β€” PRINT: run iterations, quality climbs with inspector grades."""
404
  page.get_by_role("tab", name="PRINT").click(); time.sleep(slow)
405
+ page.locator("#ce-print-run, #ce-print").first.click()
406
  time.sleep(8.0) # let the curve fill in + inspector grades appear
407
 
408
 
 
414
 
415
  # Beat sets
416
  BEATS = {
417
+ "3": [beat_load, beat_slice],
418
+ "scrub": [beat_load, beat_slice, beat_scrub],
419
+ "second": [beat_load, beat_slice, beat_second_opinion],
420
  "placement": [beat_placement],
421
  "climb": [beat_climbing_job, beat_print_loop, beat_review],
422
  "loop": [beat_print_loop, beat_review],
423
  "all": [
424
+ beat_load, beat_slice, beat_second_opinion, beat_scrub,
425
  beat_placement, beat_climbing_job, beat_print_loop, beat_review,
426
  ],
427
  }
 
433
 
434
  MANUAL_BEATS: dict[str, list[dict]] = {
435
  "3": [
436
+ {"cue": "LOAD tab β†’ QUICK-LOAD BENCHY β†’ material PLA β†’ SLICE",
437
  "say": "It recalls the closest prior jobs and says what transfers β€” before anything prints.",
438
  "wait": 8},
439
  ],
440
  "second": [
441
+ {"cue": "LOAD β†’ QUICK-LOAD BENCHY β†’ PLA β†’ SLICE β†’ Toggle to SECOND OPINION",
442
  "say": "A separate, skeptical reviewer grades the plan β€” and can hold the print.",
443
  "wait": 10},
444
  ],
445
  "scrub": [
446
+ {"cue": "LOAD β†’ QUICK-LOAD BENCHY β†’ PLA β†’ SLICE β†’ slide the LAYER scrubber",
447
  "say": "Real cross-sections of this part, layer by layer.",
448
  "wait": 8},
449
  ],
450
  "placement": [
451
+ {"cue": "LOAD β†’ ABS β†’ OVERRIDE β†’ corner β†’ QUICK-LOAD BENCHY β†’ SLICE",
452
  "say": "It even knows where on the bed matters β€” corner ABS will warp; center it.",
453
  "wait": 10},
454
  ],
455
  "climb": [
456
+ {"cue": "LOAD β†’ PETG β†’ OVERRIDE 30Β°C / 65% β†’ QUICK-LOAD BENCHY β†’ SLICE",
457
  "say": "Different conditions, different call β€” it's reasoning, not a lookup.",
458
  "wait": 8},
459
  {"cue": "PRINT tab β†’ PRINT (RUN ITERATIONS) β†’ watch quality climb",
 
464
  "wait": 5},
465
  ],
466
  "all": [
467
+ {"cue": "LOAD β†’ QUICK-LOAD BENCHY β†’ PLA β†’ SLICE",
468
  "say": "I tell it the part, the material, and the room β€” it figures out what kind of part on its own. It recalls the closest prior jobs and says what transfers β€” before anything prints.",
469
  "wait": 10},
470
  {"cue": "Point at PROPOSED SETTINGS + πŸ›‘ Spine line",
471
  "say": "The model proposes; deterministic code vetoes anything unsafe.",
472
  "wait": 4},
473
+ {"cue": "Toggle to SECOND OPINION β†’ read the Inspector card",
474
  "say": "A separate, skeptical reviewer grades the plan. The engineer never marks its own homework.",
475
  "wait": 6},
476
  {"cue": "Slide the LAYER scrubber through cross-sections",
477
  "say": "Real cross-sections of this part, layer by layer.",
478
  "wait": 6},
479
+ {"cue": "LOAD β†’ ABS β†’ OVERRIDE β†’ corner β†’ QUICK-LOAD BENCHY β†’ SLICE",
480
  "say": "It even knows where on the bed matters β€” corner ABS will warp; center it.",
481
  "wait": 10},
482
+ {"cue": "LOAD β†’ PETG β†’ OVERRIDE 30Β°C / 65% β†’ QUICK-LOAD BENCHY β†’ SLICE",
483
  "say": "Different conditions, different call β€” it's reasoning, not a lookup.",
484
  "wait": 8},
485
  {"cue": "PRINT tab β†’ PRINT (RUN ITERATIONS) β†’ watch the quality curve climb",
 
569
  return None
570
 
571
 
572
+ def record_auto(beat_name: str, slowmo: int, pause: float, url: str = SPACE_URL,
573
+ cdp_url: str | None = None) -> Path | None:
574
  """Auto mode: launch Chrome on Windows with CDP, connect Playwright from WSL,
575
+ prewarm the GPU, THEN start recording, drive beats, stop, export.
576
+ If cdp_url is provided, connect to an existing Chrome instance instead of
577
+ launching a fresh one (useful for the installed Gradio PWA window)."""
578
  print(f"\n=== RECORD (auto): beat '{beat_name}' ===\n")
579
 
580
  # 1) Launch Chrome + connect Playwright
 
584
  print(" βœ— playwright not installed")
585
  return None
586
 
 
 
 
 
 
587
  slow = slowmo / 1000.0
588
  with sync_playwright() as p:
589
+ if cdp_url:
590
+ print(f" connecting to existing Chrome CDP at {cdp_url}...")
591
+ browser = p.chromium.connect_over_cdp(cdp_url)
592
+ else:
593
+ print(" launching Chrome (Windows) with remote debugging...")
594
+ _launch_chrome()
595
+ host_ip = _windows_host_ip()
596
+ print(f" connecting to Chrome CDP at {host_ip}:{CDP_PORT}...")
597
+ browser = p.chromium.connect_over_cdp(f"http://{host_ip}:{CDP_PORT}")
598
  if browser.contexts:
599
  page = browser.contexts[0].pages[0] if browser.contexts[0].pages else browser.contexts[0].new_page()
600
  else:
601
  page = browser.new_page()
602
  page.set_viewport_size({"width": 1707, "height": 1067}) # native screen res
603
 
604
+ if not cdp_url:
605
+ # Go fullscreen (F11) β€” hides taskbar + title bar for clean recording
606
+ page.keyboard.press("F11")
607
+ time.sleep(0.5)
608
 
609
  # Dismiss Chrome restore bubble + any initial popups
610
  time.sleep(1.0)
611
+ if not cdp_url:
612
+ _dismiss_popups(page)
613
 
614
  # 2) Pre-warm the GPU (NOT recording yet)
615
  prewarm(page, slow, url)
 
685
  help="ms between Playwright actions (auto mode only)")
686
  ap.add_argument("--pause", type=float, default=2.0,
687
  help="seconds between beats (auto mode only)")
688
+ ap.add_argument("--cdp-url", default=None,
689
+ help="attach to an existing Chrome CDP endpoint instead of launching Chrome (e.g. http://host:9222)")
690
  ap.add_argument("--url", default=None,
691
  help="Space URL (default: build-small-hackathon Space; use http://localhost:7860 for local)")
692
  args = ap.parse_args()
 
701
  return
702
 
703
  if args.mode == "auto":
704
+ result = record_auto(args.beat, args.slowmo, args.pause, url, cdp_url=args.cdp_url)
705
  elif args.mode == "cues":
706
  result = record_manual(args.beat, url, no_cap=True)
707
  else: