@bycrux/montaj-skills 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bycrux/montaj-skills",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [
@@ -44,6 +44,10 @@ Run step `fetch_image` with the following args. Downloads one HTTPS URL to a wor
44
44
 
45
45
  Output: `{ "path": "/abs/path/to/file.jpg" }`. Some hosts return `403` to non-browser fetches — if one fails, run step `fetch_image` with the next candidate instead of fighting it.
46
46
 
47
+ > To register a fetched image into Hub Media (R2-backed, survives in the library), call
48
+ > `hub.ingest_media({ projectId, items: [{ workspacePath: "<absolute fetch_image out path>" }] })`.
49
+ > Do not hand-roll `create_media` + presigned PUT.
50
+
47
51
  ## Picking the right image
48
52
 
49
53
  Search returns more than you need. Before committing:
package/skills/overlay.md CHANGED
@@ -64,22 +64,25 @@ See skill `write-overlay` for the full authoring reference.
64
64
 
65
65
  ### 5. Save overlays to the project
66
66
 
67
- Overlays live in `tracks[1+]` — overlay tracks in the unified tracks array. Each inner array is one track. Items in the same track cannot overlap in time; items in different tracks are z-ordered (higher indexes render on top). `tracks[0]` is always the primary footage track.
67
+ Overlays live in `tracks[1+]` — overlay tracks in the unified tracks array. Each track is an object (`{id, items, ...}`); its `items` array holds the track's clips/overlays. Items in the same track cannot overlap in time; items in different tracks are z-ordered (higher indexes render on top). `tracks[0]` is always the primary footage track.
68
68
 
69
69
  ```json
70
70
  {
71
71
  "tracks": [
72
- [],
73
- [
74
- {
75
- "id": "ov-0",
76
- "type": "overlay",
77
- "src": "/abs/path/to/project/overlays/hook.jsx",
78
- "props": { "text": "The source code got leaked" },
79
- "start": 0.0,
80
- "end": 3.0
81
- }
82
- ]
72
+ { "id": "trk-0", "items": [] },
73
+ {
74
+ "id": "trk-1",
75
+ "items": [
76
+ {
77
+ "id": "ov-0",
78
+ "type": "overlay",
79
+ "src": "/abs/path/to/project/overlays/hook.jsx",
80
+ "props": { "text": "The source code got leaked" },
81
+ "start": 0.0,
82
+ "end": 3.0
83
+ }
84
+ ]
85
+ }
83
86
  ]
84
87
  }
85
88
  ```
@@ -100,6 +103,7 @@ Follow save discipline: **read the project**, merge the updated `tracks` array i
100
103
  - **Keep text short** — 2–6 words for lower-thirds, 4–8 for hooks. Short + large beats long + small
101
104
  - **Leave `offsetX`, `offsetY`, `scale` at defaults** (`0`, `0`, `1`) — the human positions overlays via the UI drag tool after preview
102
105
  - **Use assets from `project.assets`** — pass asset `src` paths as `props`, don't hardcode paths inside JSX
106
+ - **Expose text styling as props** — a text overlay you want editable in the editor's properties panel must READ its font, size, weight, style, color, alignment, transform, and background from `props` (the nine standard text props) with sensible defaults, not hardcode them in the JSX. A hardcoded style shows no control in the panel. See skill `write-overlay` → "Make text overlays editable in the properties panel"
103
107
 
104
108
  ## Render Constraints
105
109
 
@@ -6,7 +6,7 @@ step: true
6
6
 
7
7
  # Select Takes
8
8
 
9
- `montaj/select_takes` is an agent-authored task — no CLI step, no API call. You reason across all clip transcripts and make editorial decisions. The output is a set of cropped trim specs ready for `rm_fillers` and `concat`.
9
+ `montaj/select_takes` is an agent-authored task — no CLI step, no API call. You reason across all clip transcripts and make editorial decisions. The output is a set of cropped trim specs ready for `rm_fillers`. Nothing joins them into a file: the surviving keeps become `tracks[0]` items with their own `inPoint`/`outPoint`, and the render engine assembles them in one pass at the end.
10
10
 
