Skip to content

Commit 2f24573

Browse files
docs(skills): an agent adds media with the duration Studio gives a dropped file (#4144)
* docs(skills): an agent adds media with the duration Studio gives a dropped file * docs(skills): add-media recipe matches Studio's full-frame geometry and z-index * docs(skills): recipes start with hyperframes timeline to see tracks and clips * test(skills): split the add-media drift test so each check stays small and formatted * test(skills): split addMediaFacts into single-purpose helpers to clear the CRAP-score gate * docs(skills): add-media recipe needs only data-start for video and audio * docs(skills): add-media intro names the video and audio difference and a test guards it * docs(skills): add-media image duration is optional and defaults to 3 seconds * chore(skills): regenerate the skills manifest after rebasing onto main * docs(skills): tracks-and-clips says image duration is optional, and a test pins the 3 * test(skills): split the add-media example check into small helpers Clears the Fallow complexity finding on the test callback.
1 parent dc94692 commit 2f24573

4 files changed

Lines changed: 129 additions & 2 deletions

File tree

‎scripts/creator-editing-recipes.test.mjs‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,78 @@ test("the Studio skill's safe boxes equal the preview's", async () => {
7373
assert.match(skill, new RegExp(`Action-safe\\s*\\|\\s*${action}%`));
7474
assert.match(skill, new RegExp(`Title-safe\\s*\\|\\s*${title}%`));
7575
});
76+
77+
async function studioDefaultSeconds(kind) {
78+
const helpers = await read("packages/studio/src/utils/studioHelpers.ts");
79+
const block = helpers.match(/DEFAULT_TIMELINE_ASSET_DURATION[^=]*=\s*\{([^}]*)\}/);
80+
const defaults = block ? block[1] : "";
81+
const value = defaults.match(new RegExp(`${kind}:\\s*(\\d+(?:\\.\\d+)?)`));
82+
return value ? value[1] : undefined;
83+
}
84+
85+
async function addMediaSection() {
86+
const doc = await read(OWNER);
87+
const afterHeading = doc.split("## Add media")[1] ?? "";
88+
const section = afterHeading.split("\n## ")[0];
89+
const imageMatch = section.match(/<img[\s\S]*?\/>/);
90+
const image = imageMatch ? imageMatch[0] : "";
91+
return { section, image };
92+
}
93+
94+
test("the add-media recipe uses Studio's default durations", async () => {
95+
const { section, image } = await addMediaSection();
96+
const imageSecs = await studioDefaultSeconds("image");
97+
assert.ok(imageSecs, "could not read Studio's image default");
98+
assert.match(section, new RegExp(`defaults to ${imageSecs} seconds`));
99+
assert.match(section, /`data-start` is enough/);
100+
assert.doesNotMatch(section, /ffprobe/);
101+
assert.match(section, /root composition's `data-duration` is at least/);
102+
assert.match(section, new RegExp(`${imageSecs} for an image unless you set another`));
103+
assert.doesNotMatch(image, /data-duration/);
104+
});
105+
106+
test("the add-media recipe uses Studio's full-frame geometry", async () => {
107+
const { section, image } = await addMediaSection();
108+
const dropOps = await read("packages/studio/src/hooks/useTimelineAssetDropOps.ts");
109+
assert.match(
110+
dropOps,
111+
/fitTimelineAssetGeometry\(\s*null,/,
112+
"Studio centres by natural size now; update the doc",
113+
);
114+
assert.match(section, /fill the whole frame/);
115+
assert.match(image, /left: 0px; top: 0px; width: 1920px; height: 1080px/);
116+
});
117+
118+
test("the add-media example carries every attribute Studio's drop writes", async () => {
119+
const { image } = await addMediaSection();
120+
const drop = await read("packages/studio/src/utils/timelineAssetDrop.ts");
121+
for (const attr of ['class="clip"', "data-start", "data-duration", "data-track-index"]) {
122+
assert.ok(drop.includes(attr), `Studio no longer writes ${attr}`);
123+
}
124+
for (const attr of ['class="clip"', "data-start", "data-track-index"]) {
125+
assert.ok(image.includes(attr), `doc example lacks ${attr}`);
126+
}
127+
});
128+
129+
const CLIP_ATTRS = ["id=", 'class="clip"', "data-start", "data-track-index"];
130+
131+
const mediaExample = (section, tag) =>
132+
section.match(new RegExp(`<${tag}[\\s\\S]*?</${tag}>`))?.[0] ?? "";
133+
134+
const assertNoAuthoredDuration = (example, tag) => {
135+
assert.ok(example, `no <${tag}> example`);
136+
assert.doesNotMatch(example, /data-duration/, `${tag} example must not author a duration`);
137+
};
138+
139+
const assertHasAttrs = (example, tag, attrs) => {
140+
for (const attr of attrs) assert.ok(example.includes(attr), `${tag} example lacks ${attr}`);
141+
};
142+
143+
test("the video and audio add-media examples carry no data-duration and keep the clip attributes", async () => {
144+
const { section } = await addMediaSection();
145+
for (const tag of ["video", "audio"]) {
146+
const example = mediaExample(section, tag);
147+
assertNoAuthoredDuration(example, tag);
148+
assertHasAttrs(example, tag, CLIP_ATTRS);
149+
}
150+
});

‎skills-manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"files": 11
3535
},
3636
"hyperframes-core": {
37-
"hash": "ca3bc11c4e163225",
37+
"hash": "99a07539f407ca0d",
3838
"files": 11
3939
},
4040
"hyperframes-creative": {

‎skills/hyperframes-core/references/creator-editing-recipes.md‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Use these copyable contracts after `tracks-and-clips.md`. Global math: **consumed source = timeline duration × rate**; **natural timeline duration = remaining source / rate**.
44

5+
Before any edit, run `npx hyperframes timeline` (add `--json` for a machine-readable list) to see the project's tracks and clips instead of reading the HTML.
6+
57
These recipes keep sound on a separate `<audio>` element with the `<video>` muted, which is the pattern to reach for when picture and sound are cut independently. An unmuted `<video>` that declares `data-has-audio="true"` is also mixed, so a separate track is a choice, not a requirement.
68

79
**Every `<video>` and `<audio>` below carries an `id`, and that is not cosmetic**: `lint` errors with `media_missing_id` on timed media without one, and an id-less `<audio>` is never picked up by the mixer, so the render comes out silent. Keep the ids when you copy a recipe.
@@ -417,6 +419,56 @@ Timeline math: an audio element in the root composition has `data-start` in abso
417419

418420
Timeline math: pick the clips first and say which ones you picked (by id) if the request does not match the file exactly; then add one `delta` to every member's `data-start`, so relative spacing is preserved (here `delta = 40`). Give each copy a new unique `id` and the next unused `data-track-index`; keep `src`, `data-duration`, `data-media-start`, `data-volume` and any `data-automation` as they are. Leave the originals untouched. Check the copies still end inside the composition's duration. Owner: `/hyperframes-core`. Limit: copies of a `<video>` or a sub-composition host follow the same rule, and a copied sub-composition needs its own host `id`.
419421

422+
## Add media (image, video, audio)
423+
424+
Write what Studio writes when a person drops a file on the timeline, so an agent-added clip behaves the same as a dropped one; the one difference is that video and audio need no `data-duration`. Studio's source of truth is `DEFAULT_TIMELINE_ASSET_DURATION` in `packages/studio/src/utils/studioHelpers.ts` and `buildTimelineAssetInsertHtml` in `packages/studio/src/utils/timelineAssetDrop.ts`; a test keeps this section equal to them.
425+
426+
- **Image: `data-duration` is optional and defaults to 3 seconds**, the same as a dropped image, because a still has no length of its own. Write it only for another length. A test keeps the 3 equal to the default in code.
427+
- **Video and audio: `data-start` is enough.** The length comes from the media itself. An authored `data-duration` shorter than the file is a trim, never a requirement; leave it out unless the request asks for a shorter clip.
428+
- **Start: the playhead or the requested time, never a silent `0`.** Studio's asset-panel Add uses the playhead time on track `0`; a drop uses the drop point.
429+
- Give every clip `id`, `class="clip"`, `data-start` and `data-track-index`. Video is `muted playsinline`; audio carries `data-volume="1"`.
430+
- Then make sure the root composition's `data-duration` is at least the clip's end (`data-start` plus its length: 3 for an image unless you set another, the media's length for video and audio): Studio raises a declared root duration to cover the new clip, so an agent must too, or the clip lies past the end and never plays.
431+
- **Images and video fill the whole frame**: absolutely positioned at `left: 0; top: 0`, `width` and `height` equal to the composition's `data-width` and `data-height`, `object-fit: contain`. Studio does not know a dropped file's natural size, so it does not centre a smaller one.
432+
- `z-index` is the number of top-level clips already in that file plus one (at least `1`); later clips stack above earlier ones.
433+
- Several files dropped together share the drop's track and run end to end.
434+
435+
```html
436+
<img
437+
id="photo"
438+
class="clip"
439+
src="assets/photo.png"
440+
data-start="4"
441+
data-track-index="1"
442+
style="position: absolute; left: 0px; top: 0px; width: 1920px; height: 1080px; object-fit: contain; z-index: 2"
443+
/>
444+
```
445+
446+
```html
447+
<video
448+
id="broll"
449+
class="clip"
450+
src="assets/broll.mp4"
451+
data-start="4"
452+
data-track-index="2"
453+
muted
454+
playsinline
455+
style="position: absolute; left: 0px; top: 0px; width: 1920px; height: 1080px; object-fit: contain; z-index: 3"
456+
></video>
457+
```
458+
459+
```html
460+
<audio
461+
id="whoosh"
462+
class="clip"
463+
src="assets/whoosh.mp3"
464+
data-start="4"
465+
data-track-index="3"
466+
data-volume="1"
467+
></audio>
468+
```
469+
470+
Inside a sub-composition file, `data-start` is scene-local (see `## Align a sound to an on-screen event`). Owner: `/hyperframes-core`.
471+
420472
## Swap a media file
421473

422474
```html

‎skills/hyperframes-core/references/tracks-and-clips.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A clip is any DOM element with `data-start` and, where required, `data-duration`
1010
- **Sub-composition hosts** — `<div>` with `data-composition-src`. Always require `data-duration`.
1111
- **Video clips** — `<video>` with `muted` and `playsinline`. Duration can default to media length.
1212
- **Audio clips** — `<audio>`. Duration can default to media length.
13-
- **Image clips** — `<img>`. Always require `data-duration`.
13+
- **Image clips** — `<img>`. `data-duration` is optional and defaults to 3 seconds; write it only for another length.
1414

1515
Add `class="clip"` to authored visual clips. The runtime does not read it, but the scaffold's shared `.clip { position: absolute; inset: 0 }` rule is what gives a scene its full-frame box, Studio treats it as an edit hint, and `lint` warns without it.
1616

0 commit comments

Comments
 (0)