stillmade-sdk 0.1.8 → 0.1.10
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/.stillmade/sdk-baseline.json +1 -1
- package/AGENTS.md +8 -2
- package/DESKTOP_CAPABILITIES.md +1 -1
- package/INTERFACE_DESIGN.md +167 -83
- package/MODULES.md +1 -1
- package/NATIVE_MCP.md +1 -1
- package/PASTE_TO_LLM.md +151 -12
- package/README.md +5 -2
- package/SDK.md +510 -115
- package/UI_AESTHETIC.md +64 -0
- package/blocks/native/sm.export-video/versions/2.1.0/package.js +580 -0
- package/blocks/native/sm.panel-strip/versions/2.4.0/package.js +1176 -0
- package/blocks/native/sm.panel-strip/versions/2.4.1/package.js +1316 -0
- package/blocks/native/sm.shot-plan/versions/2.2.0/package.js +800 -0
- package/blocks/native/sm.shot-plan/versions/2.2.1/package.js +920 -0
- package/examples/README.md +10 -0
- package/examples/custom-interface.stillmade.json +3 -3
- package/package.json +7 -1
- package/packages/block-cli/dev-host/assets/host.js +219 -26
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-cli/format.js +1 -1
- package/packages/block-cli/templates.js +53 -22
- package/packages/block-platform/block-readiness.js +5 -0
- package/packages/block-platform/brief-ports.js +29 -0
- package/packages/block-platform/catalog.js +32 -9
- package/packages/block-platform/current-semantic-source.js +7 -3
- package/packages/block-platform/flow-inspection.js +14 -4
- package/packages/block-platform/plain-descriptions.js +1 -1
- package/packages/block-platform/project-context.js +7 -0
- package/packages/block-platform/project-input-samples.js +3 -1
- package/packages/block-platform/project-inputs.js +11 -2
- package/packages/block-platform/project-start-input.js +46 -0
- package/packages/block-platform/segment-runner.js +4 -2
- package/packages/block-platform/step-groups.js +4 -0
- package/packages/block-platform/steps.js +55 -4
- package/packages/block-platform/type-preview.js +19 -3
- package/packages/block-platform/workspace-input-readset.js +6 -1
- package/packages/block-sdk/builder-contract.js +11 -5
- package/packages/block-sdk/collaboration-admission.js +4 -2
- package/packages/block-sdk/connection-adapters.js +35 -2
- package/packages/block-sdk/contracts.js +5 -2
- package/packages/block-sdk/examples.js +3 -3
- package/packages/block-sdk/first-party-identities.js +190 -0
- package/packages/block-sdk/first-party-packages.js +6 -1
- package/packages/block-sdk/index.d.ts +2 -0
- package/packages/block-sdk/index.js +1 -1
- package/packages/block-sdk/javascript.js +5 -1
- package/packages/block-sdk/platform-checks.js +13 -2
- package/packages/block-sdk/shared-agent-controls.js +5 -4
- package/packages/block-sdk/shared-controls.js +13 -9
- package/packages/block-sdk/view-design.js +104 -27
- package/packages/block-sdk/view.js +4 -2
- package/packages/design-system/block-kit.css +184 -0
- package/packages/design-system/block-kit.html +187 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
// Generated by scripts/build-owned-blocks.mjs. Edit the owned source files.
|
|
2
|
+
export default {
|
|
3
|
+
"manifest": {
|
|
4
|
+
"schemaVersion": 1,
|
|
5
|
+
"sdkVersion": "0.1.0",
|
|
6
|
+
"id": "sm.export-video",
|
|
7
|
+
"version": "2.1.0",
|
|
8
|
+
"name": "Export video",
|
|
9
|
+
"description": "Render a timeline to a downloadable video file, or make one from the project's shot plan with photos, clips and narration from earlier Steps, then explicitly save it as a connected media asset.",
|
|
10
|
+
"kind": "task",
|
|
11
|
+
"runtime": "javascript",
|
|
12
|
+
"entry": "src/run.js",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"provenance": {
|
|
15
|
+
"notice": "Original source (authorship declared by the packager)\nLicense: MIT\nChanges: Original implementation declared by the packager; no reused repository or dependency code.\nFiles: LICENSE, src/run.js\n\nMIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
|
16
|
+
"portableSources": [
|
|
17
|
+
{
|
|
18
|
+
"kind": "original",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"paths": [
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"src/run.js"
|
|
23
|
+
],
|
|
24
|
+
"evidence": [
|
|
25
|
+
"upstream/0/LICENSE",
|
|
26
|
+
"upstream/0/src/run.js"
|
|
27
|
+
],
|
|
28
|
+
"changes": "Original implementation declared by the packager; no reused repository or dependency code."
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"inputs": {
|
|
33
|
+
"input": {
|
|
34
|
+
"type": "timeline",
|
|
35
|
+
"primary": true,
|
|
36
|
+
"semantic": "timeline",
|
|
37
|
+
"required": false,
|
|
38
|
+
"description": "An explicitly connected timeline to render as it is.",
|
|
39
|
+
"sources": [
|
|
40
|
+
"user",
|
|
41
|
+
"upstream"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"media": {
|
|
45
|
+
"type": "asset[]",
|
|
46
|
+
"semantic": "project_media",
|
|
47
|
+
"accepts": [
|
|
48
|
+
"legacy.asset-list.assets"
|
|
49
|
+
],
|
|
50
|
+
"required": false,
|
|
51
|
+
"description": "Photos and clips from an earlier Step, such as Import media. With no timeline they are placed on the project's shots in order, or shown one after another.",
|
|
52
|
+
"sources": [
|
|
53
|
+
"upstream",
|
|
54
|
+
"adapter"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"narration": {
|
|
58
|
+
"type": "audio",
|
|
59
|
+
"semantic": "narration_audio",
|
|
60
|
+
"required": false,
|
|
61
|
+
"description": "Narration from an earlier Step, such as Voiceover. With no timeline it plays under the video, and the shots are fitted to its length.",
|
|
62
|
+
"sources": [
|
|
63
|
+
"upstream",
|
|
64
|
+
"adapter"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"renderAspect": {
|
|
68
|
+
"type": "text",
|
|
69
|
+
"semantic": "render_aspect",
|
|
70
|
+
"required": false,
|
|
71
|
+
"default": "16:9",
|
|
72
|
+
"description": "Frame shape of a video made from shots and media. A connected or project timeline keeps its own.",
|
|
73
|
+
"sources": [
|
|
74
|
+
"user",
|
|
75
|
+
"default"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"renderFps": {
|
|
79
|
+
"type": "text",
|
|
80
|
+
"semantic": "render_fps",
|
|
81
|
+
"required": false,
|
|
82
|
+
"default": "30",
|
|
83
|
+
"description": "Render frames per second.",
|
|
84
|
+
"sources": [
|
|
85
|
+
"user",
|
|
86
|
+
"default"
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"renderResolution": {
|
|
90
|
+
"type": "text",
|
|
91
|
+
"semantic": "render_resolution",
|
|
92
|
+
"required": false,
|
|
93
|
+
"default": "1080",
|
|
94
|
+
"description": "Output height in pixels.",
|
|
95
|
+
"sources": [
|
|
96
|
+
"user",
|
|
97
|
+
"default"
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"renderQuality": {
|
|
101
|
+
"type": "text",
|
|
102
|
+
"semantic": "render_quality",
|
|
103
|
+
"required": false,
|
|
104
|
+
"default": "high",
|
|
105
|
+
"description": "Output quality.",
|
|
106
|
+
"sources": [
|
|
107
|
+
"user",
|
|
108
|
+
"default"
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
"projectTimeline": {
|
|
112
|
+
"type": "timeline",
|
|
113
|
+
"context": "timeline",
|
|
114
|
+
"semantic": "timeline",
|
|
115
|
+
"required": false,
|
|
116
|
+
"description": "Current project timeline when no Block is connected.",
|
|
117
|
+
"sources": [
|
|
118
|
+
"project"
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
"projectShotPlan": {
|
|
122
|
+
"type": "shot-plan",
|
|
123
|
+
"context": "shotPlan",
|
|
124
|
+
"semantic": "production_shot_plan",
|
|
125
|
+
"required": false,
|
|
126
|
+
"description": "The project's shot plan. Its order and timings decide when each photo or clip shows.",
|
|
127
|
+
"sources": [
|
|
128
|
+
"project"
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"outputs": {
|
|
133
|
+
"mediaAction": {
|
|
134
|
+
"type": "approval",
|
|
135
|
+
"role": "portable_media_action",
|
|
136
|
+
"semantic": "portable_media_action",
|
|
137
|
+
"description": "A bounded render request for explicit review."
|
|
138
|
+
},
|
|
139
|
+
"output": {
|
|
140
|
+
"type": "video",
|
|
141
|
+
"semantic": "rendered_video",
|
|
142
|
+
"required": false,
|
|
143
|
+
"description": "The selected durable video asset from the completed render."
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"permissions": {
|
|
147
|
+
"project": [
|
|
148
|
+
"context.timeline.read",
|
|
149
|
+
"context.shotPlan.read"
|
|
150
|
+
],
|
|
151
|
+
"network": [],
|
|
152
|
+
"filesystem": [],
|
|
153
|
+
"secrets": [],
|
|
154
|
+
"actions": [
|
|
155
|
+
"final.render"
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
"ui": [
|
|
159
|
+
{
|
|
160
|
+
"control": "select",
|
|
161
|
+
"port": "renderAspect",
|
|
162
|
+
"options": [
|
|
163
|
+
"16:9",
|
|
164
|
+
"9:16",
|
|
165
|
+
"1:1"
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"control": "select",
|
|
170
|
+
"port": "renderFps",
|
|
171
|
+
"options": [
|
|
172
|
+
"24",
|
|
173
|
+
"30",
|
|
174
|
+
"60"
|
|
175
|
+
]
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"control": "select",
|
|
179
|
+
"port": "renderResolution",
|
|
180
|
+
"options": [
|
|
181
|
+
"720",
|
|
182
|
+
"1080",
|
|
183
|
+
"2160"
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"control": "select",
|
|
188
|
+
"port": "renderQuality",
|
|
189
|
+
"options": [
|
|
190
|
+
"low",
|
|
191
|
+
"medium",
|
|
192
|
+
"high"
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
],
|
|
196
|
+
"platforms": {
|
|
197
|
+
"phone": {
|
|
198
|
+
"supported": true
|
|
199
|
+
},
|
|
200
|
+
"browser": {
|
|
201
|
+
"supported": true
|
|
202
|
+
},
|
|
203
|
+
"desktop": {
|
|
204
|
+
"supported": true
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"code": "// A connected timeline, else the project's timeline, is rendered as it is. With\n// neither, the video is made here from the project's shot plan, the connected\n// photos and clips, and the connected narration: the shots keep their order and\n// timings, each shows the next photo or clip, and the shots are fitted to the\n// narration's length. Media values are used exactly as the earlier Step stored them.\nconst list=value=>Array.isArray(value)?value:[];\nconst number=value=>typeof value==='number'?value:typeof value==='string'&&value.trim()?Number(value):NaN;\nconst positive=value=>{const n=number(value);return Number.isFinite(n)&&n>0?n:0;};\nconst round=value=>Math.round(value*1000)/1000;\nconst copy=value=>JSON.parse(JSON.stringify(value));\nconst pick=(value,keys)=>{const out={};for(const key of keys)if(value[key]!==undefined&&value[key]!==null)out[key]=copy(value[key]);return out;};\nconst playable=timeline=>!!timeline&&typeof timeline==='object'&&!Array.isArray(timeline)&&(list(timeline.clips).length>0||list(timeline.audioItems).length>0);\nconst settings={fps:Number(input.renderFps||30),resolution:Number(input.renderResolution||1080),quality:String(input.renderQuality||'high')};\nconst aspect=String(input.renderAspect||'16:9');\nif(![24,30,60].includes(settings.fps)||![720,1080,2160].includes(settings.resolution)||!['low','medium','high'].includes(settings.quality)||!['16:9','9:16','1:1'].includes(aspect))throw new Error('Choose approved export settings.');\nconst MEDIA_FIELDS=['kind','assetId','versionId','url','source','localKey','original','mimeType','width','height'];\n// A slow push in or out keeps a still photo alive on screen.\nconst motion=index=>({type:'zoom',zoomAmount:1.08,zoomDirection:index%2?'out':'in',intensity:100,easing:'ease-in-out'});\n\nfunction assemble(){\n const visuals=list(input.media).filter(item=>item&&typeof item==='object'&&(item.kind==='image'||item.kind==='video')&&typeof item.url==='string'&&item.url).slice(0,500);\n const shots=list(input.projectShotPlan&&input.projectShotPlan.shots).filter(shot=>shot&&typeof shot==='object'&&!Array.isArray(shot));\n if(!visuals.length)throw new Error(shots.length?`This project has ${shots.length} shot${shots.length===1?'':'s'} but no photos or clips to show. Connect them from an earlier Step, such as Import media, and run that Step.`:'There is nothing to show yet. Connect photos or clips from an earlier Step, such as Import media, or connect a timeline.');\n const voice=input.narration&&typeof input.narration==='object'&&input.narration.kind==='audio'&&typeof input.narration.url==='string'&&input.narration.url?input.narration:null;\n const spoken=voice?positive(voice.duration):0;\n const timing=shot=>{const start=number(shot.startTime),end=number(shot.endTime);return Number.isFinite(start)&&Number.isFinite(end)&&start>=0&&end>start?{start,end}:null;};\n const timed=shots.map(timing);\n const lengths=(items,length)=>{let cursor=0;return items.map((item,index)=>{const unit={shot:item.shot,start:cursor,end:cursor+length(item,index)};cursor=unit.end;return unit;});};\n let units;\n // Each timed shot lasts until the next one starts, so a pause stays with the shot before it.\n if(shots.length&&timed.every((t,index)=>t&&(!index||t.start>=timed[index-1].start)))units=shots.map((shot,index)=>({shot,start:index?timed[index].start:0,end:index<shots.length-1?timed[index+1].start:timed[index].end}));\n else if(shots.length)units=lengths(shots.map(shot=>({shot})),({shot},index)=>timed[index]?timed[index].end-timed[index].start:positive(shot.estimatedScreenTime)||positive(shot.durationMs)/1000||3);\n else units=[];\n units=units.filter(unit=>unit.end-unit.start>0.01);\n // No shot plan: each photo shows for 3 seconds and each clip for its own length (at most a minute).\n if(!units.length)units=lengths(visuals.map(()=>({shot:null})),(_,index)=>visuals[index].kind==='video'&&positive(visuals[index].duration)?Math.min(positive(visuals[index].duration),60):3);\n const planned=units[units.length-1].end,scale=spoken>0?spoken/planned:1;\n if(planned*scale>21600)throw new Error('This video would be longer than 6 hours. Shorten the shot plan or the narration.');\n const clips=[],count=units.length,media=visuals.length;\n const place=(item,from,to,shot)=>{\n const index=clips.length,start=round(from*scale),end=round(to*scale);\n clips.push({...pick(item,MEDIA_FIELDS),id:`clip-${index+1}`,title:String(item.name||item.label||item.title||`${item.kind==='video'?'Clip':'Photo'} ${index+1}`).slice(0,200),trackId:'video',start,duration:round(end-start),trimStart:0,speed:1,...(item.kind==='image'?{motion:motion(index)}:{}),...(shot&&typeof shot.id==='string'?{shotId:shot.id}:{})});\n };\n // More photos than shots: a shot shows its photos in turn. Fewer: a photo spans neighbouring shots.\n if(media>=count)for(let shot=0;shot<count;shot++){\n const first=Math.floor(shot*media/count),last=Math.floor((shot+1)*media/count),unit=units[shot],step=(unit.end-unit.start)/(last-first);\n for(let item=first;item<last;item++)place(visuals[item],unit.start+(item-first)*step,item===last-1?unit.end:unit.start+(item-first+1)*step,unit.shot);\n }\n else for(let item=0;item<media;item++){\n const first=Math.floor(item*count/media),last=Math.floor((item+1)*count/media);\n place(visuals[item],units[first].start,units[last-1].end,units[first].shot);\n }\n const shown=clips.reduce((max,clip)=>Math.max(max,clip.start+clip.duration),0),targetDuration=round(Math.max(shown,spoken));\n const audioItems=voice?[{...pick(voice,['kind','assetId','versionId','url','source','localKey','original','mimeType']),id:'narration',title:'Narration',trackId:'narration',start:0,duration:spoken?round(spoken):targetDuration,trimStart:0,volume:100}]:[];\n return {schemaVersion:1,aspect,clips,audioItems,captions:[],textOverlays:[],effects:[],targetDuration,trackOrder:['video']};\n}\n\nconst chosen=playable(input.input)?input.input:playable(input.projectTimeline)?input.projectTimeline:null;\nconst timeline=chosen?copy(chosen):assemble();\nconst clips=list(timeline.clips);\nconst audio=list(timeline.audioItems);\nconst overlays=[...list(timeline.captions),...list(timeline.textOverlays),...list(timeline.effects)];\nconst end=item=>Number(item.start||0)+Number(item.duration||0);\nconst targetDuration=Math.max(0,Number(timeline.targetDuration)||0,...clips.map(end),...audio.map(end),...overlays.map(end));\nif(!Number.isFinite(targetDuration)||targetDuration<=0||targetDuration>21600)throw new Error('The export duration must be between 0 and 21600 seconds.');\nreturn {mediaAction:{schemaVersion:1,kind:'stillmade.media-action',operation:'final.render',request:{schemaVersion:1,kind:'video',timeline:{...timeline,clips,audio,overlays,targetDuration},settings,source:{id:'sm.export-video',version:'2.1.0'}},resultPort:'output'}};\n",
|
|
209
|
+
"tests": [
|
|
210
|
+
{
|
|
211
|
+
"name": "Prepares an exact render request for a connected timeline without rendering or uploading",
|
|
212
|
+
"input": {
|
|
213
|
+
"input": {
|
|
214
|
+
"schemaVersion": 1,
|
|
215
|
+
"activeTimelineId": "timeline-1",
|
|
216
|
+
"clips": [
|
|
217
|
+
{
|
|
218
|
+
"id": "clip-1",
|
|
219
|
+
"trackId": "video",
|
|
220
|
+
"start": 0,
|
|
221
|
+
"duration": 2,
|
|
222
|
+
"url": "/api/media/owner/sample.mp4"
|
|
223
|
+
}
|
|
224
|
+
],
|
|
225
|
+
"audioItems": [],
|
|
226
|
+
"captions": [],
|
|
227
|
+
"textOverlays": [],
|
|
228
|
+
"effects": []
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
"expected": {
|
|
232
|
+
"mediaAction": {
|
|
233
|
+
"schemaVersion": 1,
|
|
234
|
+
"kind": "stillmade.media-action",
|
|
235
|
+
"operation": "final.render",
|
|
236
|
+
"request": {
|
|
237
|
+
"schemaVersion": 1,
|
|
238
|
+
"kind": "video",
|
|
239
|
+
"timeline": {
|
|
240
|
+
"schemaVersion": 1,
|
|
241
|
+
"activeTimelineId": "timeline-1",
|
|
242
|
+
"clips": [
|
|
243
|
+
{
|
|
244
|
+
"id": "clip-1",
|
|
245
|
+
"trackId": "video",
|
|
246
|
+
"start": 0,
|
|
247
|
+
"duration": 2,
|
|
248
|
+
"url": "/api/media/owner/sample.mp4"
|
|
249
|
+
}
|
|
250
|
+
],
|
|
251
|
+
"audioItems": [],
|
|
252
|
+
"captions": [],
|
|
253
|
+
"textOverlays": [],
|
|
254
|
+
"effects": [],
|
|
255
|
+
"audio": [],
|
|
256
|
+
"overlays": [],
|
|
257
|
+
"targetDuration": 2
|
|
258
|
+
},
|
|
259
|
+
"settings": {
|
|
260
|
+
"fps": 30,
|
|
261
|
+
"resolution": 1080,
|
|
262
|
+
"quality": "high"
|
|
263
|
+
},
|
|
264
|
+
"source": {
|
|
265
|
+
"id": "sm.export-video",
|
|
266
|
+
"version": "2.1.0"
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
"resultPort": "output"
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"name": "Makes the video from the project shot plan, connected photos and narration",
|
|
275
|
+
"input": {
|
|
276
|
+
"media": [
|
|
277
|
+
{
|
|
278
|
+
"kind": "image",
|
|
279
|
+
"assetId": "photo-1",
|
|
280
|
+
"versionId": "photo-1-v1",
|
|
281
|
+
"url": "/api/media/owner/front.jpg",
|
|
282
|
+
"name": "front.jpg"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"kind": "image",
|
|
286
|
+
"assetId": "photo-2",
|
|
287
|
+
"versionId": "photo-2-v1",
|
|
288
|
+
"url": "/api/media/owner/side.jpg",
|
|
289
|
+
"name": "side.jpg"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"kind": "image",
|
|
293
|
+
"assetId": "photo-3",
|
|
294
|
+
"versionId": "photo-3-v1",
|
|
295
|
+
"url": "/api/media/owner/interior.jpg",
|
|
296
|
+
"name": "interior.jpg"
|
|
297
|
+
}
|
|
298
|
+
],
|
|
299
|
+
"narration": {
|
|
300
|
+
"kind": "audio",
|
|
301
|
+
"assetId": "voiceover-1",
|
|
302
|
+
"versionId": "voiceover-1-v1",
|
|
303
|
+
"url": "/api/media/owner/voiceover.mp3",
|
|
304
|
+
"duration": 9
|
|
305
|
+
},
|
|
306
|
+
"projectShotPlan": {
|
|
307
|
+
"schemaVersion": 1,
|
|
308
|
+
"shots": [
|
|
309
|
+
{
|
|
310
|
+
"id": "shot-1",
|
|
311
|
+
"startTime": 0,
|
|
312
|
+
"endTime": 4,
|
|
313
|
+
"voiceoverText": "Meet the 2021 Honda CR-V EX."
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"id": "shot-2",
|
|
317
|
+
"startTime": 4,
|
|
318
|
+
"endTime": 8,
|
|
319
|
+
"voiceoverText": "Honda Sensing comes standard."
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
"expected": {
|
|
325
|
+
"mediaAction": {
|
|
326
|
+
"schemaVersion": 1,
|
|
327
|
+
"kind": "stillmade.media-action",
|
|
328
|
+
"operation": "final.render",
|
|
329
|
+
"request": {
|
|
330
|
+
"schemaVersion": 1,
|
|
331
|
+
"kind": "video",
|
|
332
|
+
"timeline": {
|
|
333
|
+
"schemaVersion": 1,
|
|
334
|
+
"aspect": "16:9",
|
|
335
|
+
"clips": [
|
|
336
|
+
{
|
|
337
|
+
"kind": "image",
|
|
338
|
+
"assetId": "photo-1",
|
|
339
|
+
"versionId": "photo-1-v1",
|
|
340
|
+
"url": "/api/media/owner/front.jpg",
|
|
341
|
+
"id": "clip-1",
|
|
342
|
+
"title": "front.jpg",
|
|
343
|
+
"trackId": "video",
|
|
344
|
+
"start": 0,
|
|
345
|
+
"duration": 4.5,
|
|
346
|
+
"trimStart": 0,
|
|
347
|
+
"speed": 1,
|
|
348
|
+
"motion": {
|
|
349
|
+
"type": "zoom",
|
|
350
|
+
"zoomAmount": 1.08,
|
|
351
|
+
"zoomDirection": "in",
|
|
352
|
+
"intensity": 100,
|
|
353
|
+
"easing": "ease-in-out"
|
|
354
|
+
},
|
|
355
|
+
"shotId": "shot-1"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"kind": "image",
|
|
359
|
+
"assetId": "photo-2",
|
|
360
|
+
"versionId": "photo-2-v1",
|
|
361
|
+
"url": "/api/media/owner/side.jpg",
|
|
362
|
+
"id": "clip-2",
|
|
363
|
+
"title": "side.jpg",
|
|
364
|
+
"trackId": "video",
|
|
365
|
+
"start": 4.5,
|
|
366
|
+
"duration": 2.25,
|
|
367
|
+
"trimStart": 0,
|
|
368
|
+
"speed": 1,
|
|
369
|
+
"motion": {
|
|
370
|
+
"type": "zoom",
|
|
371
|
+
"zoomAmount": 1.08,
|
|
372
|
+
"zoomDirection": "out",
|
|
373
|
+
"intensity": 100,
|
|
374
|
+
"easing": "ease-in-out"
|
|
375
|
+
},
|
|
376
|
+
"shotId": "shot-2"
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"kind": "image",
|
|
380
|
+
"assetId": "photo-3",
|
|
381
|
+
"versionId": "photo-3-v1",
|
|
382
|
+
"url": "/api/media/owner/interior.jpg",
|
|
383
|
+
"id": "clip-3",
|
|
384
|
+
"title": "interior.jpg",
|
|
385
|
+
"trackId": "video",
|
|
386
|
+
"start": 6.75,
|
|
387
|
+
"duration": 2.25,
|
|
388
|
+
"trimStart": 0,
|
|
389
|
+
"speed": 1,
|
|
390
|
+
"motion": {
|
|
391
|
+
"type": "zoom",
|
|
392
|
+
"zoomAmount": 1.08,
|
|
393
|
+
"zoomDirection": "in",
|
|
394
|
+
"intensity": 100,
|
|
395
|
+
"easing": "ease-in-out"
|
|
396
|
+
},
|
|
397
|
+
"shotId": "shot-2"
|
|
398
|
+
}
|
|
399
|
+
],
|
|
400
|
+
"audioItems": [
|
|
401
|
+
{
|
|
402
|
+
"kind": "audio",
|
|
403
|
+
"assetId": "voiceover-1",
|
|
404
|
+
"versionId": "voiceover-1-v1",
|
|
405
|
+
"url": "/api/media/owner/voiceover.mp3",
|
|
406
|
+
"id": "narration",
|
|
407
|
+
"title": "Narration",
|
|
408
|
+
"trackId": "narration",
|
|
409
|
+
"start": 0,
|
|
410
|
+
"duration": 9,
|
|
411
|
+
"trimStart": 0,
|
|
412
|
+
"volume": 100
|
|
413
|
+
}
|
|
414
|
+
],
|
|
415
|
+
"captions": [],
|
|
416
|
+
"textOverlays": [],
|
|
417
|
+
"effects": [],
|
|
418
|
+
"targetDuration": 9,
|
|
419
|
+
"trackOrder": [
|
|
420
|
+
"video"
|
|
421
|
+
],
|
|
422
|
+
"audio": [
|
|
423
|
+
{
|
|
424
|
+
"kind": "audio",
|
|
425
|
+
"assetId": "voiceover-1",
|
|
426
|
+
"versionId": "voiceover-1-v1",
|
|
427
|
+
"url": "/api/media/owner/voiceover.mp3",
|
|
428
|
+
"id": "narration",
|
|
429
|
+
"title": "Narration",
|
|
430
|
+
"trackId": "narration",
|
|
431
|
+
"start": 0,
|
|
432
|
+
"duration": 9,
|
|
433
|
+
"trimStart": 0,
|
|
434
|
+
"volume": 100
|
|
435
|
+
}
|
|
436
|
+
],
|
|
437
|
+
"overlays": []
|
|
438
|
+
},
|
|
439
|
+
"settings": {
|
|
440
|
+
"fps": 30,
|
|
441
|
+
"resolution": 1080,
|
|
442
|
+
"quality": "high"
|
|
443
|
+
},
|
|
444
|
+
"source": {
|
|
445
|
+
"id": "sm.export-video",
|
|
446
|
+
"version": "2.1.0"
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
"resultPort": "output"
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
"name": "Shows connected photos one after another when the project has no shot plan",
|
|
455
|
+
"input": {
|
|
456
|
+
"media": [
|
|
457
|
+
{
|
|
458
|
+
"kind": "image",
|
|
459
|
+
"assetId": "photo-1",
|
|
460
|
+
"versionId": "photo-1-v1",
|
|
461
|
+
"url": "/api/media/owner/front.jpg",
|
|
462
|
+
"name": "front.jpg"
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"kind": "image",
|
|
466
|
+
"assetId": "photo-2",
|
|
467
|
+
"versionId": "photo-2-v1",
|
|
468
|
+
"url": "/api/media/owner/side.jpg",
|
|
469
|
+
"name": "side.jpg"
|
|
470
|
+
}
|
|
471
|
+
],
|
|
472
|
+
"renderAspect": "9:16"
|
|
473
|
+
},
|
|
474
|
+
"expected": {
|
|
475
|
+
"mediaAction": {
|
|
476
|
+
"schemaVersion": 1,
|
|
477
|
+
"kind": "stillmade.media-action",
|
|
478
|
+
"operation": "final.render",
|
|
479
|
+
"request": {
|
|
480
|
+
"schemaVersion": 1,
|
|
481
|
+
"kind": "video",
|
|
482
|
+
"timeline": {
|
|
483
|
+
"schemaVersion": 1,
|
|
484
|
+
"aspect": "9:16",
|
|
485
|
+
"clips": [
|
|
486
|
+
{
|
|
487
|
+
"kind": "image",
|
|
488
|
+
"assetId": "photo-1",
|
|
489
|
+
"versionId": "photo-1-v1",
|
|
490
|
+
"url": "/api/media/owner/front.jpg",
|
|
491
|
+
"id": "clip-1",
|
|
492
|
+
"title": "front.jpg",
|
|
493
|
+
"trackId": "video",
|
|
494
|
+
"start": 0,
|
|
495
|
+
"duration": 3,
|
|
496
|
+
"trimStart": 0,
|
|
497
|
+
"speed": 1,
|
|
498
|
+
"motion": {
|
|
499
|
+
"type": "zoom",
|
|
500
|
+
"zoomAmount": 1.08,
|
|
501
|
+
"zoomDirection": "in",
|
|
502
|
+
"intensity": 100,
|
|
503
|
+
"easing": "ease-in-out"
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"kind": "image",
|
|
508
|
+
"assetId": "photo-2",
|
|
509
|
+
"versionId": "photo-2-v1",
|
|
510
|
+
"url": "/api/media/owner/side.jpg",
|
|
511
|
+
"id": "clip-2",
|
|
512
|
+
"title": "side.jpg",
|
|
513
|
+
"trackId": "video",
|
|
514
|
+
"start": 3,
|
|
515
|
+
"duration": 3,
|
|
516
|
+
"trimStart": 0,
|
|
517
|
+
"speed": 1,
|
|
518
|
+
"motion": {
|
|
519
|
+
"type": "zoom",
|
|
520
|
+
"zoomAmount": 1.08,
|
|
521
|
+
"zoomDirection": "out",
|
|
522
|
+
"intensity": 100,
|
|
523
|
+
"easing": "ease-in-out"
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
],
|
|
527
|
+
"audioItems": [],
|
|
528
|
+
"captions": [],
|
|
529
|
+
"textOverlays": [],
|
|
530
|
+
"effects": [],
|
|
531
|
+
"targetDuration": 6,
|
|
532
|
+
"trackOrder": [
|
|
533
|
+
"video"
|
|
534
|
+
],
|
|
535
|
+
"audio": [],
|
|
536
|
+
"overlays": []
|
|
537
|
+
},
|
|
538
|
+
"settings": {
|
|
539
|
+
"fps": 30,
|
|
540
|
+
"resolution": 1080,
|
|
541
|
+
"quality": "high"
|
|
542
|
+
},
|
|
543
|
+
"source": {
|
|
544
|
+
"id": "sm.export-video",
|
|
545
|
+
"version": "2.1.0"
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
"resultPort": "output"
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
],
|
|
553
|
+
"portable": {
|
|
554
|
+
"format": "stillmade-block/1",
|
|
555
|
+
"dependencies": [],
|
|
556
|
+
"sources": [
|
|
557
|
+
{
|
|
558
|
+
"kind": "original",
|
|
559
|
+
"license": "MIT",
|
|
560
|
+
"paths": [
|
|
561
|
+
"LICENSE",
|
|
562
|
+
"src/run.js"
|
|
563
|
+
],
|
|
564
|
+
"evidence": [
|
|
565
|
+
"upstream/0/LICENSE",
|
|
566
|
+
"upstream/0/src/run.js"
|
|
567
|
+
],
|
|
568
|
+
"changes": "Original implementation declared by the packager; no reused repository or dependency code."
|
|
569
|
+
}
|
|
570
|
+
],
|
|
571
|
+
"files": {
|
|
572
|
+
"upstream/0/LICENSE": "MIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
|
573
|
+
"upstream/0/src/run.js": "// A connected timeline, else the project's timeline, is rendered as it is. With\n// neither, the video is made here from the project's shot plan, the connected\n// photos and clips, and the connected narration: the shots keep their order and\n// timings, each shows the next photo or clip, and the shots are fitted to the\n// narration's length. Media values are used exactly as the earlier Step stored them.\nconst list=value=>Array.isArray(value)?value:[];\nconst number=value=>typeof value==='number'?value:typeof value==='string'&&value.trim()?Number(value):NaN;\nconst positive=value=>{const n=number(value);return Number.isFinite(n)&&n>0?n:0;};\nconst round=value=>Math.round(value*1000)/1000;\nconst copy=value=>JSON.parse(JSON.stringify(value));\nconst pick=(value,keys)=>{const out={};for(const key of keys)if(value[key]!==undefined&&value[key]!==null)out[key]=copy(value[key]);return out;};\nconst playable=timeline=>!!timeline&&typeof timeline==='object'&&!Array.isArray(timeline)&&(list(timeline.clips).length>0||list(timeline.audioItems).length>0);\nconst settings={fps:Number(input.renderFps||30),resolution:Number(input.renderResolution||1080),quality:String(input.renderQuality||'high')};\nconst aspect=String(input.renderAspect||'16:9');\nif(![24,30,60].includes(settings.fps)||![720,1080,2160].includes(settings.resolution)||!['low','medium','high'].includes(settings.quality)||!['16:9','9:16','1:1'].includes(aspect))throw new Error('Choose approved export settings.');\nconst MEDIA_FIELDS=['kind','assetId','versionId','url','source','localKey','original','mimeType','width','height'];\n// A slow push in or out keeps a still photo alive on screen.\nconst motion=index=>({type:'zoom',zoomAmount:1.08,zoomDirection:index%2?'out':'in',intensity:100,easing:'ease-in-out'});\n\nfunction assemble(){\n const visuals=list(input.media).filter(item=>item&&typeof item==='object'&&(item.kind==='image'||item.kind==='video')&&typeof item.url==='string'&&item.url).slice(0,500);\n const shots=list(input.projectShotPlan&&input.projectShotPlan.shots).filter(shot=>shot&&typeof shot==='object'&&!Array.isArray(shot));\n if(!visuals.length)throw new Error(shots.length?`This project has ${shots.length} shot${shots.length===1?'':'s'} but no photos or clips to show. Connect them from an earlier Step, such as Import media, and run that Step.`:'There is nothing to show yet. Connect photos or clips from an earlier Step, such as Import media, or connect a timeline.');\n const voice=input.narration&&typeof input.narration==='object'&&input.narration.kind==='audio'&&typeof input.narration.url==='string'&&input.narration.url?input.narration:null;\n const spoken=voice?positive(voice.duration):0;\n const timing=shot=>{const start=number(shot.startTime),end=number(shot.endTime);return Number.isFinite(start)&&Number.isFinite(end)&&start>=0&&end>start?{start,end}:null;};\n const timed=shots.map(timing);\n const lengths=(items,length)=>{let cursor=0;return items.map((item,index)=>{const unit={shot:item.shot,start:cursor,end:cursor+length(item,index)};cursor=unit.end;return unit;});};\n let units;\n // Each timed shot lasts until the next one starts, so a pause stays with the shot before it.\n if(shots.length&&timed.every((t,index)=>t&&(!index||t.start>=timed[index-1].start)))units=shots.map((shot,index)=>({shot,start:index?timed[index].start:0,end:index<shots.length-1?timed[index+1].start:timed[index].end}));\n else if(shots.length)units=lengths(shots.map(shot=>({shot})),({shot},index)=>timed[index]?timed[index].end-timed[index].start:positive(shot.estimatedScreenTime)||positive(shot.durationMs)/1000||3);\n else units=[];\n units=units.filter(unit=>unit.end-unit.start>0.01);\n // No shot plan: each photo shows for 3 seconds and each clip for its own length (at most a minute).\n if(!units.length)units=lengths(visuals.map(()=>({shot:null})),(_,index)=>visuals[index].kind==='video'&&positive(visuals[index].duration)?Math.min(positive(visuals[index].duration),60):3);\n const planned=units[units.length-1].end,scale=spoken>0?spoken/planned:1;\n if(planned*scale>21600)throw new Error('This video would be longer than 6 hours. Shorten the shot plan or the narration.');\n const clips=[],count=units.length,media=visuals.length;\n const place=(item,from,to,shot)=>{\n const index=clips.length,start=round(from*scale),end=round(to*scale);\n clips.push({...pick(item,MEDIA_FIELDS),id:`clip-${index+1}`,title:String(item.name||item.label||item.title||`${item.kind==='video'?'Clip':'Photo'} ${index+1}`).slice(0,200),trackId:'video',start,duration:round(end-start),trimStart:0,speed:1,...(item.kind==='image'?{motion:motion(index)}:{}),...(shot&&typeof shot.id==='string'?{shotId:shot.id}:{})});\n };\n // More photos than shots: a shot shows its photos in turn. Fewer: a photo spans neighbouring shots.\n if(media>=count)for(let shot=0;shot<count;shot++){\n const first=Math.floor(shot*media/count),last=Math.floor((shot+1)*media/count),unit=units[shot],step=(unit.end-unit.start)/(last-first);\n for(let item=first;item<last;item++)place(visuals[item],unit.start+(item-first)*step,item===last-1?unit.end:unit.start+(item-first+1)*step,unit.shot);\n }\n else for(let item=0;item<media;item++){\n const first=Math.floor(item*count/media),last=Math.floor((item+1)*count/media);\n place(visuals[item],units[first].start,units[last-1].end,units[first].shot);\n }\n const shown=clips.reduce((max,clip)=>Math.max(max,clip.start+clip.duration),0),targetDuration=round(Math.max(shown,spoken));\n const audioItems=voice?[{...pick(voice,['kind','assetId','versionId','url','source','localKey','original','mimeType']),id:'narration',title:'Narration',trackId:'narration',start:0,duration:spoken?round(spoken):targetDuration,trimStart:0,volume:100}]:[];\n return {schemaVersion:1,aspect,clips,audioItems,captions:[],textOverlays:[],effects:[],targetDuration,trackOrder:['video']};\n}\n\nconst chosen=playable(input.input)?input.input:playable(input.projectTimeline)?input.projectTimeline:null;\nconst timeline=chosen?copy(chosen):assemble();\nconst clips=list(timeline.clips);\nconst audio=list(timeline.audioItems);\nconst overlays=[...list(timeline.captions),...list(timeline.textOverlays),...list(timeline.effects)];\nconst end=item=>Number(item.start||0)+Number(item.duration||0);\nconst targetDuration=Math.max(0,Number(timeline.targetDuration)||0,...clips.map(end),...audio.map(end),...overlays.map(end));\nif(!Number.isFinite(targetDuration)||targetDuration<=0||targetDuration>21600)throw new Error('The export duration must be between 0 and 21600 seconds.');\nreturn {mediaAction:{schemaVersion:1,kind:'stillmade.media-action',operation:'final.render',request:{schemaVersion:1,kind:'video',timeline:{...timeline,clips,audio,overlays,targetDuration},settings,source:{id:'sm.export-video',version:'2.1.0'}},resultPort:'output'}};\n"
|
|
574
|
+
},
|
|
575
|
+
"lineage": {
|
|
576
|
+
"parents": [],
|
|
577
|
+
"changes": "Packaged original Block source for StillMade."
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|