11
11
  ## Core Purpose
12
12
 
@@ -82,11 +82,15 @@ For each flagged seam, fix it by trimming the crop window of whichever section i
82
82
 
83
83
  **This check is required before writing any spec files.** Seam problems cannot be caught by any automated step downstream.
84
84
 
85
- ### 9. Crop the trim specs — do NOT call `trim`
85
+ ### 9. Crop the trim specs — never encode an intermediate
86
86
 
87
- For each selected take, load the trim spec JSON produced by the preceding `waveform_trim` step for that clip. Crop it to the selected take's virtual-timeline window using the `crop_spec` step.
87
+ For each selected take, the trim spec JSON comes back **inline** (on stdout / in the step result) from the preceding `waveform_trim` step — it is NOT written to disk automatically. The `crop_spec` step requires an on-disk file (its `--input` argument). Before calling `crop_spec`, write the inline spec to the project scratch dir using the `write_file` tool (e.g. save it as `<clip>_spec.json`), then pass that path as `crop_spec`'s `input`.
88
88
 
89
- **Never call the `trim` step.** That encodes an intermediate video file and breaks the single-encode chain. Cropping the spec keeps the original source file all the way through to `concat`.
89
+ Crop the written spec to the selected take's virtual-timeline window using the `crop_spec` step.
90
+
91
+ **Never encode an intermediate video file here.** There is no `trim` step to call, and reaching for `materialize_cut` at this point would break the single-encode chain for no gain. Cropping the spec keeps `tracks[0].items[*].src` pointing at the original source all the way through to the final render, which is what lets the operator re-trim any cut later without losing quality — and what keeps each item's `proxySrc` valid, since a proxy covers the original file.
92
+
93
+ **Note:** the `input` path in all `crop_spec` calls below must point to a spec file you've written to disk via `write_file` first (see above).
90
94
 
91
95
  Run step `crop_spec` with `{"input": "/path/IMG_4893_spec.json", "keeps": [[8.5, 34.1]]}` → returns `{"path": "/path/IMG_4893_spec_cropped.json"}` (single window).
92
96
 
