stillmade-sdk 0.1.42 → 0.1.44

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.
Files changed (262) hide show
  1. package/.stillmade/sdk-baseline.json +6 -8
  2. package/AGENTS.md +15 -10
  3. package/FRAME_VIEWS.md +113 -0
  4. package/INTERFACE_DESIGN.md +62 -22
  5. package/MOBILE_UI.md +4 -2
  6. package/MODULES.md +152 -6
  7. package/PASTE_TO_LLM.md +35 -25
  8. package/README.md +3 -2
  9. package/SDK.md +994 -550
  10. package/UI_AESTHETIC.md +22 -2
  11. package/blocks/generate/package.js +1 -0
  12. package/blocks/generate/versions/1.0.0/package.js +463 -0
  13. package/docs/build/audio-to-editor.md +36 -0
  14. package/docs/build/block-builder-contract.md +150 -0
  15. package/docs/build/comfyui.md +227 -0
  16. package/docs/build/concepts.md +40 -0
  17. package/docs/build/context.md +159 -0
  18. package/docs/build/cookbook.md +57 -0
  19. package/docs/build/custom-interface.md +617 -0
  20. package/docs/build/data-types.md +83 -0
  21. package/docs/build/frame-views.md +259 -0
  22. package/docs/build/generate-media.md +59 -0
  23. package/docs/build/github-applications.md +314 -0
  24. package/docs/build/hosted-images.md +214 -0
  25. package/docs/build/hosted-music.md +39 -0
  26. package/docs/build/hosted-speech.md +157 -0
  27. package/docs/build/hosted-text.md +246 -0
  28. package/docs/build/image-description.md +49 -0
  29. package/docs/build/interface-design.md +343 -0
  30. package/docs/build/javascript.md +63 -0
  31. package/docs/build/local-preview.md +61 -0
  32. package/docs/build/media-analysis.md +27 -0
  33. package/docs/build/media.md +400 -0
  34. package/docs/build/mobile-ui.md +106 -0
  35. package/docs/build/modules.md +487 -0
  36. package/docs/build/original-blocks.md +70 -0
  37. package/docs/build/outside-apis.md +128 -0
  38. package/docs/build/production-context-templates.md +12 -0
  39. package/docs/build/project-storage.md +94 -0
  40. package/docs/build/quickstart.md +48 -0
  41. package/docs/build/recipes.md +103 -0
  42. package/docs/build/script-handoff.md +45 -0
  43. package/docs/build/script-writer.md +34 -0
  44. package/docs/build/state.md +223 -0
  45. package/docs/build/testing.md +112 -0
  46. package/docs/build/timeline-edits.md +116 -0
  47. package/docs/build/timeline-proposals.md +76 -0
  48. package/docs/build/transcription.md +59 -0
  49. package/docs/build/triggers.md +59 -0
  50. package/docs/build/tutorial.md +52 -0
  51. package/docs/build/typescript.md +34 -0
  52. package/docs/build/video-generation.md +61 -0
  53. package/docs/build/web-reading.md +37 -0
  54. package/docs/build/with-your-ai.md +27 -0
  55. package/docs/changelog.md +9 -0
  56. package/docs/creators.md +21 -0
  57. package/docs/distribute/discovery.md +43 -0
  58. package/docs/distribute/imports.md +58 -0
  59. package/docs/distribute/publishing.md +29 -0
  60. package/docs/distribute/sdk-versions.md +36 -0
  61. package/docs/distribute/selling.md +14 -0
  62. package/docs/distribute/teams.md +60 -0
  63. package/docs/distribute/versioning.md +28 -0
  64. package/docs/examples.md +23 -0
  65. package/docs/gallery.md +26 -0
  66. package/docs/index.md +50 -0
  67. package/docs/native-mcp.md +307 -0
  68. package/docs/project-types/automation.md +43 -0
  69. package/docs/project-types/batches.md +50 -0
  70. package/docs/project-types/build.md +42 -0
  71. package/docs/project-types/completion.md +15 -0
  72. package/docs/project-types/conditions.md +21 -0
  73. package/docs/project-types/hosted-review.md +115 -0
  74. package/docs/project-types/onboarding.md +75 -0
  75. package/docs/project-types/running.md +309 -0
  76. package/docs/project-types/testing.md +72 -0
  77. package/docs/reference/application-artifacts.md +87 -0
  78. package/docs/reference/authoring-guide.md +134 -0
  79. package/docs/reference/browser-applications.md +267 -0
  80. package/docs/reference/capabilities.md +45 -0
  81. package/docs/reference/cli.md +71 -0
  82. package/docs/reference/connection-api.md +59 -0
  83. package/docs/reference/cooperative-jobs.md +13 -0
  84. package/docs/reference/desktop-capabilities.md +172 -0
  85. package/docs/reference/desktop-execution.md +77 -0
  86. package/docs/reference/errors.md +233 -0
  87. package/docs/reference/execution.md +800 -0
  88. package/docs/reference/first-party-blocks.md +40 -0
  89. package/docs/reference/generation-models.md +59 -0
  90. package/docs/reference/limits.md +306 -0
  91. package/docs/reference/manifest-kinds-controls.md +36 -0
  92. package/docs/reference/manifest.md +181 -0
  93. package/docs/reference/operations.md +36 -0
  94. package/docs/reference/package-jobs.md +35 -0
  95. package/docs/reference/parity.md +55 -0
  96. package/docs/reference/permissions.md +86 -0
  97. package/docs/reference/ports.md +26 -0
  98. package/docs/reference/types.md +268 -0
  99. package/docs/reference/universal-resolver.md +303 -0
  100. package/docs/reference/view-bridge.md +60 -0
  101. package/docs/reference/view-rules.md +15 -0
  102. package/docs/reference/workspace-sync.md +25 -0
  103. package/docs/sdk.md +34 -0
  104. package/docs/tools/block-remix.md +211 -0
  105. package/docs/tools/llm.md +2213 -0
  106. package/docs/tools/repository-adaptation.md +613 -0
  107. package/examples/check-clip.stillmade.json +4 -1
  108. package/examples/generate-video.stillmade.json +6 -2
  109. package/examples/research-topic.stillmade.json +5 -1
  110. package/examples/text-workflow.stillmade.json +3 -32
  111. package/examples/timeline-polish.stillmade.json +11 -3
  112. package/examples/transcribe-audio.stillmade.json +11 -3
  113. package/package.json +1 -1
  114. package/packages/block-cli/cli.js +13 -6
  115. package/packages/block-cli/dev-host/assets/host.js +76 -38
  116. package/packages/block-cli/dev-host/assets/index.css +1 -1
  117. package/packages/block-cli/format.js +1 -1
  118. package/packages/block-cli/templates.js +31 -13
  119. package/packages/block-cli/type-packages.js +119 -0
  120. package/packages/block-platform/block-readiness.js +5 -0
  121. package/packages/block-platform/canvas.js +2 -1
  122. package/packages/block-platform/catalog.js +27 -48
  123. package/packages/block-platform/native-host-inventory.js +0 -226
  124. package/packages/block-platform/plain-descriptions.js +1 -25
  125. package/packages/block-platform/project-type-starters.js +13 -53
  126. package/packages/block-platform/script-handoff.js +2 -4
  127. package/packages/block-platform/type-workspace.js +2 -0
  128. package/packages/block-sdk/appearance.js +3 -1
  129. package/packages/block-sdk/block-tier.js +1 -1
  130. package/packages/block-sdk/builder-contract.js +10 -8
  131. package/packages/block-sdk/capabilities.js +1 -1
  132. package/packages/block-sdk/contracts.js +6 -2
  133. package/packages/block-sdk/first-party-identities.js +1 -84
  134. package/packages/block-sdk/first-party-packages.js +3 -85
  135. package/packages/block-sdk/generation-catalog.js +15 -3
  136. package/packages/block-sdk/index.d.ts +2 -1
  137. package/packages/block-sdk/media-analysis-example.js +1 -1
  138. package/packages/block-sdk/mock-host.js +7 -1
  139. package/packages/block-sdk/module-host-calls.js +35 -10
  140. package/packages/block-sdk/module-hosted.js +15 -2
  141. package/packages/block-sdk/module-media-access.js +71 -0
  142. package/packages/block-sdk/module-node.js +1 -1
  143. package/packages/block-sdk/portable-media-actions.js +1 -1
  144. package/packages/block-sdk/runtime.js +1 -1
  145. package/packages/block-sdk/timeline-proposal-example.js +1 -1
  146. package/packages/block-sdk/transcription-example.js +1 -1
  147. package/packages/block-sdk/video-generation-example.js +1 -1
  148. package/packages/block-sdk/view-bridge.js +4 -1
  149. package/packages/block-sdk/view-design.js +17 -14
  150. package/packages/block-sdk/web-example.js +1 -1
  151. package/packages/design-system/block-kit.css +48 -12
  152. package/packages/design-system/block-kit.html +3 -2
  153. package/blocks/mcp-motion-planner/versions/1.1.0/package.js +0 -97
  154. package/blocks/motion-designer/versions/1.0.1/package.js +0 -173318
  155. package/blocks/motion-designer/versions/1.0.2/package.js +0 -173318
  156. package/blocks/motion-frame-review/versions/1.1.1/package.js +0 -465
  157. package/blocks/motion-frame-review/versions/1.1.2/package.snapshot.js +0 -572
  158. package/blocks/narration/package.js +0 -1
  159. package/blocks/narration/versions/1.0.0/package.js +0 -221
  160. package/blocks/narration/versions/1.0.1/package.js +0 -221
  161. package/blocks/native/sm.animated-scene/versions/2.0.0/package.js +0 -475
  162. package/blocks/native/sm.animated-scene/versions/2.1.0/package.js +0 -498
  163. package/blocks/native/sm.animated-scene/versions/2.2.0/package.js +0 -524
  164. package/blocks/native/sm.animated-scene/versions/2.3.0/package.js +0 -618
  165. package/blocks/native/sm.animated-scene/versions/2.3.1/package.js +0 -618
  166. package/blocks/native/sm.animated-scene/versions/2.3.2/package.js +0 -723
  167. package/blocks/native/sm.animated-scene/versions/2.3.3/package.js +0 -723
  168. package/blocks/native/sm.animated-scene/versions/2.3.4/package.js +0 -676
  169. package/blocks/native/sm.animated-scene/versions/2.3.5/package.js +0 -693
  170. package/blocks/native/sm.animated-scene/versions/2.3.6/package.js +0 -700
  171. package/blocks/native/sm.animated-scene/versions/2.3.7/package.js +0 -747
  172. package/blocks/native/sm.animated-scene/versions/2.3.8/package.js +0 -769
  173. package/blocks/native/sm.animated-scene/versions/2.3.9/package.js +0 -769
  174. package/blocks/native/sm.animated-scene-assistant/versions/1.0.0/package.js +0 -183
  175. package/blocks/native/sm.blueprint/versions/2.0.0/package.js +0 -358
  176. package/blocks/native/sm.blueprint/versions/2.1.0/package.js +0 -358
  177. package/blocks/native/sm.blueprint/versions/2.1.1/package.js +0 -358
  178. package/blocks/native/sm.blueprint/versions/2.1.2/package.js +0 -358
  179. package/blocks/native/sm.blueprint/versions/2.1.3/package.js +0 -381
  180. package/blocks/native/sm.blueprint/versions/2.1.4/package.js +0 -381
  181. package/blocks/native/sm.blueprint/versions/2.1.5/package.js +0 -339
  182. package/blocks/native/sm.blueprint/versions/2.1.6/package.js +0 -356
  183. package/blocks/native/sm.blueprint/versions/2.1.7/package.js +0 -363
  184. package/blocks/native/sm.blueprint/versions/2.1.8/package.js +0 -410
  185. package/blocks/native/sm.blueprint/versions/2.1.9/package.js +0 -422
  186. package/blocks/native/sm.blueprint-assistant/versions/1.0.0/package.js +0 -172
  187. package/blocks/native/sm.chat/versions/2.0.0/package.js +0 -144
  188. package/blocks/native/sm.chat/versions/2.0.1/package.js +0 -97
  189. package/blocks/native/sm.chat/versions/2.0.2/package.js +0 -100
  190. package/blocks/native/sm.chat/versions/2.0.3/package.js +0 -100
  191. package/blocks/native/sm.continue-if/versions/1.0.0/package.js +0 -168
  192. package/blocks/native/sm.describe-media/versions/2.0.0/package.js +0 -153
  193. package/blocks/native/sm.describe-media/versions/2.0.1/package.js +0 -156
  194. package/blocks/native/sm.describe-media/versions/2.1.0/package.js +0 -140
  195. package/blocks/native/sm.image-editor/versions/2.0.0/package.js +0 -183
  196. package/blocks/native/sm.image-editor/versions/2.0.1/package.js +0 -190
  197. package/blocks/native/sm.new-items/versions/1.0.0/package.js +0 -191
  198. package/blocks/native/sm.pick-photos/versions/1.0.0/package.js +0 -334
  199. package/blocks/native/sm.quality-audit/versions/1.0.0/package.js +0 -192
  200. package/blocks/native/sm.quality-audit/versions/1.0.1/package.js +0 -192
  201. package/blocks/native/sm.quality-audit/versions/1.0.2/package.js +0 -195
  202. package/blocks/native/sm.research/versions/2.0.0/package.js +0 -175
  203. package/blocks/native/sm.research/versions/2.0.1/package.js +0 -184
  204. package/blocks/native/sm.research/versions/2.0.2/package.js +0 -187
  205. package/blocks/native/sm.script/versions/2.0.0/package.js +0 -144
  206. package/blocks/native/sm.script/versions/2.1.0/package.js +0 -144
  207. package/blocks/native/sm.script/versions/2.1.1/package.js +0 -144
  208. package/blocks/native/sm.script/versions/2.1.2/package.js +0 -105
  209. package/blocks/native/sm.script/versions/2.1.3/package.js +0 -152
  210. package/blocks/native/sm.script/versions/2.1.4/package.js +0 -156
  211. package/blocks/native/sm.script/versions/2.1.5/package.js +0 -156
  212. package/blocks/native/sm.shot-plan/versions/2.0.0/package.js +0 -301
  213. package/blocks/native/sm.shot-plan/versions/2.1.0/package.js +0 -528
  214. package/blocks/native/sm.shot-plan/versions/2.1.1/package.js +0 -528
  215. package/blocks/native/sm.shot-plan/versions/2.1.2/package.js +0 -528
  216. package/blocks/native/sm.shot-plan/versions/2.1.3/package.js +0 -536
  217. package/blocks/native/sm.shot-plan/versions/2.2.0/package.js +0 -800
  218. package/blocks/native/sm.shot-plan/versions/2.2.1/package.js +0 -920
  219. package/blocks/native/sm.shot-plan-assistant/versions/1.0.0/package.js +0 -173
  220. package/blocks/native/sm.stock-search/versions/2.0.0/package.js +0 -174
  221. package/blocks/native/sm.thumbnail/versions/2.0.0/package.js +0 -137
  222. package/blocks/native/sm.thumbnail/versions/2.0.1/package.js +0 -140
  223. package/blocks/native/sm.transcribe/versions/2.0.0/package.js +0 -163
  224. package/blocks/native/sm.transcribe/versions/2.0.1/package.js +0 -168
  225. package/blocks/native/sm.whiteboard-assistant/versions/1.0.0/package.js +0 -175
  226. package/blocks/native/sm.youtube/versions/2.0.0/package.js +0 -217
  227. package/blocks/occasion-story/package.js +0 -1
  228. package/blocks/occasion-story/versions/1.0.0/package.js +0 -1658
  229. package/blocks/occasion-story/versions/1.0.1/package.js +0 -1699
  230. package/blocks/occasion-story/versions/1.0.2/package.js +0 -1682
  231. package/blocks/photo-reel-composer/versions/1.0.0/package.js +0 -3043
  232. package/blocks/photo-reel-composer/versions/1.0.1/package.js +0 -3026
  233. package/blocks/photo-reel-composer/versions/1.0.2/package.js +0 -4149
  234. package/blocks/product-ad-variants/versions/1.0.0/package.js +0 -2911
  235. package/blocks/product-launch-director/versions/1.0.0/package.js +0 -103
  236. package/blocks/release-visuals/package.js +0 -1
  237. package/blocks/release-visuals/versions/1.0.0/package.js +0 -1492
  238. package/blocks/release-visuals/versions/1.0.1/package.js +0 -1517
  239. package/blocks/release-visuals/versions/1.0.2/package.js +0 -1500
  240. package/blocks/screen-recorder/package.js +0 -1
  241. package/blocks/screen-recorder/versions/1.0.0/package.js +0 -223
  242. package/blocks/screen-recorder/versions/1.0.1/package.js +0 -223
  243. package/blocks/short-form-finish/package.js +0 -1
  244. package/blocks/short-form-finish/versions/1.0.0/package.js +0 -1043
  245. package/blocks/short-form-finish/versions/1.0.1/package.js +0 -1043
  246. package/blocks/short-form-finish/versions/1.0.2/package.js +0 -1043
  247. package/blocks/short-form-finish/versions/1.0.3/package.js +0 -1026
  248. package/examples/saas-product-launch-reviewed-source.stillmade.json +0 -383
  249. package/examples/saas-product-launch.stillmade.json +0 -304
  250. package/packages/block-platform/dealer-inventory-video-project-type.js +0 -38
  251. package/packages/block-platform/first-revenue-project-types.js +0 -46
  252. package/packages/block-platform/fixture-starters.js +0 -13
  253. package/packages/block-platform/launch-project-source.js +0 -10
  254. package/packages/block-platform/launch-project.js +0 -30
  255. package/packages/block-platform/motion-project-types.js +0 -55
  256. package/packages/block-platform/motion-starters.js +0 -14
  257. package/packages/block-platform/product-ad-variants-project-type.js +0 -30
  258. package/packages/block-platform/product-launch-updated.js +0 -90
  259. package/packages/block-platform/revenue-review-project-types.js +0 -36
  260. package/packages/block-platform/revenue-review-starters.js +0 -13
  261. package/packages/block-platform/script-writer.js +0 -29
  262. package/packages/block-platform/website-source-block.js +0 -14
package/SDK.md CHANGED
@@ -221,19 +221,20 @@ Pick the runtime from the job. No hosted AI: a recipe or isolated `javascript`,
221
221
 
222
222
  ### One job per Step
223
223
 
224
- A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.
224
+ A Step is one job: the work between two moments where a person or the outside world must see the result. A guided job, one the person fills in and then gets a result from, is one Step with one Block whose own screen holds everything: the inputs and settings, then a cheap check before the expensive run (the script and a voice sample before the video), then the result. On a wide screen the inputs sit on the left and the result's place on the right; on a phone they stack, the result's place under the button. A cheap check or a review the person does on that screen never starts a new Step. Start a new Step only at one of these points; everything else is a part of the neighboring Step.
225
225
 
226
- 1. **A person works with the result**: reads, edits, picks or approves it (a workspace Block such as a script, a Canvas or an Editor, or an approval). So Bible, Canvas and Editor are separate Steps after Chat.
226
+ 1. **A person works with the result**: in a workspace they spend a while in (the Editor, a storyboard, a Canvas, a CAD or drawing tool), or someone else approves or takes it over (a hand-off). Turning what a workspace made into something else is the next Step: a CAD tool is its own Step, and the Block that takes pictures of its model and makes a video is the next one. So Bible, Canvas and Editor are separate Steps after Chat.
227
227
  2. **Finding new things turns into handling each one**: the trigger (a schedule or an app event), the app read, its filter and "only new" are the first job; what is done with each item is the next job. A trigger is a setting of the first job, never a Step of its own.
228
228
  3. **The result leaves StillMade**: it is sent, posted, published, paid or written into another app. Consecutive writes to the same app, with the glue that shapes data for them, are one job.
229
+ 4. **Setup or a wait**: a one-time setup that is not part of each job (a brand kit, capturing a voice or a likeness), or a render that takes minutes and is delivered later.
229
230
 
230
- In a production the Steps are the workspaces a person opens: work feeding the next workspace is its part, else it joins the one before. A Block with its own interface (a workspace, editor or picker) is always its own Step; two such Steps are never one. A video ends in the Editor (1.0), which has captions and export built in, so Export video is never a Step beside it.
231
+ In a production the Steps are the workspaces a person opens: work feeding the next workspace is its part, else it joins the one before. A Block with its own interface (a workspace, editor or picker) is always its own Step; two such Steps are never one. A Type whose own Block renders the finished video (`timeline.render`) needs no Editor Step; the Editor (1.0, captions and export built in) is a Step only when the person edits the cut by hand, and Export video is never a Step beside it.
231
232
 
232
- A part that only runs in the desktop app cannot share a Step with cloud parts. Before adding a Step, name which of the three reasons makes it separate; if none applies, make it a part of a neighbor. A Step nobody would ever open must not exist on its own. Prefer an app task's own filter (a Gmail query such as `has:attachment filename:png`) to an AI check, and use AI decide only for judgement.
233
+ A part that only runs in the desktop app cannot share a Step with cloud parts. Before adding a Step, name which of these reasons makes it separate; if none applies, make it a part of a neighbor. A Step nobody would ever open must not exist on its own. Prefer an app task's own filter (a Gmail query such as `has:attachment filename:png`) to an AI check, and use AI decide only for judgement.
233
234
 
234
235
  Build one Block for a job one Block can do: a module Block calls every hosted operation the job needs (it transcribes a sermon, then finds the verses said in it), a JavaScript Block does several local transforms, a workspace owns its whole document. A Block being built never takes an ingredient's result only so the ingredient can be its own part or Step (Transcribe in front of a verse finder): it calls that operation itself. Parts in one Step (`groups` in the Project Type) are for jobs made of Blocks that already exist and app tasks, each keeping its own permission, approval and credit rules. Never invent a combined runtime.
235
236
 
236
- Examples: "every new email with an image becomes a generated image" is two Steps, "Gmail · New emails with images" (When, fetch with an image filter, only new) and "Image from email" (AI write, Generate image); listing When, Fetch, Only new, AI decide, Continue only if, AI write and Generate image as seven Steps splits one job into its parts. "A new Meta lead goes to HubSpot" is three Steps: "Meta Ads · New leads", "Qualify lead" and "HubSpot · Add contact".
237
+ Examples: "a listing video from my photos and the home's facts" is one Step: one Block whose page takes the photos and facts, shows the script and a voice sample to check, then makes the video. "every new email with an image becomes a generated image" is two Steps, "Gmail · New emails with images" (When, fetch with an image filter, only new) and "Image from email" (AI write, Generate image); listing When, Fetch, Only new, AI decide, Continue only if, AI write and Generate image as seven Steps splits one job into its parts. "A new Meta lead goes to HubSpot" is three Steps: "Meta Ads · New leads", "Qualify lead" and "HubSpot · Add contact".
237
238
 
238
239
  ### Define the capability first
239
240
 
@@ -261,7 +262,7 @@ Give the Block a short, plain name of 40 characters or fewer that says what it d
261
262
 
262
263
  ### Name inputs in the job's words
263
264
 
264
- Name each input for what the person brings, in the job's words: `sermonVideo` for a sermon clip Block, `listingPhotos` for a listing, `lessonSlides` for a lesson podcast. StillMade titles the input from that name ("Sermon video") and words its drop area from it ("Drop your sermon video here", the formats, "Choose a video"), in StillMade and the SDK preview, so never draw your own upload box for an input. Keep a bare type name (`video`, `file`) only when any file of that kind does the job. The `description` says what makes a good one.
265
+ Name each input for what the person brings, in the job's words: `sermonVideo` for a sermon clip Block, `listingPhotos` for a listing, `lessonSlides` for a lesson podcast. StillMade titles the input from that name ("Sermon video") and words its drop area from it ("Drop your sermon video here", the formats, "Choose a video"), in StillMade and the SDK preview, so never draw your own upload box or `<input type="file">`: a Block's screen shows the files as tiles and adds more with `<button class="sm-add" data-stillmade-add="listingPhotos" aria-label="Add photos">`, which opens StillMade's picker and upload, and a list's tiles drop or reorder a file with `data-stillmade-remove="listingPhotos" data-index="2"` and `data-stillmade-move="listingPhotos" data-index="2" data-to="1"` buttons; the files arrive through `onInput`. Keep a bare type name (`video`, `file`) only when any file of that kind does the job. The `description` says what makes a good one.
265
266
 
266
267
  ### Speak the job
267
268
 
@@ -269,7 +270,7 @@ These are StillMade's defaults: unless the person or the creator asked for somet
269
270
 
270
271
  ### Interface
271
272
 
272
- If the capability needs UI, use the Block UI SDK: generated `manifest.ui` controls first; a sandboxed custom view (`view.html`/`view.css`/`view.js`) only when the core interaction needs one. Build custom views from the Block kit the host injects (`block-kit.css`): the result in `sm-stage`, picks as pictures in `sm-choices`, the picked one in the kit's ink fill (never restyled to an outline, a tint or the page's background) so it reads at a glance, one solid `sm-primary` naming the action in the job's words ("Plan the tour", "Generate voiceover"; never "Run" or "Make it") and no price (StillMade's run button adds it), extras in a closed `sm-more`; a look the person names wins. Fit one laptop screen without scrolling: label beside control, status in a few words, files as tiles, rare tools as icons, (i) on unclear controls. These compact rules are for a job or an ingredient. Size the view to the Block's tier (block-tier.js): an app, a workspace a person works in for a while (video editor, 3D configurator, CAD or drawing tool), looks and works like that kind of app instead of a compact page: its canvas or stage fills the view, with the panels, toolbars, inspector or timeline such an app has, direct manipulation (select, drag, zoom), keyboard shortcuts and undo; it keeps the host appearance and Block kit controls, its panels may scroll, and on a phone its tools fold into sheets while the canvas stays usable. Never shrink an app into a form of fields and one button. StillMade centers a standard view narrower than its Step and zooms a view to fill its space; an app that lays itself out (full-size canvas or WebGL, its own scroll, pan or zoom, its own Ctrl/Cmd +/- keys) puts `data-stillmade-layout="own"` on its outermost element, and StillMade then never centers, zooms or takes those keys. StillMade draws one player for every `<video controls>` (play, timeline, time, sound and full screen, shown while the pointer is on the video, with keys and touch) and a full-screen button on result pictures in `sm-stage` or `sm-gallery` and on any element marked `data-stillmade-expand` (the same value makes one set of pictures, `off` removes it); `data-stillmade-player="native"` keeps the browser's controls, and a `<video>` without `controls` is the Block's own and is never touched. A picture or clip that cannot show here (kept on another device, or it did not open) keeps its place as a quiet `sm-media-off` tile in its own shape with the `image-off` glyph and the reason in its label, never words in its place. Drop words nobody needs; put helpful ones in a closed `sm-hint`. Mark each button `data-stillmade-action="what it does"` and label each field: Chat presses and fills them. Expose the capability, not the implementation: sensible defaults, previews, progress, clear errors, visible outputs. Show no raw JSON, internal IDs, filesystem details, provider terms or developer settings unless the Block is a developer tool. When a person chooses among options they need to hear or see (voices, music, fonts, looks, templates, images), let them preview each option before choosing: a sample or thumbnail from the Block's own files or the host's free samples, never a paid generation. Skip previews only when there is nothing to preview. Choices StillMade owns (voices, models in the run review) already show samples. Follow the host appearance (Light, Dark and White) and one elastic layout: stacked on a 320px phone with no sideways scroll (result first, action pinned), and from 860px the controls left (≤380px), the result filling the right. Short settings sit side by side, each label beside its control (`sm-field`, `sm-settings`; three or four short options as one `data-segmented` row); many items are one line each (`sm-list`) that open in place; input files are small tiles (`sm-gallery data-size="small"`); on a phone chip rows scroll sideways and long lists show three. Compacting moves controls, never deletes them.
273
+ If the capability needs UI, use the Block UI SDK: generated `manifest.ui` controls first; a sandboxed custom view (`view.html`/`view.css`/`view.js`) only when the core interaction needs one. Build custom views from the Block kit the host injects (`block-kit.css`): the result in `sm-stage`, drawn before the first run in the shape and size of what is coming (`data-ratio="9:16"` for a vertical clip, lines for text, a player bar for sound) so every part of the screen is in place at once and the result fills a place the person already sees instead of popping in somewhere unexpected; StillMade draws it the same way on the Steps it draws; on a wide screen what goes in sits on the left and the result on the right, side by side, and they stack only on narrow screens and phones, the result's place under the button, picks as pictures in `sm-choices`, the picked one in the kit's ink fill (never restyled to an outline, a tint or the page's background) so it reads at a glance, one solid `sm-primary` naming the action in the job's words ("Plan the tour", "Generate voiceover"; never "Run" or "Make it") and no price (StillMade's run button adds it), extras in a closed `sm-more`; a look the person names wins. Fit one laptop screen without scrolling: label beside control, status in a few words, files as tiles, rare tools as icons, (i) on unclear controls. These compact rules are for a job or an ingredient. Size the view to the Block's tier (block-tier.js): an app, a workspace a person works in for a while (video editor, 3D configurator, CAD or drawing tool), looks and works like that kind of app instead of a compact page: its canvas or stage fills the view, with the panels, toolbars, inspector or timeline such an app has, direct manipulation (select, drag, zoom), keyboard shortcuts and undo; it keeps the host appearance and Block kit controls, its panels may scroll, and on a phone its tools fold into sheets while the canvas stays usable. Never shrink an app into a form of fields and one button. StillMade centers a standard view narrower than its Step and zooms a view to fill its space; an app that lays itself out (full-size canvas or WebGL, its own scroll, pan or zoom, its own Ctrl/Cmd +/- keys) puts `data-stillmade-layout="own"` on its outermost element, and StillMade then never centers, zooms or takes those keys. StillMade draws one player for every `<video controls>` (play, timeline, time, sound and full screen, shown while the pointer is on the video, with keys and touch) and a full-screen button on result pictures in `sm-stage` or `sm-gallery` and on any element marked `data-stillmade-expand` (the same value makes one set of pictures, `off` removes it); `data-stillmade-player="native"` keeps the browser's controls, and a `<video>` without `controls` is the Block's own and is never touched. A picture or clip that cannot show here (kept on another device, or it did not open) keeps its place as a quiet `sm-media-off` tile in its own shape with the `image-off` glyph and the reason in its label, never words in its place. Drop words nobody needs; put helpful ones in a closed `sm-hint`. Mark each button `data-stillmade-action` (`run` on the one that runs the Block, which the device checks press once; a page that checks first keeps `run` on that button for both presses; what it does on the others, `"play sample"`) and label each field: Chat presses and fills them. Expose the capability, not the implementation: sensible defaults, previews, progress, clear errors, visible outputs. Show no raw JSON, internal IDs, filesystem details, provider terms or developer settings unless the Block is a developer tool. When a person chooses among options they need to hear or see (voices, music, fonts, looks, templates, images), let them preview each option before choosing: a sample or thumbnail from the Block's own files or the host's free samples, never a paid generation. Skip previews only when there is nothing to preview. Choices StillMade owns (voices, models in the run review) already show samples. Follow the host appearance (Light, Dark and White) and one elastic layout: stacked on a 320px phone with no sideways scroll (the inputs, the button in normal flow, never pinned, then the result's place under it), and from 860px the controls left (≤380px), the result filling the right. Short settings sit side by side, each label beside its control (`sm-field`, `sm-settings`; three or four short options as one `data-segmented` row); many items are one line each (`sm-list`) that open in place; input files are small tiles (`sm-gallery data-size="small"`); on a phone chip rows scroll sideways and long lists show three. Compacting moves controls, never deletes them.
273
274
 
274
275
  ### Shared editing
275
276
 
@@ -277,11 +278,11 @@ Projects are multiplayer. A custom view subscribes through `StillMade.onShared(c
277
278
 
278
279
  ### External services
279
280
 
280
- Reach external services only through StillMade's hosted operations, the connections service (`connection.execute`), or, in a module Block, `stillmade.net.fetch` to origins declared in `permissions.network` (with the key each needs), `stillmade.net.connect` for a live WebSocket to one of them, and `stillmade.connections.call` to adapters or catalog apps in `permissions.connections`; never put keys in code, inputs or headers: StillMade adds each person's own key on its servers. The host owns the model and provider choice, BYOK keys, StillMade credits, spending consent, authorization, token refresh, rate limits, retries and provider errors; a Block never receives a secret and never names a provider unless it must. Treat provider unavailability, timeout, cancellation and partial failure as typed outcomes the Block reports, not crashes. Generated images, video, voices, music and sound effects come from hosted operations or the `media.generate` action, paid with the user's StillMade credits at StillMade's prices; for structured model output declare a `json` output with `capability.schema` instead of parsing prose. When a Block needs npm libraries, WebAssembly (threaded: `"threads": true` in module.json) or heavy compute, use `runtime: "module"` and reach hosted operations through `stillmade.hosted.run` with the operations listed in `permissions.capabilities`. Decode, cut, join, re-encode, extract or normalise audio and video with `stillmade.media.transform` on the device; never ask for a server render. For a rich interactive interface (React or another framework, SVG, canvas, drag), use a frame view: `src/view.config.json` with `{"runtime":"frame"}` and `src/view.js` bundled as one IIFE, colored with StillMade's theme variables. For files and structured data use the `file`, `document`, `table`, `url`, `date` and `color` types and their generated controls and presentations, not JSON text.
281
+ Reach external services only through StillMade's hosted operations, the connections service (`connection.execute`), or, in a module Block, `stillmade.net.fetch` to origins declared in `permissions.network` (with the key each needs), `stillmade.net.connect` for a live WebSocket to one of them, and `stillmade.connections.call` to adapters or catalog apps in `permissions.connections`; never put keys in code, inputs or headers: StillMade adds each person's own key on its servers. The host owns the model and provider choice, BYOK keys, StillMade credits, spending consent, authorization, token refresh, rate limits, retries and provider errors; a Block never receives a secret and never names a provider unless it must. Treat provider unavailability, timeout, cancellation and partial failure as outcomes the Block reports, not crashes: a call the result needs throws and fails the run (see Useful failures); an optional part's failure is caught, given its fallback and named in an optional `notes` text output. Generated images, video, voices, music and sound effects come from hosted operations or the `media.generate` action, paid with the user's StillMade credits at StillMade's prices; for structured model output declare a `json` output with `capability.schema` instead of parsing prose. When a Block needs npm libraries, WebAssembly (threaded: `"threads": true` in module.json) or heavy compute, use `runtime: "module"` and reach hosted operations through `stillmade.hosted.run` with the operations listed in `permissions.capabilities`. Decode, cut, join, re-encode, extract or normalise audio and video with `stillmade.media.transform` on the device; never ask for a server render. For a rich interactive interface (React or another framework, SVG, canvas, drag), use a frame view: `src/view.config.json` with `{"runtime":"frame"}` and `src/view.js` bundled as one IIFE, colored with StillMade's theme variables. For files and structured data use the `file`, `document`, `table`, `url`, `date` and `color` types and their generated controls and presentations, not JSON text.
281
282
 
282
283
  ### Dependencies
283
284
 
284
- A Block must declare everything it requires and must not assume anything is installed. Portable packages carry no package dependencies (`portable.dependencies` is empty): inline small, license-compatible code and record its license and notice in `manifest.license` and `manifest.provenance`. Desktop-only tools are reached solely through declared desktop permissions and the host desktop bridge. If a dependency cannot be delivered through the Block runtime, report the incompatibility instead of assuming the user's machine has it.
285
+ A Block must declare everything it requires and must not assume anything is installed. Portable packages carry no package dependencies (`portable.dependencies` is empty): inline small, license-compatible code and record its license and notice in `manifest.license` and `manifest.provenance`. The copyright holder in the Block's own `provenance.notice` is the person it is made for: replace the placeholder `create` writes with their name, never StillMade, the SDK or yourself. Desktop-only tools are reached solely through declared desktop permissions and the host desktop bridge. If a dependency cannot be delivered through the Block runtime, report the incompatibility instead of assuming the user's machine has it.
285
286
 
286
287
  ### Security
287
288
 
@@ -363,19 +364,20 @@ An instance of a Block inside a Project Type. Each Step has its own instance ID,
363
364
  An ordered production workflow. Its persisted `stages` array stores Steps for compatibility with existing projects. Users can add, remove, duplicate, configure, disable, or reorder Steps. The protected starting chat belongs to the host and cannot be replaced or remixed.
364
365
 
365
366
  ### One job per Step
366
- A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.
367
+ A Step is one job: the work between two moments where a person or the outside world must see the result. A guided job, one the person fills in and then gets a result from, is one Step with one Block whose own screen holds everything: the inputs and settings, then a cheap check before the expensive run (the script and a voice sample before the video), then the result. On a wide screen the inputs sit on the left and the result's place on the right; on a phone they stack, the result's place under the button. A cheap check or a review the person does on that screen never starts a new Step. Start a new Step only at one of these points; everything else is a part of the neighboring Step.
367
368
 
368
- 1. **A person works with the result**: reads, edits, picks or approves it (a workspace Block such as a script, a Canvas or an Editor, or an approval). So Bible, Canvas and Editor are separate Steps after Chat.
369
+ 1. **A person works with the result**: in a workspace they spend a while in (the Editor, a storyboard, a Canvas, a CAD or drawing tool), or someone else approves or takes it over (a hand-off). Turning what a workspace made into something else is the next Step: a CAD tool is its own Step, and the Block that takes pictures of its model and makes a video is the next one. So Bible, Canvas and Editor are separate Steps after Chat.
369
370
  2. **Finding new things turns into handling each one**: the trigger (a schedule or an app event), the app read, its filter and "only new" are the first job; what is done with each item is the next job. A trigger is a setting of the first job, never a Step of its own.
370
371
  3. **The result leaves StillMade**: it is sent, posted, published, paid or written into another app. Consecutive writes to the same app, with the glue that shapes data for them, are one job.
372
+ 4. **Setup or a wait**: a one-time setup that is not part of each job (a brand kit, capturing a voice or a likeness), or a render that takes minutes and is delivered later.
371
373
 
372
- In a production the Steps are the workspaces a person opens: work feeding the next workspace is its part, else it joins the one before. A Block with its own interface (a workspace, editor or picker) is always its own Step; two such Steps are never one. A video ends in the Editor (1.0), which has captions and export built in, so Export video is never a Step beside it.
374
+ In a production the Steps are the workspaces a person opens: work feeding the next workspace is its part, else it joins the one before. A Block with its own interface (a workspace, editor or picker) is always its own Step; two such Steps are never one. A Type whose own Block renders the finished video (`timeline.render`) needs no Editor Step; the Editor (1.0, captions and export built in) is a Step only when the person edits the cut by hand, and Export video is never a Step beside it.
373
375
 
374
- A part that only runs in the desktop app cannot share a Step with cloud parts. Before adding a Step, name which of the three reasons makes it separate; if none applies, make it a part of a neighbor. A Step nobody would ever open must not exist on its own. Prefer an app task's own filter (a Gmail query such as `has:attachment filename:png`) to an AI check, and use AI decide only for judgement.
376
+ A part that only runs in the desktop app cannot share a Step with cloud parts. Before adding a Step, name which of these reasons makes it separate; if none applies, make it a part of a neighbor. A Step nobody would ever open must not exist on its own. Prefer an app task's own filter (a Gmail query such as `has:attachment filename:png`) to an AI check, and use AI decide only for judgement.
375
377
 
376
378
  Build one Block for a job one Block can do: a module Block calls every hosted operation the job needs (it transcribes a sermon, then finds the verses said in it), a JavaScript Block does several local transforms, a workspace owns its whole document. A Block being built never takes an ingredient's result only so the ingredient can be its own part or Step (Transcribe in front of a verse finder): it calls that operation itself. Parts in one Step (`groups` in the Project Type) are for jobs made of Blocks that already exist and app tasks, each keeping its own permission, approval and credit rules. Never invent a combined runtime.
377
379
 
378
- Examples: "every new email with an image becomes a generated image" is two Steps, "Gmail · New emails with images" (When, fetch with an image filter, only new) and "Image from email" (AI write, Generate image); listing When, Fetch, Only new, AI decide, Continue only if, AI write and Generate image as seven Steps splits one job into its parts. "A new Meta lead goes to HubSpot" is three Steps: "Meta Ads · New leads", "Qualify lead" and "HubSpot · Add contact".
380
+ Examples: "a listing video from my photos and the home's facts" is one Step: one Block whose page takes the photos and facts, shows the script and a voice sample to check, then makes the video. "every new email with an image becomes a generated image" is two Steps, "Gmail · New emails with images" (When, fetch with an image filter, only new) and "Image from email" (AI write, Generate image); listing When, Fetch, Only new, AI decide, Continue only if, AI write and Generate image as seven Steps splits one job into its parts. "A new Meta lead goes to HubSpot" is three Steps: "Meta Ads · New leads", "Qualify lead" and "HubSpot · Add contact".
379
381
 
380
382
  ### Typed connections
381
383
  An image-producing Block can connect to an image-consuming Block. Primary ports make insertion predictable; semantic roles distinguish a source image from a character reference. Secondary inputs can bind to project context.
@@ -821,11 +823,15 @@ are an additional admission gate; they do not replace this browser boundary or
821
823
  the separate computation sandbox. Keep the UI responsive: move computation into
822
824
  `code` or `recipe`; DOM JavaScript is not the CPU-metered computation runtime.
823
825
 
824
- A file input can read a user-selected image with `FileReader.readAsDataURL`, load
825
- it into an existing `<img>`, and draw it into a declared `<canvas>`. Use
826
- `getImageData` to pass `{width,height,data:Array.from(pixels.data)}` to an image
827
- input, staying within the SDK image and message budgets. Nothing is uploaded by
828
- the view. Use the host's file controls when you do not need a custom picker.
826
+ A view adds files to an input with the + tile,
827
+ `<button class="sm-add" data-stillmade-add="<input>">`, or
828
+ `StillMade.addFiles("<input>")` from its own button, never an
829
+ `<input type="file">` of its own: StillMade's picker makes the working copies
830
+ and keeps device files on the device (see "Adding files to an input" in
831
+ [Frame views](/docs/build/frame-views)). To hand pixels the view drew to an
832
+ image input, draw them into a declared `<canvas>` and pass `getImageData` as
833
+ `{width,height,data:Array.from(pixels.data)}`, staying within the SDK image and
834
+ message budgets. Nothing is uploaded by the view.
829
835
 
830
836
  The host supplies StillMade's shared Light, Dark and White theme variables and basic controls.
831
837
 
@@ -1071,6 +1077,19 @@ codec support. Remote assets need CORS support and an appropriate media content
1071
1077
  The host omits credentials and rejects redirects. Network access remains disabled
1072
1078
  inside the iframe; larger media should use the existing native Editor workspace.
1073
1079
 
1080
+ ### A check, then the expensive run, on one page
1081
+
1082
+ A job the person fills in and gets a result from is one Block on one page
1083
+ (builder contract, "One job per Step"). When the expensive run needs a cheap
1084
+ check first (the script and a voice sample before the video), the view calls
1085
+ `StillMade.run({make: 'check'})`, shows what comes back, then calls
1086
+ `StillMade.run({make: 'video', script})` with what the person checked; `make`
1087
+ and `script` are optional inputs, and the outputs the check leaves out are
1088
+ `"required": false`. Each run is priced and approved by StillMade on its own:
1089
+ the view shows no price. The manifest, the module and what the person sees are
1090
+ in [Module Blocks](/docs/build/modules), "A check, then the expensive run, on
1091
+ one page".
1092
+
1074
1093
  ## Hosted text generation
1075
1094
 
1076
1095
  Use `runtime: "capability"` to implement a portable Block that asks StillMade to
@@ -1691,139 +1710,6 @@ The authenticated host client must additionally verify account/run identity;
1691
1710
  package code and custom interfaces cannot submit receipts as execution evidence.
1692
1711
  Optional metadata remains bounded to 8 KiB and is not proof of generation.
1693
1712
 
1694
- ## Frame views: any interface code
1695
-
1696
- A frame view (`"runtime": "frame"` in the view) is an interface written with
1697
- ordinary web code: React, Svelte, Vue or Preact bundles, SVG, canvas and
1698
- WebGL, pointer capture and drag, timers, loops, recursion, classes and Web
1699
- Workers. Use it when the reviewed subset of standard views is too small for
1700
- the interface you want.
1701
-
1702
- ### Folder layout
1703
-
1704
- ```
1705
- my-block/
1706
- stillmade.block.json
1707
- src/run.js or another runtime (recipe, module, capability)
1708
- src/view.html markup, for example <div id="root"></div>
1709
- src/view.css styles; use StillMade's theme variables
1710
- src/view.js ONE classic script: bundle with --format=iife
1711
- src/view.config.json {"runtime": "frame"}
1712
- tests/fixtures.json
1713
- ```
1714
-
1715
- `src/view.config.json` may also set `"appearance"` and `"files"` (assets, see
1716
- below). Bundle npm dependencies into `src/view.js` as an IIFE, for example:
1717
-
1718
- ```sh
1719
- esbuild src/view.jsx --bundle --format=iife --minify --jsx=automatic \
1720
- --define:process.env.NODE_ENV='"production"' --outfile=src/view.js
1721
- ```
1722
-
1723
- Limits: markup and each stylesheet up to 256 KiB, `view.js` up to 4 MiB,
1724
- and up to 32 asset files with 8 MiB in total.
1725
-
1726
- ### The `StillMade` object
1727
-
1728
- Frame views get the same `StillMade` object as standard views: `input` and
1729
- `onInput`, `run`, `useOutput`, `getShared`, `onShared`, `updateShared`,
1730
- `bindShared`, `connectDocument`, `previewImage`, `previewMedia`, `render`
1731
- and the rest of the interface bridge. In addition:
1732
-
1733
- | Member | What it does |
1734
- | --- | --- |
1735
- | `StillMade.asset(path)` | A `blob:` URL for one of the view's asset files, for `<img>`, `new FontFace()` or `<video>`. |
1736
- | `StillMade.assetBytes(path)` | A copy of an asset file's bytes as an `ArrayBuffer`. |
1737
-
1738
- Shared state works as in every view: keep each independently edited value in
1739
- its own field (for example `point0`, `point1`) so two people editing
1740
- different values never conflict.
1741
-
1742
- ### Theming
1743
-
1744
- The theme variables from StillMade's design system are always defined:
1745
- `var(--bg)`, `var(--bg-elev-1)`, `var(--fg)`, `var(--fg-muted)`,
1746
- `var(--accent)`, `var(--border-tok)`, `var(--divider)`, `var(--r-3)` and the
1747
- rest. Colors written with them follow Light, Dark and White automatically.
1748
- Frame views are not required to use them; the appearance check reports
1749
- hard-coded colors as advice instead of refusing the view.
1750
-
1751
- ### Layout
1752
-
1753
- StillMade zooms a view to fill the space its Step gives it (up to 150%), and
1754
- Ctrl/Cmd +, - and 0 size the view while it has focus. It never centers a frame
1755
- view; a standard view narrower than its Step is centered by its margins.
1756
-
1757
- An app that lays itself out (a full-size canvas or WebGL, its own scroll, pan
1758
- or zoom, its own Ctrl/Cmd +/- keys, as in a CAD or drawing tool) puts
1759
- `data-stillmade-layout="own"` on its outermost element:
1760
-
1761
- ```html
1762
- <div id="root" data-stillmade-layout="own"></div>
1763
- ```
1764
-
1765
- StillMade then never centers or zooms it and leaves Ctrl/Cmd +, - and 0 to the
1766
- app. Without the attribute StillMade also stays out of the way when the view
1767
- looks like an app: no page padding, a clipped page, a root as tall as the frame,
1768
- fixed over half of it or scrolling over half of it, a canvas, SVG or
1769
- `touch-action: none` surface over 40% of it, or a canvas or SVG of 300x200 or
1770
- more with `touch-action: none` (as Three.js's controls set it). Mark the root
1771
- anyway, in `view.html` rather than from code: StillMade reads it before the view
1772
- starts, so the app never shows StillMade's size control or loses a key to it,
1773
- even while it is still loading.
1774
-
1775
- A `<video controls>` plays in StillMade's player, the same one as everywhere in
1776
- StillMade: play, timeline, time, sound and full screen along the bottom, shown
1777
- while the pointer is on the video, with keys on the focused video (Space or K,
1778
- the arrows, M, F) and touch. Full screen goes through StillMade, right after a
1779
- click or key in the view; the frame itself has no `allow="fullscreen"`. A
1780
- `<video>` without `controls` is the Block's own and StillMade never touches it,
1781
- so a Block that drives playback (its own timeline, an editor or CAD viewer)
1782
- leaves `controls` out, and `data-stillmade-player="native"` keeps the browser's
1783
- controls. Result pictures from `StillMade.previewImage` in `sm-stage` or
1784
- `sm-gallery`, and any element marked `data-stillmade-expand`, get a full-screen
1785
- button in the bottom-right corner while the pointer is on them; pictures with
1786
- the same value (`data-stillmade-expand="shots"`) open as one set, and
1787
- `data-stillmade-expand="off"` removes the button. The keyboard reaches them
1788
- too: such a picture without a `tabindex` of its own joins the tab order,
1789
- keyboard focus on it shows the button, Enter or Space opens the viewer, and
1790
- closing the viewer gives the focus back to the picture.
1791
-
1792
- ### Assets
1793
-
1794
- Images, fonts, JSON, audio and video the view needs ship as files named by
1795
- SHA-256, like module files:
1796
-
1797
- ```json
1798
- {"runtime": "frame", "files": {"schemaVersion": 1, "files": {
1799
- "fonts/inter.woff2": {"sha256": "…", "bytes": 48256, "mediaType": "font/woff2"}
1800
- }}}
1801
- ```
1802
-
1803
- `stillmade-block pack` fills in the digests and sizes. StillMade checks every
1804
- file against its SHA-256 before the view starts.
1805
-
1806
- ### What the frame does not allow
1807
-
1808
- The view runs in an opaque-origin frame with its own policy:
1809
-
1810
- - no network (`fetch`, WebSocket and remote images, fonts or scripts fail);
1811
- - no code from strings (`eval`, `new Function`, inline event attributes) and
1812
- no extra script elements;
1813
- - no navigation, popups, downloads, forms that submit, or dialogs;
1814
- - no WebAssembly on the page (run heavy computation in the Block's runtime,
1815
- for example a module Block, and call it with `StillMade.run`);
1816
- - no storage or access to StillMade's page.
1817
-
1818
- Workers created from `blob:` URLs are allowed and run off the page's thread.
1819
-
1820
- ### Run limits
1821
-
1822
- StillMade compiles the view so every function and loop checks a time budget.
1823
- One task may run for 250 ms; past that it is stopped with an error, and a view
1824
- that keeps the page busy most of the time, stops answering, or navigates its
1825
- frame is closed with a message. Move long computation into a Worker or the Block's runtime.
1826
-
1827
1713
  ## Outside APIs: declared origins, OAuth and published adapters
1828
1714
 
1829
1715
  Module Blocks can call any outside API with the account of the person running
@@ -2192,312 +2078,73 @@ reads its text with pdf.js (bundled, its worker code imported directly so
2192
2078
  nothing is fetched), plans the shots with one `text.generate` call that has a
2193
2079
  JSON schema, and returns a `table` and a `shot[]` the Shots workspace accepts.
2194
2080
 
2195
- ## Module Blocks: JavaScript and WebAssembly
2196
-
2197
- A module Block (`"runtime": "module"`) is an ordinary ES module. It can use
2198
- everything a modern browser Worker offers: `async`/`await`, timers, typed
2199
- arrays, `WebAssembly`, `OffscreenCanvas`, `createImageBitmap`, WebCodecs, and
2200
- any npm package you bundle into it, including C and C++ libraries compiled to
2201
- WebAssembly (OpenCV, libvips builds, codecs, solvers), threaded WebAssembly
2202
- (see Threads) and live WebSocket connections to declared origins (see Live
2203
- connections). Use it when a recipe or the QuickJS `javascript` runtime is too
2204
- small for the job.
2081
+ ## Hosted media analysis
2205
2082
 
2206
- ### Folder layout
2083
+ `media.analyze` asks StillMade's media vision a question about saved media and
2084
+ returns structured evidence. Declare one `video`, `image` or `image[]` (one to
2085
+ four images) input, one `text` or `json` output and
2086
+ `permissions.capabilities: ["media.analyze"]`. A video Block may bind two
2087
+ optional `number` inputs as a time range to look at closely (at most 12 seconds):
2207
2088
 
2208
- ```
2209
- my-block/
2210
- stillmade.block.json manifest with "runtime": "module"
2211
- module/ everything the Block runs
2212
- main.js the entry (bundle npm dependencies into it)
2213
- filters.wasm optional WebAssembly and data files
2214
- module.json optional: {"entry": "other.js", "threads": true}
2215
- tests/fixtures.json [{name, input, expected, mocks?}]
2089
+ ```json
2090
+ {"schemaVersion":1,"operation":"media.analyze","media":{"$input":"clip"},
2091
+ "question":"Is the product logo clearly visible, and when?",
2092
+ "range":{"start":{"$input":"start"},"end":{"$input":"end"}},"output":"report"}
2216
2093
  ```
2217
2094
 
2218
- Start one with `stillmade-block create my-block module`. Bundle npm
2219
- dependencies into `module/main.js` with any ES module bundler (esbuild,
2220
- rolldown, Vite). A module holds up to 64 files, 64 MiB each and 128 MiB in
2221
- total.
2095
+ StillMade reads the media from the runner's own library (never from another
2096
+ account or an arbitrary URL), samples a video into a timestamped contact sheet
2097
+ or uses the still image, and asks the vision model the Block's question (1 to
2098
+ 2,000 characters). A `json` output receives
2099
+ `{"schemaVersion":1,"items":[{"kind","summary","timeline","observations","detectedText","issues","confidence","range"?}]}`
2100
+ with timestamps in seconds; each list holds up to 20 entries. A `text` output
2101
+ receives the summaries (numbered when there are several images).
2222
2102
 
2223
- ### The entry
2103
+ Each analyzed image or video is one priced call, paid with StillMade credits;
2104
+ the quote lists the price for every fixture and the sample. Media that cannot
2105
+ be read (not saved in the library, too large, or undecodable) is refunded. The
2106
+ offline SDK includes `packages/block-sdk/media-analysis-example.js`, exporting
2107
+ `mediaAnalysis`.
2224
2108
 
2225
- ```js
2226
- export default async function run(input, stillmade) {
2227
- stillmade.progress(0.1, 'Loading');
2228
- const { instance } = await WebAssembly.instantiate(stillmade.asset('filters.wasm'));
2229
- const bytes = await stillmade.files.read(input.photo); // a saved image the Block received
2230
- // ... process ...
2231
- const saved = await stillmade.files.write(pngBlob, { mediaType: 'image/png', name: 'result.png' });
2232
- return { result: saved };
2233
- }
2234
- ```
2109
+ ## Hosted web reading
2235
2110
 
2236
- Return an object whose keys are your declared outputs. Outputs are checked
2237
- against the manifest's port types.
2111
+ Two operations let a Block use the web without network permissions, cookies or
2112
+ keys. StillMade does the reading; the person running the Block pays with
2113
+ StillMade credits at the app's research prices, shown in the quote first.
2238
2114
 
2239
- ### The `stillmade` host API
2115
+ **`web.fetch`** reads one public page and answers the Block's instruction from
2116
+ it with StillMade's fast model. Declare one `text` input holding the URL, one
2117
+ `text` output and `permissions.capabilities: ["web.fetch"]`:
2240
2118
 
2241
- | Member | What it does |
2242
- | --- | --- |
2243
- | `input` | The validated input values (the same object as the first argument). |
2244
- | `signal` | An `AbortSignal` that fires when the person cancels. |
2245
- | `progress(value, label)` | Reports progress from 0 to 1 with a short label. Each report extends the run limit. |
2246
- | `log(...values)` | Adds a line to the run log (up to 500 lines). |
2247
- | `asset(path)` | A copy of one of the module's own files, such as a `.wasm` build, as an `ArrayBuffer`. |
2248
- | `files.read(ref)` | The bytes of a saved image, video, audio or asset the Block received as input, or that StillMade made in this run: files it wrote, transform results and generated media. Other files cannot be read, and transforms, hosted calls and actions accept only these references too. |
2249
- | `files.write(data, {mediaType, name})` | Saves PNG, JPEG, WebP, GIF, MP4, WebM, MP3, WAV, M4A or OGG, or a PDF, TXT, CSV, Markdown, JSON or ZIP file, to the runner's library and returns a reference you can return as an output. At most 64 files per run. |
2250
- | `media.transform(request)` | On-device media work done by StillMade: decode audio to samples, extract or loudness-normalise audio, cut, join or re-encode video, and take thumbnails. Results are saved like `files.write`. See [Media processing](#media-processing). |
2251
- | `actions.run(action)` | Runs a declared portable action. `media.generate` asks the person to approve one StillMade credit maximum for the batch; `stock.search` runs directly. |
2252
- | `hosted.run(operation, request)` | Calls a hosted operation the manifest lists in `permissions.capabilities` (for example `text.generate`, `image.generate`, `audio.speech`, `web.research`). Each call is quoted and approved like a capability Block and paid with StillMade credits. |
2253
- | `net.fetch(url, {method, headers, body})` | Calls one of the origins declared in `permissions.network` through StillMade, which adds the person's own key or OAuth token on its servers. Returns a standard `Response`. See [Outside APIs](/docs/build/outside-apis). |
2254
- | `net.connect(url, {protocols})` | Opens a live WebSocket to the `wss://` form of a declared origin and resolves with the open socket (`send`, messages, `for await`, `close`). No key is added. See [Live connections](#live-connections). |
2255
- | `connections.call(appId, operationId, input)` | Calls an operation of a published adapter declared in `permissions.connections`, with the person's own account. Returns the checked JSON result. |
2256
- | `storage.get/set/remove/list/info(...)` | The Block's own cloud storage in the current project: JSON values up to 1 MiB under folder-like keys. Kept across runs, devices and the Block's versions. See [Project storage, reads and assets](/docs/build/project-storage). |
2257
- | `storage.putFile(key, data, {mediaType})` / `storage.getFile(key)` | Files up to 64 MiB in the same storage; `getFile` returns `{key, mediaType, bytes, data}` with `data` as an `ArrayBuffer`, or `null`. |
2258
- | `project.read(fields?)` | A copy of the current project limited to the fields the manifest may read (`project.read` plus `context.<field>.read`), such as `timeline`, `shots` or `assets`. |
2259
- | `assets.create(file, {name})` / `assets.appendVersion(asset, file)` | Adds a saved image, video or audio file to the project's media as a new asset (`asset.create`), or as the newest version of an asset the Block received, read or created (`asset.version.append`). |
2119
+ ```json
2120
+ {"schemaVersion":1,"operation":"web.fetch","url":{"$input":"url"},"instruction":"Summarize the page in five bullet points.","output":"summary"}
2121
+ ```
2260
2122
 
2261
- Hosted calls take the operation's request fields, for example:
2123
+ The URL must be a public `http` or `https` address of at most 2,048
2124
+ characters. StillMade refuses private, local and credentialed addresses,
2125
+ follows at most three redirects, reads HTML or plain text up to 5 MB, removes
2126
+ scripts and markup, and treats the page as untrusted data, never as
2127
+ instructions. A page that cannot be read is refunded. The instruction is 1 to
2128
+ 4,000 characters.
2262
2129
 
2263
- ```js
2264
- const plan = await stillmade.hosted.run('text.generate', {
2265
- prompt: `Write a 20-second podcast intro about ${input.topic}.`,
2266
- system: 'Warm, short sentences.', maxTokens: 400,
2267
- schema: { type: 'object', required: ['script'], properties: { script: { type: 'string' } } },
2268
- });
2269
- const voice = await stillmade.hosted.run('audio.speech', { text: plan.script, settings: { voice: 'adam' } });
2130
+ **`web.research`** searches the web for a topic and returns a cited summary.
2131
+ Declare one `text` or `brief` topic input (1 to 500 characters) and one `text`
2132
+ or `json` output with `permissions.capabilities: ["web.research"]`:
2133
+
2134
+ ```json
2135
+ {"schemaVersion":1,"operation":"web.research","topic":{"$input":"topic"},"output":"findings"}
2270
2136
  ```
2271
2137
 
2272
- `image.describe`, `media.analyze` and `audio.transcribe` take saved references
2273
- (`image`/`images`, `media`, `audio`; transcription also takes an MP4 or WebM video and transcribes its sound); `web.fetch` takes `url` and `instruction`;
2274
- `web.research` takes `topic`; music and sound effects take `prompt` and
2275
- `durationSec`.
2138
+ A `json` output receives `{"schemaVersion":1,"summary":"...","sources":[{"url":"https://...","title":"..."}]}`
2139
+ (up to 12 sources); a `text` output receives the summary followed by a
2140
+ numbered source list. Fixture expectations are the usual text bounds, or
2141
+ `{"kind":"json"}` with optional `includes`.
2276
2142
 
2277
- ### Media processing
2143
+ The offline SDK includes `packages/block-sdk/web-example.js`, exporting
2144
+ `webFetch` and `webResearch`. Pair either with `text.generate` (as a `context`
2145
+ input) to write from what was read.
2278
2146
 
2279
- `stillmade.media.transform(request)` asks StillMade to do heavy media work on
2280
- the person's device: Web Audio for audio, and the same renderer as Final
2281
- Render for video (WebCodecs in the browser, the native renderer on desktop).
2282
- It never runs on StillMade's servers and costs no credits. A transform reads
2283
- only files the Block received, wrote or generated in this run, and saves its
2284
- result to the runner's library, so you can return it as an output or pass it
2285
- to the next transform.
2286
-
2287
- | Request | Result |
2288
- | --- | --- |
2289
- | `{operation: 'audio.decode', source, sampleRate?}` | `{sampleRate, duration, samples}`: mono `Float32Array` PCM for analysis (default 16 kHz; 8000 to 48000). |
2290
- | `{operation: 'audio.extract', source, format?}` | A saved WAV (16-bit, 48 kHz) or MP3 of the audio track. |
2291
- | `{operation: 'audio.normalize', source, targetLufs?, format?}` | A saved WAV or MP3 at the target integrated loudness (default -16 LUFS, peaks at most -1 dBFS). |
2292
- | `{operation: 'video.cut', source, keep, settings?}` | A saved MP4 with only the `keep` ranges (`[{start, end}]` seconds, ordered, up to 500). |
2293
- | `{operation: 'video.concat', sources, settings?}` | A saved MP4 of 2 to 50 videos in order. |
2294
- | `{operation: 'video.resize', source, settings}` | A saved MP4 re-encoded with new settings. |
2295
- | `{operation: 'image.thumbnail', source, at?, width?}` | A saved PNG of one frame (default the first frame, 1280 px wide). |
2296
-
2297
- `source`/`sources` are saved references (`{kind, assetId, versionId, url}`).
2298
- Render `settings` are `{fps: 24|30|60, resolution: 720|1080|2160, quality:
2299
- 'low'|'medium'|'high'}`; the default is 30 fps, 1080, high. Video keeps the
2300
- source's aspect ratio. Rendering follows the person's export plan, like any
2301
- export.
2302
-
2303
- Pure helpers ship as `@stillmade/block-sdk/media` for you to bundle:
2304
- `silentRanges(samples, sampleRate, {thresholdDb, minSilence})`,
2305
- `keepRanges(silent, duration, {padding})`, `integratedLoudness(channels,
2306
- sampleRate)` (ITU-R BS.1770), `loudnessGain`, `encodeWav` and `decodeWav`.
2307
-
2308
- ```js
2309
- import { silentRanges, keepRanges } from '@stillmade/block-sdk/media';
2310
-
2311
- export default async function run(input, stillmade) {
2312
- const audio = await stillmade.media.transform({ operation: 'audio.decode', source: input.video });
2313
- const keep = keepRanges(silentRanges(audio.samples, audio.sampleRate), audio.duration);
2314
- const trimmed = await stillmade.media.transform({ operation: 'video.cut', source: input.video, keep });
2315
- return { trimmed };
2316
- }
2317
- ```
2318
-
2319
- For anything else, decode and encode in the Block itself: the Worker has
2320
- WebCodecs (`VideoDecoder`, `VideoEncoder`, `AudioDecoder`), `OffscreenCanvas`
2321
- and `createImageBitmap`, and you can bundle `mp4-muxer`, `mp4box` or a
2322
- WebAssembly codec. Images arrive as saved references: read the bytes with
2323
- `files.read` and decode them with `createImageBitmap`, at full resolution.
2324
-
2325
- ### Sandbox and limits
2326
-
2327
- - The module runs in one dedicated Worker inside a response-sandboxed frame
2328
- with an opaque origin. It has no network of its own (`fetch`, WebSocket and
2329
- remote imports fail; `net.fetch` and `net.connect` go through StillMade), no
2330
- storage of the app, no page or DOM access, and cannot read another Block's
2331
- files.
2332
- - Every file is fetched from StillMade's store and checked against its SHA-256
2333
- before anything runs.
2334
- - A run may take 2 minutes; each progress report allows 60 more seconds. Time
2335
- StillMade spends on a host call (rendering a video, waiting for the person to
2336
- approve credits) does not count. Every run stops after 30 minutes in total.
2337
- When a limit passes, or the person cancels, the frame is removed and the
2338
- Worker stops immediately, even inside an endless loop.
2339
- - Generating code at run time (`new Function`, used by Emscripten bindings) is
2340
- allowed inside the sandbox.
2341
-
2342
- Module Blocks run in StillMade on the person's device: in the browser while
2343
- StillMade is open, or in StillMade Desktop. StillMade has no server sandbox for
2344
- them. A schedule or event that hands browser-only Steps to the owner's StillMade
2345
- Desktop ("Run ... on my StillMade Desktop") pauses at a module Step until the
2346
- owner's desktop app, open with "Run my automations on this computer" on, runs it
2347
- with the Step's exact inputs; the workflow then continues from its result. Module
2348
- Steps that call paid hosted operations, generate media or need a device grant
2349
- still wait for the owner. Without that choice a background run stops before a
2350
- module Step and the person runs it in the project. Marketplace listings show
2351
- module Blocks as **Browser only**.
2352
-
2353
- ### Live connections
2354
-
2355
- `stillmade.net.connect(url, {protocols})` opens a WebSocket to the `wss://` form
2356
- of an origin in `permissions.network` (declare `https://stream.example.org` and
2357
- connect to `wss://stream.example.org/...`) and resolves once it is open:
2358
-
2359
- ```js
2360
- const socket = await stillmade.net.connect('wss://stream.example.org/v1/live', { protocols: ['json.v1'] });
2361
- await socket.send(JSON.stringify({ subscribe: input.channel }));
2362
- for await (const data of socket) { // text arrives as strings, binary as ArrayBuffers
2363
- stillmade.progress(0.5, 'Listening'); // each progress report extends the run limit
2364
- if (JSON.parse(data).done) break; // leaving the loop closes the connection (code 1000)
2365
- }
2366
- ```
2367
-
2368
- The socket also has `onmessage`, `onclose`, `addEventListener`, `readyState`,
2369
- `protocol`, `close(code, reason)` (1000 or 3000 to 4999) and `closed`, a promise of
2370
- `{code, reason, wasClean}`. `for await` yields every message not yet read, also ones
2371
- that arrived before the loop began (up to 1,024 messages or 16 MiB; older ones are
2372
- dropped). The module's own frame still has no network: StillMade
2373
- checks each URL (`wss://`, the standard port, a declared origin, valid subprotocol
2374
- names) and opens the connection in a separate relay frame that runs no Block code,
2375
- then relays the messages. A run keeps at most 4 connections, sends or receives at
2376
- most 1 MiB a message and 256 MiB in all, and every connection closes when the run
2377
- ends. The service sees an opaque `Origin` (`null`). StillMade adds no key to a live
2378
- connection: for a service that needs one, get a short-lived token with `net.fetch`
2379
- first and pass it as the service documents.
2380
-
2381
- ### Threads (SharedArrayBuffer)
2382
-
2383
- A module that runs its own Workers with shared memory (Emscripten pthreads,
2384
- threaded WebAssembly builds) declares it in `module.json`:
2385
-
2386
- ```json
2387
- { "entry": "main.js", "threads": true }
2388
- ```
2389
-
2390
- It then runs in a Worker that can share `SharedArrayBuffer` and
2391
- `WebAssembly.Memory({shared: true})` with the Workers it starts from `blob:` URLs
2392
- (module Workers and remote scripts stay unavailable, so bundle the thread code).
2393
- That Worker is not a secure context, so it has no WebCodecs, WebGPU,
2394
- `crypto.subtle` or `crypto.randomUUID`; a module without `threads` keeps those and
2395
- cannot share memory between Workers. Threads work in Chrome and Edge (version 137
2396
- or later) and in StillMade Desktop; in other browsers `self.crossOriginIsolated` is
2397
- false and `SharedArrayBuffer` is missing, so check it and fall back to one thread.
2398
- `files.write` and `storage.putFile` accept bytes in shared memory (they are copied).
2399
-
2400
- ### Test, pack and import
2401
-
2402
- ```sh
2403
- stillmade-block test my-block # runs fixtures in a local Node worker
2404
- stillmade-block preview my-block input.json
2405
- stillmade-block dev my-block # the interface in your browser, reloaded on save
2406
- stillmade-block gate my-block # StillMade's readiness check
2407
- stillmade-block pack my-block # writes <id>-<version>.stillmade-module.json
2408
- ```
2409
-
2410
- `pack my-block` is the only packing step for a module Block, and it never takes
2411
- `--original` (original `.stillmade-block` archives hold recipe, JavaScript,
2412
- capability and ComfyUI source only and refuse a module Block).
2413
-
2414
- `test` and `preview` run your code on your machine in a disposable Node worker
2415
- with the same host API and run limits (not a security sandbox; it is your own
2416
- code). `files.read`, `files.write`, `media.transform`, actions and hosted calls need StillMade, or
2417
- a host you pass to `createNodeModuleExecutor` in your own tests.
2418
-
2419
- Import the `.stillmade-module.json` in StillMade (Create, then Import).
2420
- StillMade verifies every file digest, uploads each file to its
2421
- content-addressed store, and saves the Block. Fixtures then run in your
2422
- browser's module sandbox.
2423
-
2424
- Uploaded module files count against your cloud storage. A digest alone never
2425
- grants access: a person can read a module file only when they uploaded those
2426
- exact bytes or can open a saved Block that names it (yours, an installed or
2427
- community version, or one shared with their team).
2428
-
2429
- See `examples/module-opencv/` for a Block that runs OpenCV on a 12-megapixel
2430
- photo, `examples/module-silence-trimmer/` for one that cuts the pauses out of a
2431
- talking video, and `examples/module-audio-extract/` for one that saves a
2432
- video's audio as WAV.
2433
-
2434
- ## Hosted media analysis
2435
-
2436
- `media.analyze` asks StillMade's media vision a question about saved media and
2437
- returns structured evidence. Declare one `video`, `image` or `image[]` (one to
2438
- four images) input, one `text` or `json` output and
2439
- `permissions.capabilities: ["media.analyze"]`. A video Block may bind two
2440
- optional `number` inputs as a time range to look at closely (at most 12 seconds):
2441
-
2442
- ```json
2443
- {"schemaVersion":1,"operation":"media.analyze","media":{"$input":"clip"},
2444
- "question":"Is the product logo clearly visible, and when?",
2445
- "range":{"start":{"$input":"start"},"end":{"$input":"end"}},"output":"report"}
2446
- ```
2447
-
2448
- StillMade reads the media from the runner's own library (never from another
2449
- account or an arbitrary URL), samples a video into a timestamped contact sheet
2450
- or uses the still image, and asks the vision model the Block's question (1 to
2451
- 2,000 characters). A `json` output receives
2452
- `{"schemaVersion":1,"items":[{"kind","summary","timeline","observations","detectedText","issues","confidence","range"?}]}`
2453
- with timestamps in seconds; each list holds up to 20 entries. A `text` output
2454
- receives the summaries (numbered when there are several images).
2455
-
2456
- Each analyzed image or video is one priced call, paid with StillMade credits;
2457
- the quote lists the price for every fixture and the sample. Media that cannot
2458
- be read (not saved in the library, too large, or undecodable) is refunded. The
2459
- offline SDK includes `packages/block-sdk/media-analysis-example.js`, exporting
2460
- `mediaAnalysis`.
2461
-
2462
- ## Hosted web reading
2463
-
2464
- Two operations let a Block use the web without network permissions, cookies or
2465
- keys. StillMade does the reading; the person running the Block pays with
2466
- StillMade credits at the app's research prices, shown in the quote first.
2467
-
2468
- **`web.fetch`** reads one public page and answers the Block's instruction from
2469
- it with StillMade's fast model. Declare one `text` input holding the URL, one
2470
- `text` output and `permissions.capabilities: ["web.fetch"]`:
2471
-
2472
- ```json
2473
- {"schemaVersion":1,"operation":"web.fetch","url":{"$input":"url"},"instruction":"Summarize the page in five bullet points.","output":"summary"}
2474
- ```
2475
-
2476
- The URL must be a public `http` or `https` address of at most 2,048
2477
- characters. StillMade refuses private, local and credentialed addresses,
2478
- follows at most three redirects, reads HTML or plain text up to 5 MB, removes
2479
- scripts and markup, and treats the page as untrusted data, never as
2480
- instructions. A page that cannot be read is refunded. The instruction is 1 to
2481
- 4,000 characters.
2482
-
2483
- **`web.research`** searches the web for a topic and returns a cited summary.
2484
- Declare one `text` or `brief` topic input (1 to 500 characters) and one `text`
2485
- or `json` output with `permissions.capabilities: ["web.research"]`:
2486
-
2487
- ```json
2488
- {"schemaVersion":1,"operation":"web.research","topic":{"$input":"topic"},"output":"findings"}
2489
- ```
2490
-
2491
- A `json` output receives `{"schemaVersion":1,"summary":"...","sources":[{"url":"https://...","title":"..."}]}`
2492
- (up to 12 sources); a `text` output receives the summary followed by a
2493
- numbered source list. Fixture expectations are the usual text bounds, or
2494
- `{"kind":"json"}` with optional `includes`.
2495
-
2496
- The offline SDK includes `packages/block-sdk/web-example.js`, exporting
2497
- `webFetch` and `webResearch`. Pair either with `text.generate` (as a `context`
2498
- input) to write from what was read.
2499
-
2500
- ## Hosted music and sound effects
2147
+ ## Hosted music and sound effects
2501
2148
 
2502
2149
  Use `runtime: "capability"` with `operation: "audio.music"` or
2503
2150
  `operation: "audio.sfx"` to create music or a sound effect with the app's own
@@ -4129,7 +3776,10 @@ Standard and frame views receive the same `StillMade` object; members marked Fra
4129
3776
  | --- | --- | --- |
4130
3777
  | `readonly input: Partial<In>` | Every view | A copy of the current input values. Read it after `onInput` first fires. |
4131
3778
  | `onInput(callback: (input: Partial<In>) => void): () => void` | Every view | Receives the input when the view starts and each time the host changes it. Returns a function that stops listening. Initialize controls here. |
4132
- | `run(values: In): Promise<Out>` | Every view | Checks the named input values, runs the Block (its recipe, JavaScript, module or hosted operation, after any approval the host asks for) and resolves to the checked outputs. One run at a time, 30 a minute. |
3779
+ | `addFiles(port: string): Promise<{added: number}>` | A saved project Step and the SDK preview | Opens StillMade's own file picker for a declared `image`, `video`, `audio`, `asset` or `file` input (or a list of them) and saves what the person chooses through StillMade's upload, as the Step's own tiles do. Call it from a click or key press in the view; a list gets the files added, a single input the first. The saved values arrive through `onInput`; the view never receives a File. A `<button class="sm-add" data-stillmade-add="port">` does this with no code. Refused for an undeclared or non-file input, an input an earlier Step fills, a viewer, and without a gesture. |
3780
+ | `removeFile(port: string, index: number): Promise<{files: number}>` | A saved project Step and the SDK preview | Removes the file at `index` (from 0) of a declared list input (`image[]`, `video[]`, `audio[]`, `asset[]` or `file[]`), as the remove button on the Step's own tile does, and resolves to how many files the list now holds. Call it from a click or key press in the view; the new list arrives through `onInput`. A `<button data-stillmade-remove="port" data-index="2">` does this with no code. Refused for an undeclared, non-file or single input, an input an earlier Step fills, a viewer, Chat, a file that changed since the view was given it, and without a gesture. |
3781
+ | `moveFile(port: string, from: number, to: number): Promise<{files: number}>` | A saved project Step and the SDK preview | Moves the file at `from` to the place `to` (both from 0) in a declared list input, as the Step's own move earlier and move later buttons do, so the person sets the order (a tour, a slideshow) on the Block's own screen. Same gesture and refusals as `removeFile`. A `<button data-stillmade-move="port" data-index="2" data-to="1">` does this with no code; a `data-to` past either end does nothing. |
3782
+ | `run(values: In): Promise<Out>` | Every view | Checks the named input values, runs the Block (its recipe, JavaScript, module or hosted operation, after any approval the host asks for) and resolves to the checked outputs. On a project Step and in the SDK preview, inputs it leaves out run with the Step's current values (its photos, an earlier Step's result), so a view passes only what it sets; what it passes is kept as the Step's input. One run at a time, 30 a minute. |
4133
3783
  | `useOutput(outputs: Partial<Out>): Promise<Record<string, unknown>>` | A saved project Step | Chooses media the view already holds (input media or a desktop recording) as the Block outputs without running. Only complete references the host issued are accepted. |
4134
3784
  | `readonly schemaFields: Readonly<Record<string, unknown>>` | Every view | Helpers that build form fields from the input ports and their schemas, honoring inputs the host locked. |
4135
3785
 
@@ -4183,19 +3833,14 @@ StillMade's own Blocks run on host services. Each one is listed here with the pu
4183
3833
 
4184
3834
  | What StillMade's Blocks do | How a creator Block does it | Notes |
4185
3835
  | --- | --- | --- |
4186
- | Animated scene assets: generated props and backgrounds, scene video slicing, sprite sheets. | hosted `image.generate`, hosted `video.generate`, action `media.generate`, `stillmade.media.transform`, `stillmade.files.write`, `stillmade.assets.create`, permission `context.shots.read`, permission `workspace.animation.propose` | Same capability |
4187
- | Browses other projects' scripts the person can open. | `stillmade.project.read` | Gap: Blocks read only the current project; reading another project the person can open is first-party only. |
4188
3836
  | Canvas generation: images, videos, variations, stock and recording the asset. | hosted `image.generate`, hosted `video.generate`, action `media.generate`, action `stock.search`, `stillmade.assets.create` | Same capability |
4189
3837
  | Canvas media: stock search, uploads and generation with prices. | hosted `image.generate`, hosted `video.generate`, action `media.generate`, action `stock.search`, `stillmade.files.write`, control `file` | Same capability |
4190
3838
  | Canvas node placement for shipped workflow nodes. | permission `context.canvas.read`, permission `workspace.canvas.propose` | Same capability |
4191
3839
  | Canvas workspace chrome (low-balance ribbon, default model). | `IMAGE_GENERATION_MODELS` from the SDK | The credit balance ribbon is account UI; creator Blocks see prices in StillMade's own approval dialog before any paid run. |
4192
- | Collaborative script editing. | `StillMade.getShared`, `StillMade.updateShared`, `StillMade.connectDocument` | Same capability |
4193
3840
  | Composes the Canvas workspace from its native parts. | permission `workspace.canvas.propose` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
4194
- | Connects a YouTube channel. | `stillmade.connections.call`, `stillmade.net.fetch` | Same capability |
4195
3841
  | Creates a visual style with generated samples. | hosted `image.generate`, permission `context.styles.read` | Same capability |
4196
3842
  | Credits or the person's own key for image, video, chat, voice, music and effects. | hosted `text.generate`, hosted `image.generate`, hosted `video.generate`, hosted `audio.speech`, hosted `audio.music`, hosted `audio.sfx` | Same capability |
4197
3843
  | Crops panel images. | `stillmade.files.read`, `stillmade.files.write`, `stillmade.media.transform` | Same capability |
4198
- | Describes images and media for prompts and context. | hosted `image.describe`, hosted `media.analyze` | Same capability |
4199
3844
  | Downloads the media a controller selected, under the export policy. | control `download`, port type `file-bundle`, action `final.render` | Same capability |
4200
3845
  | Editor asset generation with a model, prompt, aspect, quality and reference images. | hosted `image.generate`, hosted `video.generate`, action `media.generate` | Same capability |
4201
3846
  | Edits panel settings with local runs and confirmations. | `StillMade.run`, `StillMade.updateShared` | Same capability |
@@ -4219,23 +3864,15 @@ StillMade's own Blocks run on host services. Each one is listed here with the pu
4219
3864
  | Runs a draft recipe or JavaScript Block locally while it is edited. | `StillMade.run` | Same capability |
4220
3865
  | Runs Editor operations: renders, previews and timeline edits. | `stillmade.media.transform`, action `final.render`, permission `timeline.propose` | Same capability |
4221
3866
  | Runs the production pipeline: assembly, generation, budgets, references and deliveries. | hosted `image.generate`, hosted `video.generate`, action `media.generate`, hosted `text.generate`, `stillmade.files.write`, permission `workspace.canvas.propose`, trigger `media.saved` | Same capability |
4222
- | Runs the shot planning workspace in its sandbox. | `StillMade.run`, permission `workspace.shotPlan.propose` | Same capability |
4223
3867
  | Score and sound effect lanes: generated music and effects placed in the Editor. | hosted `audio.music`, hosted `audio.sfx`, action `media.generate`, action `stock.search`, permission `timeline.propose` | Same capability |
4224
- | Script services: drafts, models and project context. | hosted `text.generate`, permission `context.script.read`, `stillmade.project.read` | Same capability |
4225
- | Shot planning services: shots, versions and planning with a model. | hosted `text.generate`, permission `context.shots.read`, permission `workspace.shotPlan.propose`, port type `shot-plan` | Same capability |
4226
3868
  | Shows a panel image large with its narration and text. | `StillMade.previewImage` | Same capability |
4227
- | Shows the script in its workspace layout. | port type `script`, control `document` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
4228
3869
  | StillMade AI presents a workspace and adds board elements. | action `presenter.generate`, permission `workspace.board.propose` | Same capability |
4229
3870
  | StillMade dialogs and notices for Image Panels. | `StillMade.render` | StillMade's own dialogs stay host-owned so a Block cannot imitate account, payment or permission prompts; creator interfaces show their own confirmations inside their frame. |
4230
- | Story bible editing with collaboration and the bible graph hash. | `StillMade.getShared`, `StillMade.updateShared`, `StillMade.connectDocument`, permission `context.blueprint.read`, permission `workspace.blueprint.propose`, hosted `text.generate` | Same capability |
4231
- | Story bible workspace UI (selects, step header, bottom sheet, help). | `StillMade.render`, `StillMade.schemaFields` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
4232
3871
  | Text, story segments and voiceover inside pipeline nodes. | hosted `text.generate`, hosted `audio.speech` | Same capability |
4233
3872
  | The Canvas preview shown for a Step. | `StillMade.render`, permission `context.canvas.read` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
4234
3873
  | The Editor media library: project assets and uploads. | permission `context.assets.read`, `stillmade.project.read`, `stillmade.assets.create`, `stillmade.assets.appendVersion` | Same capability |
4235
3874
  | The Editor preview shown for a Step. | `StillMade.previewMedia`, permission `context.timeline.read` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
4236
- | The host side of the shot planning sandbox. | `StillMade.run`, permission `workspace.shotPlan.propose` | Same capability |
4237
3875
  | The voiceover preview shown for a Step. | `StillMade.previewMedia` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
4238
- | Uploads a stored export to YouTube. | action `youtube.upload` | Same capability |
4239
3876
  | Uploads and checks media files, thumbnails and downloads. | control `file`, `stillmade.files.write`, `stillmade.assets.create` | Same capability |
4240
3877
  | Vectorizes and upscales panel images. | `stillmade.asset`, `stillmade.files.read`, `stillmade.files.write` | Gap: Upscaling with a hosted model is not a hosted operation yet; module Blocks can only upscale with a model they ship in WebAssembly. |
4241
3878
  | Video assets workspace: model limits, durations, voices, prices and batch generation. | hosted `image.generate`, hosted `video.generate`, action `media.generate`, hosted `audio.speech`, `VIDEO_GENERATION_MODELS` from the SDK | Same capability |
@@ -4561,12 +4198,20 @@ page, bridge, response policy, theme, run limits and guard that run your
4561
4198
  interface in the app, so what works here works there.
4562
4199
 
4563
4200
  - Your interface (standard or frame view) runs with the first fixture's input.
4564
- Edit the input as JSON beside it, pick another fixture, switch Light, Dark and
4565
- White, and try phone, tablet and full widths.
4566
- - `StillMade.run` runs the Block on your machine with the SDK runtime. Hosted
4201
+ Pick another fixture, switch Light, Dark and White, and try phone, tablet and
4202
+ full widths. The **Input data** tab of the panel below the Block shows that
4203
+ input as JSON. An edit there applies when you leave the box (click outside it
4204
+ or press Tab), not as you type; the view then hears it through `onInput`. Text
4205
+ that is not one JSON object is named under the box and not applied.
4206
+ - `StillMade.run` runs the Block on your machine with the SDK runtime. As on a
4207
+ project Step, the inputs the view leaves out are the current input's, so
4208
+ `StillMade.run({make: 'check'})` still gets the example's photos; what the
4209
+ view passes is kept in the input. Hosted
4567
4210
  operations, files, network, connections and storage are answered by
4568
4211
  [stand-ins](/docs/build/testing), and stand-in media (images, audio, video)
4569
4212
  previews in the view. Shared view state (`updateShared`) is kept in the page.
4213
+ The view's `addFiles`, `removeFile` and `moveFile` change the input's files
4214
+ here as they change a Step's.
4570
4215
  - Saving any file the package reads reloads the preview, keeping the input and
4571
4216
  shared state. With `--build "npm run build"`, saving any other source file
4572
4217
  (such as `src/view.jsx`) runs the build first, then reloads.
@@ -4730,6 +4375,8 @@ An optional `outcome` field declares the finished result required by this exact
4730
4375
 
4731
4376
  `type` is `export`, `delivery`, or `project_outcome`. `dedupe`, when present, must be `material_project_state`; that is also its default. `minimumDurationSec` is optional, finite, between 0 and 86,400 seconds. A positive minimum requires host-measured media duration. Omit it for text or editable project artifacts.
4732
4377
 
4378
+ Which result reaches each type: `export` is StillMade's server render of the project's saved Editor timeline (the export in a full-project share), `delivery` is a run's result the person publishes with a share link, and `project_outcome` is an editable project handed over through a share. A Block's own `timeline.render` is that Block's run output, not an Editor export, so it never reaches `export`: a Type whose Block renders the finished video itself, with no Editor Step, declares `delivery`, which the published video reaches.
4379
+
4733
4380
  Configure this in **Project defaults & starting questions → Production completion**. Omitting `outcome` preserves the existing host completion behavior. These settings never launch exports, create revenue or mark a client action as successful. Version the Project Type when changing its production rule.
4734
4381
 
4735
4382
  Only a trusted completion adapter can supply `materialStateHash`, measured duration, success, retained-source lineage and the actual outcome kind. The accounting qualifier reads the rule from the exact released Project Type; browser-provided contributors and success flags are not accepted. Matching material states deduplicate under the payer-period policy. Verified editable-project deliveries can satisfy `project_outcome`; a local MP4 upload alone does not prove an executed media export.
@@ -4818,6 +4465,11 @@ items may also set `references` (images), `firstFrame`/`lastFrame` (video),
4818
4465
  - `music`: `settings: {durationSec}`, 5 to 240 whole seconds (default 30).
4819
4466
  - `sfx`: `settings: {durationSec}`, 0.5 to 22 seconds in tenths (default 4).
4820
4467
 
4468
+ Video items take `settings: {model, duration, resolution, aspectRatio, generateAudio}`.
4469
+ With no `model`, an `aspectRatio` the default model (Vidu Q3 Turbo, which takes none)
4470
+ cannot make picks the cheapest catalog model that makes it with the frames and
4471
+ `duration` given, so `{aspectRatio: "9:16"}` alone asks for a vertical clip.
4472
+
4821
4473
  ```js
4822
4474
  return {request: {
4823
4475
  schemaVersion: 1, kind: "stillmade.media-action", operation: "media.generate",
@@ -4846,8 +4498,9 @@ price, a provider, a key or another Block. References and frames must be images
4846
4498
  saved in the user's own library. Up to 50 items per action.
4847
4499
 
4848
4500
  When it finishes, the declared `resultPort` receives an array of
4849
- `{key, kind, assetId, versionId, url}` in item order (`kind` is `image`,
4850
- `video` or `audio`); failed items are listed
4501
+ `{key, kind, assetId, versionId, url, duration?}` in item order (`kind` is `image`,
4502
+ `video` or `audio`; `duration`, in seconds, is the length a video, music or sound
4503
+ effect item asked for; speech has none, so decode it with `audio.decode` to time it); failed items are listed
4851
4504
  separately and are not charged beyond what the provider actually received.
4852
4505
 
4853
4506
  ## Project defaults and onboarding
@@ -4950,7 +4603,21 @@ node packages/block-cli/cli.js test my-type.json
4950
4603
  node packages/block-cli/cli.js preview my-type.json
4951
4604
  node packages/block-cli/cli.js pack my-type.json my-type.stillmade.json
4952
4605
  ```
4953
- The SDK includes `examples/text-workflow.stillmade.json`. Host workspaces use existing project context and controls; switching to a Step does not automatically call a paid generation provider.
4606
+ The SDK includes `examples/text-workflow.stillmade.json`, the Type `create-type` writes. Host workspaces use existing project context and controls; switching to a Step does not automatically call a paid generation provider.
4607
+
4608
+ ### A video and the Editor
4609
+ A Type whose own Block renders the finished video (`timeline.render`) needs no Editor Step: that Block's page holds the inputs, a cheap check and the video. The Editor is a Step only when the person cuts by hand; then the Type ends there, and since the Editor (1.0) has captions and export built in, Export video is never a Step beside it (builder contract, "One job per Step"). The `create-type` scaffold ends in the Editor that way. `validate` and `test` do not check where a Type ends.
4610
+
4611
+ ### Module Blocks in a Project Type
4612
+ A Type carries a module Block in `packages` with its files as text: `{manifest, module, moduleSource, tests}`, where `moduleSource` maps each file in `module.files` to its JavaScript text, exactly the bytes `module.files` names by size and sha256. A packed `.stillmade-module.json` stores the same files as base64; never paste that base64 into `moduleSource`. `validate` refuses a `moduleSource` that does not match its digest.
4613
+
4614
+ Let the CLI embed the Block instead. An entry of `packages` may be the path of the Block's packed file or of its folder, relative to the Type file, or the packed bundle itself:
4615
+ ```json
4616
+ {"packages": ["dist/creator.listing-voiceover-1.0.0.stillmade-module.json", "listing-video"]}
4617
+ ```
4618
+ `validate`, `test`, `preview` and `pack` read those entries and check every file against its digest, and `pack` writes the Type with each Block embedded, the file to import in StillMade. Only JavaScript travels this way: a module Block with `.wasm` or data files, or a frame view with its own files, cannot be embedded in a Type.
4619
+
4620
+ `test`, `preview` and `pack` run module Blocks as `test` runs one Block: in the SDK's local module runner, each fixture answered by its own `mocks`. In the connected run a module Step is answered by its first fixture's mocks, or with `--mock` by recorded stand-ins. Nothing calls a provider, and the report lists every stand-in with `liveVerified: false`. A file a mock names by path is read from the Type file's folder.
4954
4621
 
4955
4622
  ### Protect existing projects
4956
4623
  Saved projects retain their pinned type definition. Installing a newer type never silently replaces a workflow in progress. See [conditions](/docs/project-types/conditions), [testing](/docs/project-types/testing), and [versioning](/docs/distribute/versioning).
@@ -5748,7 +5415,7 @@ These indicators do not grant filesystem, process, system recording, or other
5748
5415
  native permissions. Unsupported native APIs still fail admission/runtime checks.
5749
5416
 
5750
5417
  ### UI schema
5751
- Use an array of `{control, port}` objects. Controls are `text`, `number`, `slider`, `checkbox`, and `asset`. Every control binds to a declared input. The host renders these fallback controls in StillMade’s style. A separate package-level `view` may supply a [sandboxed custom interface](/docs/build/custom-interface); it never loads into the parent app DOM.
5418
+ Use an array of `{control, port}` objects. Controls are `text`, `number`, `slider`, `checkbox`, `asset`, `select`, `multiselect`, `file`, `date`, `color`, `url`, `richtext`, `list`, `grid` (the list StillMade validates). Every control binds to a declared input. A `text` control is a box that grows with its lines; add `"multiline": true` for anything typed one item per line or as a list (`{"control":"text","port":"listingFacts","multiline":true}`), so no host draws it as one line. Image, video, audio and file inputs, and their lists, take `asset`; a picture list shows photo tiles. The host renders these fallback controls in StillMade’s style. A separate package-level `view` may supply a [sandboxed custom interface](/docs/build/custom-interface); it never loads into the parent app DOM.
5752
5419
 
5753
5420
  ### Permissions
5754
5421
  Supported families are `project`, `network`, `filesystem`, `secrets`, and `capabilities`. Network, filesystem, and secrets arrays must be empty. A capability runtime declares exactly one supported operation: `text.generate`, `audio.speech`, `audio.transcribe`, `audio.music`, `audio.sfx`, `image.generate`, `video.generate`, `image.describe`, `media.analyze`, `web.fetch`, `web.research`, `timeline.propose`, `workspace.propose`, `connection.execute`; reviewed ComfyUI declares `comfyui.execute`. These permissions do not grant JavaScript or a custom interface direct host access. Supported context read permissions are listed in [project context](/docs/build/context).
@@ -6356,7 +6023,8 @@ export const GENERATION_LIMITS: Readonly<{references:8;negativePrompt:2000;seed:
6356
6023
  /** Fill and check image settings; `strict` rejects values the model does not accept. */
6357
6024
  export function imageSettings(value?:Partial<ImageSettings>,options?:{strict?:boolean}):ImageSettings;
6358
6025
  /** Fill and check video settings, including Veo's 1080p-is-8-seconds rule. */
6359
- export function videoSettings(value?:Partial<VideoSettings>,options?:{strict?:boolean;firstFrame?:boolean}):VideoSettings;
6026
+ /** With no model named, the default is the cheapest model that makes the asked aspect ratio and takes the frames given. */
6027
+ export function videoSettings(value?:Partial<VideoSettings>,options?:{strict?:boolean;firstFrame?:boolean;lastFrame?:boolean}):VideoSettings;
6360
6028
  export function validateSeed(seed:number):number;
6361
6029
  export const MEDIA_GENERATE_LIMITS: Readonly<{items:50;prompt:4000}>;
6362
6030
  export const MEDIA_GENERATE_OPERATIONS: readonly ['image','video','speech','music','sfx'];
@@ -7475,19 +7143,20 @@ Pick the runtime from the job. No hosted AI: a recipe or isolated `javascript`,
7475
7143
 
7476
7144
  ### One job per Step
7477
7145
 
7478
- A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.
7146
+ A Step is one job: the work between two moments where a person or the outside world must see the result. A guided job, one the person fills in and then gets a result from, is one Step with one Block whose own screen holds everything: the inputs and settings, then a cheap check before the expensive run (the script and a voice sample before the video), then the result. On a wide screen the inputs sit on the left and the result's place on the right; on a phone they stack, the result's place under the button. A cheap check or a review the person does on that screen never starts a new Step. Start a new Step only at one of these points; everything else is a part of the neighboring Step.
7479
7147
 
7480
- 1. **A person works with the result**: reads, edits, picks or approves it (a workspace Block such as a script, a Canvas or an Editor, or an approval). So Bible, Canvas and Editor are separate Steps after Chat.
7148
+ 1. **A person works with the result**: in a workspace they spend a while in (the Editor, a storyboard, a Canvas, a CAD or drawing tool), or someone else approves or takes it over (a hand-off). Turning what a workspace made into something else is the next Step: a CAD tool is its own Step, and the Block that takes pictures of its model and makes a video is the next one. So Bible, Canvas and Editor are separate Steps after Chat.
7481
7149
  2. **Finding new things turns into handling each one**: the trigger (a schedule or an app event), the app read, its filter and "only new" are the first job; what is done with each item is the next job. A trigger is a setting of the first job, never a Step of its own.
7482
7150
  3. **The result leaves StillMade**: it is sent, posted, published, paid or written into another app. Consecutive writes to the same app, with the glue that shapes data for them, are one job.
7151
+ 4. **Setup or a wait**: a one-time setup that is not part of each job (a brand kit, capturing a voice or a likeness), or a render that takes minutes and is delivered later.
7483
7152
 
7484
- In a production the Steps are the workspaces a person opens: work feeding the next workspace is its part, else it joins the one before. A Block with its own interface (a workspace, editor or picker) is always its own Step; two such Steps are never one. A video ends in the Editor (1.0), which has captions and export built in, so Export video is never a Step beside it.
7153
+ In a production the Steps are the workspaces a person opens: work feeding the next workspace is its part, else it joins the one before. A Block with its own interface (a workspace, editor or picker) is always its own Step; two such Steps are never one. A Type whose own Block renders the finished video (`timeline.render`) needs no Editor Step; the Editor (1.0, captions and export built in) is a Step only when the person edits the cut by hand, and Export video is never a Step beside it.
7485
7154
 
7486
- A part that only runs in the desktop app cannot share a Step with cloud parts. Before adding a Step, name which of the three reasons makes it separate; if none applies, make it a part of a neighbor. A Step nobody would ever open must not exist on its own. Prefer an app task's own filter (a Gmail query such as `has:attachment filename:png`) to an AI check, and use AI decide only for judgement.
7155
+ A part that only runs in the desktop app cannot share a Step with cloud parts. Before adding a Step, name which of these reasons makes it separate; if none applies, make it a part of a neighbor. A Step nobody would ever open must not exist on its own. Prefer an app task's own filter (a Gmail query such as `has:attachment filename:png`) to an AI check, and use AI decide only for judgement.
7487
7156
 
7488
7157
  Build one Block for a job one Block can do: a module Block calls every hosted operation the job needs (it transcribes a sermon, then finds the verses said in it), a JavaScript Block does several local transforms, a workspace owns its whole document. A Block being built never takes an ingredient's result only so the ingredient can be its own part or Step (Transcribe in front of a verse finder): it calls that operation itself. Parts in one Step (`groups` in the Project Type) are for jobs made of Blocks that already exist and app tasks, each keeping its own permission, approval and credit rules. Never invent a combined runtime.
7489
7158
 
7490
- Examples: "every new email with an image becomes a generated image" is two Steps, "Gmail · New emails with images" (When, fetch with an image filter, only new) and "Image from email" (AI write, Generate image); listing When, Fetch, Only new, AI decide, Continue only if, AI write and Generate image as seven Steps splits one job into its parts. "A new Meta lead goes to HubSpot" is three Steps: "Meta Ads · New leads", "Qualify lead" and "HubSpot · Add contact".
7159
+ Examples: "a listing video from my photos and the home's facts" is one Step: one Block whose page takes the photos and facts, shows the script and a voice sample to check, then makes the video. "every new email with an image becomes a generated image" is two Steps, "Gmail · New emails with images" (When, fetch with an image filter, only new) and "Image from email" (AI write, Generate image); listing When, Fetch, Only new, AI decide, Continue only if, AI write and Generate image as seven Steps splits one job into its parts. "A new Meta lead goes to HubSpot" is three Steps: "Meta Ads · New leads", "Qualify lead" and "HubSpot · Add contact".
7491
7160
 
7492
7161
  ### Define the capability first
7493
7162
 
@@ -7515,7 +7184,7 @@ Give the Block a short, plain name of 40 characters or fewer that says what it d
7515
7184
 
7516
7185
  ### Name inputs in the job's words
7517
7186
 
7518
- Name each input for what the person brings, in the job's words: `sermonVideo` for a sermon clip Block, `listingPhotos` for a listing, `lessonSlides` for a lesson podcast. StillMade titles the input from that name ("Sermon video") and words its drop area from it ("Drop your sermon video here", the formats, "Choose a video"), in StillMade and the SDK preview, so never draw your own upload box for an input. Keep a bare type name (`video`, `file`) only when any file of that kind does the job. The `description` says what makes a good one.
7187
+ Name each input for what the person brings, in the job's words: `sermonVideo` for a sermon clip Block, `listingPhotos` for a listing, `lessonSlides` for a lesson podcast. StillMade titles the input from that name ("Sermon video") and words its drop area from it ("Drop your sermon video here", the formats, "Choose a video"), in StillMade and the SDK preview, so never draw your own upload box or `<input type="file">`: a Block's screen shows the files as tiles and adds more with `<button class="sm-add" data-stillmade-add="listingPhotos" aria-label="Add photos">`, which opens StillMade's picker and upload, and a list's tiles drop or reorder a file with `data-stillmade-remove="listingPhotos" data-index="2"` and `data-stillmade-move="listingPhotos" data-index="2" data-to="1"` buttons; the files arrive through `onInput`. Keep a bare type name (`video`, `file`) only when any file of that kind does the job. The `description` says what makes a good one.
7519
7188
 
7520
7189
  ### Speak the job
7521
7190
 
@@ -7523,7 +7192,7 @@ These are StillMade's defaults: unless the person or the creator asked for somet
7523
7192
 
7524
7193
  ### Interface
7525
7194
 
7526
- If the capability needs UI, use the Block UI SDK: generated `manifest.ui` controls first; a sandboxed custom view (`view.html`/`view.css`/`view.js`) only when the core interaction needs one. Build custom views from the Block kit the host injects (`block-kit.css`): the result in `sm-stage`, picks as pictures in `sm-choices`, the picked one in the kit's ink fill (never restyled to an outline, a tint or the page's background) so it reads at a glance, one solid `sm-primary` naming the action in the job's words ("Plan the tour", "Generate voiceover"; never "Run" or "Make it") and no price (StillMade's run button adds it), extras in a closed `sm-more`; a look the person names wins. Fit one laptop screen without scrolling: label beside control, status in a few words, files as tiles, rare tools as icons, (i) on unclear controls. These compact rules are for a job or an ingredient. Size the view to the Block's tier (block-tier.js): an app, a workspace a person works in for a while (video editor, 3D configurator, CAD or drawing tool), looks and works like that kind of app instead of a compact page: its canvas or stage fills the view, with the panels, toolbars, inspector or timeline such an app has, direct manipulation (select, drag, zoom), keyboard shortcuts and undo; it keeps the host appearance and Block kit controls, its panels may scroll, and on a phone its tools fold into sheets while the canvas stays usable. Never shrink an app into a form of fields and one button. StillMade centers a standard view narrower than its Step and zooms a view to fill its space; an app that lays itself out (full-size canvas or WebGL, its own scroll, pan or zoom, its own Ctrl/Cmd +/- keys) puts `data-stillmade-layout="own"` on its outermost element, and StillMade then never centers, zooms or takes those keys. StillMade draws one player for every `<video controls>` (play, timeline, time, sound and full screen, shown while the pointer is on the video, with keys and touch) and a full-screen button on result pictures in `sm-stage` or `sm-gallery` and on any element marked `data-stillmade-expand` (the same value makes one set of pictures, `off` removes it); `data-stillmade-player="native"` keeps the browser's controls, and a `<video>` without `controls` is the Block's own and is never touched. A picture or clip that cannot show here (kept on another device, or it did not open) keeps its place as a quiet `sm-media-off` tile in its own shape with the `image-off` glyph and the reason in its label, never words in its place. Drop words nobody needs; put helpful ones in a closed `sm-hint`. Mark each button `data-stillmade-action="what it does"` and label each field: Chat presses and fills them. Expose the capability, not the implementation: sensible defaults, previews, progress, clear errors, visible outputs. Show no raw JSON, internal IDs, filesystem details, provider terms or developer settings unless the Block is a developer tool. When a person chooses among options they need to hear or see (voices, music, fonts, looks, templates, images), let them preview each option before choosing: a sample or thumbnail from the Block's own files or the host's free samples, never a paid generation. Skip previews only when there is nothing to preview. Choices StillMade owns (voices, models in the run review) already show samples. Follow the host appearance (Light, Dark and White) and one elastic layout: stacked on a 320px phone with no sideways scroll (result first, action pinned), and from 860px the controls left (≤380px), the result filling the right. Short settings sit side by side, each label beside its control (`sm-field`, `sm-settings`; three or four short options as one `data-segmented` row); many items are one line each (`sm-list`) that open in place; input files are small tiles (`sm-gallery data-size="small"`); on a phone chip rows scroll sideways and long lists show three. Compacting moves controls, never deletes them.
7195
+ If the capability needs UI, use the Block UI SDK: generated `manifest.ui` controls first; a sandboxed custom view (`view.html`/`view.css`/`view.js`) only when the core interaction needs one. Build custom views from the Block kit the host injects (`block-kit.css`): the result in `sm-stage`, drawn before the first run in the shape and size of what is coming (`data-ratio="9:16"` for a vertical clip, lines for text, a player bar for sound) so every part of the screen is in place at once and the result fills a place the person already sees instead of popping in somewhere unexpected; StillMade draws it the same way on the Steps it draws; on a wide screen what goes in sits on the left and the result on the right, side by side, and they stack only on narrow screens and phones, the result's place under the button, picks as pictures in `sm-choices`, the picked one in the kit's ink fill (never restyled to an outline, a tint or the page's background) so it reads at a glance, one solid `sm-primary` naming the action in the job's words ("Plan the tour", "Generate voiceover"; never "Run" or "Make it") and no price (StillMade's run button adds it), extras in a closed `sm-more`; a look the person names wins. Fit one laptop screen without scrolling: label beside control, status in a few words, files as tiles, rare tools as icons, (i) on unclear controls. These compact rules are for a job or an ingredient. Size the view to the Block's tier (block-tier.js): an app, a workspace a person works in for a while (video editor, 3D configurator, CAD or drawing tool), looks and works like that kind of app instead of a compact page: its canvas or stage fills the view, with the panels, toolbars, inspector or timeline such an app has, direct manipulation (select, drag, zoom), keyboard shortcuts and undo; it keeps the host appearance and Block kit controls, its panels may scroll, and on a phone its tools fold into sheets while the canvas stays usable. Never shrink an app into a form of fields and one button. StillMade centers a standard view narrower than its Step and zooms a view to fill its space; an app that lays itself out (full-size canvas or WebGL, its own scroll, pan or zoom, its own Ctrl/Cmd +/- keys) puts `data-stillmade-layout="own"` on its outermost element, and StillMade then never centers, zooms or takes those keys. StillMade draws one player for every `<video controls>` (play, timeline, time, sound and full screen, shown while the pointer is on the video, with keys and touch) and a full-screen button on result pictures in `sm-stage` or `sm-gallery` and on any element marked `data-stillmade-expand` (the same value makes one set of pictures, `off` removes it); `data-stillmade-player="native"` keeps the browser's controls, and a `<video>` without `controls` is the Block's own and is never touched. A picture or clip that cannot show here (kept on another device, or it did not open) keeps its place as a quiet `sm-media-off` tile in its own shape with the `image-off` glyph and the reason in its label, never words in its place. Drop words nobody needs; put helpful ones in a closed `sm-hint`. Mark each button `data-stillmade-action` (`run` on the one that runs the Block, which the device checks press once; a page that checks first keeps `run` on that button for both presses; what it does on the others, `"play sample"`) and label each field: Chat presses and fills them. Expose the capability, not the implementation: sensible defaults, previews, progress, clear errors, visible outputs. Show no raw JSON, internal IDs, filesystem details, provider terms or developer settings unless the Block is a developer tool. When a person chooses among options they need to hear or see (voices, music, fonts, looks, templates, images), let them preview each option before choosing: a sample or thumbnail from the Block's own files or the host's free samples, never a paid generation. Skip previews only when there is nothing to preview. Choices StillMade owns (voices, models in the run review) already show samples. Follow the host appearance (Light, Dark and White) and one elastic layout: stacked on a 320px phone with no sideways scroll (the inputs, the button in normal flow, never pinned, then the result's place under it), and from 860px the controls left (≤380px), the result filling the right. Short settings sit side by side, each label beside its control (`sm-field`, `sm-settings`; three or four short options as one `data-segmented` row); many items are one line each (`sm-list`) that open in place; input files are small tiles (`sm-gallery data-size="small"`); on a phone chip rows scroll sideways and long lists show three. Compacting moves controls, never deletes them.
7527
7196
 
7528
7197
  ### Shared editing
7529
7198
 
@@ -7531,11 +7200,11 @@ Projects are multiplayer. A custom view subscribes through `StillMade.onShared(c
7531
7200
 
7532
7201
  ### External services
7533
7202
 
7534
- Reach external services only through StillMade's hosted operations, the connections service (`connection.execute`), or, in a module Block, `stillmade.net.fetch` to origins declared in `permissions.network` (with the key each needs), `stillmade.net.connect` for a live WebSocket to one of them, and `stillmade.connections.call` to adapters or catalog apps in `permissions.connections`; never put keys in code, inputs or headers: StillMade adds each person's own key on its servers. The host owns the model and provider choice, BYOK keys, StillMade credits, spending consent, authorization, token refresh, rate limits, retries and provider errors; a Block never receives a secret and never names a provider unless it must. Treat provider unavailability, timeout, cancellation and partial failure as typed outcomes the Block reports, not crashes. Generated images, video, voices, music and sound effects come from hosted operations or the `media.generate` action, paid with the user's StillMade credits at StillMade's prices; for structured model output declare a `json` output with `capability.schema` instead of parsing prose. When a Block needs npm libraries, WebAssembly (threaded: `"threads": true` in module.json) or heavy compute, use `runtime: "module"` and reach hosted operations through `stillmade.hosted.run` with the operations listed in `permissions.capabilities`. Decode, cut, join, re-encode, extract or normalise audio and video with `stillmade.media.transform` on the device; never ask for a server render. For a rich interactive interface (React or another framework, SVG, canvas, drag), use a frame view: `src/view.config.json` with `{"runtime":"frame"}` and `src/view.js` bundled as one IIFE, colored with StillMade's theme variables. For files and structured data use the `file`, `document`, `table`, `url`, `date` and `color` types and their generated controls and presentations, not JSON text.
7203
+ Reach external services only through StillMade's hosted operations, the connections service (`connection.execute`), or, in a module Block, `stillmade.net.fetch` to origins declared in `permissions.network` (with the key each needs), `stillmade.net.connect` for a live WebSocket to one of them, and `stillmade.connections.call` to adapters or catalog apps in `permissions.connections`; never put keys in code, inputs or headers: StillMade adds each person's own key on its servers. The host owns the model and provider choice, BYOK keys, StillMade credits, spending consent, authorization, token refresh, rate limits, retries and provider errors; a Block never receives a secret and never names a provider unless it must. Treat provider unavailability, timeout, cancellation and partial failure as outcomes the Block reports, not crashes: a call the result needs throws and fails the run (see Useful failures); an optional part's failure is caught, given its fallback and named in an optional `notes` text output. Generated images, video, voices, music and sound effects come from hosted operations or the `media.generate` action, paid with the user's StillMade credits at StillMade's prices; for structured model output declare a `json` output with `capability.schema` instead of parsing prose. When a Block needs npm libraries, WebAssembly (threaded: `"threads": true` in module.json) or heavy compute, use `runtime: "module"` and reach hosted operations through `stillmade.hosted.run` with the operations listed in `permissions.capabilities`. Decode, cut, join, re-encode, extract or normalise audio and video with `stillmade.media.transform` on the device; never ask for a server render. For a rich interactive interface (React or another framework, SVG, canvas, drag), use a frame view: `src/view.config.json` with `{"runtime":"frame"}` and `src/view.js` bundled as one IIFE, colored with StillMade's theme variables. For files and structured data use the `file`, `document`, `table`, `url`, `date` and `color` types and their generated controls and presentations, not JSON text.
7535
7204
 
7536
7205
  ### Dependencies
7537
7206
 
7538
- A Block must declare everything it requires and must not assume anything is installed. Portable packages carry no package dependencies (`portable.dependencies` is empty): inline small, license-compatible code and record its license and notice in `manifest.license` and `manifest.provenance`. Desktop-only tools are reached solely through declared desktop permissions and the host desktop bridge. If a dependency cannot be delivered through the Block runtime, report the incompatibility instead of assuming the user's machine has it.
7207
+ A Block must declare everything it requires and must not assume anything is installed. Portable packages carry no package dependencies (`portable.dependencies` is empty): inline small, license-compatible code and record its license and notice in `manifest.license` and `manifest.provenance`. The copyright holder in the Block's own `provenance.notice` is the person it is made for: replace the placeholder `create` writes with their name, never StillMade, the SDK or yourself. Desktop-only tools are reached solely through declared desktop permissions and the host desktop bridge. If a dependency cannot be delivered through the Block runtime, report the incompatibility instead of assuming the user's machine has it.
7539
7208
 
7540
7209
  ### Security
7541
7210
 
@@ -7635,17 +7304,17 @@ For hosted text generation use {manifest,capability,tests}, runtime:"capability"
7635
7304
 
7636
7305
  ### Hosted media operations
7637
7306
 
7638
- For audio transcription, use runtime:"capability", operation:"audio.transcribe" and permissions.capabilities:["audio.transcribe"]. Declare one primary audio input with an asset control, one text language input defaulting to "auto", and one primary transcript output. Bind the two inputs through capability.audio and capability.language. Provider, model, payment and credentials belong to the host. Fixtures use transcript word-count bounds, never invented provider text. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.transcribe-audio","action":{"label":"Transcribe audio","running":"Transcribing…"},"version":"1.0.0","name":"Transcribe audio","description":"Turn one persistent audio file into reviewed text with word timings.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"audio":{"type":"audio","primary":true},"language":{"type":"text","default":"auto"}},"outputs":{"transcript":{"type":"transcript","primary":true}},"permissions":{"project":[],"capabilities":["audio.transcribe"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"asset","port":"audio"},{"control":"select","port":"language","options":["auto","en","es","fr","de","it","pt","ja","ko","zh"]}]},"capability":{"schemaVersion":1,"operation":"audio.transcribe","audio":{"$input":"audio"},"language":{"$input":"language"},"output":"transcript"},"tests":[{"name":"timed speech","input":{"audio":{"kind":"audio","assetId":"audio-1","versionId":"version-1","url":"/api/media/example/audio.mp3","mimeType":"audio/mpeg"},"language":"en"},"expectations":{"transcript":{"kind":"transcript","minWords":1,"maxWords":1000}}}]}.
7307
+ For audio transcription, use runtime:"capability", operation:"audio.transcribe" and permissions.capabilities:["audio.transcribe"]. Declare one primary audio input with an asset control, one text language input defaulting to "auto", and one primary transcript output. Bind the two inputs through capability.audio and capability.language. Provider, model, payment and credentials belong to the host. Fixtures use transcript word-count bounds, never invented provider text. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.transcribe-audio","action":{"label":"Transcribe audio","running":"Transcribing…"},"version":"1.0.0","name":"Transcribe audio","description":"Turn one persistent audio file into reviewed text with word timings.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"audio":{"type":"audio","primary":true,"required":true,"semantic":"source_audio","description":"The recording to turn into text."},"language":{"type":"text","default":"auto","required":false,"semantic":"language_code","description":"auto, or a two-letter language code such as en."}},"outputs":{"transcript":{"type":"transcript","primary":true,"semantic":"timed_transcript","description":"The spoken words with their timings."}},"permissions":{"project":[],"capabilities":["audio.transcribe"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"asset","port":"audio"},{"control":"select","port":"language","options":["auto","en","es","fr","de","it","pt","ja","ko","zh"]}]},"capability":{"schemaVersion":1,"operation":"audio.transcribe","audio":{"$input":"audio"},"language":{"$input":"language"},"output":"transcript"},"tests":[{"name":"timed speech","input":{"audio":{"kind":"audio","assetId":"audio-1","versionId":"version-1","url":"/api/media/example/audio.mp3","mimeType":"audio/mpeg"},"language":"en"},"expectations":{"transcript":{"kind":"transcript","minWords":1,"maxWords":1000}}}]}.
7639
7308
  For narration or speech synthesis, use runtime:"capability", operation:"audio.speech" and permissions.capabilities:["audio.speech"]. One required primary text or script input feeds capability.text via {$input:"portName"}; a script value contains schemaVersion:1 and text. One primary audio output is named by capability.output. Text must be at most 3800 characters. The host owns the voice, model, provider, billing and BYOK. Do not put those choices in package code. Tests use audio expectations (WAV, duration and byte bounds), never invented exact audio. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.narrate-script","action":{"label":"Generate voiceover","running":"Generating voiceover…"},"version":"1.0.0","name":"Narrate a script","description":"Turn a short script into a reviewed speech recording using a host-selected voice.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"script":{"type":"script","primary":true,"semantic":"narration_script","required":true,"description":"Narration script to render as speech."}},"outputs":{"audio":{"type":"audio","primary":true,"semantic":"speech_audio","description":"Speech audio generated from the narration script."}},"permissions":{"project":[],"capabilities":["audio.speech"],"network":[],"filesystem":[],"secrets":[]},"ui":[]},"capability":{"schemaVersion":1,"operation":"audio.speech","text":{"$input":"script"},"output":"audio"},"tests":[{"name":"Read a short welcome","input":{"script":{"schemaVersion":1,"text":"Welcome to the quiet forest."}},"expectations":{"audio":{"kind":"audio","format":"wav","minDuration":0.1,"maxDuration":30,"minBytes":46,"maxBytes":1500000}}}]}.
7640
7309
  For image generation, use runtime:"capability", operation:"image.generate" and permissions.capabilities:["image.generate"]. One required primary text input feeds capability.prompt via {$input:"portName"}; one primary image output is named by capability.output. Prompt limit 32000 characters. The supported output is PNG 1024x1024, at most 12000000 bytes. Tests use image expectations, never fabricated generated URLs or pixels. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.generate-image","action":{"label":"Generate image","running":"Generating image…"},"version":"1.0.0","name":"Generate an image","description":"Generate a square PNG from a short visual prompt using an approved host request.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"prompt":{"type":"text","primary":true,"semantic":"image_prompt","required":true,"description":"Visual description of the image to generate."}},"outputs":{"image":{"type":"image","primary":true,"semantic":"generated_image","description":"The generated square PNG image."}},"permissions":{"project":[],"capabilities":["image.generate"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"text","port":"prompt"}]},"capability":{"schemaVersion":1,"operation":"image.generate","prompt":{"$input":"prompt"},"output":"image"},"tests":[{"name":"A simple forest illustration","input":{"prompt":"A simple illustration of a quiet forest with clear dark outlines and soft green colors."},"expectations":{"image":{"kind":"image","format":"png","width":1024,"height":1024,"minBytes":67,"maxBytes":12000000}}}]}.
7641
- For image or video analysis (what a photo shows, which photos show different angles, the words on a menu or sign), use runtime:"capability", operation:"media.analyze" and permissions.capabilities:["media.analyze"]: StillMade credits pay for it, while image.describe runs only on the person's own OpenRouter key, so never use it. Declare one primary image input, one image[] input of one to four images, or one video input (with optional start and end number inputs, bound through capability.range:{start:{"$input":"start"},end:{"$input":"end"}}), and one primary text or json output. capability:{schemaVersion:1,operation:"media.analyze",media:{"$input":"photo"},question:"what to look for, at most 2,000 characters",output:"notes"}; a text output gets each picture's summary, a json output {schemaVersion:1,items}. For a quick rating of photos (sorting or picking), add detail:"check": images only, one short answer each, one credit per call of up to four photos instead of four credits per picture. The host loads only owned media and controls provider, model, payment and approval. Tests give saved references and expect json that includes summary, or text length bounds, never invented live answers. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.check-clip","action":{"label":"Check clip","running":"Checking clip…"},"version":"1.0.0","name":"Check a clip","description":"Answer a production question about a saved video clip, with timestamps.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"clip":{"type":"video","primary":true,"semantic":"source_video","required":true,"description":"Saved video to check."},"start":{"type":"number","required":false,"min":0,"semantic":"range_start","description":"Optional start of the range to look at closely, in seconds."},"end":{"type":"number","required":false,"min":0,"semantic":"range_end","description":"Optional end of the range, in seconds (at most 12 seconds after the start)."}},"outputs":{"report":{"type":"json","primary":true,"semantic":"media_findings","description":"Summary, timeline, on-screen text and issues found."}},"permissions":{"project":[],"capabilities":["media.analyze"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"asset","port":"clip"},{"control":"number","port":"start"},{"control":"number","port":"end"}]},"capability":{"schemaVersion":1,"operation":"media.analyze","media":{"$input":"clip"},"question":"Is the product logo clearly visible, and when? List any on-screen text and visible defects.","range":{"start":{"$input":"start"},"end":{"$input":"end"}},"output":"report"},"tests":[{"name":"A saved clip","input":{"clip":{"kind":"video","assetId":"clip-1","versionId":"v1","url":"/api/media/owner/clip.mp4"}},"expectations":{"report":{"kind":"json","includes":["summary"]}}}]}.
7642
- For AI-directed editing, use runtime:"capability", operation:"timeline.propose" and permissions.capabilities:["timeline.propose"]. Declare one current timeline input, up to three additional typed context inputs, and one timeline-edit output. Bind every input exactly once through capability.context; declare context.timeline.read and timeline.propose. StillMade controls the provider, strict JSON validation, stale-before checks, preview and Apply. Tests declare timeline-edit command bounds, never fabricated provider results. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.timeline-polish","action":{"label":"Suggest edit","running":"Suggesting edit…"},"version":"1.0.0","name":"Timeline polish","description":"Turn a timeline and editing direction into a bounded proposal that remains reviewable before Apply.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"timeline":{"type":"timeline","context":"timeline","primary":true},"direction":{"type":"text","default":"Polish this cut conservatively."}},"outputs":{"edit":{"type":"timeline-edit","primary":true}},"permissions":{"project":["context.timeline.read","timeline.propose"],"capabilities":["timeline.propose"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"text","port":"direction"}]},"capability":{"schemaVersion":1,"operation":"timeline.propose","context":{"timeline":{"$input":"timeline"},"direction":{"$input":"direction"}},"instruction":"Use only supported timeline-edit commands, exact before snapshots, and the current activeTimelineId. Preserve media and locked items. Return strict JSON only.","maxTokens":2000,"output":"edit"},"tests":[{"name":"one restrained edit","input":{"timeline":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[{"id":"video"}],"clips":[{"id":"clip-1","trackId":"video","start":0,"duration":4}],"audioItems":[],"captions":[]},"direction":"Add a soft transition."},"expectations":{"edit":{"kind":"timeline-edit","minCommands":1,"maxCommands":10}}}]}.
7643
- For video generation use runtime:"capability", operation:"video.generate", permissions.capabilities:["video.generate"], one primary text input and one primary video output. capability.prompt references the input and capability.output names the output. Prompt limit 32000. Tests declare MP4 duration, dimensions and byte bounds; never fabricate video URLs. The enabled host selects its supported model, duration, resolution, payment and approved quote; packages cannot choose provider settings or dispatch jobs. Shipped example: {"manifest":{"schemaVersion":1,"id":"example.generate-video","action":{"label":"Generate video","running":"Generating video…"},"name":"Generate video","version":"1.0.0","sdkVersion":"0.1.0","description":"Turn a production prompt into a short video using an approved host provider.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"prompt":{"type":"text","primary":true,"required":true}},"outputs":{"video":{"type":"video","primary":true}},"permissions":{"project":[],"capabilities":["video.generate"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"port":"prompt","control":"text"}]},"capability":{"schemaVersion":1,"operation":"video.generate","prompt":{"$input":"prompt"},"output":"video"},"tests":[{"name":"Produces a bounded MP4 sample","input":{"prompt":"A paper boat floating across a calm blue pond. Gentle camera movement."},"expectations":{"video":{"kind":"video","format":"mp4","minDuration":1,"maxDuration":60,"minBytes":32,"maxBytes":67108864,"minWidth":1,"maxWidth":3840,"minHeight":1,"maxHeight":3840}}}]}.
7310
+ For image or video analysis (what a photo shows, which photos show different angles, the words on a menu or sign), use runtime:"capability", operation:"media.analyze" and permissions.capabilities:["media.analyze"]: StillMade credits pay for it, while image.describe runs only on the person's own OpenRouter key, so never use it. Declare one primary image input, one image[] input of one to four images, or one video input (with optional start and end number inputs, bound through capability.range:{start:{"$input":"start"},end:{"$input":"end"}}), and one primary text or json output. capability:{schemaVersion:1,operation:"media.analyze",media:{"$input":"photo"},question:"what to look for, at most 2,000 characters",output:"notes"}; a text output gets each picture's summary, a json output {schemaVersion:1,items}. For a quick rating of photos (sorting or picking), add detail:"check": images only, one short answer each, one credit per call of up to four photos instead of four credits per picture. The host loads only owned media and controls provider, model, payment and approval. Tests give saved references and expect json that includes summary, or text length bounds, never invented live answers. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.check-clip","action":{"label":"Check clip","running":"Checking clip…"},"version":"1.0.0","name":"Check a clip","description":"Answer a production question about a saved video clip, with timestamps.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"clip":{"type":"video","primary":true,"semantic":"source_video","required":true,"description":"Saved video to check."},"start":{"type":"number","required":false,"min":0,"semantic":"range_start","description":"Optional start of the range to look at closely, in seconds."},"end":{"type":"number","required":false,"min":0,"semantic":"range_end","description":"Optional end of the range, in seconds (at most 12 seconds after the start)."}},"outputs":{"report":{"type":"json","primary":true,"semantic":"media_findings","description":"Summary, timeline, on-screen text and issues found.","schema":{"items":"json[]"}}},"permissions":{"project":[],"capabilities":["media.analyze"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"asset","port":"clip"},{"control":"number","port":"start"},{"control":"number","port":"end"}]},"capability":{"schemaVersion":1,"operation":"media.analyze","media":{"$input":"clip"},"question":"Is the product logo clearly visible, and when? List any on-screen text and visible defects.","range":{"start":{"$input":"start"},"end":{"$input":"end"}},"output":"report"},"tests":[{"name":"A saved clip","input":{"clip":{"kind":"video","assetId":"clip-1","versionId":"v1","url":"/api/media/owner/clip.mp4"}},"expectations":{"report":{"kind":"json","includes":["summary"]}}}]}.
7311
+ For AI-directed editing, use runtime:"capability", operation:"timeline.propose" and permissions.capabilities:["timeline.propose"]. Declare one current timeline input, up to three additional typed context inputs, and one timeline-edit output. Bind every input exactly once through capability.context; declare context.timeline.read and timeline.propose. StillMade controls the provider, strict JSON validation, stale-before checks, preview and Apply. Tests declare timeline-edit command bounds, never fabricated provider results. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.timeline-polish","action":{"label":"Suggest edit","running":"Suggesting edit…"},"version":"1.0.0","name":"Timeline polish","description":"Turn a timeline and editing direction into a bounded proposal that remains reviewable before Apply.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"timeline":{"type":"timeline","context":"timeline","primary":true,"required":true,"semantic":"edit_timeline","description":"The cut to improve."},"direction":{"type":"text","default":"Polish this cut conservatively.","required":false,"semantic":"edit_direction","description":"How to change the cut, in a sentence."}},"outputs":{"edit":{"type":"timeline-edit","primary":true,"semantic":"timeline_edit","description":"The proposed changes to review before they apply."}},"permissions":{"project":["context.timeline.read","timeline.propose"],"capabilities":["timeline.propose"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"text","port":"direction"}]},"capability":{"schemaVersion":1,"operation":"timeline.propose","context":{"timeline":{"$input":"timeline"},"direction":{"$input":"direction"}},"instruction":"Use only supported timeline-edit commands, exact before snapshots, and the current activeTimelineId. Preserve media and locked items. Return strict JSON only.","maxTokens":2000,"output":"edit"},"tests":[{"name":"one restrained edit","input":{"timeline":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[{"id":"video"}],"clips":[{"id":"clip-1","trackId":"video","start":0,"duration":4}],"audioItems":[],"captions":[]},"direction":"Add a soft transition."},"expectations":{"edit":{"kind":"timeline-edit","minCommands":1,"maxCommands":10}}}]}.
7312
+ For video generation use runtime:"capability", operation:"video.generate", permissions.capabilities:["video.generate"], one primary text input and one primary video output. capability.prompt references the input and capability.output names the output. Prompt limit 32000. Tests declare MP4 duration, dimensions and byte bounds; never fabricate video URLs. The enabled host selects its supported model, duration, resolution, payment and approved quote; packages cannot choose provider settings or dispatch jobs. Shipped example: {"manifest":{"schemaVersion":1,"id":"example.generate-video","action":{"label":"Generate video","running":"Generating video…"},"name":"Generate video","version":"1.0.0","sdkVersion":"0.1.0","description":"Turn a production prompt into a short video using an approved host provider.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"prompt":{"type":"text","primary":true,"required":true,"semantic":"video_prompt","description":"What the clip shows: subject, action, setting and camera, in a sentence or two."}},"outputs":{"video":{"type":"video","primary":true,"semantic":"generated_video","description":"The generated clip."}},"permissions":{"project":[],"capabilities":["video.generate"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"port":"prompt","control":"text"}]},"capability":{"schemaVersion":1,"operation":"video.generate","prompt":{"$input":"prompt"},"output":"video"},"tests":[{"name":"Produces a bounded MP4 sample","input":{"prompt":"A paper boat floating across a calm blue pond. Gentle camera movement."},"expectations":{"video":{"kind":"video","format":"mp4","minDuration":1,"maxDuration":60,"minBytes":32,"maxBytes":67108864,"minWidth":1,"maxWidth":3840,"minHeight":1,"maxHeight":3840}}}]}.
7644
7313
  A capability package performs exactly one supported operation. A job that needs several (transcribe a recording, then find what is said in it; write words, then voice them), or code between two operations, is one module Block that calls each with stillmade.hosted.run (Module Blocks); never split one job into part Blocks, and never invent a combined capability runtime. Hosted image, speech and video source can be statically validated while authoring, but real fixture/sample generation requires the user's subsequent model/payment selection and quote confirmation. Do not claim those calls ran. Deterministic image transformations should remain sandboxed recipe or JavaScript code when supported; do not generate a new image as a substitute for processing an existing one.
7645
7314
 
7646
7315
  ### Module Blocks
7647
7316
 
7648
- A job that needs several hosted operations, or code around one (reading a transcript's words and times, shaping a list), or media work on the person's device even with no hosted operation (rendering clips and photos into one video, joining pictures into one), is a module Block: {manifest,moduleSource,tests}, runtime:"module", moduleSource:{"main.js":"<the code>"}. StillMade names main.js by its digest and adds module. StillMade's in-app builder takes one file: give no other files, and main.js is plain JavaScript that imports nothing and uses no browser APIs (no fetch, TextEncoder, timers or WebCodecs). That single-file rule is the in-app builder's; a module Block built with the SDK may use timers, WebCodecs, bundled npm packages and .wasm files as MODULES.md describes. main.js: export default async function run(input, stillmade) returns the declared outputs. It calls each hosted operation with await stillmade.hosted.run(operation, request), listed in manifest.permissions.capabilities: text.generate, image.generate, video.generate, audio.speech, audio.music, audio.sfx, audio.transcribe, image.describe, media.analyze, web.fetch, web.research. Requests, and what each call returns (a test's mocks record exactly that value): text.generate {prompt, system, maxTokens} returns the text as a string, and with schema (a JSON schema object) returns that object, so ask for JSON with schema; one text.generate prompt holds at most 32,000 characters, so a longer file or transcript is read in parts (one call per part of about 25,000 characters, then one call that joins what they found), never cut off, and when the result is the whole text again (a cleaned transcript, a translation) each part's answer is kept and joined in order in main.js, since one answer holds at most 4,096 tokens; audio.transcribe {audio: the input's saved audio or video reference (a video's own sound is transcribed, so never extract it first), language:"auto"} returns {schemaVersion:1,text,words:[{text,start,end}]}; image.generate {prompt, references?: one to eight saved images the new picture keeps, such as the product, person or place in the person's photos} and audio.speech {text} return a saved image or audio reference (an audio reference always carries its measured duration); web.fetch {url, instruction} returns a string; media.analyze {media: one to four saved images or one saved video, question, range?: {start, end} in seconds of that video, at most 12 apart, to look closely at one part of it} returns {schemaVersion:1,items:[{kind,summary,observations,detectedText}]}, one item per image, and is how a Block looks at pictures (picking the photos that show different angles, reading a menu off a photo): StillMade credits pay for it; image.describe {image, instruction} returns a string but runs only on the person's own OpenRouter key, so never use it; web.research {topic} returns {summary,sources}. A file the person gives the Block (a text, CSV, Markdown or JSON file on a file input) is read with await stillmade.files.read(input.<port>), which returns its bytes; turn them into text in main.js as the worked file input below does, and give the test's input file an assetId whose text mocks.files records as {files:{"<assetId>":{text:"..."}}}. A Block that reads a file the person gives it is a module Block, never a text.generate capability package. A PDF (lesson slides, a lease, a contract) is read with await stillmade.media.transform({operation:"document.text",source:input.<port>}), which returns {text, pages}, all the words and pages, a list holding each page's words as one string, on the person's device and without credits (a text file reads the same way); a scanned PDF has no words to read. A call that fails throws: let it fail the run rather than catching it and returning an empty result. Media work on the person's device (cut, join or resize video, decode or extract audio, thumbnails, joining images) is await stillmade.media.transform(request). Joining saved images into one picture (two photos side by side with Before and After labels, a grid or collage, a contact sheet, a storyboard sheet, a poster with a title) is {operation:"image.compose",width,height,background:"#ffffff",layers:[{source,x,y,width,height,fit:"cover" or "contain"}],text:[{text,x,y,size,color,background,align:"left" or "center" or "right",width}]}: a canvas up to 4096 pixels a side, positions in pixels worked out in main.js, layers drawn in order and text on top (y is the top of the line, background puts a box behind it, colours are #rrggbb or, see-through, #rrggbbaa such as #00000099 for a caption box, width wraps the words onto lines no wider than it: give a caption under a frame the frame's width so it never runs into the next one); it returns the saved image, and main.js never decodes pixels itself. Exporting is a transform too: {operation:"timeline.render",kind:"video" or "audio",aspect:"9:16",clips:[{source,start,duration,from,transition:{type:"Fade"},look:"Filmic",motion:"zoom-in"}],audio:[{source,start,duration}],captions:[{text,start,end,words:[{text,start,end}],highlight:"#ffd400",size:80}]} (motion, a slow camera move over a photo, is zoom-in, zoom-out, pan-left, pan-right, pan-up or pan-down; size is the caption height in pixels, 80 for a vertical video) renders the Block's own timeline with the Editor's transitions, looks and word-by-word captions under the person's export plan (start is a clip's place on that timeline, so each clip starts where the one before it ends, less the transition on the clip before it, which blends that clip into the next, and from is where it begins in its own file; overlapping clips are refused; a clip of another shape than the frame is cropped to fill it around its middle, or around focus:{x,y} on the clip, the point to keep in view as fractions 0 to 1 across the picture, such as where the speaker stands (focus.scale, 1 to 2.5 and instead of a motion, holds the clip that much closer on that point), or is fitted whole with fit:"contain"), so never ask for a separate export, captions or color Block; stillmade.log and stillmade.progress(fraction, label) report. One press of the Step's button approves each hosted operation once at the price shown beside it, and media.analyze for every picture or video the Step is given, across as many calls as that takes; any further call asks the person first. A module Block runs in the person's browser while StillMade is open, never on a schedule or event. Tests are {name,input,expected,mocks} and compare outputs, so every test is a case that runs to its outputs; an input the Block refuses (it throws a plain Error with a message for the person, such as a clip with no duration) is never a test. mocks records what each call answers, as {hosted:{"audio.transcribe":{schemaVersion:1,text:"...",words:[...]},"text.generate":{...}},transforms:{...}} (a list answers successive calls in order), and expected is the exact output main.js returns for those answers: exact values of each output's type, never capability bounds such as minDuration or maxBytes. In a module test, mock each media call with its complete saved reference (kind, assetId, versionId, url, mimeType and its measured fields: duration, bytes, sampleRate, channels for audio; width, height, bytes for images), its url an example file such as /api/media/example/clip.mp4 so the preview can play it, and repeat those references unchanged in expected; names or labels go in their own output. Worked outputs, each a passing test. A document output is the document object on a document port (not text): {"outputs":{"sheet":{"type":"document"}},"main.js":"export default async function run(input, stillmade) { const text = await stillmade.hosted.run('text.generate', {prompt: 'A Markdown care sheet for ' + input.plant, maxTokens: 800}); return {sheet: {schemaVersion: 1, format: 'markdown', text, title: input.plant + ' care'}}; }","test":{"name":"Fern","input":{"plant":"Fern"},"mocks":{"hosted":{"text.generate":"## Light\nBright, indirect light."}},"expected":{"sheet":{"schemaVersion":1,"format":"markdown","text":"## Light\nBright, indirect light.","title":"Fern care"}}}}. A file output is the reference await stillmade.files.write(data, {mediaType, name}) returns, handed on unchanged; mediaType is one of application/pdf, text/plain, text/csv, text/markdown, application/json, application/zip: {"outputs":{"report":{"type":"file"}},"main.js":"export default async function run(input, stillmade) { const csv = 'Room\\n' + input.rooms.join('\\n'); return {report: await stillmade.files.write(csv, {mediaType: 'text/csv', name: 'rooms.csv'})}; }","test":{"name":"Two rooms","input":{"rooms":["Kitchen","Hall"]},"expected":{"report":{"kind":"file","mimeType":"text/csv","name":"rooms.csv"}}}} (in a test, files.write answers with fields the host makes: assetId local-written-file-N, versionId, url /api/media/stillmade-mock/written-file-N.csv with the extension of its mediaType, and bytes; expected gives that reference as {kind:"file", mimeType, name} and it matches when those three match; passed on in an asset output as kind "asset", it is given as {kind:"asset", mimeType, name} and matches the same way). An audio[] output is the list of references audio.speech returned, unchanged; its mock is a list answering successive calls, and the labels go in a separate text[] output: {"outputs":{"tracks":{"type":"audio[]"},"names":{"type":"text[]"}},"main.js":"export default async function run(input, stillmade) { const tracks = []; for (const room of input.rooms) tracks.push(await stillmade.hosted.run('audio.speech', {text: 'Welcome to the ' + room + '.'})); return {tracks, names: input.rooms}; }","test":{"name":"Two rooms","input":{"rooms":["Kitchen","Hall"]},"mocks":{"hosted":{"audio.speech":[{"kind":"audio","assetId":"speech-1","versionId":"v1","url":"/api/media/example/kitchen.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1},{"kind":"audio","assetId":"speech-2","versionId":"v1","url":"/api/media/example/hall.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1}]}},"expected":{"tracks":[{"kind":"audio","assetId":"speech-1","versionId":"v1","url":"/api/media/example/kitchen.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1},{"kind":"audio","assetId":"speech-2","versionId":"v1","url":"/api/media/example/hall.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1}],"names":["Kitchen","Hall"]}}}. A file input read as text: {"outputs":{"guide":{"type":"text"}},"main.js":"function textOf(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.length; i += 8192) binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192)); try { return decodeURIComponent(escape(binary)); } catch (error) { return binary; } }\nexport default async function run(input, stillmade) { const notes = textOf(await stillmade.files.read(input.notes)); const guide = await stillmade.hosted.run('text.generate', {prompt: 'Rewrite these notes as numbered steps, keeping every number as written:\\n' + notes, maxTokens: 1200}); return {guide}; }","test":{"name":"Two notes","input":{"notes":{"kind":"file","assetId":"notes-1","versionId":"v1","url":"/api/media/example/notes.txt","mimeType":"text/plain","name":"notes.txt","bytes":31}},"mocks":{"files":{"notes-1":{"text":"pull the probe\nwipe the tines"}},"hosted":{"text.generate":"1. Pull the probe.\n2. Wipe the tines."}},"expected":{"guide":"1. Pull the probe.\n2. Wipe the tines."}}}. Photos joined into one image, its transform mocked with the saved image it returns: {"outputs":{"sheet":{"type":"image"}},"main.js":"export default async function run(input, stillmade) { const photos = input.photos; if (!photos.length) throw new Error('Add at least one photo.'); const columns = Math.ceil(Math.sqrt(photos.length)), rows = Math.ceil(photos.length / columns), cell = 400, gap = 20, top = 160; const width = columns * cell + (columns + 1) * gap, height = top + rows * cell + (rows + 1) * gap; const layers = photos.map((source, i) => ({source, x: gap + (i % columns) * (cell + gap), y: top + gap + Math.floor(i / columns) * (cell + gap), width: cell, height: cell, fit: 'cover'})); const sheet = await stillmade.media.transform({operation: 'image.compose', width, height, background: '#ffffff', layers, text: [{text: input.title, x: width / 2, y: 50, size: 64, align: 'center', color: '#111111'}]}); return {sheet}; }","test":{"name":"Four photos","input":{"photos":[{"kind":"image","assetId":"photo-1","versionId":"v1","url":"/api/media/example/photo-1.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-2","versionId":"v1","url":"/api/media/example/photo-2.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-3","versionId":"v1","url":"/api/media/example/photo-3.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-4","versionId":"v1","url":"/api/media/example/photo-4.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000}],"title":"Saturday games"},"mocks":{"transforms":{"image.compose":{"kind":"image","assetId":"sheet-1","versionId":"v1","url":"/api/media/example/sheet.png","mimeType":"image/png","width":1300,"height":1440,"bytes":1820000}}},"expected":{"sheet":{"kind":"image","assetId":"sheet-1","versionId":"v1","url":"/api/media/example/sheet.png","mimeType":"image/png","width":1300,"height":1440,"bytes":1820000}}}}. StillMade runs every test before anyone sees the Block, without spending credits. With the SDK instead, the same code is module/main.js in a Block folder and the same tests are tests/fixtures.json; stillmade-block pack writes the file to import (MODULES.md).
7317
+ A job that needs several hosted operations, or code around one (reading a transcript's words and times, shaping a list), or media work on the person's device even with no hosted operation (rendering clips and photos into one video, joining pictures into one), is a module Block: {manifest,moduleSource,tests}, runtime:"module", moduleSource:{"main.js":"<the code>"}. StillMade names main.js by its digest and adds module. StillMade's in-app builder takes one file: give no other files, and main.js is plain JavaScript that imports nothing and uses no browser APIs (no fetch, TextEncoder, timers or WebCodecs). That single-file rule is the in-app builder's; a module Block built with the SDK may use timers, WebCodecs, bundled npm packages and .wasm files as MODULES.md describes. main.js: export default async function run(input, stillmade) returns the declared outputs. It calls each hosted operation with await stillmade.hosted.run(operation, request), listed in manifest.permissions.capabilities: text.generate, image.generate, video.generate, audio.speech, audio.music, audio.sfx, audio.transcribe, image.describe, media.analyze, web.fetch, web.research. Requests, and what each call returns (a test's mocks record exactly that value): text.generate {prompt, system, maxTokens} returns the text as a string, and with schema (a JSON schema object) returns that object, so ask for JSON with schema; one text.generate prompt holds at most 32,000 characters, so a longer file or transcript is read in parts (one call per part of about 25,000 characters, then one call that joins what they found), never cut off, and when the result is the whole text again (a cleaned transcript, a translation) each part's answer is kept and joined in order in main.js, since one answer holds at most 4,096 tokens; audio.transcribe {audio: the input's saved audio or video reference (a video's own sound is transcribed, so never extract it first), language:"auto"} returns {schemaVersion:1,text,words:[{text,start,end}]}; image.generate {prompt, references?: one to eight saved images the new picture keeps, such as the product, person or place in the person's photos} and audio.speech {text} return a saved image or audio reference (an audio reference always carries its measured duration); web.fetch {url, instruction} returns a string; media.analyze {media: one to four saved images or one saved video, question, range?: {start, end} in seconds of that video, at most 12 apart, to look closely at one part of it} returns {schemaVersion:1,items:[{kind,summary,observations,detectedText}]}, one item per image, and is how a Block looks at pictures (picking the photos that show different angles, reading a menu off a photo): StillMade credits pay for it; image.describe {image, instruction} returns a string but runs only on the person's own OpenRouter key, so never use it; web.research {topic} returns {summary,sources}. A file the person gives the Block (a text, CSV, Markdown or JSON file on a file input) is read with await stillmade.files.read(input.<port>), which returns its bytes; turn them into text in main.js as the worked file input below does, and give the test's input file an assetId whose text mocks.files records as {files:{"<assetId>":{text:"..."}}}. A Block that reads a file the person gives it is a module Block, never a text.generate capability package. A PDF (lesson slides, a lease, a contract) is read with await stillmade.media.transform({operation:"document.text",source:input.<port>}), which returns {text, pages}, all the words and pages, a list holding each page's words as one string, on the person's device and without credits (a text file reads the same way); a scanned PDF has no words to read. A call that fails throws. When the Block cannot make its result without that call, let it fail the run (StillMade shows its message), never catching it to return an empty result; catch it only around an optional part that has a fallback (a missing room photo, a skipped sample), and name what was left out in the result, as actions.run names its failures. Media work on the person's device (cut, join or resize video, decode or extract audio, thumbnails, joining images) is await stillmade.media.transform(request). Joining saved images into one picture (two photos side by side with Before and After labels, a grid or collage, a contact sheet, a storyboard sheet, a poster with a title) is {operation:"image.compose",width,height,background:"#ffffff",layers:[{source,x,y,width,height,fit:"cover" or "contain"}],text:[{text,x,y,size,color,background,align:"left" or "center" or "right",width}]}: a canvas up to 4096 pixels a side, positions in pixels worked out in main.js, layers drawn in order and text on top (y is the top of the line, background puts a box behind it, colours are #rrggbb or, see-through, #rrggbbaa such as #00000099 for a caption box, width wraps the words onto lines no wider than it: give a caption under a frame the frame's width so it never runs into the next one); it returns the saved image, and main.js never decodes pixels itself. Exporting is a transform too: {operation:"timeline.render",kind:"video" or "audio",aspect:"9:16",clips:[{source,start,duration,from,transition:{type:"Fade"},look:"Filmic",motion:"zoom-in"}],audio:[{source,start,duration}],captions:[{text,start,end,words:[{text,start,end}],highlight:"#ffd400",size:80}]} (motion, a slow camera move over a photo, is zoom-in, zoom-out, pan-left, pan-right, pan-up or pan-down; size is the caption height in pixels, 80 for a vertical video) renders the Block's own timeline with the Editor's transitions, looks and word-by-word captions under the person's export plan (start is a clip's place on that timeline, so each clip starts where the one before it ends, less the transition on the clip before it, which blends that clip into the next, and from is where it begins in its own file; overlapping clips are refused; a clip of another shape than the frame is cropped to fill it around its middle, or around focus:{x,y} on the clip, the point to keep in view as fractions 0 to 1 across the picture, such as where the speaker stands (focus.scale, 1 to 2.5 and instead of a motion, holds the clip that much closer on that point), or is fitted whole with fit:"contain"), so never ask for a separate export, captions or color Block; stillmade.log and stillmade.progress(fraction, label) report. One press of the Step's button approves each hosted operation once at the price shown beside it, and media.analyze for every picture or video the Step is given, across as many calls as that takes; any further call asks the person first. A module Block runs in the person's browser while StillMade is open, never on a schedule or event. Tests are {name,input,expected,mocks} and compare outputs, so every test is a case that runs to its outputs; an input the Block refuses (it throws a plain Error with a message for the person, such as a clip with no duration) is never a test. mocks records what each call answers, as {hosted:{"audio.transcribe":{schemaVersion:1,text:"...",words:[...]},"text.generate":{...}},transforms:{...}} (a list answers successive calls in order), and expected is the exact output main.js returns for those answers: exact values of each output's type, never capability bounds such as minDuration or maxBytes. In a module test, mock each media call with its complete saved reference (kind, assetId, versionId, url, mimeType and its measured fields: duration, bytes, sampleRate, channels for audio; width, height, bytes for images), its url an example file such as /api/media/example/clip.mp4 so the preview can play it, and repeat those references unchanged in expected; names or labels go in their own output. Worked outputs, each a passing test. A document output is the document object on a document port (not text): {"outputs":{"sheet":{"type":"document"}},"main.js":"export default async function run(input, stillmade) { const text = await stillmade.hosted.run('text.generate', {prompt: 'A Markdown care sheet for ' + input.plant, maxTokens: 800}); return {sheet: {schemaVersion: 1, format: 'markdown', text, title: input.plant + ' care'}}; }","test":{"name":"Fern","input":{"plant":"Fern"},"mocks":{"hosted":{"text.generate":"## Light\nBright, indirect light."}},"expected":{"sheet":{"schemaVersion":1,"format":"markdown","text":"## Light\nBright, indirect light.","title":"Fern care"}}}}. A file output is the reference await stillmade.files.write(data, {mediaType, name}) returns, handed on unchanged; mediaType is one of application/pdf, text/plain, text/csv, text/markdown, application/json, application/zip: {"outputs":{"report":{"type":"file"}},"main.js":"export default async function run(input, stillmade) { const csv = 'Room\\n' + input.rooms.join('\\n'); return {report: await stillmade.files.write(csv, {mediaType: 'text/csv', name: 'rooms.csv'})}; }","test":{"name":"Two rooms","input":{"rooms":["Kitchen","Hall"]},"expected":{"report":{"kind":"file","mimeType":"text/csv","name":"rooms.csv"}}}} (in a test, files.write answers with fields the host makes: assetId local-written-file-N, versionId, url /api/media/stillmade-mock/written-file-N.csv with the extension of its mediaType, and bytes; expected gives that reference as {kind:"file", mimeType, name} and it matches when those three match; passed on in an asset output as kind "asset", it is given as {kind:"asset", mimeType, name} and matches the same way). An audio[] output is the list of references audio.speech returned, unchanged; its mock is a list answering successive calls, and the labels go in a separate text[] output: {"outputs":{"tracks":{"type":"audio[]"},"names":{"type":"text[]"}},"main.js":"export default async function run(input, stillmade) { const tracks = []; for (const room of input.rooms) tracks.push(await stillmade.hosted.run('audio.speech', {text: 'Welcome to the ' + room + '.'})); return {tracks, names: input.rooms}; }","test":{"name":"Two rooms","input":{"rooms":["Kitchen","Hall"]},"mocks":{"hosted":{"audio.speech":[{"kind":"audio","assetId":"speech-1","versionId":"v1","url":"/api/media/example/kitchen.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1},{"kind":"audio","assetId":"speech-2","versionId":"v1","url":"/api/media/example/hall.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1}]}},"expected":{"tracks":[{"kind":"audio","assetId":"speech-1","versionId":"v1","url":"/api/media/example/kitchen.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1},{"kind":"audio","assetId":"speech-2","versionId":"v1","url":"/api/media/example/hall.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1}],"names":["Kitchen","Hall"]}}}. A file input read as text: {"outputs":{"guide":{"type":"text"}},"main.js":"function textOf(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.length; i += 8192) binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192)); try { return decodeURIComponent(escape(binary)); } catch (error) { return binary; } }\nexport default async function run(input, stillmade) { const notes = textOf(await stillmade.files.read(input.notes)); const guide = await stillmade.hosted.run('text.generate', {prompt: 'Rewrite these notes as numbered steps, keeping every number as written:\\n' + notes, maxTokens: 1200}); return {guide}; }","test":{"name":"Two notes","input":{"notes":{"kind":"file","assetId":"notes-1","versionId":"v1","url":"/api/media/example/notes.txt","mimeType":"text/plain","name":"notes.txt","bytes":31}},"mocks":{"files":{"notes-1":{"text":"pull the probe\nwipe the tines"}},"hosted":{"text.generate":"1. Pull the probe.\n2. Wipe the tines."}},"expected":{"guide":"1. Pull the probe.\n2. Wipe the tines."}}}. Photos joined into one image, its transform mocked with the saved image it returns: {"outputs":{"sheet":{"type":"image"}},"main.js":"export default async function run(input, stillmade) { const photos = input.photos; if (!photos.length) throw new Error('Add at least one photo.'); const columns = Math.ceil(Math.sqrt(photos.length)), rows = Math.ceil(photos.length / columns), cell = 400, gap = 20, top = 160; const width = columns * cell + (columns + 1) * gap, height = top + rows * cell + (rows + 1) * gap; const layers = photos.map((source, i) => ({source, x: gap + (i % columns) * (cell + gap), y: top + gap + Math.floor(i / columns) * (cell + gap), width: cell, height: cell, fit: 'cover'})); const sheet = await stillmade.media.transform({operation: 'image.compose', width, height, background: '#ffffff', layers, text: [{text: input.title, x: width / 2, y: 50, size: 64, align: 'center', color: '#111111'}]}); return {sheet}; }","test":{"name":"Four photos","input":{"photos":[{"kind":"image","assetId":"photo-1","versionId":"v1","url":"/api/media/example/photo-1.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-2","versionId":"v1","url":"/api/media/example/photo-2.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-3","versionId":"v1","url":"/api/media/example/photo-3.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-4","versionId":"v1","url":"/api/media/example/photo-4.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000}],"title":"Saturday games"},"mocks":{"transforms":{"image.compose":{"kind":"image","assetId":"sheet-1","versionId":"v1","url":"/api/media/example/sheet.png","mimeType":"image/png","width":1300,"height":1440,"bytes":1820000}}},"expected":{"sheet":{"kind":"image","assetId":"sheet-1","versionId":"v1","url":"/api/media/example/sheet.png","mimeType":"image/png","width":1300,"height":1440,"bytes":1820000}}}}. StillMade runs every test before anyone sees the Block, without spending credits. With the SDK instead, the same code is module/main.js in a Block folder and the same tests are tests/fixtures.json; stillmade-block pack writes the file to import (MODULES.md).
7649
7318
 
7650
7319
  ### ComfyUI workflows
7651
7320
 
@@ -7681,7 +7350,7 @@ Write the description in third-grade language: one short sentence, ideally 8–1
7681
7350
 
7682
7351
  ### Compact interface
7683
7352
 
7684
- Every job or ingredient Block interface, generated or custom, follows StillMade's compact rules: Compact: the whole Block fits one laptop screen without scrolling. A short setting is one row, its label beside the control; no card around a single control. Status is a dot or a few words ("No narration yet"), never a paragraph; the reason sits behind an `sm-hint`. Files are tiles: a saved file is a thumbnail or chip and adding one is a + tile. Never the bare browser file picker, and no size limits in view until a file is too big. An (i) beside anything a newcomer could misread: a setting, status or number whose effect is not obvious from its label gets a closed `sm-hint` with one or two sentences. Give every input that needs one a `description`; StillMade shows it as that (i). Obvious controls get none. Use the width: from 860px wide the kit puts the controls on the left at a reading width (300–380px) and the result (`sm-stage`) fills the right, in its real shape; under that everything stacks with the result first and the action within reach. Extra width goes to the result, never to wider text fields. Before there is a result, the stage previews what will be made from the inputs. `data-layout="stack"` opts out. StillMade draws generated manifest.ui controls this way itself (label beside control, files as tiles, the result filling the right on wide screens); you supply at most four inputs that matter in view, sensible defaults, and a short `description` on every input whose effect is not obvious from its name, which StillMade shows as its (i). An app-sized Block is different: when the request is for something a person works in (an editor, a canvas, a 3D or CAD tool, a studio), build it as a workspace Block laid out like that app: its work surface (viewport, canvas, timeline, panels, tool rails) owns the page with as much tooling as the app needs, and the compact rules apply to its settings, dialogs and status, never to the work surface. Never shrink an app to one screen of four inputs.
7353
+ Every job or ingredient Block interface, generated or custom, follows StillMade's compact rules: Compact: the whole Block fits one laptop screen without scrolling. A short setting is one row, its label beside the control; no card around a single control. Status is a dot or a few words ("No narration yet"), never a paragraph; the reason sits behind an `sm-hint`. Files are tiles: a saved file is a thumbnail or chip and adding one is the + tile (`<button class="sm-add" data-stillmade-add="<input>">`), which opens StillMade's picker. Never an `<input type="file">` or the bare browser file picker, and no size limits in view until a file is too big. An (i) beside anything a newcomer could misread: a setting, status or number whose effect is not obvious from its label gets a closed `sm-hint` with one or two sentences. Give every input that needs one a `description`; StillMade shows it as that (i). Obvious controls get none. Use the width: from 860px wide the kit puts the controls on the left at a reading width (300–380px) and the result (`sm-stage`) fills the right, in its real shape; under that everything stacks in the page's order: the inputs, the action in normal flow (never pinned), then the result's place under it, so put `sm-stage` after `sm-actions`. Extra width goes to the result, never to wider text fields. Before there is a result, the stage previews what will be made from the inputs. `data-layout="stack"` opts out. StillMade draws generated manifest.ui controls this way itself (label beside control, files as tiles, the result filling the right on wide screens); you supply at most four inputs that matter in view, sensible defaults, and a short `description` on every input whose effect is not obvious from its name, which StillMade shows as its (i). An app-sized Block is different: when the request is for something a person works in (an editor, a canvas, a 3D or CAD tool, a studio), build it as a workspace Block laid out like that app: its work surface (viewport, canvas, timeline, panels, tool rails) owns the page with as much tooling as the app needs, and the compact rules apply to its settings, dialogs and status, never to the work surface. Never shrink an app to one screen of four inputs.
7685
7354
 
7686
7355
  ### Generated controls first
7687
7356
 
@@ -7693,7 +7362,7 @@ Optional custom view interfaces are supported, but generated manifest.ui control
7693
7362
 
7694
7363
  *When:* StillMade's builder adds this section when the person asks for an interface, preview or controls, or refines a Block that has one.
7695
7364
 
7696
- A custom interface is the optional package-level view:{html,css?,themedCss?,javascript?}; when refining, preserve and refine base.view. Design it by StillMade's interface principles, in order: Function first: unless the person asks for a design direction, build from the Block kit (`sm-block`, `sm-stage`, `sm-choices`, `sm-actions`, `sm-more`) and ship little or no CSS of your own. A direction the person names wins over the kit. Show the thing being made: the preview, media or result gets the largest area (`sm-stage`). A picture or clip the Block makes fills it in its own shape, as large as fits; several fill it as a grid, each big enough to watch. Small tiles are for the files a person gives the Block, never for what it makes. A clip is a `<video controls>`, which StillMade plays in its own player with full screen, and a picture in `sm-stage` or `sm-gallery` (or marked `data-stillmade-expand`) gets StillMade's full-screen button; leave out `controls` only when the Block drives playback itself. Never a JSON dump, source editor, manifest or diagnostic panel unless the task is about source. One obvious next action: one solid `sm-primary` in the kit's ink, never restyled to the background or a ghost outline; peers are `sm-quiet` buttons or named icons, and close is an X icon. Seven or fewer choices in view. Speak the job unless the person or creator asks otherwise. The action button names what it does in the job's words (`manifest.action`: "Plan the tour"; AI pictures, video and sound start with Generate and say what, the person's own media cut or rendered says Make: "Generate voiceover", "Make 6 clips"), never "Make it", "Run", "Run step", "Submit", "Process" or a bare "Generate"; progress counts the job's units ("4 of 9 rooms"), never "Processing"; results and problems use its words in one line. Show no credit numbers: StillMade puts the price beside the verb on its own run button ("Write · 1 credit"), with a chip for the model, quality or count when they can change. Pressing it runs at that price; a confirmation appears only when the server price is higher than the one shown. Pictures over words: choices people see or hear are thumbnails, swatches, samples or glyphs (`sm-choices`), not word lists. Icons carry an accessible name and tooltip; never emoji. A picked option reads at a glance: keep the kit's ink fill on an `sm-choices` pick, never restyled to an outline, a tint or the page's background. Media that cannot show here (on another device, or it did not open) keeps its place as a quiet `sm-media-off` tile in its own shape, never words in its place. Words only when needed. Drop what nobody needs to finish the task (intros, "how it works", restating the Block name). Words that might help start collapsed (`sm-hint`, `sm-more`). Errors, spending approvals, stale inputs and destructive steps stay visible, in few words. Strong defaults; secondary settings go in `sm-more`, closed. Compact: the whole Block fits one laptop screen without scrolling. A short setting is one row, its label beside the control; no card around a single control. Status is a dot or a few words ("No narration yet"), never a paragraph; the reason sits behind an `sm-hint`. Files are tiles: a saved file is a thumbnail or chip and adding one is a + tile. Never the bare browser file picker, and no size limits in view until a file is too big. Rare tools (update, schedule, history, reload) are named icon buttons beside the action, never sections under the work. An (i) beside anything a newcomer could misread: a setting, status or number whose effect is not obvious from its label gets a closed `sm-hint` with one or two sentences. Give every input that needs one a `description`; StillMade shows it as that (i). Obvious controls get none. One workspace: no cards inside cards, no second title, step counter or Back/Next bar; the host owns navigation. Status sits beside the action it describes (`sm-status`), in a few words. Keep the page in normal scroll flow; no sticky media or control strips unless the editing task needs them. Use the width: from 860px wide the kit puts the controls on the left at a reading width (300–380px) and the result (`sm-stage`) fills the right, in its real shape; under that everything stacks with the result first and the action within reach. Extra width goes to the result, never to wider text fields. Before there is a result, the stage previews what will be made from the inputs. `data-layout="stack"` opts out. Size with the kit, never fixed sizes: no fixed pixel widths or heights for the workspace or stage, no 100vh, no position:fixed panels. StillMade zooms a Block's screen to fill the space it has (up to 150%, never down to a phone layout on a computer), so a Block laid out with the kit's spacing fills its Step without empty space, and it centers a standard view narrower than its Step. An app that lays itself out (full-size canvas or WebGL, its own scroll, pan or zoom, its own Ctrl/Cmd +/- keys) puts `data-stillmade-layout="own"` on its outermost element, and StillMade then never centers, zooms or takes those keys. Group what the person sets in one `sm-fields` (or `sm-settings` for short ones): the kit gives the group its own surface, so the inputs, the settings and the result read as separate parts. Settings sit side by side (`sm-settings`), next to what they change. Three or four short options are one segmented row (`data-segmented`), not a row of big tiles. Lists fill the width at a small size: input files are small tiles (`sm-gallery data-size="small"`), many items are one line each (`sm-list`): a thumbnail, one line of text, a short fact. Tapping an item opens all of its fields in place. Extra width means more items per row, never bigger cards. On a phone chip and segment rows scroll sideways instead of wrapping; long lists show their first three with "N more" (`data-peek`) and long text five lines until tapped (`sm-clamp`). Nothing is lost when a Block gets smaller: every control stays on the screen, behind an (i), in `sm-more`, or inside the item it belongs to. Compacting moves controls; it never deletes them. Text scene descriptions are planning notes, never a storyboard or an approved frame. Say nothing about how the Block works: no notes on where it runs (this device, StillMade Desktop, the cloud), saving, syncing, recovery, accounts, credits or where a file is kept. StillMade tells the person what they need as one short notification, or nothing; never a sentence on the page, and never the same line under each item. References fill your inputs: every drop area in StillMade offers "Use a reference", and the person's saved photos, videos and notes fill the Step with no typing. Media inputs fill by kind, so take `video` or `asset` wherever a clip works as well as a photo; text inputs fill by plain names and labels (`productName`, `website`, `brandColors`, `fonts`, `voice`, and `details` or `notes` for the reference's note). Never build your own reference picker or library upload. Days and times are picked, never typed: a `date` input is a calendar, with a list of times when it is about a time (`postAt`, `startsAt`, a label with Time, When or Starts). In a custom view use `<input type="date">` or `type="datetime-local"`, never a text box for a date. Posting is its own Step: sending a result to Instagram, TikTok, email or any app is a connector Step after the Block, worded by StillMade ("Post to Instagram"). A Block never posts from inside itself. Its DOM controller is separate from manifest runtime code. The view may also declare appearance:"original" or "host". For an imported design, preserve its colors with appearance:"original" and view.css; no themed replacement is required. Preserve base.view.appearance when refining unless the user requests a change. Omitted/host appearance retains the existing Light, Dark and White requirement: supply semantic colors in view.css or a complete replacement view.themedCss. An original-style view may optionally supply valid themedCss for a StillMade appearance preview. Use StillMade variables (--bg,--fg,--fg-muted,--border-tok,--r-3) only when opting into host styling; do not redefine host tokens in the themed variant. Appearance never relaxes sandbox constraints: no CSS escapes/filters/blending, imports/url(), or element.style mutations in JS. Use classes for UI state; media pixels retain their colors. Both CSS fields have the same 32 KiB limit; all existing shared-state, security and permission checks still apply. Declare elements in html, bind with document.getElementById/querySelector and addEventListener; use literal property access, array methods, textContent, input.value, button.disabled and canvas. Initial values: StillMade.onInput(values=>{...}); await StillMade.run(inputBindings) returns validated output bindings from the isolated package runtime; catch errors and show results. No computation in the DOM controller; put it in code/recipe. Disable the action button while pending.
7365
+ A custom interface is the optional package-level view:{html,css?,themedCss?,javascript?}; when refining, preserve and refine base.view. Design it by StillMade's interface principles, in order: Function first: unless the person asks for a design direction, build from the Block kit (`sm-block`, `sm-stage`, `sm-choices`, `sm-actions`, `sm-more`) and ship little or no CSS of your own. A direction the person names wins over the kit. Show the thing being made: the preview, media or result gets the largest area (`sm-stage`), and every part shows at once: before the first run the result's place is drawn in the shape and size of what is coming (a 9:16 frame for a vertical clip, lines for text, a player bar for sound), so the result fills it instead of popping in. On a wide screen what goes in sits on the left and the result on the right; they stack only on narrow screens and phones. A picture or clip the Block makes fills it in its own shape, as large as fits; several fill it as a grid, each big enough to watch. Small tiles are for the files a person gives the Block, never for what it makes. A clip is a `<video controls>`, which StillMade plays in its own player with full screen, and a picture in `sm-stage` or `sm-gallery` (or marked `data-stillmade-expand`) gets StillMade's full-screen button; leave out `controls` only when the Block drives playback itself. Never a JSON dump, source editor, manifest or diagnostic panel unless the task is about source. One obvious next action: one solid `sm-primary` in the kit's ink, never restyled to the background or a ghost outline; peers are `sm-quiet` buttons or named icons, and close is an X icon. Seven or fewer choices in view. Speak the job unless the person or creator asks otherwise. The action button names what it does in the job's words (`manifest.action`: "Plan the tour"; AI pictures, video and sound start with Generate and say what, the person's own media cut or rendered says Make: "Generate voiceover", "Make 6 clips"), never "Make it", "Run", "Run step", "Submit", "Process" or a bare "Generate"; progress counts the job's units ("4 of 9 rooms"), never "Processing"; results and problems use its words in one line. Show no credit numbers: StillMade puts the price beside the verb on its own run button ("Write · 1 credit"), with a chip for the model, quality or count when they can change. Pressing it runs at that price; a confirmation appears only when the server price is higher than the one shown. Pictures over words: choices people see or hear are thumbnails, swatches, samples or glyphs (`sm-choices`), not word lists. Icons carry an accessible name and tooltip; never emoji. A picked option reads at a glance: keep the kit's ink fill on an `sm-choices` pick, never restyled to an outline, a tint or the page's background. Media that cannot show here (on another device, or it did not open) keeps its place as a quiet `sm-media-off` tile in its own shape, never words in its place. Words only when needed. Drop what nobody needs to finish the task (intros, "how it works", restating the Block name). Words that might help start collapsed (`sm-hint`, `sm-more`). Errors, spending approvals, stale inputs and destructive steps stay visible, in few words. Strong defaults; secondary settings go in `sm-more`, closed. Compact: the whole Block fits one laptop screen without scrolling. A short setting is one row, its label beside the control; no card around a single control. Status is a dot or a few words ("No narration yet"), never a paragraph; the reason sits behind an `sm-hint`. Files are tiles: a saved file is a thumbnail or chip and adding one is the + tile (`<button class="sm-add" data-stillmade-add="<input>">`), which opens StillMade's picker. Never an `<input type="file">` or the bare browser file picker, and no size limits in view until a file is too big. Rare tools (update, schedule, history, reload) are named icon buttons beside the action, never sections under the work. An (i) beside anything a newcomer could misread: a setting, status or number whose effect is not obvious from its label gets a closed `sm-hint` with one or two sentences. Give every input that needs one a `description`; StillMade shows it as that (i). Obvious controls get none. One workspace: no cards inside cards, no second title, step counter or Back/Next bar; the host owns navigation. One page per job (November 3, 2026): a guided job's screen holds the inputs and settings, then a cheap check before the expensive run (the script and a voice sample before the video, a second `StillMade.run` that StillMade prices on its own), then the result; never a second Block or Step for the check. A workspace a person works in (a CAD tool, a canvas, an editor) is its own Block, and turning what it made into something else is the next one. Status sits beside the action it describes (`sm-status`), in a few words. Keep the page in normal scroll flow; no sticky media or control strips unless the editing task needs them. Use the width: from 860px wide the kit puts the controls on the left at a reading width (300–380px) and the result (`sm-stage`) fills the right, in its real shape; under that everything stacks in the page's order: the inputs, the action in normal flow (never pinned), then the result's place under it, so put `sm-stage` after `sm-actions`. Extra width goes to the result, never to wider text fields. Before there is a result, the stage previews what will be made from the inputs. `data-layout="stack"` opts out. Size with the kit, never fixed sizes: no fixed pixel widths or heights for the workspace or stage, no 100vh, no position:fixed panels. StillMade zooms a Block's screen to fill the space it has (up to 150%, never down to a phone layout on a computer), so a Block laid out with the kit's spacing fills its Step without empty space, and it centers a standard view narrower than its Step. An app that lays itself out (full-size canvas or WebGL, its own scroll, pan or zoom, its own Ctrl/Cmd +/- keys) puts `data-stillmade-layout="own"` on its outermost element, and StillMade then never centers, zooms or takes those keys. Group what the person sets in one `sm-fields` (or `sm-settings` for short ones): the kit gives the group its own surface, so the inputs, the settings and the result read as separate parts. Settings sit side by side (`sm-settings`), next to what they change. Three or four short options are one segmented row (`data-segmented`), not a row of big tiles. Lists fill the width at a small size: input files are small tiles (`sm-gallery data-size="small"`), many items are one line each (`sm-list`): a thumbnail, one line of text, a short fact. Tapping an item opens all of its fields in place. Extra width means more items per row, never bigger cards. On a phone chip and segment rows scroll sideways instead of wrapping; long lists show their first three with "N more" (`data-peek`) and long text five lines until tapped (`sm-clamp`). Nothing is lost when a Block gets smaller: every control stays on the screen, behind an (i), in `sm-more`, or inside the item it belongs to. Compacting moves controls; it never deletes them. Text scene descriptions are planning notes, never a storyboard or an approved frame. Say nothing about how the Block works: no notes on where it runs (this device, StillMade Desktop, the cloud), saving, syncing, recovery, accounts, credits or where a file is kept. StillMade tells the person what they need as one short notification, or nothing; never a sentence on the page, and never the same line under each item. References fill your inputs: every drop area in StillMade offers "Use a reference", and the person's saved photos, videos and notes fill the Step with no typing. Media inputs fill by kind, so take `video` or `asset` wherever a clip works as well as a photo; text inputs fill by plain names and labels (`productName`, `website`, `brandColors`, `fonts`, `voice`, and `details` or `notes` for the reference's note). Never build your own reference picker or library upload. Days and times are picked, never typed: a `date` input is a calendar, with a list of times when it is about a time (`postAt`, `startsAt`, a label with Time, When or Starts). In a custom view use `<input type="date">` or `type="datetime-local"`, never a text box for a date. Posting is its own Step: sending a result to Instagram, TikTok, email or any app is a connector Step after the Block, worded by StillMade ("Post to Instagram"). A Block never posts from inside itself. Its DOM controller is separate from manifest runtime code. The view may also declare appearance:"original" or "host". For an imported design, preserve its colors with appearance:"original" and view.css; no themed replacement is required. Preserve base.view.appearance when refining unless the user requests a change. Omitted/host appearance retains the existing Light, Dark and White requirement: supply semantic colors in view.css or a complete replacement view.themedCss. An original-style view may optionally supply valid themedCss for a StillMade appearance preview. Use StillMade variables (--bg,--fg,--fg-muted,--border-tok,--r-3) only when opting into host styling; do not redefine host tokens in the themed variant. Appearance never relaxes sandbox constraints: no CSS escapes/filters/blending, imports/url(), or element.style mutations in JS. Use classes for UI state; media pixels retain their colors. Both CSS fields have the same 32 KiB limit; all existing shared-state, security and permission checks still apply. Declare elements in html, bind with document.getElementById/querySelector and addEventListener; use literal property access, array methods, textContent, input.value, button.disabled and canvas. Initial values: StillMade.onInput(values=>{...}); await StillMade.run(inputBindings) returns validated output bindings from the isolated package runtime; catch errors and show results. No computation in the DOM controller; put it in code/recipe. Disable the action button while pending.
7697
7366
  Standard custom view code (no `view.config.json` frame runtime) is checked before it runs. These rules come straight from the validator; frame views run in their own sandbox and may build the page with ordinary DOM calls such as `createElement`:
7698
7367
 
7699
7368
  - **HTML elements:** only `div`, `section`, `article`, `header`, `footer`, `main`, `aside`, `nav`, `h1`, `h2`, `h3`, `h4`, `p`, `span`, `strong`, `em`, `i`, `small`, `br`, `hr`, `label`, `input`, `textarea`, `button`, `select`, `option`, `optgroup`, `fieldset`, `legend`, `output`, `progress`, `meter`, `ul`, `ol`, `li`, `dl`, `dt`, `dd`, `figure`, `figcaption`, `img`, `video`, `audio`, `source`, `canvas`, `table`, `thead`, `tbody`, `tfoot`, `tr`, `th`, `td`, `pre`, `code`, `details`, `summary`. No scripts, iframes, SVG, forms, comments or inline event attributes; media URLs must be `data:` or `blob:`.
@@ -7739,7 +7408,7 @@ not proof of multiplayer operation. Test two interfaces exchanging actual edits
7739
7408
  and saved outputs, permission loss, reconnect and conflicts before claiming
7740
7409
  collaboration support. Never claim arbitrary imported JavaScript state syncs
7741
7410
  merely because the package passed its computation fixtures.
7742
- Example view for a package taking and returning text: {"html":"<main class=\"sm-block\"><output class=\"sm-stage\" id=\"result\" aria-live=\"polite\"></output><div class=\"sm-field\"><label for=\"source\">Draft</label><textarea id=\"source\"></textarea></div><div class=\"sm-actions\"><button class=\"sm-primary\" id=\"run\">Clean text</button><p class=\"sm-status\" id=\"status\" role=\"status\"></p></div></main>","css":"#result{place-items:start;min-height:120px;white-space:pre-wrap;overflow-wrap:anywhere}","javascript":"const source=document.getElementById('source'),button=document.getElementById('run'),result=document.getElementById('result'),status=document.getElementById('status');\nlet busy=false;\nStillMade.bindShared('draft','source');\nStillMade.onInput(input=>{StillMade.setSharedDefaults({draft:input.text||''});});\nStillMade.onShared(shared=>{button.disabled=busy||!shared.canEdit;result.textContent=shared.outputs.text||'';});\nbutton.addEventListener('click',async()=>{\n if(busy||!StillMade.getShared().canEdit)return;\n busy=true;button.disabled=true;status.dataset.state='loading';status.textContent='Cleaning';\n try{await StillMade.run({text:source.value});status.dataset.state='success';status.textContent='Ready';}\n catch(error){status.dataset.state='error';status.textContent=error.message||'Could not clean this draft.';}\n finally{busy=false;button.disabled=!StillMade.getShared().canEdit;}\n});"}.
7411
+ Example view for a package taking and returning text: {"html":"<main class=\"sm-block\"><div class=\"sm-field\"><label for=\"source\">Draft</label><textarea id=\"source\"></textarea></div><div class=\"sm-actions\"><button class=\"sm-primary\" id=\"run\" data-stillmade-action=\"run\">Clean text</button><p class=\"sm-status\" id=\"status\" role=\"status\"></p></div><output class=\"sm-stage\" id=\"result\" aria-live=\"polite\"></output></main>","css":"#result{place-items:start;min-height:120px;white-space:pre-wrap;overflow-wrap:anywhere}","javascript":"const source=document.getElementById('source'),button=document.getElementById('run'),result=document.getElementById('result'),status=document.getElementById('status');\nlet busy=false;\nStillMade.bindShared('draft','source');\nStillMade.onInput(input=>{StillMade.setSharedDefaults({draft:input.text||''});});\nStillMade.onShared(shared=>{button.disabled=busy||!shared.canEdit;result.textContent=shared.outputs.text||'';});\nbutton.addEventListener('click',async()=>{\n if(busy||!StillMade.getShared().canEdit)return;\n busy=true;button.disabled=true;status.dataset.state='loading';status.textContent='Cleaning';\n try{await StillMade.run({text:source.value});status.dataset.state='success';status.textContent='Ready';}\n catch(error){status.dataset.state='error';status.textContent=error.message||'Could not clean this draft.';}\n finally{busy=false;button.disabled=!StillMade.getShared().canEdit;}\n});"}.
7743
7412
 
7744
7413
  <!-- authoring-guide:end -->
7745
7414
 
@@ -8498,11 +8167,15 @@ are an additional admission gate; they do not replace this browser boundary or
8498
8167
  the separate computation sandbox. Keep the UI responsive: move computation into
8499
8168
  `code`; DOM JavaScript is not the CPU-metered computation runtime.
8500
8169
 
8501
- A file input can read a user-selected image with `FileReader.readAsDataURL`, load
8502
- it into an existing `<img>`, and draw it into a declared `<canvas>`. Use
8503
- `getImageData` to pass `{width,height,data:Array.from(pixels.data)}` to an image
8504
- input, staying within the SDK image and message budgets. Nothing is uploaded by
8505
- the view. Use the host's file controls when you do not need a custom picker.
8170
+ A view adds files to an input with the + tile,
8171
+ `<button class="sm-add" data-stillmade-add="<input>">`, or
8172
+ `StillMade.addFiles("<input>")` from its own button, never an
8173
+ `<input type="file">` of its own: StillMade's picker makes the working copies
8174
+ and keeps device files on the device (see "Adding files to an input" in
8175
+ [Frame views](/docs/build/frame-views)). To hand pixels the view drew to an
8176
+ image input, draw them into a declared `<canvas>` and pass `getImageData` as
8177
+ `{width,height,data:Array.from(pixels.data)}`, staying within the SDK image and
8178
+ message budgets. Nothing is uploaded by the view.
8506
8179
 
8507
8180
  The host supplies StillMade's shared Light, Dark and White theme variables and basic controls.
8508
8181
 
@@ -9474,9 +9147,14 @@ video), or `{key,operation:"speech",prompt,settings?:{voice,speed}}` (prompt is
9474
9147
  the text to speak, up to 3,800 characters; a catalog voice id such as `adam`;
9475
9148
  speed 0.25–4), `{key,operation:"music",prompt,settings?:{durationSec}}` (5–240
9476
9149
  whole seconds) or `{key,operation:"sfx",prompt,settings?:{durationSec}}`
9477
- (0.5–22 seconds in tenths). The user approves one StillMade credit maximum for
9478
- the batch; never include prices, providers or keys. The result port receives
9479
- `[{key,kind,assetId,versionId,url}]` with `kind` `image`, `video` or `audio`.
9150
+ (0.5–22 seconds in tenths). A video item with `settings.aspectRatio` (`"9:16"`)
9151
+ and no `model` gets the cheapest catalog model that makes that shape. StillMade
9152
+ asks once for the whole batch, showing the most it can cost in StillMade
9153
+ credits, so put every item one run needs (one clip per photo) in one batch;
9154
+ never include prices, providers or keys. The result port receives
9155
+ `[{key,kind,assetId,versionId,url,duration?}]` with `kind` `image`, `video` or
9156
+ `audio`; `duration` (seconds) is the length a video, music or sound effect asked
9157
+ for, and speech has none (decode it with `audio.decode` to time it).
9480
9158
 
9481
9159
  ### Hosted video generation
9482
9160
 
@@ -9787,8 +9465,10 @@ For deterministic browser checks, mark a custom form control with
9787
9465
  test fixture through the real UI before pressing its run action. This marker
9788
9466
  does not change the Block's input contract or grant access to host services.
9789
9467
  For a text-bearing object fixture such as `{brief:{text:"…"}}`, the profile
9790
- enters its `text` into the marked text field. Mark the primary button with
9791
- `data-stillmade-action="run"` when the view has more than one button.
9468
+ enters its `text` into the marked text field, then presses the button marked
9469
+ `data-stillmade-action="run"` (or the view's only button). Every button carries
9470
+ `data-stillmade-action` so Chat can press it: `run` on the button that runs the
9471
+ Block, and what it does on every other button (`data-stillmade-action="play sample"`).
9792
9472
  The existing `data-stillmade-share` marker is also recognized for shared text
9793
9473
  fields. A custom view without a reachable mapped control must not be counted
9794
9474
  as having passed its fixture's success interaction.
@@ -9851,14 +9531,7 @@ wins. Open `packages/design-system/block-kit.html` in a browser to see every
9851
9531
  piece in Light, Dark and White.
9852
9532
 
9853
9533
  ```html
9854
- <main class="sm-block" data-layout="split">
9855
- <section class="sm-stage" id="stage" data-ratio="16:9" aria-live="polite">
9856
- <div class="sm-empty" id="empty">
9857
- <span class="sm-glyph" data-glyph="video" aria-hidden="true"></span>
9858
- <p>Drop a clip</p>
9859
- </div>
9860
- </section>
9861
-
9534
+ <main class="sm-block">
9862
9535
  <fieldset class="sm-choices" id="look" data-stillmade-share="look" data-ratio="16:9">
9863
9536
  <legend>Look</legend>
9864
9537
  <label><input type="radio" name="look" value="warm" checked><img src="data:..." alt=""><span>Warm</span></label>
@@ -9873,21 +9546,33 @@ piece in Light, Dark and White.
9873
9546
  <input id="caption" data-stillmade-input="caption" placeholder="Optional">
9874
9547
  </div>
9875
9548
 
9876
- <div class="sm-actions">
9877
- <button type="button" class="sm-primary" id="run" data-stillmade-action="make the clip">Make the clip</button>
9878
- <p class="sm-status" id="status" role="status"></p>
9879
- </div>
9880
-
9881
9549
  <details class="sm-more">
9882
9550
  <summary>More</summary>
9883
9551
  <div class="sm-fields"><!-- secondary settings --></div>
9884
9552
  </details>
9553
+
9554
+ <div class="sm-actions">
9555
+ <button type="button" class="sm-primary" id="run" data-stillmade-action="run">Make the clip</button>
9556
+ <p class="sm-status" id="status" role="status"></p>
9557
+ </div>
9558
+
9559
+ <section class="sm-stage" id="stage" data-ratio="16:9" aria-live="polite">
9560
+ <div class="sm-empty" id="empty">
9561
+ <span class="sm-glyph" data-glyph="video" aria-hidden="true"></span>
9562
+ <p>Your clip</p>
9563
+ </div>
9564
+ </section>
9885
9565
  </main>
9886
9566
  ```
9887
9567
 
9568
+ With an `sm-stage` as its direct child and no `data-layout`, `sm-block` puts
9569
+ the inputs on the left and the stage on the right from 860 px wide. Under that
9570
+ everything stacks in the page's order, so the stage comes last in the markup:
9571
+ the inputs, the button, then the result's place under it.
9572
+
9888
9573
  | Class | Use it for |
9889
9574
  | --- | --- |
9890
- | `sm-block` | The one workspace. `data-layout="split"` puts the stage beside the controls on wide screens. |
9575
+ | `sm-block` | The one workspace. With an `sm-stage` child, the controls sit left and the stage right from 860 px; narrower, they stack in the page's order. `data-layout="stack"` keeps one column; `"split"` is an older layout with the stage on the left. |
9891
9576
  | `sm-stage` | The thing being made: preview, media or result. `data-ratio="16:9"`, `"9:16"` or `"1:1"`. |
9892
9577
  | `sm-empty` | An empty stage: a glyph, at most five words, one action. |
9893
9578
  | `sm-choices` | Picks shown as pictures (`img`, `.sm-swatch`, `.sm-glyph`) and one or two words. `data-compact` for short text chips, `data-segmented` for one connected row of three or four short options. The pick is filled with the kit's ink, so it reads at a glance; a `disabled` fieldset keeps its pick and fades the rest. Chips and segments scroll sideways on a phone. Give the `fieldset` an `id` and `data-stillmade-share` so teammates see the same pick. |
@@ -9898,10 +9583,12 @@ piece in Light, Dark and White.
9898
9583
  | `sm-actions` | One `sm-primary`, then `sm-quiet` buttons and `sm-icon` buttons (each with `aria-label` and `title`). |
9899
9584
  | `sm-status` | Live state beside the action: `data-state="loading"`, `"success"`, `"waiting"` or `"error"`. |
9900
9585
  | `sm-more` | A closed `<details>` for secondary settings. |
9901
- | `sm-hint` | A closed `<details>` info toggle for an explanation that might help. |
9586
+ | `sm-hint` | A closed `<details>` (i) for an explanation that might help. Its words show on hover or focus, or a tap on touch screens. |
9902
9587
  | `sm-gallery` | A grid of results or media. `data-size="small"` for input files: more tiles per row as the screen widens. |
9903
9588
  | `sm-media-off` | A picture or clip that cannot show here (on another device, or it did not open): a quiet tile in its own shape (`--sm-photo-ratio`, the tile's shape in a gallery) holding the `image-off` glyph, the reason in `aria-label` and `title`, no words inside. |
9904
- | `sm-glyph` | A small icon in `currentColor`: `play`, `pause`, `plus`, `close`, `check`, `chevron`, `upload`, `image`, `image-off`, `video`, `audio`, `text`, `spark`, `info`, `warning`, `spinner`. Works in standard views, which cannot use SVG. |
9589
+ | `sm-add` | The + tile that adds files to an input: `<button type="button" class="sm-add" data-stillmade-add="listingPhotos" aria-label="Add photos">`. StillMade opens its own picker and saves the files; they arrive through `onInput`. In an `sm-gallery` it takes the tile's shape. |
9590
+ | `sm-scrim` | Words over a photo or video, inside a positioned box such as an `sm-stage`: a see-through shade of the page under them, in every theme. `var(--scrim)` is the same shade for your own rules. |
9591
+ | `sm-glyph` | A small icon in `currentColor`: `play`, `pause`, `plus`, `close`, `check`, `chevron`, `chevron-left`, `chevron-right`, `upload`, `image`, `image-off`, `video`, `audio`, `text`, `spark`, `info`, `warning`, `spinner`. Works in standard views, which cannot use SVG. |
9905
9592
 
9906
9593
  **Function first.** When the person building a Block does not ask for a look,
9907
9594
  use the kit as is: they want it to work and be easy to use. When they name a
@@ -9970,6 +9657,14 @@ person or the creator has not asked for something else. What they ask for wins.
9970
9657
  `details`, `notes` or `listingDetails`; when a note has to be split across
9971
9658
  several fields StillMade does it free. Never build your own reference
9972
9659
  picker or library upload.
9660
+ - **Files are tiles, added through StillMade.** A screen that holds an
9661
+ input's files shows them as tiles (`sm-gallery`) ending in the kit's + tile,
9662
+ `<button type="button" class="sm-add" data-stillmade-add="listingPhotos"
9663
+ aria-label="Add photos">`. StillMade opens its own picker and saves the files
9664
+ as its own tiles do; they arrive through `onInput`. From your own code call
9665
+ `StillMade.addFiles("listingPhotos")` in a click. Never an
9666
+ `<input type="file">` or an upload box of the Block's own. See
9667
+ [Frame views](/docs/build/frame-views#adding-files-to-an-input).
9973
9668
  - **Days and times are picked, never typed.** Use the `date` type. StillMade
9974
9669
  shows a calendar, and a list of times as well when the input is about a time
9975
9670
  (a name such as `postAt` or `startsAt`, a label with Time, When or Starts, or
@@ -9979,8 +9674,11 @@ person or the creator has not asked for something else. What they ask for wins.
9979
9674
  any app is a connector Step after the Block, and StillMade words its button
9980
9675
  ("Post to Instagram"). A Block never posts from inside itself.
9981
9676
  - **Chat can do all of it.** Every button carries `data-stillmade-action`
9982
- naming what it does and every field a label, so the project's Chat can press
9983
- and fill them.
9677
+ and every field a label, so the project's Chat can press and fill them. The
9678
+ button that runs the Block says `run` (StillMade's phone and desktop checks
9679
+ press it with the fixture); every other button names what it does
9680
+ (`data-stillmade-action="play sample"`). Chat reads the value with the
9681
+ button's own words.
9984
9682
 
9985
9683
  ### 4. Rules
9986
9684
 
@@ -9991,7 +9689,15 @@ exception, never unrelated technical UI.
9991
9689
  1. Show the thing being made, not a technical representation of it. No JSON
9992
9690
  dumps, source editors, manifests, debug consoles or "Technical result"
9993
9691
  panels unless the task is about source. A collapsed disclosure is still an
9994
- added surface.
9692
+ added surface. Every part shows at once (November 3, 2026): before the first
9693
+ run the result's place is drawn in the shape and size of what is coming
9694
+ (`<section class="sm-stage" data-ratio="9:16">` for a vertical clip, lines for
9695
+ text, a player bar for sound), so the result fills a place the person already
9696
+ sees instead of popping in somewhere unexpected. StillMade draws the Steps it
9697
+ draws the same way (`ResultPlace`). On a wide screen what goes in sits on the
9698
+ left and the result on the right, side by side (the kit's `sm-block` with an
9699
+ `sm-stage` does this from 860px); they stack only on narrow screens and phones,
9700
+ the result's place under the button (rule 16).
9995
9701
  2. One obvious next action. Seven or fewer choices in view; strong defaults.
9996
9702
  3. Choices people see or hear (voices, music, sounds, fonts, looks, colors,
9997
9703
  templates, images) show a preview of each option: a thumbnail or a sample
@@ -10016,7 +9722,9 @@ exception, never unrelated technical UI.
10016
9722
  12. Status is a dot or a few words ("No narration yet"), never a paragraph.
10017
9723
  The reason sits behind an `sm-hint`.
10018
9724
  13. Files are tiles: a saved file is a thumbnail or chip, and adding one is a
10019
- + tile (a `label` around a hidden file input). Never the browser's bare
9725
+ + tile, `<button class="sm-add" data-stillmade-add="<input>">`, which
9726
+ opens StillMade's picker (or `StillMade.addFiles("<input>")` from your own
9727
+ button). Never an `<input type="file">` or the browser's bare
10020
9728
  "Choose File / No file chosen" picker, and no size limits in view until
10021
9729
  a file is too big.
10022
9730
  14. Rare tools (update, schedule, history, reload) are named icon buttons
@@ -10030,8 +9738,11 @@ exception, never unrelated technical UI.
10030
9738
  16. Use the width. From 860 px wide, the kit puts the controls on the left
10031
9739
  at a reading width (300–380 px) and the result (`sm-stage`, a direct
10032
9740
  child of `sm-block`) fills the right, in its real shape. Under that the
10033
- page is one column: result first, then the controls, with the action
10034
- within reach. Extra width goes to the result, never to wider text
9741
+ page is one column in the page's order: the inputs, the action in
9742
+ normal flow (rule 5: never a sticky bar), then the result's place under
9743
+ it, so `sm-stage` comes after `sm-actions` in the markup. Keep the
9744
+ inputs above the button short enough that a phone shows the button
9745
+ without scrolling far; fold the rest into `sm-more`. Extra width goes to the result, never to wider text
10035
9746
  fields. Before there is a result, the stage previews what will be made
10036
9747
  from the inputs given so far, not an empty box. `data-layout="stack"`
10037
9748
  keeps one column. StillMade zooms a view to fill its space and centers a
@@ -10074,6 +9785,15 @@ exception, never unrelated technical UI.
10074
9785
  <span class="sm-glyph" data-glyph="image-off" aria-hidden="true"></span>
10075
9786
  </span>
10076
9787
  ```
9788
+ 23. One page per job (November 3, 2026). A job the person fills in and then
9789
+ gets a result from is one Block on one page: the inputs and settings, a
9790
+ cheap check before the expensive run (the script and a voice sample
9791
+ before the video), then the result. The check is a second
9792
+ `StillMade.run` on the same page, priced by StillMade on its own (Module
9793
+ Blocks, "A check, then the expensive run, on one page"); it never becomes
9794
+ a second Block or Step. A workspace the person works in for a while (a
9795
+ CAD tool, a canvas, an editor) is its own Block, and turning what it made
9796
+ into something else is the next one.
10077
9797
 
10078
9798
  ### 5. What StillMade checks
10079
9799
 
@@ -10134,6 +9854,756 @@ an explicit upgrade, following the
10134
9854
  Never rewrite immutable releases or silently change a saved workflow's pins.
10135
9855
 
10136
9856
 
9857
+ ## Frame views: any interface code
9858
+
9859
+ # Frame views: any interface code
9860
+
9861
+ A frame view (`"runtime": "frame"` in the view) is an interface written with
9862
+ ordinary web code: React, Svelte, Vue or Preact bundles, SVG, canvas and
9863
+ WebGL, pointer capture and drag, timers, loops, recursion, classes and Web
9864
+ Workers. Use it when the reviewed subset of standard views is too small for
9865
+ the interface you want.
9866
+
9867
+ ### Folder layout
9868
+
9869
+ ```
9870
+ my-block/
9871
+ stillmade.block.json
9872
+ src/run.js or another runtime (recipe, module, capability)
9873
+ src/view.html markup, for example <div id="root"></div>
9874
+ src/view.css styles; use StillMade's theme variables
9875
+ src/view.js ONE classic script: bundle with --format=iife
9876
+ src/view.config.json {"runtime": "frame"}
9877
+ tests/fixtures.json
9878
+ ```
9879
+
9880
+ `src/view.config.json` may also set `"appearance"` and `"files"` (assets, see
9881
+ below). Bundle npm dependencies into `src/view.js` as an IIFE, for example:
9882
+
9883
+ ```sh
9884
+ esbuild src/view.jsx --bundle --format=iife --minify --jsx=automatic \
9885
+ --define:process.env.NODE_ENV='"production"' --outfile=src/view.js
9886
+ ```
9887
+
9888
+ Limits: markup and each stylesheet up to 256 KiB, `view.js` up to 4 MiB,
9889
+ and up to 32 asset files with 8 MiB in total.
9890
+
9891
+ ### The `StillMade` object
9892
+
9893
+ Frame views get the same `StillMade` object as standard views: `input` and
9894
+ `onInput`, `run`, `useOutput`, `getShared`, `onShared`, `updateShared`,
9895
+ `bindShared`, `connectDocument`, `previewImage`, `previewMedia`, `render`
9896
+ and the rest of the interface bridge. In addition:
9897
+
9898
+ | Member | What it does |
9899
+ | --- | --- |
9900
+ | `StillMade.asset(path)` | A `blob:` URL for one of the view's asset files, for `<img>`, `new FontFace()` or `<video>`. |
9901
+ | `StillMade.assetBytes(path)` | A copy of an asset file's bytes as an `ArrayBuffer`. |
9902
+
9903
+ `StillMade.live` gives any view the camera and microphone as they happen, after the
9904
+ person presses Start (see Live camera and microphone in
9905
+ [Desktop capabilities](/docs/reference/desktop-capabilities)). A frame view can hand the frames to a
9906
+ Worker it starts from a `blob:` URL.
9907
+
9908
+ Shared state works as in every view: keep each independently edited value in
9909
+ its own field (for example `point0`, `point1`) so two people editing
9910
+ different values never conflict.
9911
+
9912
+ ### Adding files to an input
9913
+
9914
+ A one-page Block shows its input's files on its own screen as tiles and adds
9915
+ more there. Never draw an upload box or an `<input type="file">` of your own:
9916
+ the + tile asks StillMade, which opens its own file picker and saves the
9917
+ files exactly as the Step's own tiles do (working copies, files kept on the
9918
+ person's device, the limits and the error messages).
9919
+
9920
+ ```html
9921
+ <ul class="sm-gallery" data-size="small" id="photos">
9922
+ <!-- one <li> per photo, drawn from onInput -->
9923
+ <li><button type="button" class="sm-add" data-stillmade-add="listingPhotos"
9924
+ aria-label="Add photos" title="Add photos"></button></li>
9925
+ </ul>
9926
+ ```
9927
+
9928
+ `data-stillmade-add` names a declared `image`, `video`, `audio`, `asset` or
9929
+ `file` input, or a list of one of them (`image[]`). It needs no code: a click
9930
+ or key press on the tile opens StillMade's picker. A list gets every chosen
9931
+ file added; a single input takes the first. The saved values reach the view
9932
+ the way every input does, so draw the tiles from `onInput`:
9933
+
9934
+ ```js
9935
+ StillMade.onInput(async input => {
9936
+ for (const photo of input.listingPhotos || []) {
9937
+ const preview = await StillMade.previewImage(photo); // display copy
9938
+ // ...add an <li><img src="preview.url"></li> before the + tile
9939
+ }
9940
+ });
9941
+ ```
9942
+
9943
+ From your own button, call `await StillMade.addFiles('listingPhotos')` inside
9944
+ its click handler. It resolves to `{added}` (0 when the person closes the
9945
+ picker) and rejects with a message for an undeclared input, an input that is
9946
+ not a file input, an input an earlier Step fills, a person who can only view
9947
+ the project, or a call that does not come from a click or key press in the
9948
+ view. The view never receives a `File` or a `blob:` URL, only the saved
9949
+ values. StillMade hides `[data-stillmade-add]` tiles from viewers and for
9950
+ inputs an earlier Step fills. In the SDK preview, the tile opens your
9951
+ computer's file chooser and the preview keeps the files like your own picks.
9952
+
9953
+ ### Removing and reordering files
9954
+
9955
+ A list input's files are removed and put in order on the same screen, so a
9956
+ person drops a bad photo or sets the tour's order without leaving the Block.
9957
+ Put the buttons in each tile, with `data-index` (from 0) naming its place in
9958
+ the list `onInput` gave you:
9959
+
9960
+ ```html
9961
+ <li>
9962
+ <img src="..." alt="Photo 3">
9963
+ <span class="sm-tile-tools">
9964
+ <button type="button" data-stillmade-move="listingPhotos" data-index="2" data-to="1"
9965
+ aria-label="Move earlier" title="Move earlier"><span class="sm-glyph" data-glyph="chevron-left" aria-hidden="true"></span></button>
9966
+ <button type="button" data-stillmade-move="listingPhotos" data-index="2" data-to="3"
9967
+ aria-label="Move later" title="Move later"><span class="sm-glyph" data-glyph="chevron-right" aria-hidden="true"></span></button>
9968
+ <button type="button" data-stillmade-remove="listingPhotos" data-index="2"
9969
+ aria-label="Remove" title="Remove"><span class="sm-glyph" data-glyph="close" aria-hidden="true"></span></button>
9970
+ </span>
9971
+ </li>
9972
+ ```
9973
+
9974
+ `data-stillmade-remove` removes the file at `data-index`; `data-stillmade-move`
9975
+ moves it to the place `data-to`. A `data-to` past either end of the list does
9976
+ nothing, so the first tile's Move earlier is harmless (disable it if you like).
9977
+ From your own code, call `await StillMade.removeFile('listingPhotos', 2)` or
9978
+ `await StillMade.moveFile('listingPhotos', 2, 1)` inside a click handler; each
9979
+ resolves to `{files}`, how many the list now holds. StillMade changes the
9980
+ Step's list as its own tile buttons do, keeping each saved file's upload record
9981
+ with it, and the new list reaches the view through `onInput`: redraw the tiles
9982
+ from it, never from your own copy.
9983
+
9984
+ Both work only on list inputs (`image[]`, `video[]`, `audio[]`, `asset[]`,
9985
+ `file[]`); `addFiles` replaces a single input's file. They are refused, as
9986
+ `addFiles` is, for an undeclared input, an input an earlier Step fills, a
9987
+ viewer, Chat and a call that does not come from a click or key press in the
9988
+ view, and also when the file at that place changed after the view was given it
9989
+ (another person edited the list): the view gets the current list and the person
9990
+ tries again. StillMade hides these buttons wherever it hides the + tile. The SDK
9991
+ preview changes its example input the same way.
9992
+
9993
+ ### Theming
9994
+
9995
+ The theme variables from StillMade's design system are always defined:
9996
+ `var(--bg)`, `var(--bg-elev-1)`, `var(--fg)`, `var(--fg-muted)`,
9997
+ `var(--accent)`, `var(--border-tok)`, `var(--divider)`, `var(--r-3)` and the
9998
+ rest. Colors written with them follow Light, Dark and White automatically.
9999
+ Frame views are not required to use them; the appearance check reports
10000
+ hard-coded colors as advice instead of refusing the view.
10001
+
10002
+ A color the check takes is a theme variable, `transparent` or `currentColor`,
10003
+ or a gradient of them (`linear-gradient(to top, var(--bg), transparent)`).
10004
+ `opacity` is not a color and is always allowed. `color-mix()`, `rgba()`,
10005
+ `#rrggbbaa` and other written colors are what it reports. For words over a
10006
+ photo or video, use the kit's shade: `var(--scrim)` is the page's background,
10007
+ partly see-through, in every theme, and `class="sm-scrim"` lays it over the
10008
+ bottom of a positioned box (an `sm-stage`) under the words inside it:
10009
+
10010
+ ```html
10011
+ <figure class="sm-stage" data-fill>
10012
+ <img src="..." alt="">
10013
+ <figcaption class="sm-scrim">12 Elm Street · 3 bed</figcaption>
10014
+ </figure>
10015
+ ```
10016
+
10017
+ `--scrim` and `sm-scrim` are the only shade a Block uses; StillMade's own
10018
+ `--scrim-top` and `--scrim-bottom` (theme.css) are for its pages and the check
10019
+ reports them.
10020
+
10021
+ ### Layout
10022
+
10023
+ StillMade zooms a view to fill the space its Step gives it (up to 150%), and
10024
+ Ctrl/Cmd +, - and 0 size the view while it has focus. It never centers a frame
10025
+ view; a standard view narrower than its Step is centered by its margins.
10026
+
10027
+ An app that lays itself out (a full-size canvas or WebGL, its own scroll, pan
10028
+ or zoom, its own Ctrl/Cmd +/- keys, as in a CAD or drawing tool) puts
10029
+ `data-stillmade-layout="own"` on its outermost element:
10030
+
10031
+ ```html
10032
+ <div id="root" data-stillmade-layout="own"></div>
10033
+ ```
10034
+
10035
+ StillMade then never centers or zooms it and leaves Ctrl/Cmd +, - and 0 to the
10036
+ app. Without the attribute StillMade also stays out of the way when the view
10037
+ looks like an app: no page padding, a clipped page, a root as tall as the frame,
10038
+ fixed over half of it or scrolling over half of it, a canvas, SVG or
10039
+ `touch-action: none` surface over 40% of it, or a canvas or SVG of 300x200 or
10040
+ more with `touch-action: none` (as Three.js's controls set it). Mark the root
10041
+ anyway, in `view.html` rather than from code: StillMade reads it before the view
10042
+ starts, so the app never shows StillMade's size control or loses a key to it,
10043
+ even while it is still loading.
10044
+
10045
+ A `<video controls>` plays in StillMade's player, the same one as everywhere in
10046
+ StillMade: play, timeline, time, sound and full screen along the bottom, shown
10047
+ while the pointer is on the video, with keys on the focused video (Space or K,
10048
+ the arrows, M, F) and touch. Full screen goes through StillMade, right after a
10049
+ click or key in the view; the frame itself has no `allow="fullscreen"`. A
10050
+ `<video>` without `controls` is the Block's own and StillMade never touches it,
10051
+ so a Block that drives playback (its own timeline, an editor or CAD viewer)
10052
+ leaves `controls` out, and `data-stillmade-player="native"` keeps the browser's
10053
+ controls. Result pictures from `StillMade.previewImage` in `sm-stage` or
10054
+ `sm-gallery`, and any element marked `data-stillmade-expand`, get a full-screen
10055
+ button in the bottom-right corner while the pointer is on them; pictures with
10056
+ the same value (`data-stillmade-expand="shots"`) open as one set, and
10057
+ `data-stillmade-expand="off"` removes the button. The keyboard reaches them
10058
+ too: such a picture without a `tabindex` of its own joins the tab order,
10059
+ keyboard focus on it shows the button, Enter or Space opens the viewer, and
10060
+ closing the viewer gives the focus back to the picture.
10061
+
10062
+ ### A job on one page: a check, then the run
10063
+
10064
+ A job the person fills in and gets a result from is one page: the inputs on the
10065
+ left and the result's place on the right on a wide screen; on a phone they
10066
+ stack in the page's order, the inputs, the button in normal flow (never a sticky
10067
+ bar), then the result's place under it. When the expensive run needs a cheap
10068
+ check first (the script and a voice sample before the video), keep it on the
10069
+ same page: call `StillMade.run({make: 'check'})`, show what comes back, then
10070
+ `StillMade.run({make: 'video', script})` with what the person checked. Each run
10071
+ is priced and approved by StillMade on its own, so the view shows no price. The
10072
+ module side and what the person sees are in MODULES.md, "A check, then the
10073
+ expensive run, on one page".
10074
+
10075
+ ### Assets
10076
+
10077
+ Images, fonts, JSON, audio and video the view needs ship as files named by
10078
+ SHA-256, like module files:
10079
+
10080
+ ```json
10081
+ {"runtime": "frame", "files": {"schemaVersion": 1, "files": {
10082
+ "fonts/inter.woff2": {"sha256": "…", "bytes": 48256, "mediaType": "font/woff2"}
10083
+ }}}
10084
+ ```
10085
+
10086
+ `stillmade-block pack` fills in the digests and sizes. StillMade checks every
10087
+ file against its SHA-256 before the view starts.
10088
+
10089
+ ### What the frame does not allow
10090
+
10091
+ The view runs in an opaque-origin frame with its own policy:
10092
+
10093
+ - no network (`fetch`, WebSocket and remote images, fonts or scripts fail);
10094
+ - no code from strings (`eval`, `new Function`, inline event attributes) and
10095
+ no extra script elements;
10096
+ - no navigation, popups, downloads, forms that submit, or dialogs;
10097
+ - no WebAssembly on the page (run heavy computation in the Block's runtime,
10098
+ for example a module Block, and call it with `StillMade.run`);
10099
+ - no storage or access to StillMade's page.
10100
+
10101
+ Workers created from `blob:` URLs are allowed and run off the page's thread.
10102
+
10103
+ ### Run limits
10104
+
10105
+ StillMade compiles the view so every function and loop checks a time budget.
10106
+ One task may run for 250 ms; past that it is stopped with an error, and a view
10107
+ that keeps the page busy most of the time, stops answering, or navigates its
10108
+ frame is closed with a message. A single built-in operation that blocks for a
10109
+ second or more (a huge sort, or a regular expression that backtracks) also closes
10110
+ the view where the browser reports it. Move long computation into a Worker or the
10111
+ Block's runtime.
10112
+
10113
+ The host enforces every permission itself: a view's code can message the host
10114
+ directly, so a read-only viewer cannot run the Block or save outputs from any
10115
+ view.
10116
+
10117
+
10118
+ ## Module Blocks: JavaScript and WebAssembly
10119
+
10120
+ # Module Blocks: modern JavaScript and WebAssembly
10121
+
10122
+ A module Block (`"runtime": "module"`) is an ordinary ES module. It can use
10123
+ everything a modern browser Worker offers: `async`/`await`, timers, typed
10124
+ arrays, `WebAssembly`, `OffscreenCanvas`, `createImageBitmap`, WebCodecs, and
10125
+ any npm package you bundle into it, including C and C++ libraries compiled to
10126
+ WebAssembly (OpenCV, libvips builds, codecs, solvers), threaded WebAssembly
10127
+ (see Threads) and live WebSocket connections to declared origins (see Live
10128
+ connections). Use it when a recipe or the QuickJS `javascript` runtime is too
10129
+ small for the job. (StillMade's in-app builder writes `main.js` as one file that
10130
+ imports nothing and uses no browser APIs; that single-file rule is the builder's,
10131
+ not the runtime's. A module Block you build with the SDK can use all of this.)
10132
+
10133
+ ### Folder layout
10134
+
10135
+ ```
10136
+ my-block/
10137
+ stillmade.block.json manifest with "runtime": "module"
10138
+ module/ everything the Block runs
10139
+ main.js the entry (bundle npm dependencies into it)
10140
+ filters.wasm optional WebAssembly and data files
10141
+ module.json optional: {"entry": "other.js", "threads": true}
10142
+ tests/fixtures.json [{name, input, expected, mocks?}]
10143
+ ```
10144
+
10145
+ Start one with `stillmade-block create my-block module` (`module-hosted` for
10146
+ one that calls hosted operations). `stillmade-block` is the SDK's command line:
10147
+ `npx stillmade-block` with the npm package `stillmade-sdk`, or
10148
+ `node <sdk>/packages/block-cli/cli.js` where `<sdk>` is the extracted
10149
+ `/block-sdk/stillmade-sdk-0.1.0.zip`. Bundle npm
10150
+ dependencies into `module/main.js` with any ES module bundler (esbuild,
10151
+ rolldown, Vite). A module holds up to 64 files, 64 MiB each and 128 MiB in
10152
+ total.
10153
+
10154
+ ### The entry
10155
+
10156
+ ```js
10157
+ export default async function run(input, stillmade) {
10158
+ stillmade.progress(0.1, 'Loading');
10159
+ const { instance } = await WebAssembly.instantiate(stillmade.asset('filters.wasm'));
10160
+ const bytes = await stillmade.files.read(input.photo); // a saved image the Block received
10161
+ // ... process ...
10162
+ const saved = await stillmade.files.write(pngBlob, { mediaType: 'image/png', name: 'result.png' });
10163
+ return { result: saved };
10164
+ }
10165
+ ```
10166
+
10167
+ Return an object whose keys are your declared outputs. Outputs are checked
10168
+ against the manifest's port types.
10169
+
10170
+ ### The `stillmade` host API
10171
+
10172
+ | Member | What it does |
10173
+ | --- | --- |
10174
+ | `input` | The validated input values (the same object as the first argument). |
10175
+ | `signal` | An `AbortSignal` that fires when the person cancels. |
10176
+ | `progress(value, label)` | Reports progress from 0 to 1 with a short label. Each report extends the run limit. |
10177
+ | `log(...values)` | Adds a line to the run log (up to 500 lines). |
10178
+ | `asset(path)` | A copy of one of the module's own files, such as a `.wasm` build, as an `ArrayBuffer`. |
10179
+ | `files.read(ref)` | The bytes, as an `ArrayBuffer`, of a saved image, video, audio, asset or file (a PDF, text, CSV, Markdown or JSON file on a `file` input) the Block received as input, or that StillMade made in this run: files it wrote, transform results and generated media. Media the person kept on their device (a `sm-media://device/...` address in a project Step) reads the same way, without leaving the device. Other files cannot be read, and transforms, hosted calls and actions accept only these references too; a hosted call or action gets a cloud copy of device media, made once per version. A reference is the file its `kind`, `assetId`, `versionId` and `url` name, so `{kind, assetId, versionId, url}` rebuilt from a `media.generate` result item (without its `key`) is the same file; any other file is refused with `MODULE_FILE_READ`, by `stillmade-block test --mock`, `ready` and `gate` as by StillMade. |
10180
+ | `files.write(data, {mediaType, name})` | Saves PNG, JPEG, WebP, GIF, MP4, WebM, MP3, WAV, M4A or OGG media, or a PDF, TXT, CSV, Markdown, JSON or ZIP file (`application/pdf`, `text/plain`, `text/csv`, `text/markdown`, `application/json`, `application/zip`; a `{kind: 'file'}` reference), to the runner's library and returns a reference you can return as an output. At most 64 files per run. |
10181
+ | `media.transform(request)` | On-device media work done by StillMade: decode audio to samples, extract or loudness-normalise audio, cut, join or re-encode video, and take thumbnails. Results are saved like `files.write`. See [Media processing](#media-processing). |
10182
+ | `actions.run(action)` | Runs a declared portable action. `media.generate` opens one approval for its whole batch, naming the most it can cost in StillMade credits; `stock.search` runs directly. |
10183
+ | `hosted.run(operation, request)` | Calls a hosted operation the manifest lists in `permissions.capabilities` (for example `text.generate`, `image.generate`, `audio.speech`, `web.research`). Each call is quoted and approved like a capability Block and paid with StillMade credits. |
10184
+ | `net.fetch(url, {method, headers, body})` | Calls one of the origins declared in `permissions.network` through StillMade, which adds the person's own key or OAuth token. Returns a standard `Response`. See [Outside APIs](/docs/build/outside-apis). |
10185
+ | `net.connect(url, {protocols})` | Opens a live WebSocket to the `wss://` form of a declared origin and resolves with the open socket. No key is added. See [Live connections](#live-connections). |
10186
+ | `connections.call(appId, operationId, input)` | Calls an operation of a published adapter declared in `permissions.connections`, with the person's own account. Returns the checked JSON result. |
10187
+ | `storage.get/set/remove/list/info(...)` | The Block's own cloud storage in the current project: JSON values up to 1 MiB under folder-like keys. Kept across runs, devices and the Block's versions. See [Project storage, reads and assets](/docs/build/project-storage). |
10188
+ | `storage.putFile(key, data, {mediaType})` / `storage.getFile(key)` | Files up to 64 MiB in the same storage; `getFile` returns `{key, mediaType, bytes, data}` with `data` as an `ArrayBuffer`, or `null`. |
10189
+ | `project.read(fields?)` | A copy of the current project limited to the fields the manifest may read (`project.read` plus `context.<field>.read`), such as `timeline`, `shots` or `assets`. |
10190
+ | `assets.create(file, {name})` / `assets.appendVersion(asset, file)` | Adds a saved image, video or audio file to the project's media as a new asset (`asset.create`), or as the newest version of an asset the Block received, read or created (`asset.version.append`). |
10191
+
10192
+ Hosted calls take the operation's request fields, for example:
10193
+
10194
+ ```js
10195
+ const plan = await stillmade.hosted.run('text.generate', {
10196
+ prompt: `Write a 20-second podcast intro about ${input.topic}.`,
10197
+ system: 'Warm, short sentences.', maxTokens: 400,
10198
+ schema: { type: 'object', required: ['script'], properties: { script: { type: 'string' } } },
10199
+ });
10200
+ const voice = await stillmade.hosted.run('audio.speech', { text: plan.script, settings: { voice: 'adam' } });
10201
+ ```
10202
+
10203
+ `image.describe`, `media.analyze` and `audio.transcribe` take saved references
10204
+ (`image`/`images`, `media`, `audio`). `audio.transcribe` takes a saved audio or a
10205
+ saved video, whose own sound it transcribes, so there is no need to extract the
10206
+ audio first. `media.analyze` returns `{schemaVersion: 1, items}`, one item per
10207
+ picture or video, and for one video takes an optional `range: {start, end}` in
10208
+ seconds, at most 12 apart, to look closely at that part. `image.generate` takes `prompt` and optional
10209
+ `references`, one to eight saved images the new picture keeps. `video.generate` takes `prompt` and optional
10210
+ `firstFrame` (a saved image the video starts on, for image-to-video), `lastFrame` (one it ends on; needs
10211
+ `firstFrame`), `negativePrompt` (up to 2,000 characters) and `settings`; a model named in `settings` must take
10212
+ those frames (Kling 3.0 Turbo and Wan 2.6 Flash take no last frame; Wan 2.6 Flash and Wan 2.7 need a first frame).
10213
+ For a vertical or square clip, set `settings.aspectRatio` (`'9:16'`, `'1:1'`) and name no model: the default
10214
+ model (Vidu Q3 Turbo) takes no aspect ratio, so StillMade uses the cheapest model that makes that shape with the
10215
+ frames and `duration` given (Wan 2.6 Flash with a first frame, else Veo 3.1 Lite, else Seedance 2.0 Mini; see
10216
+ [Generation models](/docs/reference/generation-models)). A model you name is kept, and one that cannot make the shape
10217
+ is refused. Look at pictures with `media.analyze`:
10218
+ StillMade credits pay for it, while `image.describe` runs only on the person's
10219
+ own OpenRouter key. `web.fetch` takes `url` and `instruction`;
10220
+ `web.research` takes `topic`; music and sound effects take `prompt` and
10221
+ `durationSec`.
10222
+
10223
+ ### Actions (`permissions.actions`)
10224
+
10225
+ `actions.run` runs only the actions listed in `permissions.actions`, the same list a
10226
+ JavaScript Block declares: `"actions": ["media.generate"]` (or `"stock.search"`). A
10227
+ module that runs actions and calls no hosted operation leaves
10228
+ `permissions.capabilities` out; an empty list means the same.
10229
+
10230
+ ```js
10231
+ const { value, failures } = await stillmade.actions.run({
10232
+ schemaVersion: 1, kind: 'stillmade.media-action', operation: 'media.generate',
10233
+ resultPort: null,
10234
+ request: { items: [
10235
+ { key: 'front', operation: 'image', prompt: `The front of ${input.address} at dusk`,
10236
+ settings: { model: 'nano-banana-pro', aspectRatio: '16:9' } },
10237
+ { key: 'tour', operation: 'speech', prompt: input.script },
10238
+ ] },
10239
+ });
10240
+ ```
10241
+
10242
+ The action is the object a JavaScript Block returns, with every key present; its
10243
+ items, settings and limits are in [Generate media from
10244
+ JavaScript](/docs/build/generate-media). Pass `resultPort: null`: no
10245
+ output port receives the media, `actions.run` returns it to your code and `run()`
10246
+ returns what the Block outputs. Before anything is generated StillMade opens one
10247
+ approval for the whole batch with the most it can cost in StillMade credits; credits
10248
+ the batch does not use come back when it closes. Each `actions.run` call is its own
10249
+ approval, so put every item one run needs in one batch: one clip per photo, the
10250
+ voice and the music are one `media.generate` call of up to 50 items (StillMade makes
10251
+ four at a time), never a call per photo, and never `hosted.run('video.generate')` once
10252
+ per photo, which asks again from the second call. Video items take the same
10253
+ `settings` as `video.generate` above, `aspectRatio` included. `actions.run` resolves with:
10254
+
10255
+ - `value`: the items that were made, in request order, each
10256
+ `{key, kind, assetId, versionId, url, duration?}` with `kind` `image`, `video` or `audio`
10257
+ (speech, music and sound effects). `duration` is the length in seconds of a
10258
+ video, music or sound effect item: the length it asked for (`settings.duration`,
10259
+ `settings.durationSec`, or the model's default). A speech item has none, since
10260
+ its length is what the words take: decode it with
10261
+ `media.transform({operation: 'audio.decode', source})`, whose result has
10262
+ `duration`, before timing a cut to it. Return them as outputs, or pass them to
10263
+ `files.read`, `media.transform` or a hosted call.
10264
+ - `failures`: one `{key, message}` for each item that was not made; those items are
10265
+ not in `value`.
10266
+
10267
+ It throws when no item was made, when the person declines (`MODULE_ACTION_DECLINED`)
10268
+ and when the action is not declared (`MODULE_PERMISSION`). `stock.search` resolves
10269
+ with `{results}`. With `--mock` or a `tests/mocks.json`, `media.generate` answers
10270
+ with stand-in media and no failures; record your own answer under
10271
+ `actions["media.generate"]`.
10272
+
10273
+ ### A check, then the expensive run, on one page
10274
+
10275
+ A job the person fills in and then gets a result from is one Block on one page
10276
+ (builder contract, "One job per Step"): the inputs, a cheap check before the
10277
+ expensive run, and the result. A listing video writes the script and voices a
10278
+ few seconds of it first, and makes the video only once the person has read the
10279
+ script and heard the voice. Build that as one module Block with a custom view,
10280
+ never as two Blocks or two Steps.
10281
+
10282
+ The manifest takes the mode and the checked values as optional inputs, and the
10283
+ outputs the check leaves out are optional (a run may leave out any output with
10284
+ `"required": false`). `action` names the main button, the expensive run:
10285
+
10286
+ ```json
10287
+ "inputs": {
10288
+ "listingPhotos": { "type": "image[]", "primary": true, "description": "Photos of the home, in the order of the tour." },
10289
+ "homeFacts": { "type": "text", "description": "Address, rooms and what stands out." },
10290
+ "make": { "type": "text", "required": false, "default": "video", "description": "check: the script and a voice sample; video: the tour." },
10291
+ "script": { "type": "script", "required": false, "description": "The checked script, as the person left it." }
10292
+ },
10293
+ "outputs": {
10294
+ "tour": { "type": "video", "primary": true, "required": false },
10295
+ "script": { "type": "script", "required": false },
10296
+ "voiceSample": { "type": "audio", "required": false }
10297
+ },
10298
+ "action": { "label": "Generate tour", "running": "Generating tour…" },
10299
+ "permissions": { "capabilities": ["text.generate", "audio.speech"] }
10300
+ ```
10301
+
10302
+ The module does only the cheap work when asked for the check:
10303
+
10304
+ ```js
10305
+ export default async function run(input, stillmade) {
10306
+ const write = async () => (await stillmade.hosted.run('text.generate', {
10307
+ prompt: `A 40-second listing tour script from these facts: ${input.homeFacts}`,
10308
+ schema: { type: 'object', required: ['script'], properties: { script: { type: 'string' } } },
10309
+ })).script;
10310
+ if (input.make === 'check') {
10311
+ const script = await write();
10312
+ const voiceSample = await stillmade.hosted.run('audio.speech', { text: script.split(/(?<=[.!?])\s/)[0] });
10313
+ return { script, voiceSample };
10314
+ }
10315
+ const script = input.script || await write();
10316
+ const voice = await stillmade.hosted.run('audio.speech', { text: script });
10317
+ const { duration } = await stillmade.media.transform({ operation: 'audio.decode', source: voice });
10318
+ const each = duration / input.listingPhotos.length;
10319
+ const tour = await stillmade.media.transform({
10320
+ operation: 'timeline.render', aspect: '9:16',
10321
+ clips: input.listingPhotos.map((photo, index) => ({ source: photo, start: index * each, duration: each, motion: 'zoom-in' })),
10322
+ audio: [{ source: voice, start: 0, duration }],
10323
+ });
10324
+ return { tour, script };
10325
+ }
10326
+ ```
10327
+
10328
+ The view runs it twice with `StillMade.run`, which takes the inputs the view
10329
+ sets (StillMade keeps them on the Step and fills the rest): `{make: 'check'}`
10330
+ first ("Write the script"), then `{make: 'video', script}` with the script the
10331
+ person checked or edited. One `sm-primary` at a time: the check until there is
10332
+ a script, then the expensive run, labelled with `manifest.action`, while
10333
+ "Write again" stays a quiet button. The result's place
10334
+ (`sm-stage data-ratio="9:16"`) is drawn before either run; the script and the sample sit in it or beside the inputs
10335
+ until the video replaces them. Each run starts empty: return the script again
10336
+ with the video so it stays on the Step. StillMade saves the inputs a view sets,
10337
+ so the mode last pressed stays on the Step; until the view first sets it, a run
10338
+ started elsewhere (Chat, an automation) takes the default and makes the video.
10339
+
10340
+ Each run is priced and approved by StillMade on its own. Beside the Block's
10341
+ page StillMade shows what each hosted call costs at the Step's saved or default
10342
+ model ("Write · 1 credit · Make voiceover · 3 credits"), with an (i) saying that
10343
+ pressing the Block's button is the approval. A run started by the person's own
10344
+ click or key in the view then runs each hosted operation once without a dialog,
10345
+ up to that price. StillMade checks the price with the provider first and opens
10346
+ its approval dialog, with the quote, only when the quote is higher, when the run
10347
+ calls the same operation a second time, or when the run did not come from the
10348
+ person's press (in a preview outside a project every call shows the dialog;
10349
+ `stillmade-block dev` answers with stand-ins and spends nothing). The view shows
10350
+ no prices. `media.transform`, `timeline.render` included, runs on the device
10351
+ and costs nothing; `actions.run` `media.generate` always asks first.
10352
+
10353
+ ### Media processing
10354
+
10355
+ `stillmade.media.transform(request)` asks StillMade to do heavy media work on
10356
+ the person's device: Web Audio for audio, and the same renderer as Final
10357
+ Render for video (WebCodecs in the browser, the native renderer on desktop).
10358
+ It never runs on StillMade's servers and costs no credits. A transform reads
10359
+ only files the Block received, wrote or generated in this run (including media
10360
+ from `actions.run` `media.generate`; see `files.read` above), and saves its
10361
+ result to the runner's library, so you can return it as an output or pass it
10362
+ to the next transform.
10363
+
10364
+ | Request | Result |
10365
+ | --- | --- |
10366
+ | `{operation: 'audio.decode', source, sampleRate?}` | `{sampleRate, duration, samples}`: mono `Float32Array` PCM for analysis (default 16 kHz; 8000 to 48000). |
10367
+ | `{operation: 'audio.extract', source, format?}` | A saved WAV (16-bit, 48 kHz) or MP3 of the audio track. |
10368
+ | `{operation: 'audio.normalize', source, targetLufs?, format?}` | A saved WAV or MP3 at the target integrated loudness (default -16 LUFS, peaks at most -1 dBFS). |
10369
+ | `{operation: 'video.cut', source, keep, settings?}` | A saved MP4 with only the `keep` ranges (`[{start, end}]` seconds, ordered, up to 500). |
10370
+ | `{operation: 'video.concat', sources, settings?}` | A saved MP4 of 2 to 50 videos in order. |
10371
+ | `{operation: 'video.resize', source, settings}` | A saved MP4 re-encoded with new settings. |
10372
+ | `{operation: 'image.thumbnail', source, at?, width?}` | A saved PNG of one frame (default the first frame, 1280 px wide). |
10373
+ | `{operation: 'document.text', source}` | `{text, pages}`: all the words of a PDF or text file the person gave, and `pages`, a list holding each page's words as one string (up to 500 pages). A file with no words (a scanned PDF) is refused. |
10374
+ | `{operation: 'image.compose', width, height, layers, background?, format?, text?}` | A saved PNG (or JPEG) of saved images joined into one picture: side by side, a grid, a contact sheet, a poster with a title. |
10375
+ | `{operation: 'timeline.render', clips, kind?, aspect?, audio?, captions?, settings?}` | A saved MP4 (or, with `kind: 'audio'`, an MP3) of your own timeline, rendered the way the Editor exports. |
10376
+
10377
+ `timeline.render` is how a Block exports: there is no separate Export, Captions,
10378
+ Transition or Color grade Block to call. Each clip is `{source, start, duration,
10379
+ from?, transition?: {type, duration?}, look?, mute?, motion?, fit?, focus?}` (a saved
10380
+ image or video; a video keeps its own sound unless `mute`; `motion` is a slow
10381
+ camera move over the clip: `zoom-in`, `zoom-out`, `pan-left`, `pan-right`,
10382
+ `pan-up` or `pan-down`, which always keeps the picture filling the frame), each
10383
+ audio item `{source, start, duration, from?, volume?}` (`volume` is a percentage of the
10384
+ recording's own level, 0 to 200: 100, the default, plays it as recorded, 50 at half
10385
+ the level, 0 silent; put music under a voice at about 20 to 30), and each caption
10386
+ `{text, start, end, words?, position?, highlight?, animation?, size?}`, where
10387
+ `words` (`[{text, start, end}]`) highlights each word as it is spoken and `size`
10388
+ is the text height in pixels of the rendered frame (16 to 200; default 44, so
10389
+ give a 1080 by 1920 vertical video about 80). Times are seconds on the timeline: each
10390
+ clip starts where the one before it ends, less the transition on that earlier
10391
+ clip, which blends it into the next one; clips that overlap more are refused.
10392
+ `aspect` is `16:9`, `9:16`, `1:1` or `4:5`. A clip of another shape than the
10393
+ frame (a wide sermon in a `9:16` clip) is cropped to fill it, around its middle
10394
+ or around `focus: {x, y}`, the point to keep in view as fractions 0 to 1 across
10395
+ the picture (`{x: 0.3}` keeps a speaker standing left of centre), and
10396
+ `focus.scale` (1 to 2.5, instead of a motion) holds the clip that much closer on
10397
+ that point; `fit: 'contain'` fits it whole with bars instead. Looks, transitions and caption
10398
+ animations use the Editor's names (`TIMELINE_LOOKS`, `TIMELINE_TRANSITIONS`,
10399
+ `CAPTION_ANIMATIONS`). The render follows the person's export plan, like any
10400
+ export from StillMade.
10401
+
10402
+ `image.compose` draws on a canvas `width` by `height` pixels (16 to 4096),
10403
+ filled with `background` (`#rrggbb`, default white). Each layer is `{source, x,
10404
+ y, width, height, fit?}`: a saved image placed in that box, in order, cropped
10405
+ to fill it (`fit: 'cover'`, the default) or fitted whole inside it (`'contain'`).
10406
+ Each piece of text is `{text, x, y, size?, color?, background?, align?,
10407
+ weight?, width?}`, drawn on top: `y` is the top of the line, `size` is 8 to 400
10408
+ pixels (default 48), `color` defaults to black, `background` puts a box behind it
10409
+ (for a Before label over a photo; `color` and `background` are `#rrggbb`, or `#rrggbbaa`
10410
+ or `rgba()` for a see-through box such as `#00000099`), `align` is `left`, `center` or `right` around
10411
+ `x`, and `weight` is `bold` (default) or `normal`. `width` wraps the words onto
10412
+ lines no wider than it, 1.2 times `size` apart, breaking a word wider than a line
10413
+ (or a caption with no spaces) between its characters: give a caption under a
10414
+ frame the frame's width so it never runs into the next one. Up to 64 layers and 64 pieces of
10415
+ text; `format` is `png` (default) or `jpeg`. A grid is arithmetic in your code:
10416
+ for 9 photos on a 1800 by 1800 sheet, three columns of 600 pixels.
10417
+
10418
+ `source`/`sources` are saved references (`{kind, assetId, versionId, url}`).
10419
+ Render `settings` are `{fps: 24|30|60, resolution: 720|1080|2160, quality:
10420
+ 'low'|'medium'|'high'}`; the default is 30 fps, 1080, high. `video.cut`,
10421
+ `video.concat` and `video.resize` keep the source's aspect ratio;
10422
+ `timeline.render` makes the `aspect` it is given. Rendering follows the person's export plan, like any
10423
+ export.
10424
+
10425
+ Pure helpers ship as `@stillmade/block-sdk/media` for you to bundle (the file is
10426
+ `stillmade-sdk/packages/block-sdk/media-transform.js` in the npm package and
10427
+ `<sdk>/packages/block-sdk/media-transform.js` in the extracted download; alias
10428
+ the name in your bundler or import that path):
10429
+ `silentRanges(samples, sampleRate, {thresholdDb, minSilence})`,
10430
+ `keepRanges(silent, duration, {padding})`, `integratedLoudness(channels,
10431
+ sampleRate)` (ITU-R BS.1770), `loudnessGain`, `encodeWav` and `decodeWav`.
10432
+
10433
+ ```js
10434
+ import { silentRanges, keepRanges } from '@stillmade/block-sdk/media';
10435
+
10436
+ export default async function run(input, stillmade) {
10437
+ const audio = await stillmade.media.transform({ operation: 'audio.decode', source: input.video });
10438
+ const keep = keepRanges(silentRanges(audio.samples, audio.sampleRate), audio.duration);
10439
+ const trimmed = await stillmade.media.transform({ operation: 'video.cut', source: input.video, keep });
10440
+ return { trimmed };
10441
+ }
10442
+ ```
10443
+
10444
+ For anything else, decode and encode in the Block itself: the Worker has
10445
+ WebCodecs (`VideoDecoder`, `VideoEncoder`, `AudioDecoder`), `OffscreenCanvas`
10446
+ and `createImageBitmap`, and you can bundle `mp4-muxer`, `mp4box` or a
10447
+ WebAssembly codec. Images arrive as saved references: read the bytes with
10448
+ `files.read` and decode them with `createImageBitmap`, at full resolution.
10449
+
10450
+ ### Sandbox and limits
10451
+
10452
+ - The module runs in one dedicated Worker inside a response-sandboxed frame
10453
+ with an opaque origin. It has no network of its own (`fetch`, WebSocket and
10454
+ remote imports fail; `net.fetch` and `net.connect` go through StillMade), no
10455
+ storage of the app, no page or DOM access, and cannot read another Block's
10456
+ files.
10457
+ - Every file is fetched from StillMade's store and checked against its SHA-256
10458
+ before anything runs.
10459
+ - A run may take 2 minutes; each progress report allows 60 more seconds. Time
10460
+ StillMade spends on a host call (rendering a video, waiting for the person to
10461
+ approve credits) does not count. Every run stops after 30 minutes in total.
10462
+ When a limit passes, or the person cancels, the frame is removed and the
10463
+ Worker stops immediately, even inside an endless loop.
10464
+ - Generating code at run time (`new Function`, used by Emscripten bindings) is
10465
+ allowed inside the sandbox.
10466
+
10467
+ Module Blocks run in StillMade on the person's device: in the browser while
10468
+ StillMade is open, or in StillMade Desktop. StillMade has no server sandbox for
10469
+ them. A schedule or event that hands browser-only Steps to the owner's StillMade
10470
+ Desktop ("Run ... on my StillMade Desktop") pauses at a module Step until the
10471
+ owner's desktop app, open with "Run my automations on this computer" on, runs it
10472
+ with the Step's exact inputs; the workflow then continues from its result. Module
10473
+ Steps that call paid hosted operations, generate media or need a device grant
10474
+ still wait for the owner. Without that choice a background run stops before a
10475
+ module Step and the person runs it in the project. Marketplace listings show
10476
+ module Blocks as **Browser only**.
10477
+
10478
+ ### Live connections
10479
+
10480
+ `stillmade.net.connect(url, {protocols})` opens a WebSocket to the `wss://` form
10481
+ of an origin in `permissions.network` (declare `https://stream.example.org` and
10482
+ connect to `wss://stream.example.org/...`) and resolves once it is open:
10483
+
10484
+ ```js
10485
+ const socket = await stillmade.net.connect('wss://stream.example.org/v1/live', { protocols: ['json.v1'] });
10486
+ await socket.send(JSON.stringify({ subscribe: input.channel }));
10487
+ for await (const data of socket) { // text arrives as strings, binary as ArrayBuffers
10488
+ stillmade.progress(0.5, 'Listening'); // each progress report extends the run limit
10489
+ if (JSON.parse(data).done) break; // leaving the loop closes the connection (code 1000)
10490
+ }
10491
+ ```
10492
+
10493
+ The socket also has `onmessage`, `onclose`, `addEventListener`, `readyState`,
10494
+ `protocol`, `close(code, reason)` (1000 or 3000 to 4999) and `closed`, a promise of
10495
+ `{code, reason, wasClean}`. `for await` yields every message not yet read, also ones
10496
+ that arrived before the loop began (up to 1,024 messages or 16 MiB; older ones are
10497
+ dropped). The module's own frame still has no network: StillMade
10498
+ checks each URL (`wss://`, the standard port, a declared origin, valid subprotocol
10499
+ names) and opens the connection in a separate relay frame that runs no Block code,
10500
+ then relays the messages. A run keeps at most 4 connections, sends or receives at
10501
+ most 1 MiB a message and 256 MiB in all, and every connection closes when the run
10502
+ ends. The service sees an opaque `Origin` (`null`). StillMade adds no key to a live
10503
+ connection: for a service that needs one, get a short-lived token with `net.fetch`
10504
+ first and pass it as the service documents.
10505
+
10506
+ ### Threads (SharedArrayBuffer)
10507
+
10508
+ A module that runs its own Workers with shared memory (Emscripten pthreads,
10509
+ threaded WebAssembly builds) declares it in `module.json`:
10510
+
10511
+ ```json
10512
+ { "entry": "main.js", "threads": true }
10513
+ ```
10514
+
10515
+ It then runs in a Worker that can share `SharedArrayBuffer` and
10516
+ `WebAssembly.Memory({shared: true})` with the Workers it starts from `blob:` URLs
10517
+ (module Workers and remote scripts stay unavailable, so bundle the thread code).
10518
+ That Worker is not a secure context, so it has no WebCodecs, WebGPU,
10519
+ `crypto.subtle` or `crypto.randomUUID`; a module without `threads` keeps those and
10520
+ cannot share memory between Workers. Threads work in Chrome and Edge (version 137
10521
+ or later) and in StillMade Desktop; in other browsers `self.crossOriginIsolated` is
10522
+ false and `SharedArrayBuffer` is missing, so check it and fall back to one thread.
10523
+ `files.write` and `storage.putFile` accept bytes in shared memory (they are copied).
10524
+
10525
+ ### Test, pack and import
10526
+
10527
+ ```sh
10528
+ stillmade-block test my-block # runs fixtures in a local Node worker
10529
+ stillmade-block preview my-block input.json
10530
+ stillmade-block dev my-block # the interface in your browser, reloaded on save
10531
+ stillmade-block gate my-block # StillMade's readiness check
10532
+ stillmade-block pack my-block # writes <id>-<version>.stillmade-module.json
10533
+ ```
10534
+
10535
+ `dev` serves the Block's real controls or view at `http://127.0.0.1:4800`
10536
+ (`--port` picks another) and answers hosted calls with stand-ins, never credits.
10537
+ The Block is not ready until `gate` passes. Name the Block's own folder: `gate .`
10538
+ in a folder that also holds the extracted SDK checks the SDK's examples too.
10539
+ `pack my-block` is the only packing step for a module Block, and it never takes
10540
+ `--original` (original `.stillmade-block` archives hold recipe, JavaScript,
10541
+ capability and ComfyUI source only and refuse a module Block).
10542
+
10543
+ `test` and `preview` run your code on your machine in a disposable Node worker
10544
+ with the same host API and run limits (not a security sandbox; it is your own
10545
+ code). `files.read`, `files.write`, `media.transform`, actions and hosted calls need StillMade, or
10546
+ a host you pass to `createNodeModuleExecutor` in your own tests. The Node worker has no
10547
+ `OffscreenCanvas` or `createImageBitmap`: a Block that decodes or draws images there
10548
+ needs its own fallback for tests, or tests only what it does without drawing.
10549
+
10550
+ A fixture's `files` mock is a path inside the Block folder (local runs only: it does not
10551
+ travel in the pack), `{"text": "..."}` for a text file, or `{"base64": "..."}` for a small
10552
+ binary file (at most 256 KB), which travels in the pack and the import review. Text can
10553
+ never hold a NUL character (`\u0000`): StillMade cannot store it, and import refuses it.
10554
+
10555
+ A fixture's photo, recording, clip or file can be one of StillMade's example files,
10556
+ named by address: `/api/media/example/photo-1.jpg` to `photo-4.jpg`, `sheet.png`,
10557
+ `room.png`, `kitchen.wav`, `hall.wav`, `click-120.wav`, `audio.mp3`, `clip.mp4`,
10558
+ `notes.txt`, `sales.csv` and `lesson.pdf`, a two-page PDF of words (sizes in
10559
+ PASTE_TO_LLM.md). `test` reads their real bytes
10560
+ when the fixture gives no mock for them, and StillMade, its preview and its phone and
10561
+ browser check serve the same files, so the example a person sees is the one you tested.
10562
+ Point what a fixture's mocks answer at them too (`/api/media/example/clip.mp4` for a
10563
+ rendered clip, `kitchen.wav` for speech): `dev` then shows and plays the result, where a
10564
+ made-up address shows an empty player.
10565
+
10566
+ A fixture can record what the host answers, so a test of a Block that calls
10567
+ hosted operations costs nothing and gives the same result every run:
10568
+
10569
+ ```json
10570
+ {
10571
+ "name": "Finds John 3:16",
10572
+ "input": { "sermon": { "kind": "audio", "assetId": "a1", "versionId": "v1", "url": "https://example.com/sermon.wav" } },
10573
+ "mocks": {
10574
+ "hosted": {
10575
+ "audio.transcribe": { "schemaVersion": 1, "text": "Turn to John 3:16", "words": [{ "text": "John", "start": 0.6, "end": 0.9 }] },
10576
+ "text.generate": { "verses": [{ "reference": "John 3:16", "word": 0 }] }
10577
+ }
10578
+ },
10579
+ "expected": { "verses": [{ "reference": "John 3:16", "start": 0.6 }] }
10580
+ }
10581
+ ```
10582
+
10583
+ `mocks` has the shape of `tests/mocks.json` (`hosted`, `transforms`, `actions`,
10584
+ `files`, `storage`, `project`, `connections`, `network`); a list answers
10585
+ successive calls in order. `files` maps an input's asset ID to a file in the Block
10586
+ folder, or to `{"text": "..."}` for a text file, so a Block that reads a notes or
10587
+ CSV file the person gives it is tested without a folder (StillMade's builder has none). A fixture's own `mocks` take the place of
10588
+ `tests/mocks.json` for that fixture. StillMade's own builder writes module
10589
+ Blocks the same way and runs their tests before anyone sees them.
10590
+
10591
+ Import the `.stillmade-module.json` in StillMade (Create, then Import).
10592
+ StillMade verifies every file digest, uploads each file to its
10593
+ content-addressed store, and saves the Block. Fixtures then run in your
10594
+ browser's module sandbox.
10595
+
10596
+ Uploaded module files count against your cloud storage. A digest alone never
10597
+ grants access: a person can read a module file only when they uploaded those
10598
+ exact bytes or can open a saved Block that names it (yours, an installed or
10599
+ community version, or one shared with their team).
10600
+
10601
+ See `examples/module-opencv/` for a Block that runs OpenCV on a 12-megapixel
10602
+ photo, `examples/module-silence-trimmer/` for one that cuts the pauses out of a
10603
+ talking video, and `examples/module-audio-extract/` for one that saves a
10604
+ video's audio as WAV.
10605
+
10606
+
10137
10607
  ## Describe an image
10138
10608
 
10139
10609
  Use `runtime: "capability"` with `operation: "image.describe"` when a Block must inspect one image and return plain text. This is the portable boundary for visual analysis: the Block declares its image input, analysis instructions, output, tests, and license; StillMade owns image loading, provider credentials, model/payment selection, approval, accounting, and the retained execution record.
@@ -12578,7 +13048,7 @@ Capability Blocks (`image.generate`, `video.generate`, `audio.music`, `audio.sfx
12578
13048
  | `vidu-q3-turbo` | Vidu Q3 Turbo | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 | model default | model default | yes | yes | yes |
12579
13049
  | `wan-2.7` | Wan 2.7 | 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 | 720p, 1080p | model default | required | yes | no |
12580
13050
 
12581
- Veo 3.1 Lite at 1080p is always 8 seconds. Generated images are stored as measured PNGs (up to 8192 pixels a side); videos are stored as the provider's MP4.
13051
+ Veo 3.1 Lite at 1080p is always 8 seconds. With no model named, a video uses `vidu-q3-turbo`; when its `aspectRatio` is one that model cannot make (`9:16` for a vertical clip), it uses the first of `wan-2.6-flash`, `veo-3.1-lite`, `seedance-2.0-mini`, `kling-v3-turbo`, `seedance-2.0-fast`, `seedance-2.0` (cheapest first) that makes that shape with the frames and `duration` given. A model the Block names is kept, and refused if it cannot make the shape. Generated images are stored as measured PNGs (up to 8192 pixels a side); videos are stored as the provider's MP4.
12582
13052
 
12583
13053
  ### Music and sound effects
12584
13054
  | Operation | Model | Length | Price |
@@ -12716,19 +13186,12 @@ The latest version of each Block bundled with the SDK, generated from the packag
12716
13186
  | --- | --- | --- | --- | --- |
12717
13187
  | `sm.ai-decide` 1.0.0 | AI decide | task | `data` text (external_data) · primary; `rules` text (decision_rules) | `decision` object (ai_decision) · primary |
12718
13188
  | `sm.ai-write` 1.0.0 | AI write | task | `data` text (external_data) · primary; `instructions` text (writing_instructions) | `text` text (written_message) · primary |
12719
- | `sm.animated-scene` 2.3.9 | Animated Scene | workspace | `animation` scene-document (animated_scene_document) · project context only · primary; `shotPlan` shot-plan (production_shot_plan) · project context only; `updatedAnimation` scene-document (animated_scene_document); `commit` boolean (apply_changes); `generatedImage` image (image); `targetShotId` text (target_shot); `targetLayerId` text (target_layer); `attachGeneratedImage` boolean (attach_image); `action` text (animated_scene_action); `versions` metadata[] (revision_history) · project context only; `presence` metadata[] (collaborator_presence); `assets` asset[] (project_media) · project context only | `animation` scene-document (animated_scene_document) · primary; `edit` workspace-edit (workspace_edit); `qualityAction` approval (portable_media_action); `renderAction` approval (portable_media_action); `qualityReport` metadata (quality_report); `editorTimeline` timeline (editor_handoff); `revision` metadata (revision_event) |
12720
- | `sm.animated-scene-assistant` 1.0.0 | Plan Animated Scene | task | `animation` scene-document · project context only; `request` text · primary | `edit` workspace-edit · primary |
12721
- | `sm.blueprint` 2.1.9 | Blueprint | workspace | `blueprint` bible (production_blueprint) · project context only · primary; `updatedBlueprint` bible (production_blueprint); `commit` boolean (apply_changes); `assistantEdit` workspace-edit (workspace_edit); `versions` metadata[] (revision_history) · project context only; `presence` metadata[] (collaborator_presence); `assets` asset[] (project_media) · project context only | `blueprint` bible (production_blueprint) · primary; `edit` workspace-edit (workspace_edit); `shotPlan` shot-plan (shot_plan_handoff); `revision` metadata (revision_event) |
12722
- | `sm.blueprint-assistant` 1.0.0 | Improve Blueprint | task | `blueprint` bible · project context only; `request` text · primary | `edit` workspace-edit · primary |
12723
13189
  | `sm.board-editor` 2.2.11 | Whiteboard | workspace | `board` board-document (whiteboard_document) · project context only · primary; `updatedBoard` board-document (whiteboard_document); `commit` boolean (apply_changes); `action` text (whiteboard_action); `presenterPrompt` text (presenter_prompt); `generatedImage` image (image); `versions` metadata[] (revision_history) · project context only; `presence` metadata[] (collaborator_presence); `assets` asset[] (project_media) · project context only | `board` board-document (whiteboard_document) · primary; `edit` workspace-edit (workspace_edit); `presenterAction` approval (portable_media_action); `qualityAction` approval (portable_media_action); `renderAction` approval (portable_media_action); `actionEdit` workspace-edit (workspace_edit); `qualityReport` metadata (quality_report); `editorTimeline` timeline (editor_handoff); `revision` metadata (revision_event) |
12724
13190
  | `sm.captions` 2.0.0 | Captions | editor-extension | `transcript` transcript · primary; `projectTranscript` transcript · project context only; `timeline` timeline · project context only; `text` text; `start` number; `duration` number; `highlightColor` text; `animation` text | `edit` timeline-edit · primary |
12725
13191
  | `sm.character` 2.0.1 | Character reference | workspace | `input` text (character_description) · primary; `name` text (character_name); `role` text (character_role); `appearance` text (character_appearance); `personality` text (character_personality) | `output` character (character_reference) · primary |
12726
- | `sm.chat` 2.0.3 | Production chat | workspace | `brief` brief (production_brief) · primary | `script` script (narration_script) · primary |
12727
13192
  | `sm.compare` 2.0.2 | Compare assets | workspace | `imageA` image (comparison_image_a) · primary; `imageB` image (comparison_image_b); `selection` text (comparison_selected_image); `position` integer (comparison_split_position) | `output` image (selected_comparison_image) · primary |
12728
13193
  | `sm.connect-app` 1.0.0 | Connect an app | task | `app` text (connected_app) · primary | `result` object (external_data) · primary |
12729
13194
  | `sm.contact-fields` 1.0.0 | Contact fields | task | `record` object (external_data) · primary | `email` text (contact_email) · primary; `first_name` text (contact_first_name); `last_name` text (contact_last_name); `full_name` text (contact_full_name); `phone` text (contact_phone); `company` text (contact_company); `city` text (contact_city); `other_answers` text (contact_other_answers) |
12730
- | `sm.continue-if` 1.0.0 | Continue only if | task | `decision` object (ai_decision) · primary; `item` object (external_data); `minimum_score` integer (minimum_score) | `item` object (external_data) · primary |
12731
- | `sm.describe-media` 2.1.0 | Describe media | task | `image` image (source_image) · primary | `description` text (visual_description) · primary |
12732
13195
  | `sm.edit-assistant` 2.0.2 | Edit Assistant | task | `timeline` timeline (timeline) · project context only · primary; `selection` timeline-range (timeline_range); `request` text (edit_request) | `edit` timeline-edit (timeline_edit_proposal) · primary |
12733
13196
  | `sm.editor` 2.2.12 | Editor | workspace | `editor` timeline (editor_timeline) · project context only · primary; `assets` asset[] (legacy.asset-list.assets) · project context only; `updatedEditor` timeline (legacy.timeline.updatededitor); `commit` boolean (legacy.boolean.commit); `action` text (legacy.text.action); `renderFps` integer (legacy.integer.renderfps); `renderResolution` integer (legacy.integer.renderresolution); `renderQuality` text (legacy.text.renderquality) | `editor` timeline (editor_timeline) · primary; `edit` workspace-edit (legacy.workspace-edit.edit); `mediaAction` approval (portable_media_action) |
12734
13197
  | `sm.export-audio` 2.0.0 | Export audio | task | `input` timeline (timeline) · primary; `renderFps` text (render_fps); `renderResolution` text (render_resolution); `renderQuality` text (render_quality); `projectTimeline` timeline (timeline) · project context only | `mediaAction` approval (portable_media_action); `output` audio (rendered_audio) |
@@ -12738,41 +13201,22 @@ The latest version of each Block bundled with the SDK, generated from the packag
12738
13201
  | `sm.generate-image` 2.1.0 | Generate image | task | `input` text · primary; `photos` image[] (reference_image) | `output` image · primary |
12739
13202
  | `sm.generate-video` 2.0.1 | Generate video | task | `prompt` brief (video_generation_brief) · primary | `video` video (generated_video) · primary |
12740
13203
  | `sm.grade` 2.0.2 | Color grade | editor-extension | `timeline` timeline (editor_timeline) · project context only · primary; `clipIds` text[] (timeline_clip_ids); `look` text (color_grade_look); `adjustments` json (color_grade_adjustments) | `edit` timeline-edit (timeline_edit) · primary |
12741
- | `sm.image-editor` 2.0.1 | Image editor | workspace | `input` image (source_image) · primary; `brightness` number (brightness_adjustment); `contrast` number (contrast_adjustment) | `output` image (processed_image) · primary |
12742
13204
  | `sm.import-media` 2.0.2 | Import media | task | `files` asset[] (legacy.asset-list.files) · primary; `include` text (legacy.text.include); `duplicates` text (legacy.text.duplicates) | `assets` asset[] (legacy.asset-list.assets) · primary |
12743
13205
  | `sm.location` 2.0.1 | Location reference | workspace | `input` text (location_description) · primary; `name` text (location_name); `visualDetails` text (location_visual_details); `timeOfDay` text (location_time); `atmosphere` text (location_atmosphere) | `output` location (location_reference) · primary |
12744
13206
  | `sm.meta-creative` 1.0.0 | Meta ad creative | task | `upload` object (external_data) · primary; `message` text (ad_primary_text); `website_url` text (ad_destination_url); `page_id` text (facebook_page_id); `call_to_action` text (ad_call_to_action) | `creative` object (meta_ad_creative) · primary |
12745
13207
  | `sm.motion-graphics` 2.0.0 | Motion graphics | editor-extension | `timeline` timeline (timeline) · primary; `projectTimeline` timeline (timeline) · project context only; `plan` json (motion_graphics_plan) | `timeline` timeline (timeline) · primary; `edit` workspace-edit (editor_workspace_edit) |
12746
- | `sm.new-items` 1.0.0 | Only new items | task | `items` object (external_data) · primary; `id_field` text (item_id_field) | `item` object (external_data) · primary; `waiting` integer (pending_item_count) |
12747
13208
  | `sm.panel-strip` 2.4.2 | Image Panels | workspace | `panels` panel-document (image_panel_document) · project context only · primary; `transcript` transcript (timed_transcript) · project context only; `script` script (narration_script) · project context only; `narrationScript` script (narration_script); `narrationTiming` transcript (timed_transcript); `blueprint` bible (production_blueprint) · project context only; `updatedPanels` panel-document (image_panel_document); `commit` boolean (apply_changes); `generatedImage` image (image); `targetPanelId` text (panel_id); `attachGeneratedImage` boolean (boolean); `action` text (host_action); `stockQuery` text (stock_query); `generatedImages` image[] (image_list) | `panels` panel-document (image_panel_document) · primary; `edit` workspace-edit (workspace_edit); `stockAction` approval (portable_media_action); `qualityAction` approval (portable_media_action); `renderAction` approval (portable_media_action); `stockAsset` asset (licensed_asset); `stockImage` image (licensed_stock_image); `qualityReport` metadata (quality_report); `generationPlan` metadata (generation_plan); `generateAction` approval (portable_media_action); `generatedImage` image (generated_image); `batchGenerateAction` approval (portable_media_action); `generatedImages` image[] (generated_image_list) |
12748
13209
  | `sm.photo-notes` 1.1.0 | Photo notes | task | `photos` image[] (photo_set) · primary | `notes` text (photo_notes) · primary |
12749
- | `sm.pick-photos` 1.0.0 | Pick best photos | task | `photos` image[] (image) · primary; `keep` integer (photo_keep_count); `instruction` text (photo_selection_instruction); `notes` json (photo_notes) | `photos` image[] (image) · primary; `reasons` text (plain_text) |
12750
13210
  | `sm.pipeline` 2.2.6 | Canvas | workspace | `canvas` pipeline (canvas_document) · project context only · primary; `assets` asset[] (project_assets) · project context only; `versions` metadata[] (asset_versions) · project context only; `updatedCanvas` pipeline (canvas_document); `commit` boolean (canvas_commit_intent); `generatedImage` image (generated_image_version); `addGeneratedImage` boolean (attach_generated_image); `generatedImageTitle` text (generated_image_title); `generatedVideo` video (generated_video_version); `addGeneratedVideo` boolean (attach_generated_video); `generatedVideoTitle` text (generated_video_title); `generatedAudio` audio (generated_audio_version); `addGeneratedAudio` boolean (attach_generated_audio); `generatedAudioTitle` text (generated_audio_title); `connectionProposal` json (context_connection_proposal); `applyContextConnections` boolean (context_connection_intent) | `canvas` pipeline (canvas_document) · primary; `edit` workspace-edit (canvas_edit_proposal) |
12751
- | `sm.quality-audit` 1.0.2 | Quality review plan | task | `assets` asset[] (saved_asset_versions) · project context only · primary | `plan` metadata (visual_quality_review_plan) · primary |
12752
- | `sm.research` 2.0.2 | Research | task | `input` text (research_question) · primary; `sourceTitle` text (source_title); `sourceUrl` text (source_url); `sourceNotes` text (source_notes) | `output` research (research_dossier) · primary |
12753
13211
  | `sm.rough-cut` 2.0.2 | Rough Cut | task | `timeline` timeline (timeline) · project context only · primary; `clipIds` text (timeline_clip_selection); `startAt` number (timeline_start_seconds); `gap` number (timeline_clip_gap_seconds) | `edit` timeline-edit (timeline_edit_proposal) · primary |
12754
13212
  | `sm.score` 2.0.0 | Audio scoring | workspace | `timeline` timeline (timeline) · primary; `projectTimeline` timeline (timeline) · project context only; `audio` audio; `stockAsset` asset (licensed_asset); `audios` audio[]; `plan` json (audio_score_plan) | `timeline` timeline (timeline) · primary; `edit` workspace-edit (editor_workspace_edit) |
12755
- | `sm.script` 2.1.5 | Script Writer | workspace | `brief` brief (project_brief) · primary; `currentScript` script (narration_script) · project context only | `script` script (narration_script) · primary |
12756
- | `sm.shot-plan` 2.2.1 | Shots | workspace | `shotPlan` shot-plan (production_shot_plan) · project context only · primary; `transcript` transcript (timed_transcript) · project context only; `script` script (narration_script) · project context only; `narrationScript` script (narration_script); `narrationTiming` transcript (timed_transcript); `updatedShotPlan` shot-plan (production_shot_plan); `commit` boolean (apply_changes) | `shotPlan` shot-plan (production_shot_plan) · primary; `edit` workspace-edit (workspace_edit) |
12757
- | `sm.shot-plan-assistant` 1.0.0 | Plan Shots | task | `shotPlan` shot-plan · project context only; `request` text · primary | `edit` workspace-edit · primary |
12758
- | `sm.stock-search` 2.0.0 | Stock media search | task | `query` text (stock_query) · primary; `mediaType` text (stock_media_type) | `stockAction` approval (portable_media_action); `asset` asset (licensed_asset) |
12759
13213
  | `sm.style` 2.0.1 | Style reference | workspace | `input` text (visual_style_direction) · primary; `name` text (style_name); `palette` text (color_palette); `lighting` text (lighting_direction); `camera` text (camera_direction) | `output` style (visual_style_reference) · primary |
12760
- | `sm.thumbnail` 2.0.1 | Thumbnail | task | `brief` brief (thumbnail_brief) · primary | `thumbnail` image (generated_thumbnail) · primary |
12761
- | `sm.transcribe` 2.0.1 | Transcription | task | `audio` audio (source_audio) · primary; `language` text (transcription_language) | `transcript` transcript (transcript_text) · primary |
12762
13214
  | `sm.transition` 2.0.2 | Transition | editor-extension | `timeline` timeline (editor_timeline) · project context only · primary; `clipIds` text[] (timeline_clip_ids); `transition` text (timeline_transition_type); `duration` number (timeline_transition_duration); `soundOn` boolean (timeline_transition_sound_enabled); `soundId` text (timeline_transition_sound_id) | `edit` timeline-edit (timeline_edit) · primary |
12763
13215
  | `sm.video-assets` 2.1.4 | Video Assets | workspace | `assets` asset[] (media_collection) · project context only · primary; `versions` metadata[] (media_version_history) · project context only; `incoming` asset[] (media_collection); `kind` text (media_filter); `selectedIds` text (media_selection); `selectedVersions` text (media_version_selection) | `output` asset[] (production_media) · primary |
12764
13216
  | `sm.voiceover` 2.1.6 | Voiceover | workspace | `input` script (narration_script) · primary; `take` audio (narration_audio); `library` asset[] (project_media) · project context only; `timing` transcript (timed_transcript) · project context only | `output` audio (narration_audio) · primary |
12765
13217
  | `sm.when` 1.1.0 | Automation | task | `event` object (workflow_event); `start` object (automation_start); `only_if` text (automation_condition) | `item` object (external_data) · primary; `trigger` object (automation_trigger); `check` object (ai_decision) |
12766
- | `sm.whiteboard-assistant` 1.0.0 | Build Whiteboard | task | `board` board-document · project context only; `request` text · primary | `edit` workspace-edit · primary |
12767
13218
  | `sm.write-summary` 1.0.0 | Write summary | task | `data` text (external_data) · primary | `summary` text (summary_report) · primary |
12768
- | `sm.youtube` 2.0.0 | YouTube publication | task | `video` video (finished_video) · primary; `title` text; `description` text; `tags` text; `visibility` text; `publishAt` text; `madeForKids` boolean; `aiGenerated` boolean | `uploadAction` approval (portable_media_action); `publication` json (youtube_publication) |
12769
- | `stillmade.narration` 1.0.1 | Narration | workspace | `brief` brief (project_brief) · primary; `script` script (narration_script); `tone` text (narration_tone); `rules` text (writing_rules); `voice` text (speech_voice); `speed` number (speech_speed); `take` audio (narration_audio) | `script` script (narration_script); `audio` audio (narration_audio) · primary |
12770
- | `stillmade.occasion-story` 1.0.2 | Occasion Story | task | `assets` asset[] (legacy.asset-list.assets) · primary; `occasion` text (stillmade.occasion_story.occasion); `title` text (stillmade.occasion_story.title); `ending` text (stillmade.occasion_story.ending); `notes` text (stillmade.occasion_story.notes); `selections` text (stillmade.occasion_story.selections); `music` audio[] (stillmade.occasion_story.music); `musicVolume` number (stillmade.occasion_story.musicvolume); `muteVideoAudio` boolean (stillmade.occasion_story.mutevideoaudio); `aspect` text (stillmade.occasion_story.aspect); `disclosure` text (stillmade.occasion_story.disclosure) | `timeline` timeline (legacy.timeline.updatededitor) · primary; `review` json (stillmade.occasion_story.review) |
12771
- | `stillmade.photo-reel-composer` 1.0.2 | Photo Reel Composer | task | `assets` asset[] (legacy.asset-list.assets) · primary; `purpose` text (stillmade.photo_reel.purpose); `title` text (stillmade.photo_reel.title); `captions` text (stillmade.photo_reel.captions); `cta` text (stillmade.photo_reel.cta); `contact` text (stillmade.photo_reel.contact); `demoDisclosure` text (stillmade.photo_reel.demodisclosure); `duration` number (stillmade.photo_reel.duration); `aspect` text (stillmade.photo_reel.aspect); `logo` asset[] (stillmade.photo_reel.logo); `music` asset[] (stillmade.photo_reel.music) | `timeline` timeline (legacy.timeline.updatededitor) · primary; `review` json (stillmade.photo_reel.review); `provenance` json (stillmade.photo_reel.provenance) |
12772
- | `stillmade.product-ad-variants` 1.0.0 | Product Ad Variants | task | `timeline` timeline (legacy.timeline.updatededitor) · primary; `hookA` text (stillmade.product_ad_variants.hooka); `hookB` text (stillmade.product_ad_variants.hookb); `hookC` text (stillmade.product_ad_variants.hookc); `openingA` number (stillmade.product_ad_variants.openinga); `openingB` number (stillmade.product_ad_variants.openingb); `openingC` number (stillmade.product_ad_variants.openingc); `openingDuration` number (stillmade.product_ad_variants.openingduration); `copyApproved` boolean (stillmade.product_ad_variants.copyapproved) | `variantA` timeline (legacy.timeline.updatededitor) · primary; `variantB` timeline (legacy.timeline.updatededitor); `variantC` timeline (legacy.timeline.updatededitor); `review` json (stillmade.product_ad_variants.review) |
12773
- | `stillmade.release-visuals` 1.0.2 | Release Visuals | task | `assets` asset[] (legacy.asset-list.assets) · primary; `purpose` text (stillmade.release_visuals.purpose); `title` text (stillmade.release_visuals.title); `artist` text (stillmade.release_visuals.artist); `cta` text (stillmade.release_visuals.cta); `duration` number (stillmade.release_visuals.duration); `songVolume` number (stillmade.release_visuals.songvolume); `offset` number (stillmade.release_visuals.offset); `lyrics` text (stillmade.release_visuals.lyrics); `aspect` text (stillmade.release_visuals.aspect); `disclosure` text (stillmade.release_visuals.disclosure) | `timeline` timeline (legacy.timeline.updatededitor) · primary; `review` json (stillmade.release_visuals.review) |
12774
- | `stillmade.screen-recorder` 1.0.1 | Screen Recorder | task | `video` video (source_video) · primary; `original` video (source_video) | `video` video (edited_video) · primary; `original` video (source_video) |
12775
- | `stillmade.short-form-finish` 1.0.3 | Short-form Finish | task | `assets` video[] (legacy.asset-list.assets) · primary; `music` audio[] (legacy.asset-list.assets); `audioMode` text (stillmade.short_form.audiomode); `speechVolume` number (stillmade.short_form.speechvolume); `purpose` text (stillmade.short_form.purpose); `cuts` text (stillmade.short_form.cuts); `subtitles` text (stillmade.short_form.subtitles); `title` text (stillmade.short_form.title); `byline` text (stillmade.short_form.byline); `cta` text (stillmade.short_form.cta); `disclosure` text (stillmade.short_form.disclosure); `captionStyle` text (stillmade.short_form.captionstyle); `aspect` text (stillmade.short_form.aspect); `fit` text (stillmade.short_form.fit); `cropX` number (stillmade.short_form.cropx) | `timeline` timeline (legacy.timeline.updatededitor) · primary; `review` json (stillmade.short_form.review) |
13219
+ | `stillmade.generate` 1.0.0 | Generate | task | `prompt` text (generation_prompt) · primary; `mode` text (generation_mode); `model` text (generation_model); `aspectRatio` text (aspect_ratio); `size` text (render_resolution); `quality` text (render_quality); `duration` integer (duration_seconds); `sound` boolean (generate_audio); `count` integer (generation_count); `avoid` text (negative_prompt); `seed` integer (generation_seed); `references` image[] (reference_image); `startFrame` image (first_frame); `endFrame` image (last_frame) | `media` asset[] (generated_media) · primary; `notMade` text (generation_failures) |
12776
13220
 
12777
13221
  ## Custom view rules
12778
13222
 
@@ -12819,17 +13263,17 @@ For hosted text generation use {manifest,capability,tests}, runtime:"capability"
12819
13263
 
12820
13264
  ### Hosted media operations
12821
13265
 
12822
- For audio transcription, use runtime:"capability", operation:"audio.transcribe" and permissions.capabilities:["audio.transcribe"]. Declare one primary audio input with an asset control, one text language input defaulting to "auto", and one primary transcript output. Bind the two inputs through capability.audio and capability.language. Provider, model, payment and credentials belong to the host. Fixtures use transcript word-count bounds, never invented provider text. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.transcribe-audio","action":{"label":"Transcribe audio","running":"Transcribing…"},"version":"1.0.0","name":"Transcribe audio","description":"Turn one persistent audio file into reviewed text with word timings.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"audio":{"type":"audio","primary":true},"language":{"type":"text","default":"auto"}},"outputs":{"transcript":{"type":"transcript","primary":true}},"permissions":{"project":[],"capabilities":["audio.transcribe"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"asset","port":"audio"},{"control":"select","port":"language","options":["auto","en","es","fr","de","it","pt","ja","ko","zh"]}]},"capability":{"schemaVersion":1,"operation":"audio.transcribe","audio":{"$input":"audio"},"language":{"$input":"language"},"output":"transcript"},"tests":[{"name":"timed speech","input":{"audio":{"kind":"audio","assetId":"audio-1","versionId":"version-1","url":"/api/media/example/audio.mp3","mimeType":"audio/mpeg"},"language":"en"},"expectations":{"transcript":{"kind":"transcript","minWords":1,"maxWords":1000}}}]}.
13266
+ For audio transcription, use runtime:"capability", operation:"audio.transcribe" and permissions.capabilities:["audio.transcribe"]. Declare one primary audio input with an asset control, one text language input defaulting to "auto", and one primary transcript output. Bind the two inputs through capability.audio and capability.language. Provider, model, payment and credentials belong to the host. Fixtures use transcript word-count bounds, never invented provider text. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.transcribe-audio","action":{"label":"Transcribe audio","running":"Transcribing…"},"version":"1.0.0","name":"Transcribe audio","description":"Turn one persistent audio file into reviewed text with word timings.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"audio":{"type":"audio","primary":true,"required":true,"semantic":"source_audio","description":"The recording to turn into text."},"language":{"type":"text","default":"auto","required":false,"semantic":"language_code","description":"auto, or a two-letter language code such as en."}},"outputs":{"transcript":{"type":"transcript","primary":true,"semantic":"timed_transcript","description":"The spoken words with their timings."}},"permissions":{"project":[],"capabilities":["audio.transcribe"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"asset","port":"audio"},{"control":"select","port":"language","options":["auto","en","es","fr","de","it","pt","ja","ko","zh"]}]},"capability":{"schemaVersion":1,"operation":"audio.transcribe","audio":{"$input":"audio"},"language":{"$input":"language"},"output":"transcript"},"tests":[{"name":"timed speech","input":{"audio":{"kind":"audio","assetId":"audio-1","versionId":"version-1","url":"/api/media/example/audio.mp3","mimeType":"audio/mpeg"},"language":"en"},"expectations":{"transcript":{"kind":"transcript","minWords":1,"maxWords":1000}}}]}.
12823
13267
  For narration or speech synthesis, use runtime:"capability", operation:"audio.speech" and permissions.capabilities:["audio.speech"]. One required primary text or script input feeds capability.text via {$input:"portName"}; a script value contains schemaVersion:1 and text. One primary audio output is named by capability.output. Text must be at most 3800 characters. The host owns the voice, model, provider, billing and BYOK. Do not put those choices in package code. Tests use audio expectations (WAV, duration and byte bounds), never invented exact audio. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.narrate-script","action":{"label":"Generate voiceover","running":"Generating voiceover…"},"version":"1.0.0","name":"Narrate a script","description":"Turn a short script into a reviewed speech recording using a host-selected voice.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"script":{"type":"script","primary":true,"semantic":"narration_script","required":true,"description":"Narration script to render as speech."}},"outputs":{"audio":{"type":"audio","primary":true,"semantic":"speech_audio","description":"Speech audio generated from the narration script."}},"permissions":{"project":[],"capabilities":["audio.speech"],"network":[],"filesystem":[],"secrets":[]},"ui":[]},"capability":{"schemaVersion":1,"operation":"audio.speech","text":{"$input":"script"},"output":"audio"},"tests":[{"name":"Read a short welcome","input":{"script":{"schemaVersion":1,"text":"Welcome to the quiet forest."}},"expectations":{"audio":{"kind":"audio","format":"wav","minDuration":0.1,"maxDuration":30,"minBytes":46,"maxBytes":1500000}}}]}.
12824
13268
  For image generation, use runtime:"capability", operation:"image.generate" and permissions.capabilities:["image.generate"]. One required primary text input feeds capability.prompt via {$input:"portName"}; one primary image output is named by capability.output. Prompt limit 32000 characters. The supported output is PNG 1024x1024, at most 12000000 bytes. Tests use image expectations, never fabricated generated URLs or pixels. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.generate-image","action":{"label":"Generate image","running":"Generating image…"},"version":"1.0.0","name":"Generate an image","description":"Generate a square PNG from a short visual prompt using an approved host request.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"prompt":{"type":"text","primary":true,"semantic":"image_prompt","required":true,"description":"Visual description of the image to generate."}},"outputs":{"image":{"type":"image","primary":true,"semantic":"generated_image","description":"The generated square PNG image."}},"permissions":{"project":[],"capabilities":["image.generate"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"text","port":"prompt"}]},"capability":{"schemaVersion":1,"operation":"image.generate","prompt":{"$input":"prompt"},"output":"image"},"tests":[{"name":"A simple forest illustration","input":{"prompt":"A simple illustration of a quiet forest with clear dark outlines and soft green colors."},"expectations":{"image":{"kind":"image","format":"png","width":1024,"height":1024,"minBytes":67,"maxBytes":12000000}}}]}.
12825
- For image or video analysis (what a photo shows, which photos show different angles, the words on a menu or sign), use runtime:"capability", operation:"media.analyze" and permissions.capabilities:["media.analyze"]: StillMade credits pay for it, while image.describe runs only on the person's own OpenRouter key, so never use it. Declare one primary image input, one image[] input of one to four images, or one video input (with optional start and end number inputs, bound through capability.range:{start:{"$input":"start"},end:{"$input":"end"}}), and one primary text or json output. capability:{schemaVersion:1,operation:"media.analyze",media:{"$input":"photo"},question:"what to look for, at most 2,000 characters",output:"notes"}; a text output gets each picture's summary, a json output {schemaVersion:1,items}. For a quick rating of photos (sorting or picking), add detail:"check": images only, one short answer each, one credit per call of up to four photos instead of four credits per picture. The host loads only owned media and controls provider, model, payment and approval. Tests give saved references and expect json that includes summary, or text length bounds, never invented live answers. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.check-clip","action":{"label":"Check clip","running":"Checking clip…"},"version":"1.0.0","name":"Check a clip","description":"Answer a production question about a saved video clip, with timestamps.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"clip":{"type":"video","primary":true,"semantic":"source_video","required":true,"description":"Saved video to check."},"start":{"type":"number","required":false,"min":0,"semantic":"range_start","description":"Optional start of the range to look at closely, in seconds."},"end":{"type":"number","required":false,"min":0,"semantic":"range_end","description":"Optional end of the range, in seconds (at most 12 seconds after the start)."}},"outputs":{"report":{"type":"json","primary":true,"semantic":"media_findings","description":"Summary, timeline, on-screen text and issues found."}},"permissions":{"project":[],"capabilities":["media.analyze"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"asset","port":"clip"},{"control":"number","port":"start"},{"control":"number","port":"end"}]},"capability":{"schemaVersion":1,"operation":"media.analyze","media":{"$input":"clip"},"question":"Is the product logo clearly visible, and when? List any on-screen text and visible defects.","range":{"start":{"$input":"start"},"end":{"$input":"end"}},"output":"report"},"tests":[{"name":"A saved clip","input":{"clip":{"kind":"video","assetId":"clip-1","versionId":"v1","url":"/api/media/owner/clip.mp4"}},"expectations":{"report":{"kind":"json","includes":["summary"]}}}]}.
12826
- For AI-directed editing, use runtime:"capability", operation:"timeline.propose" and permissions.capabilities:["timeline.propose"]. Declare one current timeline input, up to three additional typed context inputs, and one timeline-edit output. Bind every input exactly once through capability.context; declare context.timeline.read and timeline.propose. StillMade controls the provider, strict JSON validation, stale-before checks, preview and Apply. Tests declare timeline-edit command bounds, never fabricated provider results. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.timeline-polish","action":{"label":"Suggest edit","running":"Suggesting edit…"},"version":"1.0.0","name":"Timeline polish","description":"Turn a timeline and editing direction into a bounded proposal that remains reviewable before Apply.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"timeline":{"type":"timeline","context":"timeline","primary":true},"direction":{"type":"text","default":"Polish this cut conservatively."}},"outputs":{"edit":{"type":"timeline-edit","primary":true}},"permissions":{"project":["context.timeline.read","timeline.propose"],"capabilities":["timeline.propose"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"text","port":"direction"}]},"capability":{"schemaVersion":1,"operation":"timeline.propose","context":{"timeline":{"$input":"timeline"},"direction":{"$input":"direction"}},"instruction":"Use only supported timeline-edit commands, exact before snapshots, and the current activeTimelineId. Preserve media and locked items. Return strict JSON only.","maxTokens":2000,"output":"edit"},"tests":[{"name":"one restrained edit","input":{"timeline":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[{"id":"video"}],"clips":[{"id":"clip-1","trackId":"video","start":0,"duration":4}],"audioItems":[],"captions":[]},"direction":"Add a soft transition."},"expectations":{"edit":{"kind":"timeline-edit","minCommands":1,"maxCommands":10}}}]}.
12827
- For video generation use runtime:"capability", operation:"video.generate", permissions.capabilities:["video.generate"], one primary text input and one primary video output. capability.prompt references the input and capability.output names the output. Prompt limit 32000. Tests declare MP4 duration, dimensions and byte bounds; never fabricate video URLs. The enabled host selects its supported model, duration, resolution, payment and approved quote; packages cannot choose provider settings or dispatch jobs. Shipped example: {"manifest":{"schemaVersion":1,"id":"example.generate-video","action":{"label":"Generate video","running":"Generating video…"},"name":"Generate video","version":"1.0.0","sdkVersion":"0.1.0","description":"Turn a production prompt into a short video using an approved host provider.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"prompt":{"type":"text","primary":true,"required":true}},"outputs":{"video":{"type":"video","primary":true}},"permissions":{"project":[],"capabilities":["video.generate"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"port":"prompt","control":"text"}]},"capability":{"schemaVersion":1,"operation":"video.generate","prompt":{"$input":"prompt"},"output":"video"},"tests":[{"name":"Produces a bounded MP4 sample","input":{"prompt":"A paper boat floating across a calm blue pond. Gentle camera movement."},"expectations":{"video":{"kind":"video","format":"mp4","minDuration":1,"maxDuration":60,"minBytes":32,"maxBytes":67108864,"minWidth":1,"maxWidth":3840,"minHeight":1,"maxHeight":3840}}}]}.
13269
+ For image or video analysis (what a photo shows, which photos show different angles, the words on a menu or sign), use runtime:"capability", operation:"media.analyze" and permissions.capabilities:["media.analyze"]: StillMade credits pay for it, while image.describe runs only on the person's own OpenRouter key, so never use it. Declare one primary image input, one image[] input of one to four images, or one video input (with optional start and end number inputs, bound through capability.range:{start:{"$input":"start"},end:{"$input":"end"}}), and one primary text or json output. capability:{schemaVersion:1,operation:"media.analyze",media:{"$input":"photo"},question:"what to look for, at most 2,000 characters",output:"notes"}; a text output gets each picture's summary, a json output {schemaVersion:1,items}. For a quick rating of photos (sorting or picking), add detail:"check": images only, one short answer each, one credit per call of up to four photos instead of four credits per picture. The host loads only owned media and controls provider, model, payment and approval. Tests give saved references and expect json that includes summary, or text length bounds, never invented live answers. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.check-clip","action":{"label":"Check clip","running":"Checking clip…"},"version":"1.0.0","name":"Check a clip","description":"Answer a production question about a saved video clip, with timestamps.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"clip":{"type":"video","primary":true,"semantic":"source_video","required":true,"description":"Saved video to check."},"start":{"type":"number","required":false,"min":0,"semantic":"range_start","description":"Optional start of the range to look at closely, in seconds."},"end":{"type":"number","required":false,"min":0,"semantic":"range_end","description":"Optional end of the range, in seconds (at most 12 seconds after the start)."}},"outputs":{"report":{"type":"json","primary":true,"semantic":"media_findings","description":"Summary, timeline, on-screen text and issues found.","schema":{"items":"json[]"}}},"permissions":{"project":[],"capabilities":["media.analyze"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"asset","port":"clip"},{"control":"number","port":"start"},{"control":"number","port":"end"}]},"capability":{"schemaVersion":1,"operation":"media.analyze","media":{"$input":"clip"},"question":"Is the product logo clearly visible, and when? List any on-screen text and visible defects.","range":{"start":{"$input":"start"},"end":{"$input":"end"}},"output":"report"},"tests":[{"name":"A saved clip","input":{"clip":{"kind":"video","assetId":"clip-1","versionId":"v1","url":"/api/media/owner/clip.mp4"}},"expectations":{"report":{"kind":"json","includes":["summary"]}}}]}.
13270
+ For AI-directed editing, use runtime:"capability", operation:"timeline.propose" and permissions.capabilities:["timeline.propose"]. Declare one current timeline input, up to three additional typed context inputs, and one timeline-edit output. Bind every input exactly once through capability.context; declare context.timeline.read and timeline.propose. StillMade controls the provider, strict JSON validation, stale-before checks, preview and Apply. Tests declare timeline-edit command bounds, never fabricated provider results. Shipped example: {"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"example.timeline-polish","action":{"label":"Suggest edit","running":"Suggesting edit…"},"version":"1.0.0","name":"Timeline polish","description":"Turn a timeline and editing direction into a bounded proposal that remains reviewable before Apply.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"timeline":{"type":"timeline","context":"timeline","primary":true,"required":true,"semantic":"edit_timeline","description":"The cut to improve."},"direction":{"type":"text","default":"Polish this cut conservatively.","required":false,"semantic":"edit_direction","description":"How to change the cut, in a sentence."}},"outputs":{"edit":{"type":"timeline-edit","primary":true,"semantic":"timeline_edit","description":"The proposed changes to review before they apply."}},"permissions":{"project":["context.timeline.read","timeline.propose"],"capabilities":["timeline.propose"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"control":"text","port":"direction"}]},"capability":{"schemaVersion":1,"operation":"timeline.propose","context":{"timeline":{"$input":"timeline"},"direction":{"$input":"direction"}},"instruction":"Use only supported timeline-edit commands, exact before snapshots, and the current activeTimelineId. Preserve media and locked items. Return strict JSON only.","maxTokens":2000,"output":"edit"},"tests":[{"name":"one restrained edit","input":{"timeline":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[{"id":"video"}],"clips":[{"id":"clip-1","trackId":"video","start":0,"duration":4}],"audioItems":[],"captions":[]},"direction":"Add a soft transition."},"expectations":{"edit":{"kind":"timeline-edit","minCommands":1,"maxCommands":10}}}]}.
13271
+ For video generation use runtime:"capability", operation:"video.generate", permissions.capabilities:["video.generate"], one primary text input and one primary video output. capability.prompt references the input and capability.output names the output. Prompt limit 32000. Tests declare MP4 duration, dimensions and byte bounds; never fabricate video URLs. The enabled host selects its supported model, duration, resolution, payment and approved quote; packages cannot choose provider settings or dispatch jobs. Shipped example: {"manifest":{"schemaVersion":1,"id":"example.generate-video","action":{"label":"Generate video","running":"Generating video…"},"name":"Generate video","version":"1.0.0","sdkVersion":"0.1.0","description":"Turn a production prompt into a short video using an approved host provider.","kind":"task","runtime":"capability","entry":"src/capability.json","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"prompt":{"type":"text","primary":true,"required":true,"semantic":"video_prompt","description":"What the clip shows: subject, action, setting and camera, in a sentence or two."}},"outputs":{"video":{"type":"video","primary":true,"semantic":"generated_video","description":"The generated clip."}},"permissions":{"project":[],"capabilities":["video.generate"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"port":"prompt","control":"text"}]},"capability":{"schemaVersion":1,"operation":"video.generate","prompt":{"$input":"prompt"},"output":"video"},"tests":[{"name":"Produces a bounded MP4 sample","input":{"prompt":"A paper boat floating across a calm blue pond. Gentle camera movement."},"expectations":{"video":{"kind":"video","format":"mp4","minDuration":1,"maxDuration":60,"minBytes":32,"maxBytes":67108864,"minWidth":1,"maxWidth":3840,"minHeight":1,"maxHeight":3840}}}]}.
12828
13272
  A capability package performs exactly one supported operation. A job that needs several (transcribe a recording, then find what is said in it; write words, then voice them), or code between two operations, is one module Block that calls each with stillmade.hosted.run (Module Blocks); never split one job into part Blocks, and never invent a combined capability runtime. Hosted image, speech and video source can be statically validated while authoring, but real fixture/sample generation requires the user's subsequent model/payment selection and quote confirmation. Do not claim those calls ran. Deterministic image transformations should remain sandboxed recipe or JavaScript code when supported; do not generate a new image as a substitute for processing an existing one.
12829
13273
 
12830
13274
  ### Module Blocks
12831
13275
 
12832
- A job that needs several hosted operations, or code around one (reading a transcript's words and times, shaping a list), or media work on the person's device even with no hosted operation (rendering clips and photos into one video, joining pictures into one), is a module Block: {manifest,moduleSource,tests}, runtime:"module", moduleSource:{"main.js":"<the code>"}. StillMade names main.js by its digest and adds module. StillMade's in-app builder takes one file: give no other files, and main.js is plain JavaScript that imports nothing and uses no browser APIs (no fetch, TextEncoder, timers or WebCodecs). That single-file rule is the in-app builder's; a module Block built with the SDK may use timers, WebCodecs, bundled npm packages and .wasm files as MODULES.md describes. main.js: export default async function run(input, stillmade) returns the declared outputs. It calls each hosted operation with await stillmade.hosted.run(operation, request), listed in manifest.permissions.capabilities: text.generate, image.generate, video.generate, audio.speech, audio.music, audio.sfx, audio.transcribe, image.describe, media.analyze, web.fetch, web.research. Requests, and what each call returns (a test's mocks record exactly that value): text.generate {prompt, system, maxTokens} returns the text as a string, and with schema (a JSON schema object) returns that object, so ask for JSON with schema; one text.generate prompt holds at most 32,000 characters, so a longer file or transcript is read in parts (one call per part of about 25,000 characters, then one call that joins what they found), never cut off, and when the result is the whole text again (a cleaned transcript, a translation) each part's answer is kept and joined in order in main.js, since one answer holds at most 4,096 tokens; audio.transcribe {audio: the input's saved audio or video reference (a video's own sound is transcribed, so never extract it first), language:"auto"} returns {schemaVersion:1,text,words:[{text,start,end}]}; image.generate {prompt, references?: one to eight saved images the new picture keeps, such as the product, person or place in the person's photos} and audio.speech {text} return a saved image or audio reference (an audio reference always carries its measured duration); web.fetch {url, instruction} returns a string; media.analyze {media: one to four saved images or one saved video, question, range?: {start, end} in seconds of that video, at most 12 apart, to look closely at one part of it} returns {schemaVersion:1,items:[{kind,summary,observations,detectedText}]}, one item per image, and is how a Block looks at pictures (picking the photos that show different angles, reading a menu off a photo): StillMade credits pay for it; image.describe {image, instruction} returns a string but runs only on the person's own OpenRouter key, so never use it; web.research {topic} returns {summary,sources}. A file the person gives the Block (a text, CSV, Markdown or JSON file on a file input) is read with await stillmade.files.read(input.<port>), which returns its bytes; turn them into text in main.js as the worked file input below does, and give the test's input file an assetId whose text mocks.files records as {files:{"<assetId>":{text:"..."}}}. A Block that reads a file the person gives it is a module Block, never a text.generate capability package. A PDF (lesson slides, a lease, a contract) is read with await stillmade.media.transform({operation:"document.text",source:input.<port>}), which returns {text, pages}, all the words and pages, a list holding each page's words as one string, on the person's device and without credits (a text file reads the same way); a scanned PDF has no words to read. A call that fails throws: let it fail the run rather than catching it and returning an empty result. Media work on the person's device (cut, join or resize video, decode or extract audio, thumbnails, joining images) is await stillmade.media.transform(request). Joining saved images into one picture (two photos side by side with Before and After labels, a grid or collage, a contact sheet, a storyboard sheet, a poster with a title) is {operation:"image.compose",width,height,background:"#ffffff",layers:[{source,x,y,width,height,fit:"cover" or "contain"}],text:[{text,x,y,size,color,background,align:"left" or "center" or "right",width}]}: a canvas up to 4096 pixels a side, positions in pixels worked out in main.js, layers drawn in order and text on top (y is the top of the line, background puts a box behind it, colours are #rrggbb or, see-through, #rrggbbaa such as #00000099 for a caption box, width wraps the words onto lines no wider than it: give a caption under a frame the frame's width so it never runs into the next one); it returns the saved image, and main.js never decodes pixels itself. Exporting is a transform too: {operation:"timeline.render",kind:"video" or "audio",aspect:"9:16",clips:[{source,start,duration,from,transition:{type:"Fade"},look:"Filmic",motion:"zoom-in"}],audio:[{source,start,duration}],captions:[{text,start,end,words:[{text,start,end}],highlight:"#ffd400",size:80}]} (motion, a slow camera move over a photo, is zoom-in, zoom-out, pan-left, pan-right, pan-up or pan-down; size is the caption height in pixels, 80 for a vertical video) renders the Block's own timeline with the Editor's transitions, looks and word-by-word captions under the person's export plan (start is a clip's place on that timeline, so each clip starts where the one before it ends, less the transition on the clip before it, which blends that clip into the next, and from is where it begins in its own file; overlapping clips are refused; a clip of another shape than the frame is cropped to fill it around its middle, or around focus:{x,y} on the clip, the point to keep in view as fractions 0 to 1 across the picture, such as where the speaker stands (focus.scale, 1 to 2.5 and instead of a motion, holds the clip that much closer on that point), or is fitted whole with fit:"contain"), so never ask for a separate export, captions or color Block; stillmade.log and stillmade.progress(fraction, label) report. One press of the Step's button approves each hosted operation once at the price shown beside it, and media.analyze for every picture or video the Step is given, across as many calls as that takes; any further call asks the person first. A module Block runs in the person's browser while StillMade is open, never on a schedule or event. Tests are {name,input,expected,mocks} and compare outputs, so every test is a case that runs to its outputs; an input the Block refuses (it throws a plain Error with a message for the person, such as a clip with no duration) is never a test. mocks records what each call answers, as {hosted:{"audio.transcribe":{schemaVersion:1,text:"...",words:[...]},"text.generate":{...}},transforms:{...}} (a list answers successive calls in order), and expected is the exact output main.js returns for those answers: exact values of each output's type, never capability bounds such as minDuration or maxBytes. In a module test, mock each media call with its complete saved reference (kind, assetId, versionId, url, mimeType and its measured fields: duration, bytes, sampleRate, channels for audio; width, height, bytes for images), its url an example file such as /api/media/example/clip.mp4 so the preview can play it, and repeat those references unchanged in expected; names or labels go in their own output. Worked outputs, each a passing test. A document output is the document object on a document port (not text): {"outputs":{"sheet":{"type":"document"}},"main.js":"export default async function run(input, stillmade) { const text = await stillmade.hosted.run('text.generate', {prompt: 'A Markdown care sheet for ' + input.plant, maxTokens: 800}); return {sheet: {schemaVersion: 1, format: 'markdown', text, title: input.plant + ' care'}}; }","test":{"name":"Fern","input":{"plant":"Fern"},"mocks":{"hosted":{"text.generate":"## Light\nBright, indirect light."}},"expected":{"sheet":{"schemaVersion":1,"format":"markdown","text":"## Light\nBright, indirect light.","title":"Fern care"}}}}. A file output is the reference await stillmade.files.write(data, {mediaType, name}) returns, handed on unchanged; mediaType is one of application/pdf, text/plain, text/csv, text/markdown, application/json, application/zip: {"outputs":{"report":{"type":"file"}},"main.js":"export default async function run(input, stillmade) { const csv = 'Room\\n' + input.rooms.join('\\n'); return {report: await stillmade.files.write(csv, {mediaType: 'text/csv', name: 'rooms.csv'})}; }","test":{"name":"Two rooms","input":{"rooms":["Kitchen","Hall"]},"expected":{"report":{"kind":"file","mimeType":"text/csv","name":"rooms.csv"}}}} (in a test, files.write answers with fields the host makes: assetId local-written-file-N, versionId, url /api/media/stillmade-mock/written-file-N.csv with the extension of its mediaType, and bytes; expected gives that reference as {kind:"file", mimeType, name} and it matches when those three match; passed on in an asset output as kind "asset", it is given as {kind:"asset", mimeType, name} and matches the same way). An audio[] output is the list of references audio.speech returned, unchanged; its mock is a list answering successive calls, and the labels go in a separate text[] output: {"outputs":{"tracks":{"type":"audio[]"},"names":{"type":"text[]"}},"main.js":"export default async function run(input, stillmade) { const tracks = []; for (const room of input.rooms) tracks.push(await stillmade.hosted.run('audio.speech', {text: 'Welcome to the ' + room + '.'})); return {tracks, names: input.rooms}; }","test":{"name":"Two rooms","input":{"rooms":["Kitchen","Hall"]},"mocks":{"hosted":{"audio.speech":[{"kind":"audio","assetId":"speech-1","versionId":"v1","url":"/api/media/example/kitchen.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1},{"kind":"audio","assetId":"speech-2","versionId":"v1","url":"/api/media/example/hall.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1}]}},"expected":{"tracks":[{"kind":"audio","assetId":"speech-1","versionId":"v1","url":"/api/media/example/kitchen.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1},{"kind":"audio","assetId":"speech-2","versionId":"v1","url":"/api/media/example/hall.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1}],"names":["Kitchen","Hall"]}}}. A file input read as text: {"outputs":{"guide":{"type":"text"}},"main.js":"function textOf(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.length; i += 8192) binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192)); try { return decodeURIComponent(escape(binary)); } catch (error) { return binary; } }\nexport default async function run(input, stillmade) { const notes = textOf(await stillmade.files.read(input.notes)); const guide = await stillmade.hosted.run('text.generate', {prompt: 'Rewrite these notes as numbered steps, keeping every number as written:\\n' + notes, maxTokens: 1200}); return {guide}; }","test":{"name":"Two notes","input":{"notes":{"kind":"file","assetId":"notes-1","versionId":"v1","url":"/api/media/example/notes.txt","mimeType":"text/plain","name":"notes.txt","bytes":31}},"mocks":{"files":{"notes-1":{"text":"pull the probe\nwipe the tines"}},"hosted":{"text.generate":"1. Pull the probe.\n2. Wipe the tines."}},"expected":{"guide":"1. Pull the probe.\n2. Wipe the tines."}}}. Photos joined into one image, its transform mocked with the saved image it returns: {"outputs":{"sheet":{"type":"image"}},"main.js":"export default async function run(input, stillmade) { const photos = input.photos; if (!photos.length) throw new Error('Add at least one photo.'); const columns = Math.ceil(Math.sqrt(photos.length)), rows = Math.ceil(photos.length / columns), cell = 400, gap = 20, top = 160; const width = columns * cell + (columns + 1) * gap, height = top + rows * cell + (rows + 1) * gap; const layers = photos.map((source, i) => ({source, x: gap + (i % columns) * (cell + gap), y: top + gap + Math.floor(i / columns) * (cell + gap), width: cell, height: cell, fit: 'cover'})); const sheet = await stillmade.media.transform({operation: 'image.compose', width, height, background: '#ffffff', layers, text: [{text: input.title, x: width / 2, y: 50, size: 64, align: 'center', color: '#111111'}]}); return {sheet}; }","test":{"name":"Four photos","input":{"photos":[{"kind":"image","assetId":"photo-1","versionId":"v1","url":"/api/media/example/photo-1.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-2","versionId":"v1","url":"/api/media/example/photo-2.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-3","versionId":"v1","url":"/api/media/example/photo-3.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-4","versionId":"v1","url":"/api/media/example/photo-4.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000}],"title":"Saturday games"},"mocks":{"transforms":{"image.compose":{"kind":"image","assetId":"sheet-1","versionId":"v1","url":"/api/media/example/sheet.png","mimeType":"image/png","width":1300,"height":1440,"bytes":1820000}}},"expected":{"sheet":{"kind":"image","assetId":"sheet-1","versionId":"v1","url":"/api/media/example/sheet.png","mimeType":"image/png","width":1300,"height":1440,"bytes":1820000}}}}. StillMade runs every test before anyone sees the Block, without spending credits. With the SDK instead, the same code is module/main.js in a Block folder and the same tests are tests/fixtures.json; stillmade-block pack writes the file to import (MODULES.md).
13276
+ A job that needs several hosted operations, or code around one (reading a transcript's words and times, shaping a list), or media work on the person's device even with no hosted operation (rendering clips and photos into one video, joining pictures into one), is a module Block: {manifest,moduleSource,tests}, runtime:"module", moduleSource:{"main.js":"<the code>"}. StillMade names main.js by its digest and adds module. StillMade's in-app builder takes one file: give no other files, and main.js is plain JavaScript that imports nothing and uses no browser APIs (no fetch, TextEncoder, timers or WebCodecs). That single-file rule is the in-app builder's; a module Block built with the SDK may use timers, WebCodecs, bundled npm packages and .wasm files as MODULES.md describes. main.js: export default async function run(input, stillmade) returns the declared outputs. It calls each hosted operation with await stillmade.hosted.run(operation, request), listed in manifest.permissions.capabilities: text.generate, image.generate, video.generate, audio.speech, audio.music, audio.sfx, audio.transcribe, image.describe, media.analyze, web.fetch, web.research. Requests, and what each call returns (a test's mocks record exactly that value): text.generate {prompt, system, maxTokens} returns the text as a string, and with schema (a JSON schema object) returns that object, so ask for JSON with schema; one text.generate prompt holds at most 32,000 characters, so a longer file or transcript is read in parts (one call per part of about 25,000 characters, then one call that joins what they found), never cut off, and when the result is the whole text again (a cleaned transcript, a translation) each part's answer is kept and joined in order in main.js, since one answer holds at most 4,096 tokens; audio.transcribe {audio: the input's saved audio or video reference (a video's own sound is transcribed, so never extract it first), language:"auto"} returns {schemaVersion:1,text,words:[{text,start,end}]}; image.generate {prompt, references?: one to eight saved images the new picture keeps, such as the product, person or place in the person's photos} and audio.speech {text} return a saved image or audio reference (an audio reference always carries its measured duration); web.fetch {url, instruction} returns a string; media.analyze {media: one to four saved images or one saved video, question, range?: {start, end} in seconds of that video, at most 12 apart, to look closely at one part of it} returns {schemaVersion:1,items:[{kind,summary,observations,detectedText}]}, one item per image, and is how a Block looks at pictures (picking the photos that show different angles, reading a menu off a photo): StillMade credits pay for it; image.describe {image, instruction} returns a string but runs only on the person's own OpenRouter key, so never use it; web.research {topic} returns {summary,sources}. A file the person gives the Block (a text, CSV, Markdown or JSON file on a file input) is read with await stillmade.files.read(input.<port>), which returns its bytes; turn them into text in main.js as the worked file input below does, and give the test's input file an assetId whose text mocks.files records as {files:{"<assetId>":{text:"..."}}}. A Block that reads a file the person gives it is a module Block, never a text.generate capability package. A PDF (lesson slides, a lease, a contract) is read with await stillmade.media.transform({operation:"document.text",source:input.<port>}), which returns {text, pages}, all the words and pages, a list holding each page's words as one string, on the person's device and without credits (a text file reads the same way); a scanned PDF has no words to read. A call that fails throws. When the Block cannot make its result without that call, let it fail the run (StillMade shows its message), never catching it to return an empty result; catch it only around an optional part that has a fallback (a missing room photo, a skipped sample), and name what was left out in the result, as actions.run names its failures. Media work on the person's device (cut, join or resize video, decode or extract audio, thumbnails, joining images) is await stillmade.media.transform(request). Joining saved images into one picture (two photos side by side with Before and After labels, a grid or collage, a contact sheet, a storyboard sheet, a poster with a title) is {operation:"image.compose",width,height,background:"#ffffff",layers:[{source,x,y,width,height,fit:"cover" or "contain"}],text:[{text,x,y,size,color,background,align:"left" or "center" or "right",width}]}: a canvas up to 4096 pixels a side, positions in pixels worked out in main.js, layers drawn in order and text on top (y is the top of the line, background puts a box behind it, colours are #rrggbb or, see-through, #rrggbbaa such as #00000099 for a caption box, width wraps the words onto lines no wider than it: give a caption under a frame the frame's width so it never runs into the next one); it returns the saved image, and main.js never decodes pixels itself. Exporting is a transform too: {operation:"timeline.render",kind:"video" or "audio",aspect:"9:16",clips:[{source,start,duration,from,transition:{type:"Fade"},look:"Filmic",motion:"zoom-in"}],audio:[{source,start,duration}],captions:[{text,start,end,words:[{text,start,end}],highlight:"#ffd400",size:80}]} (motion, a slow camera move over a photo, is zoom-in, zoom-out, pan-left, pan-right, pan-up or pan-down; size is the caption height in pixels, 80 for a vertical video) renders the Block's own timeline with the Editor's transitions, looks and word-by-word captions under the person's export plan (start is a clip's place on that timeline, so each clip starts where the one before it ends, less the transition on the clip before it, which blends that clip into the next, and from is where it begins in its own file; overlapping clips are refused; a clip of another shape than the frame is cropped to fill it around its middle, or around focus:{x,y} on the clip, the point to keep in view as fractions 0 to 1 across the picture, such as where the speaker stands (focus.scale, 1 to 2.5 and instead of a motion, holds the clip that much closer on that point), or is fitted whole with fit:"contain"), so never ask for a separate export, captions or color Block; stillmade.log and stillmade.progress(fraction, label) report. One press of the Step's button approves each hosted operation once at the price shown beside it, and media.analyze for every picture or video the Step is given, across as many calls as that takes; any further call asks the person first. A module Block runs in the person's browser while StillMade is open, never on a schedule or event. Tests are {name,input,expected,mocks} and compare outputs, so every test is a case that runs to its outputs; an input the Block refuses (it throws a plain Error with a message for the person, such as a clip with no duration) is never a test. mocks records what each call answers, as {hosted:{"audio.transcribe":{schemaVersion:1,text:"...",words:[...]},"text.generate":{...}},transforms:{...}} (a list answers successive calls in order), and expected is the exact output main.js returns for those answers: exact values of each output's type, never capability bounds such as minDuration or maxBytes. In a module test, mock each media call with its complete saved reference (kind, assetId, versionId, url, mimeType and its measured fields: duration, bytes, sampleRate, channels for audio; width, height, bytes for images), its url an example file such as /api/media/example/clip.mp4 so the preview can play it, and repeat those references unchanged in expected; names or labels go in their own output. Worked outputs, each a passing test. A document output is the document object on a document port (not text): {"outputs":{"sheet":{"type":"document"}},"main.js":"export default async function run(input, stillmade) { const text = await stillmade.hosted.run('text.generate', {prompt: 'A Markdown care sheet for ' + input.plant, maxTokens: 800}); return {sheet: {schemaVersion: 1, format: 'markdown', text, title: input.plant + ' care'}}; }","test":{"name":"Fern","input":{"plant":"Fern"},"mocks":{"hosted":{"text.generate":"## Light\nBright, indirect light."}},"expected":{"sheet":{"schemaVersion":1,"format":"markdown","text":"## Light\nBright, indirect light.","title":"Fern care"}}}}. A file output is the reference await stillmade.files.write(data, {mediaType, name}) returns, handed on unchanged; mediaType is one of application/pdf, text/plain, text/csv, text/markdown, application/json, application/zip: {"outputs":{"report":{"type":"file"}},"main.js":"export default async function run(input, stillmade) { const csv = 'Room\\n' + input.rooms.join('\\n'); return {report: await stillmade.files.write(csv, {mediaType: 'text/csv', name: 'rooms.csv'})}; }","test":{"name":"Two rooms","input":{"rooms":["Kitchen","Hall"]},"expected":{"report":{"kind":"file","mimeType":"text/csv","name":"rooms.csv"}}}} (in a test, files.write answers with fields the host makes: assetId local-written-file-N, versionId, url /api/media/stillmade-mock/written-file-N.csv with the extension of its mediaType, and bytes; expected gives that reference as {kind:"file", mimeType, name} and it matches when those three match; passed on in an asset output as kind "asset", it is given as {kind:"asset", mimeType, name} and matches the same way). An audio[] output is the list of references audio.speech returned, unchanged; its mock is a list answering successive calls, and the labels go in a separate text[] output: {"outputs":{"tracks":{"type":"audio[]"},"names":{"type":"text[]"}},"main.js":"export default async function run(input, stillmade) { const tracks = []; for (const room of input.rooms) tracks.push(await stillmade.hosted.run('audio.speech', {text: 'Welcome to the ' + room + '.'})); return {tracks, names: input.rooms}; }","test":{"name":"Two rooms","input":{"rooms":["Kitchen","Hall"]},"mocks":{"hosted":{"audio.speech":[{"kind":"audio","assetId":"speech-1","versionId":"v1","url":"/api/media/example/kitchen.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1},{"kind":"audio","assetId":"speech-2","versionId":"v1","url":"/api/media/example/hall.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1}]}},"expected":{"tracks":[{"kind":"audio","assetId":"speech-1","versionId":"v1","url":"/api/media/example/kitchen.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1},{"kind":"audio","assetId":"speech-2","versionId":"v1","url":"/api/media/example/hall.wav","mimeType":"audio/wav","duration":2,"bytes":96044,"sampleRate":24000,"channels":1}],"names":["Kitchen","Hall"]}}}. A file input read as text: {"outputs":{"guide":{"type":"text"}},"main.js":"function textOf(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.length; i += 8192) binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192)); try { return decodeURIComponent(escape(binary)); } catch (error) { return binary; } }\nexport default async function run(input, stillmade) { const notes = textOf(await stillmade.files.read(input.notes)); const guide = await stillmade.hosted.run('text.generate', {prompt: 'Rewrite these notes as numbered steps, keeping every number as written:\\n' + notes, maxTokens: 1200}); return {guide}; }","test":{"name":"Two notes","input":{"notes":{"kind":"file","assetId":"notes-1","versionId":"v1","url":"/api/media/example/notes.txt","mimeType":"text/plain","name":"notes.txt","bytes":31}},"mocks":{"files":{"notes-1":{"text":"pull the probe\nwipe the tines"}},"hosted":{"text.generate":"1. Pull the probe.\n2. Wipe the tines."}},"expected":{"guide":"1. Pull the probe.\n2. Wipe the tines."}}}. Photos joined into one image, its transform mocked with the saved image it returns: {"outputs":{"sheet":{"type":"image"}},"main.js":"export default async function run(input, stillmade) { const photos = input.photos; if (!photos.length) throw new Error('Add at least one photo.'); const columns = Math.ceil(Math.sqrt(photos.length)), rows = Math.ceil(photos.length / columns), cell = 400, gap = 20, top = 160; const width = columns * cell + (columns + 1) * gap, height = top + rows * cell + (rows + 1) * gap; const layers = photos.map((source, i) => ({source, x: gap + (i % columns) * (cell + gap), y: top + gap + Math.floor(i / columns) * (cell + gap), width: cell, height: cell, fit: 'cover'})); const sheet = await stillmade.media.transform({operation: 'image.compose', width, height, background: '#ffffff', layers, text: [{text: input.title, x: width / 2, y: 50, size: 64, align: 'center', color: '#111111'}]}); return {sheet}; }","test":{"name":"Four photos","input":{"photos":[{"kind":"image","assetId":"photo-1","versionId":"v1","url":"/api/media/example/photo-1.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-2","versionId":"v1","url":"/api/media/example/photo-2.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-3","versionId":"v1","url":"/api/media/example/photo-3.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000},{"kind":"image","assetId":"photo-4","versionId":"v1","url":"/api/media/example/photo-4.jpg","mimeType":"image/jpeg","width":1600,"height":1200,"bytes":412000}],"title":"Saturday games"},"mocks":{"transforms":{"image.compose":{"kind":"image","assetId":"sheet-1","versionId":"v1","url":"/api/media/example/sheet.png","mimeType":"image/png","width":1300,"height":1440,"bytes":1820000}}},"expected":{"sheet":{"kind":"image","assetId":"sheet-1","versionId":"v1","url":"/api/media/example/sheet.png","mimeType":"image/png","width":1300,"height":1440,"bytes":1820000}}}}. StillMade runs every test before anyone sees the Block, without spending credits. With the SDK instead, the same code is module/main.js in a Block folder and the same tests are tests/fixtures.json; stillmade-block pack writes the file to import (MODULES.md).
12833
13277
 
12834
13278
  ### ComfyUI workflows
12835
13279
 
@@ -12865,7 +13309,7 @@ Write the description in third-grade language: one short sentence, ideally 8–1
12865
13309
 
12866
13310
  ### Compact interface
12867
13311
 
12868
- Every job or ingredient Block interface, generated or custom, follows StillMade's compact rules: Compact: the whole Block fits one laptop screen without scrolling. A short setting is one row, its label beside the control; no card around a single control. Status is a dot or a few words ("No narration yet"), never a paragraph; the reason sits behind an `sm-hint`. Files are tiles: a saved file is a thumbnail or chip and adding one is a + tile. Never the bare browser file picker, and no size limits in view until a file is too big. An (i) beside anything a newcomer could misread: a setting, status or number whose effect is not obvious from its label gets a closed `sm-hint` with one or two sentences. Give every input that needs one a `description`; StillMade shows it as that (i). Obvious controls get none. Use the width: from 860px wide the kit puts the controls on the left at a reading width (300–380px) and the result (`sm-stage`) fills the right, in its real shape; under that everything stacks with the result first and the action within reach. Extra width goes to the result, never to wider text fields. Before there is a result, the stage previews what will be made from the inputs. `data-layout="stack"` opts out. StillMade draws generated manifest.ui controls this way itself (label beside control, files as tiles, the result filling the right on wide screens); you supply at most four inputs that matter in view, sensible defaults, and a short `description` on every input whose effect is not obvious from its name, which StillMade shows as its (i). An app-sized Block is different: when the request is for something a person works in (an editor, a canvas, a 3D or CAD tool, a studio), build it as a workspace Block laid out like that app: its work surface (viewport, canvas, timeline, panels, tool rails) owns the page with as much tooling as the app needs, and the compact rules apply to its settings, dialogs and status, never to the work surface. Never shrink an app to one screen of four inputs.
13312
+ Every job or ingredient Block interface, generated or custom, follows StillMade's compact rules: Compact: the whole Block fits one laptop screen without scrolling. A short setting is one row, its label beside the control; no card around a single control. Status is a dot or a few words ("No narration yet"), never a paragraph; the reason sits behind an `sm-hint`. Files are tiles: a saved file is a thumbnail or chip and adding one is the + tile (`<button class="sm-add" data-stillmade-add="<input>">`), which opens StillMade's picker. Never an `<input type="file">` or the bare browser file picker, and no size limits in view until a file is too big. An (i) beside anything a newcomer could misread: a setting, status or number whose effect is not obvious from its label gets a closed `sm-hint` with one or two sentences. Give every input that needs one a `description`; StillMade shows it as that (i). Obvious controls get none. Use the width: from 860px wide the kit puts the controls on the left at a reading width (300–380px) and the result (`sm-stage`) fills the right, in its real shape; under that everything stacks in the page's order: the inputs, the action in normal flow (never pinned), then the result's place under it, so put `sm-stage` after `sm-actions`. Extra width goes to the result, never to wider text fields. Before there is a result, the stage previews what will be made from the inputs. `data-layout="stack"` opts out. StillMade draws generated manifest.ui controls this way itself (label beside control, files as tiles, the result filling the right on wide screens); you supply at most four inputs that matter in view, sensible defaults, and a short `description` on every input whose effect is not obvious from its name, which StillMade shows as its (i). An app-sized Block is different: when the request is for something a person works in (an editor, a canvas, a 3D or CAD tool, a studio), build it as a workspace Block laid out like that app: its work surface (viewport, canvas, timeline, panels, tool rails) owns the page with as much tooling as the app needs, and the compact rules apply to its settings, dialogs and status, never to the work surface. Never shrink an app to one screen of four inputs.
12869
13313
 
12870
13314
  ### Generated controls first
12871
13315
 
@@ -12877,7 +13321,7 @@ Optional custom view interfaces are supported, but generated manifest.ui control
12877
13321
 
12878
13322
  *When:* StillMade's builder adds this section when the person asks for an interface, preview or controls, or refines a Block that has one.
12879
13323
 
12880
- A custom interface is the optional package-level view:{html,css?,themedCss?,javascript?}; when refining, preserve and refine base.view. Design it by StillMade's interface principles, in order: Function first: unless the person asks for a design direction, build from the Block kit (`sm-block`, `sm-stage`, `sm-choices`, `sm-actions`, `sm-more`) and ship little or no CSS of your own. A direction the person names wins over the kit. Show the thing being made: the preview, media or result gets the largest area (`sm-stage`). A picture or clip the Block makes fills it in its own shape, as large as fits; several fill it as a grid, each big enough to watch. Small tiles are for the files a person gives the Block, never for what it makes. A clip is a `<video controls>`, which StillMade plays in its own player with full screen, and a picture in `sm-stage` or `sm-gallery` (or marked `data-stillmade-expand`) gets StillMade's full-screen button; leave out `controls` only when the Block drives playback itself. Never a JSON dump, source editor, manifest or diagnostic panel unless the task is about source. One obvious next action: one solid `sm-primary` in the kit's ink, never restyled to the background or a ghost outline; peers are `sm-quiet` buttons or named icons, and close is an X icon. Seven or fewer choices in view. Speak the job unless the person or creator asks otherwise. The action button names what it does in the job's words (`manifest.action`: "Plan the tour"; AI pictures, video and sound start with Generate and say what, the person's own media cut or rendered says Make: "Generate voiceover", "Make 6 clips"), never "Make it", "Run", "Run step", "Submit", "Process" or a bare "Generate"; progress counts the job's units ("4 of 9 rooms"), never "Processing"; results and problems use its words in one line. Show no credit numbers: StillMade puts the price beside the verb on its own run button ("Write · 1 credit"), with a chip for the model, quality or count when they can change. Pressing it runs at that price; a confirmation appears only when the server price is higher than the one shown. Pictures over words: choices people see or hear are thumbnails, swatches, samples or glyphs (`sm-choices`), not word lists. Icons carry an accessible name and tooltip; never emoji. A picked option reads at a glance: keep the kit's ink fill on an `sm-choices` pick, never restyled to an outline, a tint or the page's background. Media that cannot show here (on another device, or it did not open) keeps its place as a quiet `sm-media-off` tile in its own shape, never words in its place. Words only when needed. Drop what nobody needs to finish the task (intros, "how it works", restating the Block name). Words that might help start collapsed (`sm-hint`, `sm-more`). Errors, spending approvals, stale inputs and destructive steps stay visible, in few words. Strong defaults; secondary settings go in `sm-more`, closed. Compact: the whole Block fits one laptop screen without scrolling. A short setting is one row, its label beside the control; no card around a single control. Status is a dot or a few words ("No narration yet"), never a paragraph; the reason sits behind an `sm-hint`. Files are tiles: a saved file is a thumbnail or chip and adding one is a + tile. Never the bare browser file picker, and no size limits in view until a file is too big. Rare tools (update, schedule, history, reload) are named icon buttons beside the action, never sections under the work. An (i) beside anything a newcomer could misread: a setting, status or number whose effect is not obvious from its label gets a closed `sm-hint` with one or two sentences. Give every input that needs one a `description`; StillMade shows it as that (i). Obvious controls get none. One workspace: no cards inside cards, no second title, step counter or Back/Next bar; the host owns navigation. Status sits beside the action it describes (`sm-status`), in a few words. Keep the page in normal scroll flow; no sticky media or control strips unless the editing task needs them. Use the width: from 860px wide the kit puts the controls on the left at a reading width (300–380px) and the result (`sm-stage`) fills the right, in its real shape; under that everything stacks with the result first and the action within reach. Extra width goes to the result, never to wider text fields. Before there is a result, the stage previews what will be made from the inputs. `data-layout="stack"` opts out. Size with the kit, never fixed sizes: no fixed pixel widths or heights for the workspace or stage, no 100vh, no position:fixed panels. StillMade zooms a Block's screen to fill the space it has (up to 150%, never down to a phone layout on a computer), so a Block laid out with the kit's spacing fills its Step without empty space, and it centers a standard view narrower than its Step. An app that lays itself out (full-size canvas or WebGL, its own scroll, pan or zoom, its own Ctrl/Cmd +/- keys) puts `data-stillmade-layout="own"` on its outermost element, and StillMade then never centers, zooms or takes those keys. Group what the person sets in one `sm-fields` (or `sm-settings` for short ones): the kit gives the group its own surface, so the inputs, the settings and the result read as separate parts. Settings sit side by side (`sm-settings`), next to what they change. Three or four short options are one segmented row (`data-segmented`), not a row of big tiles. Lists fill the width at a small size: input files are small tiles (`sm-gallery data-size="small"`), many items are one line each (`sm-list`): a thumbnail, one line of text, a short fact. Tapping an item opens all of its fields in place. Extra width means more items per row, never bigger cards. On a phone chip and segment rows scroll sideways instead of wrapping; long lists show their first three with "N more" (`data-peek`) and long text five lines until tapped (`sm-clamp`). Nothing is lost when a Block gets smaller: every control stays on the screen, behind an (i), in `sm-more`, or inside the item it belongs to. Compacting moves controls; it never deletes them. Text scene descriptions are planning notes, never a storyboard or an approved frame. Say nothing about how the Block works: no notes on where it runs (this device, StillMade Desktop, the cloud), saving, syncing, recovery, accounts, credits or where a file is kept. StillMade tells the person what they need as one short notification, or nothing; never a sentence on the page, and never the same line under each item. References fill your inputs: every drop area in StillMade offers "Use a reference", and the person's saved photos, videos and notes fill the Step with no typing. Media inputs fill by kind, so take `video` or `asset` wherever a clip works as well as a photo; text inputs fill by plain names and labels (`productName`, `website`, `brandColors`, `fonts`, `voice`, and `details` or `notes` for the reference's note). Never build your own reference picker or library upload. Days and times are picked, never typed: a `date` input is a calendar, with a list of times when it is about a time (`postAt`, `startsAt`, a label with Time, When or Starts). In a custom view use `<input type="date">` or `type="datetime-local"`, never a text box for a date. Posting is its own Step: sending a result to Instagram, TikTok, email or any app is a connector Step after the Block, worded by StillMade ("Post to Instagram"). A Block never posts from inside itself. Its DOM controller is separate from manifest runtime code. The view may also declare appearance:"original" or "host". For an imported design, preserve its colors with appearance:"original" and view.css; no themed replacement is required. Preserve base.view.appearance when refining unless the user requests a change. Omitted/host appearance retains the existing Light, Dark and White requirement: supply semantic colors in view.css or a complete replacement view.themedCss. An original-style view may optionally supply valid themedCss for a StillMade appearance preview. Use StillMade variables (--bg,--fg,--fg-muted,--border-tok,--r-3) only when opting into host styling; do not redefine host tokens in the themed variant. Appearance never relaxes sandbox constraints: no CSS escapes/filters/blending, imports/url(), or element.style mutations in JS. Use classes for UI state; media pixels retain their colors. Both CSS fields have the same 32 KiB limit; all existing shared-state, security and permission checks still apply. Declare elements in html, bind with document.getElementById/querySelector and addEventListener; use literal property access, array methods, textContent, input.value, button.disabled and canvas. Initial values: StillMade.onInput(values=>{...}); await StillMade.run(inputBindings) returns validated output bindings from the isolated package runtime; catch errors and show results. No computation in the DOM controller; put it in code/recipe. Disable the action button while pending.
13324
+ A custom interface is the optional package-level view:{html,css?,themedCss?,javascript?}; when refining, preserve and refine base.view. Design it by StillMade's interface principles, in order: Function first: unless the person asks for a design direction, build from the Block kit (`sm-block`, `sm-stage`, `sm-choices`, `sm-actions`, `sm-more`) and ship little or no CSS of your own. A direction the person names wins over the kit. Show the thing being made: the preview, media or result gets the largest area (`sm-stage`), and every part shows at once: before the first run the result's place is drawn in the shape and size of what is coming (a 9:16 frame for a vertical clip, lines for text, a player bar for sound), so the result fills it instead of popping in. On a wide screen what goes in sits on the left and the result on the right; they stack only on narrow screens and phones. A picture or clip the Block makes fills it in its own shape, as large as fits; several fill it as a grid, each big enough to watch. Small tiles are for the files a person gives the Block, never for what it makes. A clip is a `<video controls>`, which StillMade plays in its own player with full screen, and a picture in `sm-stage` or `sm-gallery` (or marked `data-stillmade-expand`) gets StillMade's full-screen button; leave out `controls` only when the Block drives playback itself. Never a JSON dump, source editor, manifest or diagnostic panel unless the task is about source. One obvious next action: one solid `sm-primary` in the kit's ink, never restyled to the background or a ghost outline; peers are `sm-quiet` buttons or named icons, and close is an X icon. Seven or fewer choices in view. Speak the job unless the person or creator asks otherwise. The action button names what it does in the job's words (`manifest.action`: "Plan the tour"; AI pictures, video and sound start with Generate and say what, the person's own media cut or rendered says Make: "Generate voiceover", "Make 6 clips"), never "Make it", "Run", "Run step", "Submit", "Process" or a bare "Generate"; progress counts the job's units ("4 of 9 rooms"), never "Processing"; results and problems use its words in one line. Show no credit numbers: StillMade puts the price beside the verb on its own run button ("Write · 1 credit"), with a chip for the model, quality or count when they can change. Pressing it runs at that price; a confirmation appears only when the server price is higher than the one shown. Pictures over words: choices people see or hear are thumbnails, swatches, samples or glyphs (`sm-choices`), not word lists. Icons carry an accessible name and tooltip; never emoji. A picked option reads at a glance: keep the kit's ink fill on an `sm-choices` pick, never restyled to an outline, a tint or the page's background. Media that cannot show here (on another device, or it did not open) keeps its place as a quiet `sm-media-off` tile in its own shape, never words in its place. Words only when needed. Drop what nobody needs to finish the task (intros, "how it works", restating the Block name). Words that might help start collapsed (`sm-hint`, `sm-more`). Errors, spending approvals, stale inputs and destructive steps stay visible, in few words. Strong defaults; secondary settings go in `sm-more`, closed. Compact: the whole Block fits one laptop screen without scrolling. A short setting is one row, its label beside the control; no card around a single control. Status is a dot or a few words ("No narration yet"), never a paragraph; the reason sits behind an `sm-hint`. Files are tiles: a saved file is a thumbnail or chip and adding one is the + tile (`<button class="sm-add" data-stillmade-add="<input>">`), which opens StillMade's picker. Never an `<input type="file">` or the bare browser file picker, and no size limits in view until a file is too big. Rare tools (update, schedule, history, reload) are named icon buttons beside the action, never sections under the work. An (i) beside anything a newcomer could misread: a setting, status or number whose effect is not obvious from its label gets a closed `sm-hint` with one or two sentences. Give every input that needs one a `description`; StillMade shows it as that (i). Obvious controls get none. One workspace: no cards inside cards, no second title, step counter or Back/Next bar; the host owns navigation. One page per job (November 3, 2026): a guided job's screen holds the inputs and settings, then a cheap check before the expensive run (the script and a voice sample before the video, a second `StillMade.run` that StillMade prices on its own), then the result; never a second Block or Step for the check. A workspace a person works in (a CAD tool, a canvas, an editor) is its own Block, and turning what it made into something else is the next one. Status sits beside the action it describes (`sm-status`), in a few words. Keep the page in normal scroll flow; no sticky media or control strips unless the editing task needs them. Use the width: from 860px wide the kit puts the controls on the left at a reading width (300–380px) and the result (`sm-stage`) fills the right, in its real shape; under that everything stacks in the page's order: the inputs, the action in normal flow (never pinned), then the result's place under it, so put `sm-stage` after `sm-actions`. Extra width goes to the result, never to wider text fields. Before there is a result, the stage previews what will be made from the inputs. `data-layout="stack"` opts out. Size with the kit, never fixed sizes: no fixed pixel widths or heights for the workspace or stage, no 100vh, no position:fixed panels. StillMade zooms a Block's screen to fill the space it has (up to 150%, never down to a phone layout on a computer), so a Block laid out with the kit's spacing fills its Step without empty space, and it centers a standard view narrower than its Step. An app that lays itself out (full-size canvas or WebGL, its own scroll, pan or zoom, its own Ctrl/Cmd +/- keys) puts `data-stillmade-layout="own"` on its outermost element, and StillMade then never centers, zooms or takes those keys. Group what the person sets in one `sm-fields` (or `sm-settings` for short ones): the kit gives the group its own surface, so the inputs, the settings and the result read as separate parts. Settings sit side by side (`sm-settings`), next to what they change. Three or four short options are one segmented row (`data-segmented`), not a row of big tiles. Lists fill the width at a small size: input files are small tiles (`sm-gallery data-size="small"`), many items are one line each (`sm-list`): a thumbnail, one line of text, a short fact. Tapping an item opens all of its fields in place. Extra width means more items per row, never bigger cards. On a phone chip and segment rows scroll sideways instead of wrapping; long lists show their first three with "N more" (`data-peek`) and long text five lines until tapped (`sm-clamp`). Nothing is lost when a Block gets smaller: every control stays on the screen, behind an (i), in `sm-more`, or inside the item it belongs to. Compacting moves controls; it never deletes them. Text scene descriptions are planning notes, never a storyboard or an approved frame. Say nothing about how the Block works: no notes on where it runs (this device, StillMade Desktop, the cloud), saving, syncing, recovery, accounts, credits or where a file is kept. StillMade tells the person what they need as one short notification, or nothing; never a sentence on the page, and never the same line under each item. References fill your inputs: every drop area in StillMade offers "Use a reference", and the person's saved photos, videos and notes fill the Step with no typing. Media inputs fill by kind, so take `video` or `asset` wherever a clip works as well as a photo; text inputs fill by plain names and labels (`productName`, `website`, `brandColors`, `fonts`, `voice`, and `details` or `notes` for the reference's note). Never build your own reference picker or library upload. Days and times are picked, never typed: a `date` input is a calendar, with a list of times when it is about a time (`postAt`, `startsAt`, a label with Time, When or Starts). In a custom view use `<input type="date">` or `type="datetime-local"`, never a text box for a date. Posting is its own Step: sending a result to Instagram, TikTok, email or any app is a connector Step after the Block, worded by StillMade ("Post to Instagram"). A Block never posts from inside itself. Its DOM controller is separate from manifest runtime code. The view may also declare appearance:"original" or "host". For an imported design, preserve its colors with appearance:"original" and view.css; no themed replacement is required. Preserve base.view.appearance when refining unless the user requests a change. Omitted/host appearance retains the existing Light, Dark and White requirement: supply semantic colors in view.css or a complete replacement view.themedCss. An original-style view may optionally supply valid themedCss for a StillMade appearance preview. Use StillMade variables (--bg,--fg,--fg-muted,--border-tok,--r-3) only when opting into host styling; do not redefine host tokens in the themed variant. Appearance never relaxes sandbox constraints: no CSS escapes/filters/blending, imports/url(), or element.style mutations in JS. Use classes for UI state; media pixels retain their colors. Both CSS fields have the same 32 KiB limit; all existing shared-state, security and permission checks still apply. Declare elements in html, bind with document.getElementById/querySelector and addEventListener; use literal property access, array methods, textContent, input.value, button.disabled and canvas. Initial values: StillMade.onInput(values=>{...}); await StillMade.run(inputBindings) returns validated output bindings from the isolated package runtime; catch errors and show results. No computation in the DOM controller; put it in code/recipe. Disable the action button while pending.
12881
13325
  Standard custom view code (no `view.config.json` frame runtime) is checked before it runs. These rules come straight from the validator; frame views run in their own sandbox and may build the page with ordinary DOM calls such as `createElement`:
12882
13326
 
12883
13327
  - **HTML elements:** only `div`, `section`, `article`, `header`, `footer`, `main`, `aside`, `nav`, `h1`, `h2`, `h3`, `h4`, `p`, `span`, `strong`, `em`, `i`, `small`, `br`, `hr`, `label`, `input`, `textarea`, `button`, `select`, `option`, `optgroup`, `fieldset`, `legend`, `output`, `progress`, `meter`, `ul`, `ol`, `li`, `dl`, `dt`, `dd`, `figure`, `figcaption`, `img`, `video`, `audio`, `source`, `canvas`, `table`, `thead`, `tbody`, `tfoot`, `tr`, `th`, `td`, `pre`, `code`, `details`, `summary`. No scripts, iframes, SVG, forms, comments or inline event attributes; media URLs must be `data:` or `blob:`.
@@ -12923,7 +13367,7 @@ not proof of multiplayer operation. Test two interfaces exchanging actual edits
12923
13367
  and saved outputs, permission loss, reconnect and conflicts before claiming
12924
13368
  collaboration support. Never claim arbitrary imported JavaScript state syncs
12925
13369
  merely because the package passed its computation fixtures.
12926
- Example view for a package taking and returning text: {"html":"<main class=\"sm-block\"><output class=\"sm-stage\" id=\"result\" aria-live=\"polite\"></output><div class=\"sm-field\"><label for=\"source\">Draft</label><textarea id=\"source\"></textarea></div><div class=\"sm-actions\"><button class=\"sm-primary\" id=\"run\">Clean text</button><p class=\"sm-status\" id=\"status\" role=\"status\"></p></div></main>","css":"#result{place-items:start;min-height:120px;white-space:pre-wrap;overflow-wrap:anywhere}","javascript":"const source=document.getElementById('source'),button=document.getElementById('run'),result=document.getElementById('result'),status=document.getElementById('status');\nlet busy=false;\nStillMade.bindShared('draft','source');\nStillMade.onInput(input=>{StillMade.setSharedDefaults({draft:input.text||''});});\nStillMade.onShared(shared=>{button.disabled=busy||!shared.canEdit;result.textContent=shared.outputs.text||'';});\nbutton.addEventListener('click',async()=>{\n if(busy||!StillMade.getShared().canEdit)return;\n busy=true;button.disabled=true;status.dataset.state='loading';status.textContent='Cleaning';\n try{await StillMade.run({text:source.value});status.dataset.state='success';status.textContent='Ready';}\n catch(error){status.dataset.state='error';status.textContent=error.message||'Could not clean this draft.';}\n finally{busy=false;button.disabled=!StillMade.getShared().canEdit;}\n});"}.
13370
+ Example view for a package taking and returning text: {"html":"<main class=\"sm-block\"><div class=\"sm-field\"><label for=\"source\">Draft</label><textarea id=\"source\"></textarea></div><div class=\"sm-actions\"><button class=\"sm-primary\" id=\"run\" data-stillmade-action=\"run\">Clean text</button><p class=\"sm-status\" id=\"status\" role=\"status\"></p></div><output class=\"sm-stage\" id=\"result\" aria-live=\"polite\"></output></main>","css":"#result{place-items:start;min-height:120px;white-space:pre-wrap;overflow-wrap:anywhere}","javascript":"const source=document.getElementById('source'),button=document.getElementById('run'),result=document.getElementById('result'),status=document.getElementById('status');\nlet busy=false;\nStillMade.bindShared('draft','source');\nStillMade.onInput(input=>{StillMade.setSharedDefaults({draft:input.text||''});});\nStillMade.onShared(shared=>{button.disabled=busy||!shared.canEdit;result.textContent=shared.outputs.text||'';});\nbutton.addEventListener('click',async()=>{\n if(busy||!StillMade.getShared().canEdit)return;\n busy=true;button.disabled=true;status.dataset.state='loading';status.textContent='Cleaning';\n try{await StillMade.run({text:source.value});status.dataset.state='success';status.textContent='Ready';}\n catch(error){status.dataset.state='error';status.textContent=error.message||'Could not clean this draft.';}\n finally{busy=false;button.disabled=!StillMade.getShared().canEdit;}\n});"}.
12927
13371
 
12928
13372
  ## Port fields
12929
13373