Spaces:
Runtime error
Runtime error
deploy: update Space from deploy_preflight --push
Browse files- .codeboarding/.codeboardingignore +130 -0
- .codeboarding/logs/wrapper-server.log +25 -0
- .gitattributes +2 -0
- 3ca9c96d1d92.cap/content/cursors/cursor_0.png +0 -0
- 3ca9c96d1d92.cap/content/segments/segment-0/cursor.json +4 -0
- 3ca9c96d1d92.cap/content/segments/segment-0/display.mp4 +3 -0
- 3ca9c96d1d92.cap/content/segments/segment-0/keyboard.bin +3 -0
- 3ca9c96d1d92.cap/project-config.json +113 -0
- 3ca9c96d1d92.cap/recording-meta.json +29 -0
- 3ca9c96d1d92.cap/screenshots/display.jpg +3 -0
- Makefile +9 -3
- README.md +1 -1
- app.py +3 -2
- core/field_log.py +44 -46
- scripts/deploy_preflight.py +71 -2
- scripts/record.py +38 -17
.codeboarding/.codeboardingignore
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CodeBoarding Ignore File
|
| 2 |
+
# Add patterns here for files and directories that should be excluded from CodeBoarding analysis.
|
| 3 |
+
# Use the same format as .gitignore (gitignore syntax / gitwildmatch patterns).
|
| 4 |
+
#
|
| 5 |
+
# To stop ignoring a pattern, prefix it with ! (e.g., !important_file.txt)
|
| 6 |
+
#
|
| 7 |
+
# NOTE: The following are ALWAYS excluded (not configurable):
|
| 8 |
+
# - Hidden directories (starting with .)
|
| 9 |
+
# - .git/, .codeboarding/, node_modules/, __pycache__/
|
| 10 |
+
# - Build output: build/, dist/, coverage/
|
| 11 |
+
#
|
| 12 |
+
# This file is automatically loaded by CodeBoarding analysis tools to exclude
|
| 13 |
+
# specified paths from code analysis, architecture generation, and other processing.
|
| 14 |
+
|
| 15 |
+
# ============================================================================
|
| 16 |
+
# Ignored directories (customizable β remove lines to include them)
|
| 17 |
+
# ============================================================================
|
| 18 |
+
|
| 19 |
+
# Python virtual environments
|
| 20 |
+
venv/
|
| 21 |
+
env/
|
| 22 |
+
*.egg-info/
|
| 23 |
+
|
| 24 |
+
# Java (Maven/Gradle) and Rust (Cargo) build output. Both ecosystems
|
| 25 |
+
# produce a top-level ``target/`` directory full of compiled artifacts β
|
| 26 |
+
# kept here as well as in ``_ALWAYS_IGNORED_DIRS`` so users who customize
|
| 27 |
+
# their ``.codeboardingignore`` continue to skip it even after edits.
|
| 28 |
+
target/
|
| 29 |
+
bin/
|
| 30 |
+
out/
|
| 31 |
+
|
| 32 |
+
# .NET / C# build output
|
| 33 |
+
obj/
|
| 34 |
+
|
| 35 |
+
# Go
|
| 36 |
+
vendor/
|
| 37 |
+
testdata/
|
| 38 |
+
|
| 39 |
+
# PHP
|
| 40 |
+
cache/
|
| 41 |
+
|
| 42 |
+
# Custom
|
| 43 |
+
temp/
|
| 44 |
+
repos/
|
| 45 |
+
runs/
|
| 46 |
+
|
| 47 |
+
# ============================================================================
|
| 48 |
+
# Test and infrastructure files
|
| 49 |
+
# ============================================================================
|
| 50 |
+
|
| 51 |
+
# Test directories
|
| 52 |
+
**/__tests__/**
|
| 53 |
+
**/tests/**
|
| 54 |
+
**/test/**
|
| 55 |
+
**/__test__/**
|
| 56 |
+
**/testing/**
|
| 57 |
+
**/testutil/**
|
| 58 |
+
|
| 59 |
+
# Java/Kotlin test directories (Maven/Gradle structure)
|
| 60 |
+
**/src/test/**
|
| 61 |
+
**/src/testFixtures/**
|
| 62 |
+
**/src/integration-test/**
|
| 63 |
+
**/src/jmh/**
|
| 64 |
+
**/src/contractTest/**
|
| 65 |
+
**/osgi-tests/**
|
| 66 |
+
|
| 67 |
+
# Test files by naming convention
|
| 68 |
+
*.test.*
|
| 69 |
+
*.spec.*
|
| 70 |
+
*_test.*
|
| 71 |
+
*test_*.py
|
| 72 |
+
test_*.py
|
| 73 |
+
*Test.java
|
| 74 |
+
*IT.java
|
| 75 |
+
*Test.kt
|
| 76 |
+
*IT.kt
|
| 77 |
+
*Tests.java
|
| 78 |
+
|
| 79 |
+
# Mock, fixture, and stub directories
|
| 80 |
+
**/__mocks__/**
|
| 81 |
+
**/mocks/**
|
| 82 |
+
**/fixtures/**
|
| 83 |
+
**/fixture/**
|
| 84 |
+
**/stubs/**
|
| 85 |
+
**/stub/**
|
| 86 |
+
**/fakes/**
|
| 87 |
+
**/fake/**
|
| 88 |
+
|
| 89 |
+
# E2E and integration test directories
|
| 90 |
+
**/e2e/**
|
| 91 |
+
**/integration-tests/**
|
| 92 |
+
**/integration_test*/**
|
| 93 |
+
|
| 94 |
+
# ============================================================================
|
| 95 |
+
# Non-production code
|
| 96 |
+
# ============================================================================
|
| 97 |
+
|
| 98 |
+
# Example and documentation code
|
| 99 |
+
**/examples/**
|
| 100 |
+
**/documentation/examples/**
|
| 101 |
+
|
| 102 |
+
# Generated code
|
| 103 |
+
*.pb.go
|
| 104 |
+
**/generated_parser*
|
| 105 |
+
|
| 106 |
+
# Java/Kotlin metadata files
|
| 107 |
+
module-info.java
|
| 108 |
+
|
| 109 |
+
# ============================================================================
|
| 110 |
+
# Build artifacts and minified files
|
| 111 |
+
# ============================================================================
|
| 112 |
+
|
| 113 |
+
*.bundle.js
|
| 114 |
+
*.bundle.js.map
|
| 115 |
+
*.min.js
|
| 116 |
+
*.min.css
|
| 117 |
+
*.chunk.js
|
| 118 |
+
*.chunk.js.map
|
| 119 |
+
|
| 120 |
+
# ============================================================================
|
| 121 |
+
# Build tool configs and infrastructure
|
| 122 |
+
# ============================================================================
|
| 123 |
+
|
| 124 |
+
esbuild*
|
| 125 |
+
webpack*
|
| 126 |
+
rollup*
|
| 127 |
+
vite.config.*
|
| 128 |
+
gulpfile*
|
| 129 |
+
gruntfile*
|
| 130 |
+
*.config.*
|
.codeboarding/logs/wrapper-server.log
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[stderr] INFO: Started server process [95234]
|
| 2 |
+
[stderr] INFO: Waiting for application startup.
|
| 3 |
+
[stderr] INFO: Application startup complete.
|
| 4 |
+
[stderr] INFO: Uvicorn running on http://127.0.0.1:8765 (Press CTRL+C to quit)
|
| 5 |
+
INFO: 127.0.0.1:50676 - "GET /health HTTP/1.1" 200 OK
|
| 6 |
+
[stderr] INFO: ('127.0.0.1', 50686) - "WebSocket /ws" [accepted]
|
| 7 |
+
[stderr] 2026-06-13 13:34:27 INFO [codeboarding_pro.ws.server:226] WebSocket connected: session ce9a8523-69cd-43d2-81c9-3cce7f7414f9
|
| 8 |
+
[stderr] INFO: connection open
|
| 9 |
+
[stderr] 2026-06-13 13:34:27 INFO [tool_registry.installers:451] Installing Node.js packages: ['pyright@1.1.400', 'typescript-language-server@4.3.4', 'typescript@5.7', 'intelephense@1.16.5']
|
| 10 |
+
[stderr] 2026-06-13 13:34:29 INFO [tool_registry.installers:465] Node.js packages installed successfully
|
| 11 |
+
[stderr] 2026-06-13 13:34:29 INFO [tool_registry.installers:225] tokei: already installed, skipping
|
| 12 |
+
[stderr] 2026-06-13 13:34:29 INFO [tool_registry.installers:225] gopls: already installed, skipping
|
| 13 |
+
[stderr] 2026-06-13 13:34:29 INFO [tool_registry.installers:225] rust-analyzer: already installed, skipping
|
| 14 |
+
[stderr] 2026-06-13 13:34:29 INFO [tool_registry.installers:489] java already installed
|
| 15 |
+
[stderr] 2026-06-13 13:34:29 WARNING [tool_registry.installers:367] csharp-ls: dotnet not found on PATH; skipping install. Users must install it before running analysis.
|
| 16 |
+
[stderr] 2026-06-13 13:34:29 INFO [static_analyzer.java_utils:125] Found Java 21 at /usr/lib/jvm/java-21-openjdk-amd64
|
| 17 |
+
[stderr] 2026-06-13 13:34:29 INFO [codeboarding_pro.lsp.bootstrap:56] LSP startup attempt 1/3
|
| 18 |
+
[stderr] 2026-06-13 13:34:29 INFO [codeboarding_pro.session:185] Session ce9a8523-69cd-43d2-81c9-3cce7f7414f9 initialized: repo=/home/kylebrodeur/projects/microfactory-lab/chief-engineer, project=chief-engineer, output=/home/kylebrodeur/projects/microfactory-lab/chief-engineer/.codeboarding
|
| 19 |
+
[stderr] 2026-06-13 13:34:29 INFO [static_analyzer:227] Starting engine LSP client for Python at /home/kylebrodeur/projects/microfactory-lab/chief-engineer
|
| 20 |
+
[stderr] 2026-06-13 13:34:30 INFO [static_analyzer:252] Python LSP start: 0.3s
|
| 21 |
+
[stderr] 2026-06-13 13:34:30 INFO [codeboarding_pro.file_monitor:106] FileMonitor started for /home/kylebrodeur/projects/microfactory-lab/chief-engineer
|
| 22 |
+
[stderr] 2026-06-13 13:34:30 INFO [codeboarding_pro.session:203] Session ce9a8523-69cd-43d2-81c9-3cce7f7414f9: FileMonitor activated
|
| 23 |
+
[stderr] 2026-06-13 13:34:30 INFO [codeboarding_pro.head_watcher:48] HeadWatcher started on /home/kylebrodeur/projects/microfactory-lab/.git/HEAD
|
| 24 |
+
[stderr] 2026-06-13 13:34:30 INFO [codeboarding_pro.session:248] Session ce9a8523-69cd-43d2-81c9-3cce7f7414f9: LSP wiring complete
|
| 25 |
+
[stderr] 2026-06-13 13:34:30 INFO [codeboarding_pro.lsp.bootstrap:67] LSP clients started successfully (attempt 1)
|
.gitattributes
CHANGED
|
@@ -35,3 +35,5 @@ saved_model/**/* 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
|
| 37 |
a5c557efbc56.cap/content/segments/segment-0/display.mp4 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
|
| 37 |
a5c557efbc56.cap/content/segments/segment-0/display.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
3ca9c96d1d92.cap/content/segments/segment-0/display.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
3ca9c96d1d92.cap/screenshots/display.jpg filter=lfs diff=lfs merge=lfs -text
|
3ca9c96d1d92.cap/content/cursors/cursor_0.png
ADDED
|
3ca9c96d1d92.cap/content/segments/segment-0/cursor.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"clicks": [],
|
| 3 |
+
"moves": []
|
| 4 |
+
}
|
3ca9c96d1d92.cap/content/segments/segment-0/display.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:663a6075dcceca04cc0843d031353ed1110a8008de58fb6a16fb81e15cae7774
|
| 3 |
+
size 5982983
|
3ca9c96d1d92.cap/content/segments/segment-0/keyboard.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e5cc23716d9b28b4661a952cb0f768a4babdcd0a0b5eac73110d2fac232bef41
|
| 3 |
+
size 7
|
3ca9c96d1d92.cap/project-config.json
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"aspectRatio": null,
|
| 3 |
+
"background": {
|
| 4 |
+
"source": {
|
| 5 |
+
"type": "color",
|
| 6 |
+
"value": [
|
| 7 |
+
255,
|
| 8 |
+
255,
|
| 9 |
+
255
|
| 10 |
+
],
|
| 11 |
+
"alpha": 255
|
| 12 |
+
},
|
| 13 |
+
"blur": 0.0,
|
| 14 |
+
"padding": 0.0,
|
| 15 |
+
"rounding": 0.0,
|
| 16 |
+
"roundingType": "squircle",
|
| 17 |
+
"inset": 0,
|
| 18 |
+
"crop": null,
|
| 19 |
+
"shadow": 73.6,
|
| 20 |
+
"advancedShadow": {
|
| 21 |
+
"size": 14.4,
|
| 22 |
+
"opacity": 68.1,
|
| 23 |
+
"blur": 3.8
|
| 24 |
+
},
|
| 25 |
+
"border": null
|
| 26 |
+
},
|
| 27 |
+
"camera": {
|
| 28 |
+
"hide": false,
|
| 29 |
+
"mirror": false,
|
| 30 |
+
"position": {
|
| 31 |
+
"x": "right",
|
| 32 |
+
"y": "bottom"
|
| 33 |
+
},
|
| 34 |
+
"size": 30.0,
|
| 35 |
+
"zoomSize": 60.0,
|
| 36 |
+
"rounding": 100.0,
|
| 37 |
+
"shadow": 62.5,
|
| 38 |
+
"advancedShadow": {
|
| 39 |
+
"size": 33.9,
|
| 40 |
+
"opacity": 44.2,
|
| 41 |
+
"blur": 10.5
|
| 42 |
+
},
|
| 43 |
+
"shape": "square",
|
| 44 |
+
"roundingType": "squircle",
|
| 45 |
+
"scaleDuringZoom": 0.7,
|
| 46 |
+
"backgroundBlur": {
|
| 47 |
+
"mode": "off"
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"audio": {
|
| 51 |
+
"mute": false,
|
| 52 |
+
"improve": false,
|
| 53 |
+
"micVolumeDb": 0.0,
|
| 54 |
+
"micStereoMode": "stereo",
|
| 55 |
+
"systemVolumeDb": 0.0
|
| 56 |
+
},
|
| 57 |
+
"cursor": {
|
| 58 |
+
"hide": false,
|
| 59 |
+
"hideWhenIdle": false,
|
| 60 |
+
"hideWhenIdleDelay": 2.0,
|
| 61 |
+
"size": 100,
|
| 62 |
+
"type": "auto",
|
| 63 |
+
"animationStyle": "mellow",
|
| 64 |
+
"tension": 470.0,
|
| 65 |
+
"mass": 3.0,
|
| 66 |
+
"friction": 70.0,
|
| 67 |
+
"raw": false,
|
| 68 |
+
"motionBlur": 0.5,
|
| 69 |
+
"useSvg": true,
|
| 70 |
+
"rotationAmount": 0.15,
|
| 71 |
+
"baseRotation": 0.0,
|
| 72 |
+
"clickSpring": null,
|
| 73 |
+
"stopMovementInLastSeconds": null
|
| 74 |
+
},
|
| 75 |
+
"hotkeys": {
|
| 76 |
+
"show": false
|
| 77 |
+
},
|
| 78 |
+
"timeline": {
|
| 79 |
+
"segments": [
|
| 80 |
+
{
|
| 81 |
+
"recordingSegment": 0,
|
| 82 |
+
"timescale": 1.0,
|
| 83 |
+
"start": 0.0,
|
| 84 |
+
"end": 69.656689
|
| 85 |
+
}
|
| 86 |
+
],
|
| 87 |
+
"zoomSegments": [],
|
| 88 |
+
"sceneSegments": [],
|
| 89 |
+
"maskSegments": [],
|
| 90 |
+
"textSegments": [],
|
| 91 |
+
"captionSegments": [],
|
| 92 |
+
"keyboardSegments": []
|
| 93 |
+
},
|
| 94 |
+
"captions": null,
|
| 95 |
+
"keyboard": null,
|
| 96 |
+
"clips": [
|
| 97 |
+
{
|
| 98 |
+
"index": 0,
|
| 99 |
+
"offsets": {
|
| 100 |
+
"camera": 0.0,
|
| 101 |
+
"mic": 0.0,
|
| 102 |
+
"system_audio": 0.0
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
],
|
| 106 |
+
"annotations": [],
|
| 107 |
+
"screenMotionBlur": 0.5,
|
| 108 |
+
"screenMovementSpring": {
|
| 109 |
+
"stiffness": 200.0,
|
| 110 |
+
"damping": 40.0,
|
| 111 |
+
"mass": 2.25
|
| 112 |
+
}
|
| 113 |
+
}
|
3ca9c96d1d92.cap/recording-meta.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"platform": "Windows",
|
| 3 |
+
"pretty_name": "Cap 2026-06-13 at 13.15.50",
|
| 4 |
+
"sharing": null,
|
| 5 |
+
"segments": [
|
| 6 |
+
{
|
| 7 |
+
"display": {
|
| 8 |
+
"path": "content/segments/segment-0/display.mp4",
|
| 9 |
+
"fps": 33,
|
| 10 |
+
"start_time": 0.2475809
|
| 11 |
+
},
|
| 12 |
+
"cursor": "content/segments/segment-0/cursor.json",
|
| 13 |
+
"keyboard": "content/segments/segment-0/keyboard.bin"
|
| 14 |
+
}
|
| 15 |
+
],
|
| 16 |
+
"cursors": {
|
| 17 |
+
"0": {
|
| 18 |
+
"imagePath": "content/cursors/cursor_0.png",
|
| 19 |
+
"hotspot": {
|
| 20 |
+
"x": 0.34375,
|
| 21 |
+
"y": 0.3125
|
| 22 |
+
},
|
| 23 |
+
"shape": "Windows|Hand"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
"status": {
|
| 27 |
+
"status": "Complete"
|
| 28 |
+
}
|
| 29 |
+
}
|
3ca9c96d1d92.cap/screenshots/display.jpg
ADDED
|
Git LFS Details
|
Makefile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
.PHONY: setup setup-zerogpu assets run test demo bench trace preflight record record-check
|
| 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
|
|
@@ -23,9 +23,12 @@ test: ## headless core tests (no Ollama required)
|
|
| 23 |
preflight: ## GO/NO-GO gate on the real stack (see RUNBOOK)
|
| 24 |
uv run python -m scripts.preflight
|
| 25 |
|
| 26 |
-
deploy-check: ## deploy/record readiness gate (offline: build + files + creds + Space)
|
| 27 |
uv run python -m scripts.deploy_preflight
|
| 28 |
|
|
|
|
|
|
|
|
|
|
| 29 |
demo: ## scripted integration run / video-beat dry run
|
| 30 |
uv run python -m scripts.scripted_demo
|
| 31 |
|
|
@@ -41,5 +44,8 @@ record-check: ## recording preflight (cap-cli + Space + playwright gates)
|
|
| 41 |
record: ## full recording (manual mode): preflight β cap β beat cues β export mp4
|
| 42 |
uv run python -m scripts.record
|
| 43 |
|
| 44 |
-
record-
|
|
|
|
|
|
|
|
|
|
| 45 |
uv run python -m scripts.record --mode auto
|
|
|
|
| 1 |
+
.PHONY: setup setup-zerogpu assets run test demo bench trace preflight deploy-check deploy record record-check record-auto
|
| 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
|
|
|
|
| 23 |
preflight: ## GO/NO-GO gate on the real stack (see RUNBOOK)
|
| 24 |
uv run python -m scripts.preflight
|
| 25 |
|
| 26 |
+
deploy-check: ## deploy/record readiness gate (offline: build + files + creds + Space + dataset)
|
| 27 |
uv run python -m scripts.deploy_preflight
|
| 28 |
|
| 29 |
+
deploy: ## run the gates, then UPDATE the Space files (hf upload) + factory reboot (needs HF_TOKEN)
|
| 30 |
+
uv run python -m scripts.deploy_preflight --push
|
| 31 |
+
|
| 32 |
demo: ## scripted integration run / video-beat dry run
|
| 33 |
uv run python -m scripts.scripted_demo
|
| 34 |
|
|
|
|
| 44 |
record: ## full recording (manual mode): preflight β cap β beat cues β export mp4
|
| 45 |
uv run python -m scripts.record
|
| 46 |
|
| 47 |
+
record-cues: ## beat cues only (no cap) β you record with Cap desktop at high quality
|
| 48 |
+
uv run python -m scripts.record --mode cues
|
| 49 |
+
|
| 50 |
+
record-auto: ## recording with Playwright auto-driver (WSL only)
|
| 51 |
uv run python -m scripts.record --mode auto
|
README.md
CHANGED
|
@@ -139,7 +139,7 @@ Astrometrics UI lands (see `../DESIGN.md`). **Not** Well-Tuned β fine-tuning i
|
|
| 139 |
- **Ledger dataset:** [kylebrodeur/chief-engineer-ledger](https://huggingface.co/datasets/kylebrodeur/chief-engineer-ledger)
|
| 140 |
- **Demo video:** [<!-- TODO: add video URL after recording -->]()
|
| 141 |
- **Social post:** [<!-- TODO: add social post URL after publishing -->]()
|
| 142 |
-
- **
|
| 143 |
- **Source:** [kylebrodeur/microfactory-lab](https://github.com/kylebrodeur/microfactory-lab)
|
| 144 |
|
| 145 |
## License
|
|
|
|
| 139 |
- **Ledger dataset:** [kylebrodeur/chief-engineer-ledger](https://huggingface.co/datasets/kylebrodeur/chief-engineer-ledger)
|
| 140 |
- **Demo video:** [<!-- TODO: add video URL after recording -->]()
|
| 141 |
- **Social post:** [<!-- TODO: add social post URL after publishing -->]()
|
| 142 |
+
- **How to use it (guided tour):** [`docs/RUNBOOK.md` Β§2](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/RUNBOOK.md#2--use-the-tool-the-guided-tour--also-the-judges-tour)
|
| 143 |
- **Source:** [kylebrodeur/microfactory-lab](https://github.com/kylebrodeur/microfactory-lab)
|
| 144 |
|
| 145 |
## License
|
app.py
CHANGED
|
@@ -293,7 +293,8 @@ def second_opinion(state):
|
|
| 293 |
advice = Advice(**state["advice"])
|
| 294 |
verdict = inspector.second_opinion(job, env, settings, advice)
|
| 295 |
field_log.log_event("second_opinion", {"material": job.material, "geometry": job.geometry_type,
|
| 296 |
-
"
|
|
|
|
| 297 |
panel = inspector_panel(verdict, label="LA FORGE Β· SECOND OPINION (PRE-PRINT)")
|
| 298 |
if verdict.stance.lower() == "dispute":
|
| 299 |
panel += ("<div style='margin-top:6px;padding:6px 10px;border-left:3px solid var(--ao-red,#d9534f);"
|
|
@@ -345,7 +346,7 @@ def run_print(state, iterations):
|
|
| 345 |
"env_temp": env.temp, "env_humidity": env.humidity,
|
| 346 |
"iterations": len(sess.records), "q_start": round(traj[0], 3),
|
| 347 |
"q_end": round(traj[-1], 3), "first_clean": first,
|
| 348 |
-
"
|
| 349 |
headline = (
|
| 350 |
f"**{state.get('label') or geometry_type} Β· {material} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β "
|
| 351 |
f"started at quality **{traj[0]:.2f}** ({sess.records[0].result.outcome}); "
|
|
|
|
| 293 |
advice = Advice(**state["advice"])
|
| 294 |
verdict = inspector.second_opinion(job, env, settings, advice)
|
| 295 |
field_log.log_event("second_opinion", {"material": job.material, "geometry": job.geometry_type,
|
| 296 |
+
"inspector_stance": verdict.stance,
|
| 297 |
+
"inspector_headline": verdict.headline})
|
| 298 |
panel = inspector_panel(verdict, label="LA FORGE Β· SECOND OPINION (PRE-PRINT)")
|
| 299 |
if verdict.stance.lower() == "dispute":
|
| 300 |
panel += ("<div style='margin-top:6px;padding:6px 10px;border-left:3px solid var(--ao-red,#d9534f);"
|
|
|
|
| 346 |
"env_temp": env.temp, "env_humidity": env.humidity,
|
| 347 |
"iterations": len(sess.records), "q_start": round(traj[0], 3),
|
| 348 |
"q_end": round(traj[-1], 3), "first_clean": first,
|
| 349 |
+
"inspector_stance": run_summary.stance})
|
| 350 |
headline = (
|
| 351 |
f"**{state.get('label') or geometry_type} Β· {material} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β "
|
| 352 |
f"started at quality **{traj[0]:.2f}** ({sess.records[0].result.outcome}); "
|
core/field_log.py
CHANGED
|
@@ -62,56 +62,28 @@ def is_active() -> bool:
|
|
| 62 |
return _get_scheduler() is not None
|
| 63 |
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
"backend": backend,
|
| 81 |
-
"used_fallback": used_fallback,
|
| 82 |
-
"settings": {
|
| 83 |
-
k: v for k, v in settings.items()
|
| 84 |
-
if k in ("nozzle_temp", "bed_temp", "retraction_mm", "fan_pct",
|
| 85 |
-
"first_layer_fan_pct", "print_speed")
|
| 86 |
-
},
|
| 87 |
-
"risks": advice.get("risks", []),
|
| 88 |
-
"inspector_verdict": None, # filled later if visitor runs Second Opinion
|
| 89 |
-
"simulated_outcome": None, # filled later if visitor clicks Simulate
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
with _lock:
|
| 93 |
-
with FIELD_LOG_FILE.open("a", encoding="utf-8") as f:
|
| 94 |
-
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 95 |
-
|
| 96 |
-
# Tell the scheduler we have new data (it normally scans on a timer,
|
| 97 |
-
# but this nudges it to notice the append).
|
| 98 |
-
try:
|
| 99 |
-
sched.trigger()
|
| 100 |
-
except Exception:
|
| 101 |
-
pass # trigger is best-effort; the timer will catch it
|
| 102 |
-
|
| 103 |
-
return True
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def log_event(kind: str, payload: dict) -> bool:
|
| 107 |
-
"""Append one interaction row of any KIND β build | second_opinion | simulate |
|
| 108 |
-
record | print_run β so EVERY run (not just BUILD) lands in the shared dataset.
|
| 109 |
-
Same gate (HF_TOKEN) + privacy rules: config/outcomes only, never PII or files."""
|
| 110 |
try:
|
| 111 |
sched = _get_scheduler()
|
| 112 |
if sched is None:
|
| 113 |
return False
|
| 114 |
-
row = {
|
|
|
|
|
|
|
| 115 |
with _lock:
|
| 116 |
with FIELD_LOG_FILE.open("a", encoding="utf-8") as f:
|
| 117 |
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
@@ -124,6 +96,32 @@ def log_event(kind: str, payload: dict) -> bool:
|
|
| 124 |
return False # logging is best-effort β never break a run
|
| 125 |
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
def privacy_notice() -> str:
|
| 128 |
"""One-line UI disclosure, shown only when logging is active."""
|
| 129 |
return (
|
|
|
|
| 62 |
return _get_scheduler() is not None
|
| 63 |
|
| 64 |
|
| 65 |
+
# Canonical FLAT schema β every row carries exactly these keys (None when N/A), all
|
| 66 |
+
# scalars/strings (no nested dicts/lists). This is what makes the HF dataset viewer
|
| 67 |
+
# render cleanly: a rectangular, well-typed table instead of ragged/nested JSON.
|
| 68 |
+
_CANON = (
|
| 69 |
+
"ts", "kind", "material", "geometry", "env_temp", "env_humidity",
|
| 70 |
+
"bed_position", "printer", "backend", "used_fallback",
|
| 71 |
+
"nozzle_temp", "bed_temp", "fan_pct", "retraction_mm", "first_layer_fan_pct",
|
| 72 |
+
"risks", "risk_count", "inspector_stance", "inspector_headline", "agreement",
|
| 73 |
+
"outcome", "quality", "iterations", "q_start", "q_end", "first_clean",
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _write_row(fields: dict) -> bool:
|
| 78 |
+
"""Normalize to the canonical flat schema (drop unknown keys, fill missing with
|
| 79 |
+
None) and append one JSONL line. Gated + exception-safe β never breaks a run."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
try:
|
| 81 |
sched = _get_scheduler()
|
| 82 |
if sched is None:
|
| 83 |
return False
|
| 84 |
+
row = {k: None for k in _CANON}
|
| 85 |
+
row.update({k: v for k, v in fields.items() if k in _CANON})
|
| 86 |
+
row["ts"] = datetime.now(timezone.utc).isoformat()
|
| 87 |
with _lock:
|
| 88 |
with FIELD_LOG_FILE.open("a", encoding="utf-8") as f:
|
| 89 |
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
|
|
| 96 |
return False # logging is best-effort β never break a run
|
| 97 |
|
| 98 |
|
| 99 |
+
def log_event(kind: str, payload: dict) -> bool:
|
| 100 |
+
"""Append one interaction row of any KIND β build | second_opinion | simulate |
|
| 101 |
+
record | print_run β normalized to the canonical flat schema. Same gate (HF_TOKEN)
|
| 102 |
+
+ privacy rules: config/outcomes only, never PII or files."""
|
| 103 |
+
return _write_row({**payload, "kind": kind})
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def log_build(job: dict, env: dict, settings: dict, advice: dict,
|
| 107 |
+
backend: str, used_fallback: bool) -> bool:
|
| 108 |
+
"""Append one BUILD row (flattened settings + risks-as-string for the viewer)."""
|
| 109 |
+
risks = advice.get("risks", []) or []
|
| 110 |
+
return _write_row({
|
| 111 |
+
"kind": "build",
|
| 112 |
+
"material": job.get("material"), "geometry": job.get("geometry_type"),
|
| 113 |
+
"env_temp": env.get("temp"), "env_humidity": env.get("humidity"),
|
| 114 |
+
"bed_position": job.get("bed_position"),
|
| 115 |
+
"printer": job.get("printer", "Creality Ender 3 V2"),
|
| 116 |
+
"backend": backend, "used_fallback": used_fallback,
|
| 117 |
+
"nozzle_temp": settings.get("nozzle_temp"), "bed_temp": settings.get("bed_temp"),
|
| 118 |
+
"fan_pct": settings.get("fan_pct"), "retraction_mm": settings.get("retraction_mm"),
|
| 119 |
+
"first_layer_fan_pct": settings.get("first_layer_fan_pct"),
|
| 120 |
+
"risks": ", ".join(str(r.get("risk")) for r in risks if isinstance(r, dict)) or None,
|
| 121 |
+
"risk_count": len(risks),
|
| 122 |
+
})
|
| 123 |
+
|
| 124 |
+
|
| 125 |
def privacy_notice() -> str:
|
| 126 |
"""One-line UI disclosure, shown only when logging is active."""
|
| 127 |
return (
|
scripts/deploy_preflight.py
CHANGED
|
@@ -16,6 +16,7 @@ GO β safe to `hf upload` + reboot + record. Run: make deploy-check
|
|
| 16 |
from __future__ import annotations
|
| 17 |
|
| 18 |
import json
|
|
|
|
| 19 |
import os
|
| 20 |
import re
|
| 21 |
import shutil
|
|
@@ -25,6 +26,14 @@ from pathlib import Path
|
|
| 25 |
|
| 26 |
ROOT = Path(__file__).resolve().parent.parent
|
| 27 |
SPACE = "build-small-hackathon/microfactory-lab"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
_fail: list[str] = []
|
| 29 |
_warn: list[str] = []
|
| 30 |
|
|
@@ -208,7 +217,59 @@ def d9_space(authed: bool) -> None:
|
|
| 208 |
warn("D9 space", f"could not query Space: {e!r}")
|
| 209 |
|
| 210 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
def main() -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
print("Deploy / record preflight β " + SPACE)
|
| 213 |
print("=" * 70)
|
| 214 |
d1_build()
|
|
@@ -221,16 +282,24 @@ def main() -> None:
|
|
| 221 |
d7_data()
|
| 222 |
authed = d8_credentials()
|
| 223 |
d9_space(authed)
|
|
|
|
| 224 |
print("=" * 70)
|
| 225 |
if _fail:
|
| 226 |
print(f"π΄ NO-GO: fix {len(_fail)} blocker(s) β {', '.join(_fail)}")
|
| 227 |
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
if _warn:
|
| 229 |
print(f"π‘ GO with warnings ({', '.join(_warn)}) β read them; credentials/Space "
|
| 230 |
-
"warnings just mean 'deploy from an authenticated machine'."
|
|
|
|
| 231 |
sys.exit(0)
|
| 232 |
print("π’ GO β local build clean, files + frontmatter ready, authenticated. "
|
| 233 |
-
"
|
| 234 |
|
| 235 |
|
| 236 |
if __name__ == "__main__":
|
|
|
|
| 16 |
from __future__ import annotations
|
| 17 |
|
| 18 |
import json
|
| 19 |
+
import argparse
|
| 20 |
import os
|
| 21 |
import re
|
| 22 |
import shutil
|
|
|
|
| 26 |
|
| 27 |
ROOT = Path(__file__).resolve().parent.parent
|
| 28 |
SPACE = "build-small-hackathon/microfactory-lab"
|
| 29 |
+
FIELD_LOG_DATASET = "build-small-hackathon/chief-engineer-field-log"
|
| 30 |
+
# Uploaded to the Space = everything EXCEPT these (keeps learn/ + assets/ + data/*.jsonl,
|
| 31 |
+
# which the app imports/needs; drops docs, spikes, secrets, caches, runtime/transient files).
|
| 32 |
+
SPACE_IGNORE = [
|
| 33 |
+
"docs/**", "spike/**", "field_logs/**", ".venv/**", "**/__pycache__/**",
|
| 34 |
+
"**/*.pyc", ".git/**", ".env", "data/policy.json", "data/_generated.glb",
|
| 35 |
+
"data/_vprint.gif", "uv.lock", ".pytest_cache/**",
|
| 36 |
+
]
|
| 37 |
_fail: list[str] = []
|
| 38 |
_warn: list[str] = []
|
| 39 |
|
|
|
|
| 217 |
warn("D9 space", f"could not query Space: {e!r}")
|
| 218 |
|
| 219 |
|
| 220 |
+
# ββ D10 Β· the field-log dataset is set + reachable (Sharing-is-Caring / all-runs) ββ
|
| 221 |
+
def d10_dataset(authed: bool) -> None:
|
| 222 |
+
if not authed:
|
| 223 |
+
warn("D10 dataset", "skipped β no credentials to verify the field-log dataset")
|
| 224 |
+
return
|
| 225 |
+
try:
|
| 226 |
+
from huggingface_hub import HfApi
|
| 227 |
+
api = HfApi()
|
| 228 |
+
if api.repo_exists(FIELD_LOG_DATASET, repo_type="dataset"):
|
| 229 |
+
files = api.list_repo_files(FIELD_LOG_DATASET, repo_type="dataset")
|
| 230 |
+
logged = any(f.endswith("interactions.jsonl") for f in files)
|
| 231 |
+
ok("D10 dataset", f"{FIELD_LOG_DATASET} exists"
|
| 232 |
+
+ (" Β· interactions.jsonl present (runs are logging)" if logged
|
| 233 |
+
else " Β· no interactions.jsonl yet (do one BUILD on the Space to confirm)"))
|
| 234 |
+
else:
|
| 235 |
+
warn("D10 dataset", f"{FIELD_LOG_DATASET} not found β create it, or let CommitScheduler "
|
| 236 |
+
"make it on first run (needs HF_TOKEN as a Space secret).")
|
| 237 |
+
except Exception as e: # noqa: BLE001
|
| 238 |
+
warn("D10 dataset", f"could not verify dataset: {e!r}")
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# ββ push: actually update the Space files (gated on green + auth) βββββββββββββ
|
| 242 |
+
def push_space(factory_reboot: bool = True) -> None:
|
| 243 |
+
"""Upload the app to the Space (everything except SPACE_IGNORE) and reboot.
|
| 244 |
+
Only runs after the gates pass + credentials are present."""
|
| 245 |
+
try:
|
| 246 |
+
from huggingface_hub import HfApi
|
| 247 |
+
except Exception as e: # noqa: BLE001
|
| 248 |
+
fail("PUSH", f"huggingface_hub unavailable: {e!r}")
|
| 249 |
+
return
|
| 250 |
+
api = HfApi()
|
| 251 |
+
print(f"\nβ« uploading {ROOT.name}/ β {SPACE} (excluding docs, spike, caches, secrets)β¦")
|
| 252 |
+
try:
|
| 253 |
+
api.upload_folder(repo_id=SPACE, repo_type="space", folder_path=str(ROOT),
|
| 254 |
+
ignore_patterns=SPACE_IGNORE,
|
| 255 |
+
commit_message="deploy: update Space from deploy_preflight --push")
|
| 256 |
+
ok("PUSH", "files uploaded")
|
| 257 |
+
if factory_reboot:
|
| 258 |
+
api.restart_space(SPACE, factory_reboot=True)
|
| 259 |
+
ok("PUSH", "factory reboot requested β Space rebuilding (~1-2 min)")
|
| 260 |
+
print(" Next: wait for build, then smoke-test (BUILD shows reasoning not Error; "
|
| 261 |
+
"O'Brien/La Forge; reset button; wide UI).")
|
| 262 |
+
except Exception as e: # noqa: BLE001
|
| 263 |
+
fail("PUSH", f"upload/restart failed: {e!r}")
|
| 264 |
+
|
| 265 |
+
|
| 266 |
def main() -> None:
|
| 267 |
+
ap = argparse.ArgumentParser(description="Deploy/record readiness gate (+ optional Space push).")
|
| 268 |
+
ap.add_argument("--push", action="store_true",
|
| 269 |
+
help="after the gates pass, UPDATE the Space files (hf upload) + factory reboot")
|
| 270 |
+
ap.add_argument("--no-reboot", action="store_true", help="with --push, skip the factory reboot")
|
| 271 |
+
args = ap.parse_args()
|
| 272 |
+
|
| 273 |
print("Deploy / record preflight β " + SPACE)
|
| 274 |
print("=" * 70)
|
| 275 |
d1_build()
|
|
|
|
| 282 |
d7_data()
|
| 283 |
authed = d8_credentials()
|
| 284 |
d9_space(authed)
|
| 285 |
+
d10_dataset(authed)
|
| 286 |
print("=" * 70)
|
| 287 |
if _fail:
|
| 288 |
print(f"π΄ NO-GO: fix {len(_fail)} blocker(s) β {', '.join(_fail)}")
|
| 289 |
sys.exit(1)
|
| 290 |
+
if args.push:
|
| 291 |
+
if not authed:
|
| 292 |
+
print("π΄ --push needs HF credentials (HF_TOKEN or `hf auth login`). Nothing pushed.")
|
| 293 |
+
sys.exit(1)
|
| 294 |
+
push_space(factory_reboot=not args.no_reboot)
|
| 295 |
+
sys.exit(1 if _fail else 0)
|
| 296 |
if _warn:
|
| 297 |
print(f"π‘ GO with warnings ({', '.join(_warn)}) β read them; credentials/Space "
|
| 298 |
+
"warnings just mean 'deploy from an authenticated machine'. "
|
| 299 |
+
"Run with --push (authenticated) to update the Space.")
|
| 300 |
sys.exit(0)
|
| 301 |
print("π’ GO β local build clean, files + frontmatter ready, authenticated. "
|
| 302 |
+
"Re-run with --push to update the Space + reboot, then smoke-test β record.")
|
| 303 |
|
| 304 |
|
| 305 |
if __name__ == "__main__":
|
scripts/record.py
CHANGED
|
@@ -39,6 +39,8 @@ EXPORT_DIR_WIN = "D:\\workspace\\recordings" # must exist from Windows side
|
|
| 39 |
EXPORT_DIR_WSL = "/mnt/d/workspace/recordings" # WSL equivalent
|
| 40 |
SLOWMO_DEFAULT = 400 # ms between Playwright actions
|
| 41 |
CAP_FPS = "60" # recording quality (1-120)
|
|
|
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
# ---------------------------------------------------------------------------
|
|
@@ -472,21 +474,29 @@ MANUAL_BEATS: dict[str, list[dict]] = {
|
|
| 472 |
# record
|
| 473 |
# ---------------------------------------------------------------------------
|
| 474 |
|
| 475 |
-
def record_manual(beat_name: str, url: str = SPACE_URL) -> Path | None:
|
| 476 |
-
"""Manual mode:
|
| 477 |
-
|
|
|
|
| 478 |
print(f"\n=== RECORD (manual): beat '{beat_name}' ===\n")
|
| 479 |
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
print("
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
|
| 491 |
# 2) Print beat cues β user drives the browser
|
| 492 |
beats = MANUAL_BEATS.get(beat_name, MANUAL_BEATS["all"])
|
|
@@ -512,6 +522,11 @@ def record_manual(beat_name: str, url: str = SPACE_URL) -> Path | None:
|
|
| 512 |
time.sleep(1)
|
| 513 |
print(f" β cut{' ' * 20}")
|
| 514 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
# 3) Stop recording
|
| 516 |
print("\n stopping recording...")
|
| 517 |
stopped = _cap_json("record", "stop", "--id", rec_id, "--json")
|
|
@@ -524,7 +539,7 @@ def record_manual(beat_name: str, url: str = SPACE_URL) -> Path | None:
|
|
| 524 |
ts = time.strftime("%Y%m%d-%H%M%S")
|
| 525 |
out_win = f"{EXPORT_DIR_WIN}\\demo-{beat_name}-{ts}.mp4"
|
| 526 |
print(f" exporting to {out_win} ...")
|
| 527 |
-
export_result = _cap("export", cap_path, "--output", out_win, "--json")
|
| 528 |
if "Completed" in export_result.stdout or "Progress" in export_result.stdout:
|
| 529 |
print(f" β exported β {out_win}")
|
| 530 |
return Path(EXPORT_DIR_WSL) / f"demo-{beat_name}-{ts}.mp4"
|
|
@@ -559,6 +574,10 @@ def record_auto(beat_name: str, slowmo: int, pause: float, url: str = SPACE_URL)
|
|
| 559 |
page = browser.new_page()
|
| 560 |
page.set_viewport_size({"width": 1707, "height": 1067}) # native screen res
|
| 561 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
# Dismiss Chrome restore bubble + any initial popups
|
| 563 |
time.sleep(1.0)
|
| 564 |
_dismiss_popups(page)
|
|
@@ -610,7 +629,7 @@ def record_auto(beat_name: str, slowmo: int, pause: float, url: str = SPACE_URL)
|
|
| 610 |
ts = time.strftime("%Y%m%d-%H%M%S")
|
| 611 |
out_win = f"{EXPORT_DIR_WIN}\\demo-{beat_name}-{ts}.mp4"
|
| 612 |
print(f" exporting to {out_win} ...")
|
| 613 |
-
export_result = _cap("export", cap_path, "--output", out_win, "--json")
|
| 614 |
if "Completed" in export_result.stdout or "Progress" in export_result.stdout:
|
| 615 |
print(f" β exported β {out_win}")
|
| 616 |
return Path(EXPORT_DIR_WSL) / f"demo-{beat_name}-{ts}.mp4"
|
|
@@ -629,8 +648,8 @@ def main() -> None:
|
|
| 629 |
)
|
| 630 |
ap.add_argument("--beat", default="all", choices=sorted(MANUAL_BEATS),
|
| 631 |
help="which beat(s) to record (default: all)")
|
| 632 |
-
ap.add_argument("--mode", default="manual", choices=["manual", "auto"],
|
| 633 |
-
help="manual=
|
| 634 |
ap.add_argument("--preflight-only", action="store_true",
|
| 635 |
help="run preflight checks and exit")
|
| 636 |
ap.add_argument("--slowmo", type=int, default=SLOWMO_DEFAULT,
|
|
@@ -652,6 +671,8 @@ def main() -> None:
|
|
| 652 |
|
| 653 |
if args.mode == "auto":
|
| 654 |
result = record_auto(args.beat, args.slowmo, args.pause, url)
|
|
|
|
|
|
|
| 655 |
else:
|
| 656 |
result = record_manual(args.beat, url)
|
| 657 |
|
|
|
|
| 39 |
EXPORT_DIR_WSL = "/mnt/d/workspace/recordings" # WSL equivalent
|
| 40 |
SLOWMO_DEFAULT = 400 # ms between Playwright actions
|
| 41 |
CAP_FPS = "60" # recording quality (1-120)
|
| 42 |
+
EXPORT_QUALITY = "maximum" # mp4 compression: maximum, social, web, potato
|
| 43 |
+
EXPORT_RES = "1707x1067" # native screen resolution
|
| 44 |
|
| 45 |
|
| 46 |
# ---------------------------------------------------------------------------
|
|
|
|
| 474 |
# record
|
| 475 |
# ---------------------------------------------------------------------------
|
| 476 |
|
| 477 |
+
def record_manual(beat_name: str, url: str = SPACE_URL, no_cap: bool = False) -> Path | None:
|
| 478 |
+
"""Manual mode: print beat cues with countdowns. If no_cap=False, also
|
| 479 |
+
starts/stops/exports via cap CLI. If no_cap=True, just prints cues β you
|
| 480 |
+
handle recording yourself (e.g. Cap desktop at higher quality)."""
|
| 481 |
print(f"\n=== RECORD (manual): beat '{beat_name}' ===\n")
|
| 482 |
|
| 483 |
+
rec_id = None
|
| 484 |
+
cap_path = None
|
| 485 |
+
|
| 486 |
+
if not no_cap:
|
| 487 |
+
# 1) Start detached Cap recording
|
| 488 |
+
print(" starting Cap recording (detached)...")
|
| 489 |
+
screen_id = _get_screen_id()
|
| 490 |
+
started = _cap_json("record", "start", "--screen", screen_id, "--fps", CAP_FPS, "--detach", "--json")
|
| 491 |
+
if not started or "recordingId" not in started:
|
| 492 |
+
print(" β failed to start recording")
|
| 493 |
+
return None
|
| 494 |
+
rec_id = started["recordingId"]
|
| 495 |
+
cap_path = started.get("path", "unknown")
|
| 496 |
+
print(f" β recording started (id={rec_id})")
|
| 497 |
+
else:
|
| 498 |
+
print(" π¬ Start your Cap desktop recording NOW (high quality).")
|
| 499 |
+
time.sleep(2)
|
| 500 |
|
| 501 |
# 2) Print beat cues β user drives the browser
|
| 502 |
beats = MANUAL_BEATS.get(beat_name, MANUAL_BEATS["all"])
|
|
|
|
| 522 |
time.sleep(1)
|
| 523 |
print(f" β cut{' ' * 20}")
|
| 524 |
|
| 525 |
+
if no_cap:
|
| 526 |
+
print("\n π Stop your Cap desktop recording now.")
|
| 527 |
+
print(f" π Export from Cap desktop to: D:\\workspace\\recordings\\demo-{beat_name}-<ts>.mp4")
|
| 528 |
+
return None
|
| 529 |
+
|
| 530 |
# 3) Stop recording
|
| 531 |
print("\n stopping recording...")
|
| 532 |
stopped = _cap_json("record", "stop", "--id", rec_id, "--json")
|
|
|
|
| 539 |
ts = time.strftime("%Y%m%d-%H%M%S")
|
| 540 |
out_win = f"{EXPORT_DIR_WIN}\\demo-{beat_name}-{ts}.mp4"
|
| 541 |
print(f" exporting to {out_win} ...")
|
| 542 |
+
export_result = _cap("export", cap_path, "--output", out_win, "--quality", EXPORT_QUALITY, "--resolution", EXPORT_RES, "--json")
|
| 543 |
if "Completed" in export_result.stdout or "Progress" in export_result.stdout:
|
| 544 |
print(f" β exported β {out_win}")
|
| 545 |
return Path(EXPORT_DIR_WSL) / f"demo-{beat_name}-{ts}.mp4"
|
|
|
|
| 574 |
page = browser.new_page()
|
| 575 |
page.set_viewport_size({"width": 1707, "height": 1067}) # native screen res
|
| 576 |
|
| 577 |
+
# Go fullscreen (F11) β hides taskbar + title bar for clean recording
|
| 578 |
+
page.keyboard.press("F11")
|
| 579 |
+
time.sleep(0.5)
|
| 580 |
+
|
| 581 |
# Dismiss Chrome restore bubble + any initial popups
|
| 582 |
time.sleep(1.0)
|
| 583 |
_dismiss_popups(page)
|
|
|
|
| 629 |
ts = time.strftime("%Y%m%d-%H%M%S")
|
| 630 |
out_win = f"{EXPORT_DIR_WIN}\\demo-{beat_name}-{ts}.mp4"
|
| 631 |
print(f" exporting to {out_win} ...")
|
| 632 |
+
export_result = _cap("export", cap_path, "--output", out_win, "--quality", EXPORT_QUALITY, "--resolution", EXPORT_RES, "--json")
|
| 633 |
if "Completed" in export_result.stdout or "Progress" in export_result.stdout:
|
| 634 |
print(f" β exported β {out_win}")
|
| 635 |
return Path(EXPORT_DIR_WSL) / f"demo-{beat_name}-{ts}.mp4"
|
|
|
|
| 648 |
)
|
| 649 |
ap.add_argument("--beat", default="all", choices=sorted(MANUAL_BEATS),
|
| 650 |
help="which beat(s) to record (default: all)")
|
| 651 |
+
ap.add_argument("--mode", default="manual", choices=["manual", "auto", "cues"],
|
| 652 |
+
help="manual=cap CLI + cues; auto=Playwright; cues=just print beat cues (you record with Cap desktop)")
|
| 653 |
ap.add_argument("--preflight-only", action="store_true",
|
| 654 |
help="run preflight checks and exit")
|
| 655 |
ap.add_argument("--slowmo", type=int, default=SLOWMO_DEFAULT,
|
|
|
|
| 671 |
|
| 672 |
if args.mode == "auto":
|
| 673 |
result = record_auto(args.beat, args.slowmo, args.pause, url)
|
| 674 |
+
elif args.mode == "cues":
|
| 675 |
+
result = record_manual(args.beat, url, no_cap=True)
|
| 676 |
else:
|
| 677 |
result = record_manual(args.beat, url)
|
| 678 |
|