@@ -116,7 +120,7 @@ Run step `virtual_to_original` with `{"input": "spec.json", "inverse": true, "ti
116
120
 
117
121
  ### 10. Output
118
122
 
119
- An ordered list of `_selected.json` trim spec paths — one per selected section, in narrative order. These become the inputs to `rm_fillers` and ultimately `concat`.
123
+ An ordered list of `_selected.json` trim spec paths — one per selected section, in narrative order. These become the inputs to `rm_fillers`, and the keeps that survive it become the `tracks[0]` items the render engine assembles.
120
124
 
121
125
  ## What to Log
122
126
 
@@ -66,7 +66,7 @@ export default function List() {
66
66
 
67
67
  ## Writing the JSX
68
68
 
69
- > **Carousel text overlays follow a stricter contract.** For carousel projects, every text-bearing overlay must accept its font size, family, weight, style, color, alignment, transform, and background as props with string defaults — see skill `editable-text`. The "go large — for video" guidance below, and the hardcoded-style style of the `Hook` example, **do not apply** to carousel editable-text overlays.
69
+ > **Expose text styling as props to make an overlay editable.** A text overlay is only restyleable in the editor's properties panel for the props it declares — see "Make text overlays editable in the properties panel" below, which applies to video overlays too. **Carousel text overlays follow a stricter, required version of that contract:** every text-bearing overlay must accept its font size, family, weight, style, color, alignment, transform, and background as props with string defaults — see skill `editable-text`. The "go large — for video" guidance below, and the hardcoded-style `Hook` example just under it, **do not apply** to carousel editable-text overlays.
70
70
 
71
71
  The default aesthetic is **plain bold text directly on video** — no card, no background, just a text shadow for legibility. Big text (96–160px) that covers the footage, including the speaker's face if needed.
72
72
 
@@ -111,6 +111,58 @@ Only add a card or background when the prompt explicitly asks, or when a specifi
111
111
 
112
112
  ---
113
113
 
114
+ ## Make text overlays editable in the properties panel
115
+
116
+ The editor's right-hand properties panel (and the floating text toolbar) can restyle a text overlay **only for the props it actually declares**. It reads the nine standard text props off the item's `props` object:
117
+
118
+ `text`, `fontSize`, `fontFamily`, `fontWeight`, `fontStyle`, `color`, `textAlign`, `textTransform`, `bgColor`
119
+
120
+ A control appears for each of those that is present (non-null) on `props`; anything the overlay **hardcodes** in its JSX style instead of reading from `props` shows **no control at all**, and the operator can't change it. This is exactly why an overlay that writes `textTransform: 'uppercase'` straight into its style has no text-transform control in the editor.
121
+
122
+ **So a text overlay you want a human to be able to restyle must READ its text styling from `props`, with sensible defaults — not hardcode it.** Same house style (big, bold, plain on video), just sourced from props:
123
+
124
+ ```jsx
125
+ // Editable Hook — every text property is adjustable in the panel.
126
+ export default function Hook() {
127
+ const progress = interpolate(frame, [0, 8], [0, 1], { extrapolateRight: 'clamp' })
128
+ const slideY = interpolate(frame, [0, 10], [40, 0], { extrapolateRight: 'clamp' })
129
+
130
+ return (
131
+ <div style={{ position: 'absolute', bottom: 180, left: 48, right: 48, opacity: progress, transform: `translateY(${slideY}px)` }}>
132
+ <div style={{
133
+ fontFamily: props.fontFamily ?? 'Anton, Impact, sans-serif',
134
+ fontSize: props.fontSize ?? 120,
135
+ fontWeight: props.fontWeight ?? 900,
136
+ fontStyle: props.fontStyle ?? 'normal',
137
+ color: props.color ?? '#fff',
138
+ textAlign: props.textAlign ?? 'left',
139
+ textTransform: props.textTransform ?? 'uppercase',
140
+ background: props.bgColor ?? 'transparent',
141
+ lineHeight: 1.05, letterSpacing: '-1px',
142
+ textShadow: '0 2px 24px rgba(0,0,0,0.9), 0 0 60px rgba(0,0,0,0.5)',
143
+ }}>
144
+ {props.text}
145
+ </div>
146
+ </div>
147
+ )
148
+ }
149
+ ```
150
+
151
+ Declare those same values in the item's `props` too, so the panel opens on the real values rather than blank controls:
152
+
153
+ ```json
154
+ "props": {
155
+ "text": "She built an AI employee",
156
+ "fontSize": 120, "fontFamily": "Anton, Impact, sans-serif",
157
+ "fontWeight": 900, "fontStyle": "normal", "color": "#ffffff",
158
+ "textAlign": "left", "textTransform": "uppercase", "bgColor": "transparent"
159
+ }
160
+ ```
161
+
162
+ This now applies to **video** text overlays, not just carousel slides — the video editor has the same properties panel. For the full contract (types and validation) see skill `editable-text`. A non-text overlay (logo, image card, chart) has no text props to expose; the same principle still holds for whatever a human would want to tweak — pass it through `props`, don't bury it in the JSX.
163
+
164
+ ---
165
+
114
166
  ## Splitting background from content across tracks
115
167
 
116
168
  The most reliable way to use frosted-glass / blurred card backgrounds is to **put the background on a separate, lower track** and the animated content on a higher track. The render pipeline composites tracks in order, so the content renders on top.
@@ -129,25 +181,31 @@ The most reliable way to use frosted-glass / blurred card backgrounds is to **pu
129
181
  ```json
130
182
  {
131
183
  "tracks": [
132
- [],
133
- [
134
- {
135
- "id": "ov-card-bg",
136
- "type": "overlay",
137
- "src": "/path/overlays/card-bg.jsx",
138
- "start": 2.0,
139
- "end": 6.0
140
- }
141
- ],
142
- [
143
- {
144
- "id": "ov-card-content",
145
- "type": "overlay",
146
- "src": "/path/overlays/card-content.jsx",
147
- "start": 2.0,
148
- "end": 6.0
149
- }
150
- ]
184
+ { "id": "trk-0", "items": [] },
185
+ {
186
+ "id": "trk-1",
187
+ "items": [
188
+ {
189
+ "id": "ov-card-bg",
190
+ "type": "overlay",
191
+ "src": "/path/overlays/card-bg.jsx",
192
+ "start": 2.0,
193
+ "end": 6.0
194
+ }
195
+ ]
196
+ },
197
+ {
198
+ "id": "trk-2",
199
+ "items": [
200
+ {
201
+ "id": "ov-card-content",
202
+ "type": "overlay",
203
+ "src": "/path/overlays/card-content.jsx",
204
+ "start": 2.0,
205
+ "end": 6.0
206
+ }
207
+ ]
208
+ }
151
209
  ]
152
210
  }
153
211
  ```
@@ -446,29 +504,32 @@ Place overlay items in `tracks[1+]` in `project.json`. Each item must have `type
446
504
  ```json
447
505
  {
448
506
  "tracks": [
449
- [],
450
- [
451
- {
452
- "id": "ov-hook",
453
- "type": "overlay",
454
- "src": "/abs/path/to/project/overlays/hook.jsx",
455
- "start": 0.0,
456
- "end": 3.0,
457
- "props": {
458
- "text": "She built an AI employee"
507
+ { "id": "trk-0", "items": [] },
508
+ {
509
+ "id": "trk-1",
510
+ "items": [
511
+ {
512
+ "id": "ov-hook",
513
+ "type": "overlay",
514
+ "src": "/abs/path/to/project/overlays/hook.jsx",
515
+ "start": 0.0,
516
+ "end": 3.0,
517
+ "props": {
518
+ "text": "She built an AI employee"
519
+ }
520
+ },
521
+ {
522
+ "id": "ov-logo",
523
+ "type": "overlay",
524
+ "src": "/abs/path/to/project/overlays/logo.jsx",
525
+ "start": 0.0,
526
+ "end": 999.0,
527
+ "props": {
528
+ "logoSrc": "/abs/path/to/project/assets/logo.png"
529
+ }
459
530
  }
460
- },
461
- {
462
- "id": "ov-logo",
463
- "type": "overlay",
464
- "src": "/abs/path/to/project/overlays/logo.jsx",
465
- "start": 0.0,
466
- "end": 999.0,
467
- "props": {
468
- "logoSrc": "/abs/path/to/project/assets/logo.png"
469
- }
470
- }
471
- ]
531
+ ]
532
+ }
472
533
  ]
473
534
  }
474
535
  ```
@@ -576,7 +637,12 @@ Common overlay set for a social reel:
576
637
 
577
638
  **Write all of your overlay JSX first. Then sample them in a single batch pass — do not sample after each file.** Per-file sampling stalls authoring and spins up a fresh Puppeteer process each time; one pass at the end over the finished set is faster and just as safe, since nothing downstream consumes an overlay until you save the project with the whole batch.
578
639
 
579
- Once every JSX file is written, loop over them in one pass — for each JSX file, **run step `sample_overlay`** with args `{ path, measure: true, googleFonts, props }`:
640
+ Once every JSX file is written, loop over them in one pass — for each JSX file, **run step `sample_overlay`** with args `{ overlay, out, measure: true, google_fonts, props }`:
641
+
642
+ - `overlay` — absolute path to the JSX file
643
+ - `out` — absolute path where the step writes the sample PNG (required)
644
+ - `google_fonts` — the overlay's declared `googleFonts` value (snake_case step arg)
645
+ - `props` — representative props for the overlay
580
646
 
581
647
  Pass each overlay's declared `googleFonts` (and representative `props`) so the step measures with the real render-time font — see the Syne case study below.
582
648