stillmade-sdk 0.1.41 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.stillmade/sdk-baseline.json +19 -21
- package/AGENTS.md +17 -12
- package/FRAME_VIEWS.md +113 -0
- package/INTERFACE_DESIGN.md +67 -22
- package/MOBILE_UI.md +4 -2
- package/MODULES.md +152 -6
- package/PASTE_TO_LLM.md +42 -32
- package/README.md +3 -2
- package/SDK.md +1028 -563
- package/UI_AESTHETIC.md +30 -3
- package/blocks/generate/package.js +1 -0
- package/blocks/generate/versions/1.0.0/package.js +463 -0
- package/docs/build/audio-to-editor.md +36 -0
- package/docs/build/block-builder-contract.md +150 -0
- package/docs/build/comfyui.md +227 -0
- package/docs/build/concepts.md +40 -0
- package/docs/build/context.md +159 -0
- package/docs/build/cookbook.md +57 -0
- package/docs/build/custom-interface.md +617 -0
- package/docs/build/data-types.md +83 -0
- package/docs/build/frame-views.md +259 -0
- package/docs/build/generate-media.md +59 -0
- package/docs/build/github-applications.md +314 -0
- package/docs/build/hosted-images.md +214 -0
- package/docs/build/hosted-music.md +39 -0
- package/docs/build/hosted-speech.md +157 -0
- package/docs/build/hosted-text.md +246 -0
- package/docs/build/image-description.md +49 -0
- package/docs/build/interface-design.md +343 -0
- package/docs/build/javascript.md +63 -0
- package/docs/build/local-preview.md +61 -0
- package/docs/build/media-analysis.md +27 -0
- package/docs/build/media.md +400 -0
- package/docs/build/mobile-ui.md +106 -0
- package/docs/build/modules.md +487 -0
- package/docs/build/original-blocks.md +70 -0
- package/docs/build/outside-apis.md +128 -0
- package/docs/build/production-context-templates.md +12 -0
- package/docs/build/project-storage.md +94 -0
- package/docs/build/quickstart.md +48 -0
- package/docs/build/recipes.md +103 -0
- package/docs/build/script-handoff.md +45 -0
- package/docs/build/script-writer.md +34 -0
- package/docs/build/state.md +223 -0
- package/docs/build/testing.md +112 -0
- package/docs/build/timeline-edits.md +116 -0
- package/docs/build/timeline-proposals.md +76 -0
- package/docs/build/transcription.md +59 -0
- package/docs/build/triggers.md +59 -0
- package/docs/build/tutorial.md +52 -0
- package/docs/build/typescript.md +34 -0
- package/docs/build/video-generation.md +61 -0
- package/docs/build/web-reading.md +37 -0
- package/docs/build/with-your-ai.md +27 -0
- package/docs/changelog.md +9 -0
- package/docs/creators.md +21 -0
- package/docs/distribute/discovery.md +43 -0
- package/docs/distribute/imports.md +58 -0
- package/docs/distribute/publishing.md +29 -0
- package/docs/distribute/sdk-versions.md +36 -0
- package/docs/distribute/selling.md +14 -0
- package/docs/distribute/teams.md +60 -0
- package/docs/distribute/versioning.md +28 -0
- package/docs/examples.md +23 -0
- package/docs/gallery.md +26 -0
- package/docs/index.md +50 -0
- package/docs/native-mcp.md +307 -0
- package/docs/project-types/automation.md +43 -0
- package/docs/project-types/batches.md +50 -0
- package/docs/project-types/build.md +42 -0
- package/docs/project-types/completion.md +15 -0
- package/docs/project-types/conditions.md +21 -0
- package/docs/project-types/hosted-review.md +115 -0
- package/docs/project-types/onboarding.md +75 -0
- package/docs/project-types/running.md +309 -0
- package/docs/project-types/testing.md +72 -0
- package/docs/reference/application-artifacts.md +87 -0
- package/docs/reference/authoring-guide.md +134 -0
- package/docs/reference/browser-applications.md +267 -0
- package/docs/reference/capabilities.md +45 -0
- package/docs/reference/cli.md +71 -0
- package/docs/reference/connection-api.md +59 -0
- package/docs/reference/cooperative-jobs.md +13 -0
- package/docs/reference/desktop-capabilities.md +172 -0
- package/docs/reference/desktop-execution.md +77 -0
- package/docs/reference/errors.md +233 -0
- package/docs/reference/execution.md +800 -0
- package/docs/reference/first-party-blocks.md +40 -0
- package/docs/reference/generation-models.md +59 -0
- package/docs/reference/limits.md +306 -0
- package/docs/reference/manifest-kinds-controls.md +36 -0
- package/docs/reference/manifest.md +181 -0
- package/docs/reference/operations.md +36 -0
- package/docs/reference/package-jobs.md +35 -0
- package/docs/reference/parity.md +55 -0
- package/docs/reference/permissions.md +86 -0
- package/docs/reference/ports.md +26 -0
- package/docs/reference/types.md +268 -0
- package/docs/reference/universal-resolver.md +303 -0
- package/docs/reference/view-bridge.md +60 -0
- package/docs/reference/view-rules.md +15 -0
- package/docs/reference/workspace-sync.md +25 -0
- package/docs/sdk.md +34 -0
- package/docs/tools/block-remix.md +211 -0
- package/docs/tools/llm.md +2213 -0
- package/docs/tools/repository-adaptation.md +613 -0
- package/examples/check-clip.stillmade.json +8 -1
- package/examples/clean-text.stillmade.json +4 -0
- package/examples/custom-interface.stillmade.json +4 -0
- package/examples/describe-image.stillmade.json +4 -0
- package/examples/foley-cue.stillmade.json +4 -0
- package/examples/generate-image.stillmade.json +4 -0
- package/examples/generate-video.stillmade.json +10 -2
- package/examples/module-word-stats/stillmade.block.json +4 -0
- package/examples/narrate-script.stillmade.json +4 -0
- package/examples/numbered-takes.stillmade.json +4 -0
- package/examples/research-topic.stillmade.json +9 -1
- package/examples/score-a-scene.stillmade.json +4 -0
- package/examples/script-draft.stillmade.json +4 -0
- package/examples/summarize-page.stillmade.json +4 -0
- package/examples/text-workflow.stillmade.json +7 -32
- package/examples/thicken-lines.stillmade.json +4 -0
- package/examples/timeline-polish.stillmade.json +15 -3
- package/examples/transcribe-audio.stillmade.json +15 -3
- package/examples/word-count.stillmade.json +4 -0
- package/package.json +1 -1
- package/packages/block-cli/cli.js +13 -6
- package/packages/block-cli/dev-host/assets/host.js +81 -38
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-cli/format.js +1 -1
- package/packages/block-cli/ready-gate.js +4 -0
- package/packages/block-cli/templates.js +37 -19
- package/packages/block-cli/type-packages.js +119 -0
- package/packages/block-platform/block-readiness.js +8 -1
- package/packages/block-platform/canvas.js +2 -1
- package/packages/block-platform/catalog.js +27 -48
- package/packages/block-platform/native-host-inventory.js +0 -226
- package/packages/block-platform/plain-descriptions.js +1 -25
- package/packages/block-platform/project-type-starters.js +13 -53
- package/packages/block-platform/script-handoff.js +2 -4
- package/packages/block-platform/type-workspace.js +2 -0
- package/packages/block-sdk/appearance.js +3 -1
- package/packages/block-sdk/audio-generation-example.js +2 -2
- package/packages/block-sdk/block-tier.js +1 -1
- package/packages/block-sdk/builder-contract.js +13 -11
- package/packages/block-sdk/capabilities.js +1 -1
- package/packages/block-sdk/capability-example.js +1 -1
- package/packages/block-sdk/contracts.js +6 -2
- package/packages/block-sdk/examples.js +3 -3
- package/packages/block-sdk/first-party-identities.js +1 -84
- package/packages/block-sdk/first-party-packages.js +3 -85
- package/packages/block-sdk/generation-catalog.js +15 -3
- package/packages/block-sdk/image-description-example.js +1 -1
- package/packages/block-sdk/image-generation-example.js +1 -1
- package/packages/block-sdk/index.d.ts +2 -1
- package/packages/block-sdk/media-analysis-example.js +2 -2
- package/packages/block-sdk/mock-host.js +7 -1
- package/packages/block-sdk/module-example.js +1 -1
- package/packages/block-sdk/module-host-calls.js +35 -10
- package/packages/block-sdk/module-hosted.js +15 -2
- package/packages/block-sdk/module-media-access.js +71 -0
- package/packages/block-sdk/module-node.js +1 -1
- package/packages/block-sdk/portable-media-actions.js +1 -1
- package/packages/block-sdk/runtime.js +1 -1
- package/packages/block-sdk/speech-example.js +1 -1
- package/packages/block-sdk/state-example.js +1 -1
- package/packages/block-sdk/timeline-proposal-example.js +1 -1
- package/packages/block-sdk/transcription-example.js +1 -1
- package/packages/block-sdk/trigger-examples.js +2 -2
- package/packages/block-sdk/video-generation-example.js +1 -1
- package/packages/block-sdk/view-bridge.js +4 -1
- package/packages/block-sdk/view-design.js +44 -15
- package/packages/block-sdk/web-example.js +2 -2
- package/packages/design-system/block-kit.css +53 -12
- package/packages/design-system/block-kit.html +3 -2
- package/blocks/mcp-motion-planner/versions/1.1.0/package.js +0 -97
- package/blocks/motion-designer/versions/1.0.1/package.js +0 -173318
- package/blocks/motion-designer/versions/1.0.2/package.js +0 -173318
- package/blocks/motion-frame-review/versions/1.1.1/package.js +0 -465
- package/blocks/motion-frame-review/versions/1.1.2/package.snapshot.js +0 -572
- package/blocks/narration/package.js +0 -1
- package/blocks/narration/versions/1.0.0/package.js +0 -221
- package/blocks/narration/versions/1.0.1/package.js +0 -221
- package/blocks/native/sm.animated-scene/versions/2.0.0/package.js +0 -475
- package/blocks/native/sm.animated-scene/versions/2.1.0/package.js +0 -498
- package/blocks/native/sm.animated-scene/versions/2.2.0/package.js +0 -524
- package/blocks/native/sm.animated-scene/versions/2.3.0/package.js +0 -618
- package/blocks/native/sm.animated-scene/versions/2.3.1/package.js +0 -618
- package/blocks/native/sm.animated-scene/versions/2.3.2/package.js +0 -723
- package/blocks/native/sm.animated-scene/versions/2.3.3/package.js +0 -723
- package/blocks/native/sm.animated-scene/versions/2.3.4/package.js +0 -676
- package/blocks/native/sm.animated-scene/versions/2.3.5/package.js +0 -693
- package/blocks/native/sm.animated-scene/versions/2.3.6/package.js +0 -700
- package/blocks/native/sm.animated-scene/versions/2.3.7/package.js +0 -747
- package/blocks/native/sm.animated-scene/versions/2.3.8/package.js +0 -769
- package/blocks/native/sm.animated-scene/versions/2.3.9/package.js +0 -769
- package/blocks/native/sm.animated-scene-assistant/versions/1.0.0/package.js +0 -183
- package/blocks/native/sm.blueprint/versions/2.0.0/package.js +0 -358
- package/blocks/native/sm.blueprint/versions/2.1.0/package.js +0 -358
- package/blocks/native/sm.blueprint/versions/2.1.1/package.js +0 -358
- package/blocks/native/sm.blueprint/versions/2.1.2/package.js +0 -358
- package/blocks/native/sm.blueprint/versions/2.1.3/package.js +0 -381
- package/blocks/native/sm.blueprint/versions/2.1.4/package.js +0 -381
- package/blocks/native/sm.blueprint/versions/2.1.5/package.js +0 -339
- package/blocks/native/sm.blueprint/versions/2.1.6/package.js +0 -356
- package/blocks/native/sm.blueprint/versions/2.1.7/package.js +0 -363
- package/blocks/native/sm.blueprint/versions/2.1.8/package.js +0 -410
- package/blocks/native/sm.blueprint/versions/2.1.9/package.js +0 -422
- package/blocks/native/sm.blueprint-assistant/versions/1.0.0/package.js +0 -172
- package/blocks/native/sm.chat/versions/2.0.0/package.js +0 -144
- package/blocks/native/sm.chat/versions/2.0.1/package.js +0 -97
- package/blocks/native/sm.chat/versions/2.0.2/package.js +0 -100
- package/blocks/native/sm.chat/versions/2.0.3/package.js +0 -100
- package/blocks/native/sm.continue-if/versions/1.0.0/package.js +0 -168
- package/blocks/native/sm.describe-media/versions/2.0.0/package.js +0 -153
- package/blocks/native/sm.describe-media/versions/2.0.1/package.js +0 -156
- package/blocks/native/sm.describe-media/versions/2.1.0/package.js +0 -140
- package/blocks/native/sm.image-editor/versions/2.0.0/package.js +0 -183
- package/blocks/native/sm.image-editor/versions/2.0.1/package.js +0 -190
- package/blocks/native/sm.new-items/versions/1.0.0/package.js +0 -191
- package/blocks/native/sm.pick-photos/versions/1.0.0/package.js +0 -334
- package/blocks/native/sm.quality-audit/versions/1.0.0/package.js +0 -192
- package/blocks/native/sm.quality-audit/versions/1.0.1/package.js +0 -192
- package/blocks/native/sm.quality-audit/versions/1.0.2/package.js +0 -195
- package/blocks/native/sm.research/versions/2.0.0/package.js +0 -175
- package/blocks/native/sm.research/versions/2.0.1/package.js +0 -184
- package/blocks/native/sm.research/versions/2.0.2/package.js +0 -187
- package/blocks/native/sm.script/versions/2.0.0/package.js +0 -144
- package/blocks/native/sm.script/versions/2.1.0/package.js +0 -144
- package/blocks/native/sm.script/versions/2.1.1/package.js +0 -144
- package/blocks/native/sm.script/versions/2.1.2/package.js +0 -105
- package/blocks/native/sm.script/versions/2.1.3/package.js +0 -152
- package/blocks/native/sm.script/versions/2.1.4/package.js +0 -156
- package/blocks/native/sm.script/versions/2.1.5/package.js +0 -156
- package/blocks/native/sm.shot-plan/versions/2.0.0/package.js +0 -301
- package/blocks/native/sm.shot-plan/versions/2.1.0/package.js +0 -528
- package/blocks/native/sm.shot-plan/versions/2.1.1/package.js +0 -528
- package/blocks/native/sm.shot-plan/versions/2.1.2/package.js +0 -528
- package/blocks/native/sm.shot-plan/versions/2.1.3/package.js +0 -536
- package/blocks/native/sm.shot-plan/versions/2.2.0/package.js +0 -800
- package/blocks/native/sm.shot-plan/versions/2.2.1/package.js +0 -920
- package/blocks/native/sm.shot-plan-assistant/versions/1.0.0/package.js +0 -173
- package/blocks/native/sm.stock-search/versions/2.0.0/package.js +0 -174
- package/blocks/native/sm.thumbnail/versions/2.0.0/package.js +0 -137
- package/blocks/native/sm.thumbnail/versions/2.0.1/package.js +0 -140
- package/blocks/native/sm.transcribe/versions/2.0.0/package.js +0 -163
- package/blocks/native/sm.transcribe/versions/2.0.1/package.js +0 -168
- package/blocks/native/sm.whiteboard-assistant/versions/1.0.0/package.js +0 -175
- package/blocks/native/sm.youtube/versions/2.0.0/package.js +0 -217
- package/blocks/occasion-story/package.js +0 -1
- package/blocks/occasion-story/versions/1.0.0/package.js +0 -1658
- package/blocks/occasion-story/versions/1.0.1/package.js +0 -1699
- package/blocks/occasion-story/versions/1.0.2/package.js +0 -1682
- package/blocks/photo-reel-composer/versions/1.0.0/package.js +0 -3043
- package/blocks/photo-reel-composer/versions/1.0.1/package.js +0 -3026
- package/blocks/photo-reel-composer/versions/1.0.2/package.js +0 -4149
- package/blocks/product-ad-variants/versions/1.0.0/package.js +0 -2911
- package/blocks/product-launch-director/versions/1.0.0/package.js +0 -103
- package/blocks/release-visuals/package.js +0 -1
- package/blocks/release-visuals/versions/1.0.0/package.js +0 -1492
- package/blocks/release-visuals/versions/1.0.1/package.js +0 -1517
- package/blocks/release-visuals/versions/1.0.2/package.js +0 -1500
- package/blocks/screen-recorder/package.js +0 -1
- package/blocks/screen-recorder/versions/1.0.0/package.js +0 -223
- package/blocks/screen-recorder/versions/1.0.1/package.js +0 -223
- package/blocks/short-form-finish/package.js +0 -1
- package/blocks/short-form-finish/versions/1.0.0/package.js +0 -1043
- package/blocks/short-form-finish/versions/1.0.1/package.js +0 -1043
- package/blocks/short-form-finish/versions/1.0.2/package.js +0 -1043
- package/blocks/short-form-finish/versions/1.0.3/package.js +0 -1026
- package/examples/saas-product-launch-reviewed-source.stillmade.json +0 -383
- package/examples/saas-product-launch.stillmade.json +0 -304
- package/packages/block-platform/dealer-inventory-video-project-type.js +0 -38
- package/packages/block-platform/first-revenue-project-types.js +0 -46
- package/packages/block-platform/fixture-starters.js +0 -13
- package/packages/block-platform/launch-project-source.js +0 -10
- package/packages/block-platform/launch-project.js +0 -30
- package/packages/block-platform/motion-project-types.js +0 -55
- package/packages/block-platform/motion-starters.js +0 -14
- package/packages/block-platform/product-ad-variants-project-type.js +0 -30
- package/packages/block-platform/product-launch-updated.js +0 -90
- package/packages/block-platform/revenue-review-project-types.js +0 -36
- package/packages/block-platform/revenue-review-starters.js +0 -13
- package/packages/block-platform/script-writer.js +0 -29
- package/packages/block-platform/website-source-block.js +0 -14
|
@@ -2,7 +2,7 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
2
2
|
// Portable image request. This fixture describes a result to review; it is not
|
|
3
3
|
// evidence of a live generation. Model, quality and payment belong to the host.
|
|
4
4
|
export const imageGenerate={
|
|
5
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.generate-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:EXAMPLE_PROVENANCE,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'}]},
|
|
5
|
+
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:EXAMPLE_PROVENANCE,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'}]},
|
|
6
6
|
capability:{schemaVersion:1,operation:'image.generate',prompt:{$input:'prompt'},output:'image'},
|
|
7
7
|
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}}}],
|
|
8
8
|
};
|
|
@@ -278,7 +278,8 @@ export const GENERATION_LIMITS: Readonly<{references:8;negativePrompt:2000;seed:
|
|
|
278
278
|
/** Fill and check image settings; `strict` rejects values the model does not accept. */
|
|
279
279
|
export function imageSettings(value?:Partial<ImageSettings>,options?:{strict?:boolean}):ImageSettings;
|
|
280
280
|
/** Fill and check video settings, including Veo's 1080p-is-8-seconds rule. */
|
|
281
|
-
|
|
281
|
+
/** With no model named, the default is the cheapest model that makes the asked aspect ratio and takes the frames given. */
|
|
282
|
+
export function videoSettings(value?:Partial<VideoSettings>,options?:{strict?:boolean;firstFrame?:boolean;lastFrame?:boolean}):VideoSettings;
|
|
282
283
|
export function validateSeed(seed:number):number;
|
|
283
284
|
export const MEDIA_GENERATE_LIMITS: Readonly<{items:50;prompt:4000}>;
|
|
284
285
|
export const MEDIA_GENERATE_OPERATIONS: readonly ['image','video','speech','music','sfx'];
|
|
@@ -2,9 +2,9 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
2
2
|
// Ask StillMade's media vision about a saved video, optionally inside one time
|
|
3
3
|
// range. Each analyzed image or video is one priced call on StillMade credits.
|
|
4
4
|
export const mediaAnalysis={
|
|
5
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.check-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:EXAMPLE_PROVENANCE,
|
|
5
|
+
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:EXAMPLE_PROVENANCE,
|
|
6
6
|
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).'}},
|
|
7
|
-
outputs:{report:{type:'json',primary:true,semantic:'media_findings',description:'Summary, timeline, on-screen text and issues found.'}},
|
|
7
|
+
outputs:{report:{type:'json',primary:true,semantic:'media_findings',description:'Summary, timeline, on-screen text and issues found.',schema:{items:'json[]'}}},
|
|
8
8
|
permissions:{project:[],capabilities:['media.analyze'],network:[],filesystem:[],secrets:[]},ui:[{control:'asset',port:'clip'},{control:'number',port:'start'},{control:'number',port:'end'}]},
|
|
9
9
|
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'},
|
|
10
10
|
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']}}}],
|
|
@@ -13,6 +13,7 @@ import {createMemoryStorage,storageKey} from './cloud-storage.js';
|
|
|
13
13
|
import {mediaTransformRequest,decodeWav} from './media-transform.js';
|
|
14
14
|
import {FILE_FORMATS} from './data-values.js';
|
|
15
15
|
import {mockPng,mockWav,mockMp4,MOCK_IMAGE_SIZE,MOCK_VIDEO_SIZE} from './mock-media.js';
|
|
16
|
+
import {videoSettings,audioSettings} from './generation-catalog.js';
|
|
16
17
|
// The words of a simple PDF whose pages are uncompressed text, such as the example lesson.pdf, page by page; null for
|
|
17
18
|
// anything else. StillMade reads real PDFs on the person's device; a local test of another PDF mocks document.text
|
|
18
19
|
// (November 1, 2026: the stand-in read a PDF's own code as its words).
|
|
@@ -201,7 +202,12 @@ export function createMockHost({mocks:given={},readLocalFile,manifest,pickedMedi
|
|
|
201
202
|
const operation=action?.operation;
|
|
202
203
|
if(recorded('actions',operation)){note('actions.run',operation,'mocks');return pick(mocks.actions[operation],count(`action:${operation}`));}
|
|
203
204
|
note('actions.run',operation,'recorded');
|
|
204
|
-
|
|
205
|
+
// As in StillMade: a video, music or sound effect carries the length it asked for; speech carries none (decode it).
|
|
206
|
+
if(operation==='media.generate')return {value:action.request.items.map(item=>{
|
|
207
|
+
const seconds=item.operation==='video'?videoSettings(item.settings,{firstFrame:item.firstFrame!==undefined,lastFrame:item.lastFrame!==undefined}).duration:['music','sfx'].includes(item.operation)?audioSettings(item.operation,item.settings).durationSec:undefined;
|
|
208
|
+
const made=item.operation==='image'||item.operation==='video'?reference[item.operation]():reference.audio(seconds?{seconds,sampleRate:8000}:undefined);
|
|
209
|
+
return {key:item.key,kind:made.kind,assetId:made.assetId,versionId:made.versionId,url:made.url,...(Number.isFinite(seconds)?{duration:seconds}:{})};
|
|
210
|
+
}),failures:[]};
|
|
205
211
|
if(operation==='stock.search')return {results:[]};
|
|
206
212
|
fail(`There is no recorded stand-in for the ${operation} action. Add one under actions["${operation}"] in ${MOCKS_FILE}.`);
|
|
207
213
|
},
|
|
@@ -12,7 +12,7 @@ export default async function run(input, stillmade) {
|
|
|
12
12
|
}
|
|
13
13
|
`;
|
|
14
14
|
export const moduleWordStats={
|
|
15
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.word-stats',version:'1.0.0',name:'Word stats',description:'Counts words and finds the longest one, as a modern JavaScript module.',kind:'task',runtime:'module',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
15
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.word-stats',action:{label:'Count words',running:'Counting words…'},version:'1.0.0',name:'Word stats',description:'Counts words and finds the longest one, as a modern JavaScript module.',kind:'task',runtime:'module',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
16
16
|
inputs:{text:{type:'text',primary:true,semantic:'plain_text',required:true,description:'Text to measure.'}},
|
|
17
17
|
outputs:{words:{type:'integer',semantic:'word_count',description:'Number of words.'},longest:{type:'text',primary:true,semantic:'longest_word',description:'The longest word.'}},
|
|
18
18
|
permissions:{project:[],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'text'}]},
|
|
@@ -4,20 +4,45 @@
|
|
|
4
4
|
import {networkDeclarations,networkRequest,connectionDeclarations,connectionCall} from './network.js';
|
|
5
5
|
import {storageHostHandlers} from './cloud-storage.js';
|
|
6
6
|
import {projectReadSnapshot} from './context.js';
|
|
7
|
+
import {mediaReferences,moduleMediaAccess,MEDIA_RESULT_OPERATIONS} from './module-media-access.js';
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* {api: args => Promise} for one run of `pkg` against `host` (see createNodeModuleExecutor). `input` is the run's input:
|
|
11
|
+
* files, transforms, actions, hosted calls and asset changes use only files the run received or the host made in it,
|
|
12
|
+
* the rule StillMade's own host applies (module-media-access.js).
|
|
13
|
+
*/
|
|
14
|
+
export function moduleHostCalls(pkg,host,storage,input){
|
|
15
|
+
const access=moduleMediaAccess(input);
|
|
16
|
+
const made=async pending=>access.remember(await pending);
|
|
10
17
|
return {
|
|
11
|
-
'files.read':args=>host.readFile?host.readFile(args.ref):Promise.reject(Object.assign(new Error('files.read needs a host file reader in local runs.'),{code:'MODULE_HOST_UNAVAILABLE'})),
|
|
12
|
-
'files.write':args=>host.writeFile?host.writeFile(args.buffer,args.options):Promise.reject(Object.assign(new Error('files.write saves to the StillMade library; pass a host writer in local runs.'),{code:'MODULE_HOST_UNAVAILABLE'})),
|
|
13
|
-
'media.transform':args=>host.transformMedia?host.transformMedia(args.request):Promise.reject(Object.assign(new Error('Media transforms run in StillMade; pass a host transformMedia in local runs.'),{code:'MODULE_HOST_UNAVAILABLE'})),
|
|
14
|
-
'actions.run':args=>
|
|
15
|
-
|
|
18
|
+
'files.read':async args=>{access.checkFile(args.ref,'files.read reads');return host.readFile?host.readFile(args.ref):Promise.reject(Object.assign(new Error('files.read needs a host file reader in local runs.'),{code:'MODULE_HOST_UNAVAILABLE'}));},
|
|
19
|
+
'files.write':args=>host.writeFile?made(host.writeFile(args.buffer,args.options)):Promise.reject(Object.assign(new Error('files.write saves to the StillMade library; pass a host writer in local runs.'),{code:'MODULE_HOST_UNAVAILABLE'})),
|
|
20
|
+
'media.transform':async args=>{access.check(args.request,'Media transforms');return host.transformMedia?made(host.transformMedia(args.request)):Promise.reject(Object.assign(new Error('Media transforms run in StillMade; pass a host transformMedia in local runs.'),{code:'MODULE_HOST_UNAVAILABLE'}));},
|
|
21
|
+
'actions.run':async args=>{
|
|
22
|
+
access.check(args.action,'Actions');
|
|
23
|
+
if(!host.runAction)throw Object.assign(new Error('Actions run in StillMade.'),{code:'MODULE_HOST_UNAVAILABLE'});
|
|
24
|
+
const result=await host.runAction(args.action);
|
|
25
|
+
if(args.action?.operation==='media.generate')for(const ref of mediaReferences(result?.value))access.remember(ref);
|
|
26
|
+
return result;
|
|
27
|
+
},
|
|
28
|
+
'hosted.run':async args=>{
|
|
29
|
+
access.check(args.request,'Hosted operations');
|
|
30
|
+
if(!host.runHosted)throw Object.assign(new Error('Hosted operations run in StillMade.'),{code:'MODULE_HOST_UNAVAILABLE'});
|
|
31
|
+
const result=await host.runHosted(args.operation,args.request);
|
|
32
|
+
return MEDIA_RESULT_OPERATIONS.includes(args.operation)?access.remember(result):result;
|
|
33
|
+
},
|
|
16
34
|
'connections.call':async args=>{const request=connectionCall(args.request,connectionDeclarations(pkg.manifest.permissions?.connections));return host.callConnection?host.callConnection(request):Promise.reject(Object.assign(new Error('Connections run through StillMade, which adds your account; pass a host callConnection in local runs.'),{code:'MODULE_HOST_UNAVAILABLE'}));},
|
|
17
35
|
...storageHostHandlers(storage),
|
|
18
|
-
|
|
19
|
-
'
|
|
20
|
-
|
|
36
|
+
// Only the host-built media indexes make media usable, as in StillMade: free-form documents can hold media-shaped values.
|
|
37
|
+
'project.read':async args=>{
|
|
38
|
+
if(!host.project)throw Object.assign(new Error('project.read needs a project snapshot; pass host.project in local runs.'),{code:'MODULE_HOST_UNAVAILABLE'});
|
|
39
|
+
const snapshot=projectReadSnapshot(pkg.manifest,host.project,args.fields);
|
|
40
|
+
for(const field of ['assets','files'])for(const ref of mediaReferences(snapshot[field]))access.add(ref);
|
|
41
|
+
for(const shot of Array.isArray(snapshot.shots)?snapshot.shots:[])if(shot?.selectedImage)access.remember(shot.selectedImage);
|
|
42
|
+
return snapshot;
|
|
43
|
+
},
|
|
44
|
+
'assets.create':async args=>{if(!(pkg.manifest.permissions?.project||[]).includes('asset.create'))throw Object.assign(new Error('Declare asset.create to add project assets.'),{code:'MODULE_PERMISSION'});access.checkFile(args.file,'assets.create adds');return host.createAsset?made(host.createAsset(args.file,args.options||{})):Promise.reject(Object.assign(new Error('Project assets are saved in StillMade; pass a host createAsset in local runs.'),{code:'MODULE_HOST_UNAVAILABLE'}));},
|
|
45
|
+
'assets.appendVersion':async args=>{if(!(pkg.manifest.permissions?.project||[]).includes('asset.version.append'))throw Object.assign(new Error('Declare asset.version.append to add asset versions.'),{code:'MODULE_PERMISSION'});access.checkFile(args.file,'assets.appendVersion adds');return host.appendAssetVersion?made(host.appendAssetVersion(args.asset,args.file)):Promise.reject(Object.assign(new Error('Project assets are saved in StillMade; pass a host appendAssetVersion in local runs.'),{code:'MODULE_HOST_UNAVAILABLE'}));},
|
|
21
46
|
'net.fetch':async args=>{const request=networkRequest(args.request,networkDeclarations(pkg.manifest.permissions?.network));return host.netFetch?host.netFetch(request):Promise.reject(Object.assign(new Error('Network calls run through StillMade, which adds your key; pass a host netFetch in local runs.'),{code:'MODULE_HOST_UNAVAILABLE'}));},
|
|
22
47
|
};
|
|
23
48
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// operation in `permissions.capabilities`.
|
|
6
6
|
import {assert,plain,stableStringify} from './contracts.js';
|
|
7
7
|
import {defaultCapabilityExpectations,MEDIA_ANALYZE_LIMITS} from './capabilities.js';
|
|
8
|
-
import {GENERATION_LIMITS,IMAGE_GENERATION_MODELS} from './generation-catalog.js';
|
|
8
|
+
import {GENERATION_LIMITS,IMAGE_GENERATION_MODELS,VIDEO_GENERATION_MODELS} from './generation-catalog.js';
|
|
9
9
|
|
|
10
10
|
const text=(value,max)=>typeof value==='string'&&value.trim().length>0&&value.length<=max;
|
|
11
11
|
const media=(value,kind)=>plain(value)&&value.kind===kind&&['assetId','versionId','url'].every(key=>typeof value[key]==='string'&&value[key].trim());
|
|
@@ -32,7 +32,20 @@ const SHAPES={
|
|
|
32
32
|
},
|
|
33
33
|
'video.generate':request=>{
|
|
34
34
|
if(!text(request.prompt,32000))fail('video.generate needs a prompt');
|
|
35
|
-
|
|
35
|
+
// firstFrame and lastFrame: saved images the video starts and ends on, bound as a capability Block binds them; and the
|
|
36
|
+
// negativePrompt (November 3, 2026: they were dropped, so a listing video's camera move over each photo was made from
|
|
37
|
+
// the words alone, and the stand-in host passed it).
|
|
38
|
+
const {firstFrame,lastFrame,negativePrompt}=request;
|
|
39
|
+
for(const [name,frame] of [['firstFrame',firstFrame],['lastFrame',lastFrame]])if(frame!==undefined&&!media(frame,'image'))fail(`video.generate ${name} is one saved image`);
|
|
40
|
+
if(lastFrame!==undefined&&firstFrame===undefined)fail('video.generate lastFrame needs a firstFrame');
|
|
41
|
+
if(negativePrompt!==undefined&&!(typeof negativePrompt==='string'&&negativePrompt.length<=GENERATION_LIMITS.negativePrompt))fail(`video.generate negativePrompt is text of at most ${GENERATION_LIMITS.negativePrompt.toLocaleString('en-US')} characters`);
|
|
42
|
+
// A model named in settings must take those frames (every model takes a first frame; Kling 3.0 Turbo and Wan 2.6 Flash
|
|
43
|
+
// take no last frame; Wan 2.6 Flash and Wan 2.7 need a first frame).
|
|
44
|
+
const model=VIDEO_GENERATION_MODELS[request.settings?.model];
|
|
45
|
+
if(model&&firstFrame&&!model.firstFrame)fail(`${model.label} takes no first frame: choose a model that does, or leave the model to the person`);
|
|
46
|
+
if(model&&lastFrame&&!model.lastFrame)fail(`${model.label} takes no last frame: choose a model that does, or leave the model to the person`);
|
|
47
|
+
if(model?.requiresFirstFrame&&!firstFrame)fail(`${model.label} needs a firstFrame`);
|
|
48
|
+
return {inputs:{prompt:port('text','Video prompt'),...(firstFrame?{firstFrame:port('image','First frame')}:{}),...(lastFrame?{lastFrame:port('image','Last frame')}:{})},outputs:{result:port('video','Generated video')},capability:{prompt:{$input:'prompt'},output:'result',...(firstFrame?{firstFrame:{$input:'firstFrame'}}:{}),...(lastFrame?{lastFrame:{$input:'lastFrame'}}:{}),...(request.settings?{settings:request.settings}:{}),...(negativePrompt?{negativePrompt}:{})},input:{prompt:request.prompt,...(firstFrame?{firstFrame}:{}),...(lastFrame?{lastFrame}:{})}};
|
|
36
49
|
},
|
|
37
50
|
'audio.speech':request=>{
|
|
38
51
|
if(!text(request.text,3800))fail('audio.speech needs text of up to 3,800 characters');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Which saved files a module Block may use in one run: the references it
|
|
2
|
+
// received as input, and those the host made during the run (files it wrote,
|
|
3
|
+
// transform results, generated media, project media it read). files.read,
|
|
4
|
+
// transforms, actions, hosted calls and project asset changes use only these.
|
|
5
|
+
// StillMade's browser host (client/src/blocks/moduleRuntimeHost.js) and the
|
|
6
|
+
// SDK's local runners (module-host-calls.js: `test --mock`, `ready`, `gate`,
|
|
7
|
+
// and the QuickJS runner for Blocks built in StillMade) apply this one rule, so
|
|
8
|
+
// a Block gets the same verdict on a creator's machine and in the import review.
|
|
9
|
+
import {stableStringify} from './contracts.js';
|
|
10
|
+
|
|
11
|
+
const fail=(message,code)=>Object.assign(new Error(message),{code});
|
|
12
|
+
export const MODULE_MEDIA_KINDS=Object.freeze(['image','video','audio','asset','file']);
|
|
13
|
+
// Hosted operations whose result is media the host generated and checked.
|
|
14
|
+
export const MEDIA_RESULT_OPERATIONS=Object.freeze(['image.generate','video.generate','audio.speech','audio.music','audio.sfx']);
|
|
15
|
+
// A reference names its file by these fields. Its other fields describe the
|
|
16
|
+
// file (a media.generate item's key, a duration, a name) and may be dropped or
|
|
17
|
+
// added (November 3, 2026: a listing video Block rebuilt each generated clip as
|
|
18
|
+
// {kind, assetId, versionId, url}, without the result item's `key`, and every
|
|
19
|
+
// render was refused although the clips were generated in that run).
|
|
20
|
+
export const MEDIA_REFERENCE_FIELDS=Object.freeze(['kind','assetId','versionId','url']);
|
|
21
|
+
const isReference=value=>MODULE_MEDIA_KINDS.includes(value.kind)&&(typeof value.url==='string'||typeof value.assetId==='string');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Saved references in a value: anything shaped like media (a media kind with a
|
|
25
|
+
* url or assetId), at any depth. With `strict`, a value too large to inspect is
|
|
26
|
+
* refused. With `within`, the other fields of a reference are searched too, so
|
|
27
|
+
* a reference cannot carry another file past the check.
|
|
28
|
+
*/
|
|
29
|
+
export function mediaReferences(input,{strict=false,within=false}={}){
|
|
30
|
+
const found=[];let visited=0;
|
|
31
|
+
const visit=(value,depth)=>{
|
|
32
|
+
if(!value||typeof value!=='object'||value instanceof ArrayBuffer||ArrayBuffer.isView(value))return;
|
|
33
|
+
if(++visited>20000||depth>16){if(strict)throw fail('This request is too large or too deeply nested to check.','MODULE_HOST');return;}
|
|
34
|
+
if(Array.isArray(value)){for(const item of value)visit(item,depth+1);return;}
|
|
35
|
+
if(isReference(value)){
|
|
36
|
+
found.push(value);
|
|
37
|
+
if(within)for(const [key,item] of Object.entries(value))if(!MEDIA_REFERENCE_FIELDS.includes(key))visit(item,depth+1);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for(const item of Object.values(value))visit(item,depth+1);
|
|
41
|
+
};
|
|
42
|
+
visit(input,0);return found;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The file a reference names, as a comparable string. */
|
|
46
|
+
export function mediaReferenceKey(ref){
|
|
47
|
+
return stableStringify(MEDIA_REFERENCE_FIELDS.map(key=>typeof ref?.[key]==='string'?ref[key]:null));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const RULE='this Block received, wrote or generated in this run';
|
|
51
|
+
const FIX='Pass the reference the Block received as input, or the one StillMade returned in this run (files.write, a transform, hosted.run or actions.run media.generate), keeping its kind, assetId, versionId and url.';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The run's usable files, starting from its input. `remember` adds a value the
|
|
55
|
+
* host made when the value is itself a reference (a text, JSON or web result
|
|
56
|
+
* can contain media-shaped values chosen by a model or a web page, so only
|
|
57
|
+
* references the host returned count); `add` adds a reference the host
|
|
58
|
+
* vouches for (project media it read). The checks throw MODULE_FILE_READ.
|
|
59
|
+
*/
|
|
60
|
+
export function moduleMediaAccess(input){
|
|
61
|
+
const usable=new Set(mediaReferences(input).map(mediaReferenceKey));
|
|
62
|
+
const allowed=ref=>!!ref&&typeof ref==='object'&&usable.has(mediaReferenceKey(ref));
|
|
63
|
+
return {
|
|
64
|
+
allowed,
|
|
65
|
+
add(ref){usable.add(mediaReferenceKey(ref));},
|
|
66
|
+
remember(value){if(value&&typeof value==='object'&&mediaReferences(value)[0]===value)usable.add(mediaReferenceKey(value));return value;},
|
|
67
|
+
// Every reference in a request, and inside its references, must be usable.
|
|
68
|
+
check(value,what){if(!mediaReferences(value,{strict:true,within:true}).every(allowed))throw fail(`${what} use only files ${RULE}. ${FIX}`,'MODULE_FILE_READ');},
|
|
69
|
+
checkFile(ref,call){if(!allowed(ref))throw fail(`${call} only files ${RULE}. ${FIX}`,'MODULE_FILE_READ');},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -43,7 +43,7 @@ export function createNodeModuleExecutor({files,host={},onProgress,onLog,now=()=
|
|
|
43
43
|
try{declared=networkDeclarations(pkg.manifest.permissions?.network);}catch{/* no valid origins */}
|
|
44
44
|
const sockets=createModuleSockets({declarations:declared,open:host.netConnect,post:(message,transfer)=>worker.postMessage(message,transfer)});
|
|
45
45
|
// The shared host calls (files, media, hosted operations, connections, storage, project, assets, net.fetch) plus live connections.
|
|
46
|
-
const handlers={...sockets.handlers,...moduleHostCalls(pkg,host,storage)};
|
|
46
|
+
const handlers={...sockets.handlers,...moduleHostCalls(pkg,host,storage,input)};
|
|
47
47
|
try{
|
|
48
48
|
return await new Promise((resolve,reject)=>{
|
|
49
49
|
const stop=error=>{clearInterval(timer);reject(error);};
|
|
@@ -37,7 +37,7 @@ export function mediaGenerateRequest(request){
|
|
|
37
37
|
if(item.references!==undefined)assert(Array.isArray(item.references)&&item.references.length>0&&item.references.length<=Math.min(GENERATION_LIMITS.references,IMAGE_GENERATION_MODELS[settings.model].references)&&item.references.every(asset),'PORTABLE_ACTION',`${IMAGE_GENERATION_MODELS[settings.model].label} accepts up to ${IMAGE_GENERATION_MODELS[settings.model].references} saved reference images.`);
|
|
38
38
|
}else{
|
|
39
39
|
assert(item.references===undefined,'PORTABLE_ACTION','References apply to image items; use firstFrame and lastFrame for video.');
|
|
40
|
-
const settings=videoSettings(item.settings,{strict:true,firstFrame:item.firstFrame!==undefined}),entry=VIDEO_GENERATION_MODELS[settings.model];
|
|
40
|
+
const settings=videoSettings(item.settings,{strict:true,firstFrame:item.firstFrame!==undefined,lastFrame:item.lastFrame!==undefined}),entry=VIDEO_GENERATION_MODELS[settings.model];
|
|
41
41
|
if(item.firstFrame!==undefined)assert(entry.firstFrame&&asset(item.firstFrame),'PORTABLE_ACTION',`${entry.label} needs a saved image as its first frame.`);
|
|
42
42
|
if(item.lastFrame!==undefined)assert(entry.lastFrame&&item.firstFrame!==undefined&&asset(item.lastFrame),'PORTABLE_ACTION',`${entry.label} cannot use this last frame.`);
|
|
43
43
|
}
|
|
@@ -58,7 +58,7 @@ export async function runPackageAsync(pkg, values, options = {}) {
|
|
|
58
58
|
// A module runs only through an injected executor: StillMade's sandboxed
|
|
59
59
|
// browser Worker, or the CLI's local Node worker for the creator's own tests.
|
|
60
60
|
const pinned=structuredClone(pkg),input=validateValues(pinned.manifest.inputs,values);
|
|
61
|
-
assert(typeof options.module==='function','RUNTIME_UNAVAILABLE','Module Blocks run in StillMade in the browser, or
|
|
61
|
+
assert(typeof options.module==='function','RUNTIME_UNAVAILABLE','Module Blocks run in StillMade in the browser, or on your own machine with the SDK CLI (test, preview and pack for a Block or a Project Type, and ready for a Block), which passes them a local module executor; code calling the SDK directly passes options.module (createNodeModuleExecutor in module-node.js). They cannot run unattended: StillMade has no server sandbox for module Blocks.');
|
|
62
62
|
const result=await executeHostCall(structuredClone(pinned),input,{signal:options.signal,execute:options.module});
|
|
63
63
|
assert(!options.signal?.aborted,'CANCELLED','Run cancelled');
|
|
64
64
|
assert(result&&typeof result==='object'&&Object.keys(result).every(key=>['outputs','logs'].includes(key)),'MODULE_RESULT','The module host returned an unexpected result');
|
|
@@ -2,7 +2,7 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
2
2
|
// Portable speech request. These fixtures are expectations to review, not
|
|
3
3
|
// evidence that a provider has generated audio. Host controls choose the voice.
|
|
4
4
|
export const audioSpeech={
|
|
5
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.narrate-script',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:EXAMPLE_PROVENANCE,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:[]},
|
|
5
|
+
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:EXAMPLE_PROVENANCE,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:[]},
|
|
6
6
|
capability:{schemaVersion:1,operation:'audio.speech',text:{$input:'script'},output:'audio'},
|
|
7
7
|
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}}}],
|
|
8
8
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
2
2
|
export const numberedTakes={
|
|
3
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.numbered-takes',version:'1.0.0',name:'Numbered takes',description:'Label successive creative takes with an independent counter for each production placement.',kind:'task',runtime:'javascript',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{text:{type:'text',primary:true,semantic:'draft_text',required:true,description:'Creative take text to label with the next number.'}},outputs:{text:{type:'text',primary:true,semantic:'numbered_take',description:'The supplied take prefixed with its saved sequence number.'}},permissions:{project:['state.step']},state:{scope:'step',version:1,initial:{count:0}},ui:[{port:'text',control:'text'}]},
|
|
3
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.numbered-takes',action:{label:'Number takes',running:'Numbering takes…'},version:'1.0.0',name:'Numbered takes',description:'Label successive creative takes with an independent counter for each production placement.',kind:'task',runtime:'javascript',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{text:{type:'text',primary:true,semantic:'draft_text',required:true,description:'Creative take text to label with the next number.'}},outputs:{text:{type:'text',primary:true,semantic:'numbered_take',description:'The supplied take prefixed with its saved sequence number.'}},permissions:{project:['state.step']},state:{scope:'step',version:1,initial:{count:0}},ui:[{port:'text',control:'text'}]},
|
|
4
4
|
code:'state.count += 1; return {text: "Take " + state.count + ": " + input.text.trim()};',
|
|
5
5
|
tests:[{name:'First take',input:{text:' Opening scene '},expected:{text:'Take 1: Opening scene'},expectedState:{version:1,value:{count:1}}},{name:'Continues saved numbering',input:{text:'Alternate ending'},state:{version:1,value:{count:4}},expected:{text:'Take 5: Alternate ending'},expectedState:{version:1,value:{count:5}}}]
|
|
6
6
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
2
2
|
|
|
3
3
|
export const timelinePolish={
|
|
4
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.timeline-polish',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:EXAMPLE_PROVENANCE,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'}]},
|
|
4
|
+
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:EXAMPLE_PROVENANCE,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'}]},
|
|
5
5
|
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'},
|
|
6
6
|
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}}}],
|
|
7
7
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
2
2
|
|
|
3
3
|
export const audioTranscription={
|
|
4
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.transcribe-audio',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:EXAMPLE_PROVENANCE,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']}]},
|
|
4
|
+
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:EXAMPLE_PROVENANCE,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']}]},
|
|
5
5
|
capability:{schemaVersion:1,operation:'audio.transcribe',audio:{$input:'audio'},language:{$input:'language'},output:'transcript'},
|
|
6
6
|
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}}}]
|
|
7
7
|
};
|
|
@@ -5,7 +5,7 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
5
5
|
// credits per call, total credits, expiry), every new video saved to the
|
|
6
6
|
// project is transcribed unattended into timed captions.
|
|
7
7
|
export const autoCaptions={
|
|
8
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.auto-captions',version:'1.0.0',name:'Auto-caption new uploads',description:'Transcribe each new video into timed captions, automatically within the budget the project owner approves.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
8
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.auto-captions',action:{label:'Make captions',running:'Making captions…'},version:'1.0.0',name:'Auto-caption new uploads',description:'Transcribe each new video into timed captions, automatically within the budget the project owner approves.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
9
9
|
inputs:{video:{type:'video',primary:true,required:true,semantic:'source_video',description:'The new video to caption.'},language:{type:'text',default:'auto',required:false,semantic:'language_code',description:'auto, or a two-letter language code such as en.'}},
|
|
10
10
|
outputs:{captions:{type:'transcript',primary:true,semantic:'timed_captions',description:'The spoken words with their timings.'}},
|
|
11
11
|
permissions:{project:[],capabilities:['audio.transcribe'],network:[],filesystem:[],secrets:[]},
|
|
@@ -19,7 +19,7 @@ export const autoCaptions={
|
|
|
19
19
|
// turns it into a short production note. It runs in StillMade's sandbox with
|
|
20
20
|
// no network and no credits.
|
|
21
21
|
export const orderNote={
|
|
22
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.order-note',version:'1.0.0',name:'Order to production note',description:'Turn an order a shop sends by webhook into a short production note.',kind:'task',runtime:'javascript',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
22
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.order-note',action:{label:'Write order note',running:'Writing order note…'},version:'1.0.0',name:'Order to production note',description:'Turn an order a shop sends by webhook into a short production note.',kind:'task',runtime:'javascript',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
23
23
|
inputs:{event:{type:'object',semantic:'workflow_event',schema:{id:'string',kind:'string',occurredAt:'string',preview:'boolean',data:'object'},sources:['project'],required:true,description:'The webhook event StillMade received.'}},
|
|
24
24
|
outputs:{note:{type:'text',primary:true,semantic:'production_note',description:'A one-line note for the team.'}},
|
|
25
25
|
permissions:{project:[],network:[],filesystem:[],secrets:[]},
|
|
@@ -2,7 +2,7 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
2
2
|
// Declarative request; the host chooses provider, model, duration, payment and
|
|
3
3
|
// storage. This fixture is executed only after explicit host approval.
|
|
4
4
|
export const videoGenerate={
|
|
5
|
-
manifest:{schemaVersion:1,id:'example.generate-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:EXAMPLE_PROVENANCE,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'}]},
|
|
5
|
+
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:EXAMPLE_PROVENANCE,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'}]},
|
|
6
6
|
capability:{schemaVersion:1,operation:'video.generate',prompt:{$input:'prompt'},output:'video'},
|
|
7
7
|
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}}}],
|
|
8
8
|
};
|
|
@@ -11,7 +11,10 @@ export const VIEW_BRIDGE=Object.freeze([
|
|
|
11
11
|
// Inputs and runs
|
|
12
12
|
{name:'input',group:'Inputs and runs',signature:'readonly input: Partial<In>',summary:'A copy of the current input values. Read it after `onInput` first fires.',where:all},
|
|
13
13
|
{name:'onInput',group:'Inputs and runs',signature:'onInput(callback: (input: Partial<In>) => void): () => void',summary:'Receives the input when the view starts and each time the host changes it. Returns a function that stops listening. Initialize controls here.',where:all},
|
|
14
|
-
{name:'
|
|
14
|
+
{name:'addFiles',group:'Inputs and runs',signature:'addFiles(port: string): Promise<{added: number}>',summary:'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.',where:`${saved} and the SDK preview`},
|
|
15
|
+
{name:'removeFile',group:'Inputs and runs',signature:'removeFile(port: string, index: number): Promise<{files: number}>',summary:'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.',where:`${saved} and the SDK preview`},
|
|
16
|
+
{name:'moveFile',group:'Inputs and runs',signature:'moveFile(port: string, from: number, to: number): Promise<{files: number}>',summary:'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.',where:`${saved} and the SDK preview`},
|
|
17
|
+
{name:'run',group:'Inputs and runs',signature:'run(values: In): Promise<Out>',summary:'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.',where:all},
|
|
15
18
|
{name:'useOutput',group:'Inputs and runs',signature:'useOutput(outputs: Partial<Out>): Promise<Record<string, unknown>>',summary:'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.',where:saved},
|
|
16
19
|
{name:'schemaFields',group:'Inputs and runs',signature:'readonly schemaFields: Readonly<Record<string, unknown>>',summary:'Helpers that build form fields from the input ports and their schemas, honoring inputs the host locked.',where:all},
|
|
17
20
|
// Shared state
|
|
@@ -19,16 +19,18 @@ export const BLOCK_KIT_CLASSES=Object.freeze({
|
|
|
19
19
|
'sm-actions':'The action row: one .sm-primary, optional .sm-quiet buttons and .sm-icon buttons (aria-label and title required).',
|
|
20
20
|
'sm-status':'Live state beside the action: data-state="loading|success|waiting|error" and a few words.',
|
|
21
21
|
'sm-more':'A <details> for secondary settings; starts closed.',
|
|
22
|
-
'sm-hint':'A <details>
|
|
22
|
+
'sm-hint':'A <details> (i) for an explanation that might help; its words show on hover or focus, or a tap on touch screens.',
|
|
23
23
|
'sm-gallery':'A grid of results or media. data-size="small" for input files: more tiles per row as the screen widens, never bigger tiles.',
|
|
24
24
|
'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, with the reason in aria-label and title and no words inside.',
|
|
25
|
-
'sm-
|
|
25
|
+
'sm-add':'The add tile for an input\'s files: <button class="sm-add" data-stillmade-add="<input>" aria-label="Add photos">. StillMade opens its own picker and saves the files; never an <input type=file>. Each tile of a list can carry .sm-tile-tools with data-stillmade-move and data-stillmade-remove buttons (data-index, data-to).',
|
|
26
|
+
'sm-scrim':'Words over a photo or video, in 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; never color-mix() or a fixed rgba().',
|
|
27
|
+
'sm-glyph':'A small icon drawn in currentColor: data-glyph="play|pause|plus|close|check|chevron|chevron-left|chevron-right|upload|image|image-off|video|audio|text|spark|info|warning|spinner".',
|
|
26
28
|
});
|
|
27
29
|
|
|
28
30
|
// The design principles for every Block interface, in priority order.
|
|
29
31
|
export const BLOCK_INTERFACE_PRINCIPLES=Object.freeze([
|
|
30
32
|
'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.',
|
|
31
|
-
'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.',
|
|
33
|
+
'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.',
|
|
32
34
|
'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.',
|
|
33
35
|
'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.',
|
|
34
36
|
'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.',
|
|
@@ -38,15 +40,16 @@ export const BLOCK_INTERFACE_PRINCIPLES=Object.freeze([
|
|
|
38
40
|
'Strong defaults; secondary settings go in `sm-more`, closed.',
|
|
39
41
|
'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.',
|
|
40
42
|
'Status is a dot or a few words ("No narration yet"), never a paragraph; the reason sits behind an `sm-hint`.',
|
|
41
|
-
'Files are tiles: a saved file is a thumbnail or chip and adding one is
|
|
43
|
+
'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.',
|
|
42
44
|
'Rare tools (update, schedule, history, reload) are named icon buttons beside the action, never sections under the work.',
|
|
43
45
|
'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.',
|
|
44
46
|
'One workspace: no cards inside cards, no second title, step counter or Back/Next bar; the host owns navigation.',
|
|
47
|
+
'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.',
|
|
45
48
|
'Status sits beside the action it describes (`sm-status`), in a few words.',
|
|
46
49
|
'Keep the page in normal scroll flow; no sticky media or control strips unless the editing task needs them.',
|
|
47
|
-
'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
|
|
50
|
+
'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.',
|
|
48
51
|
'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.',
|
|
49
|
-
'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.',
|
|
52
|
+
'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.',
|
|
50
53
|
'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.',
|
|
51
54
|
'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`).',
|
|
52
55
|
'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.',
|
|
@@ -68,16 +71,14 @@ export const INTERFACE_DESIGN_RULES=Object.freeze([
|
|
|
68
71
|
]);
|
|
69
72
|
|
|
70
73
|
const occurrences=(source,pattern)=>(source.match(pattern)||[]).length;
|
|
71
|
-
// A file input
|
|
74
|
+
// A file input of the view's own, shown or hidden in a label tile (November 3, 2026: a hidden one behind a label
|
|
75
|
+
// still skips StillMade's picker, its working copies and device media). Files are added with the sm-add tile.
|
|
72
76
|
// One forward scan with indexOf, so odd source never makes it slow.
|
|
73
|
-
function
|
|
77
|
+
function ownFileInput(html){
|
|
74
78
|
const lower=html.toLowerCase();
|
|
75
79
|
for(let at=lower.indexOf('<input');at!==-1;at=lower.indexOf('<input',at+6)){
|
|
76
80
|
const end=lower.indexOf('>',at);if(end===-1)return false;
|
|
77
|
-
|
|
78
|
-
if(!/\btype\s*=\s*["']?file\b/.test(tag)||/\bhidden\b|display\s*:\s*none|\bsm-sr\b/.test(tag))continue;
|
|
79
|
-
const label=lower.lastIndexOf('<label',at),closed=lower.lastIndexOf('</label>',at);
|
|
80
|
-
if(label===-1||closed>label)return true;
|
|
81
|
+
if(/\btype\s*=\s*["']?file\b/.test(lower.slice(at,end)))return true;
|
|
81
82
|
}
|
|
82
83
|
return false;
|
|
83
84
|
}
|
|
@@ -248,6 +249,30 @@ function quotedText(javascript){
|
|
|
248
249
|
}
|
|
249
250
|
return strings;
|
|
250
251
|
}
|
|
252
|
+
// November 3, 2026 (owner, of a Step whose settings floated on the page: "make sure it's more separated"): what the person
|
|
253
|
+
// sets sits in one group (.sm-fields, or .sm-settings for short ones), which the kit puts on its own surface. Settings
|
|
254
|
+
// loose on the page are counted in one pass over the tags, never by a regular expression that could backtrack.
|
|
255
|
+
const SETTING_GROUPS=/\b(?:sm-fields|sm-settings|sm-more|sm-list|sm-actions)\b/;
|
|
256
|
+
export function looseFields(html){
|
|
257
|
+
const stack=[],source=String(html||'');let loose=0,grouped=0;
|
|
258
|
+
for(let start=source.indexOf('<');start!==-1;start=source.indexOf('<',start+1)){
|
|
259
|
+
const end=source.indexOf('>',start);if(end===-1)break;
|
|
260
|
+
const tag=source.slice(start+1,end),closing=tag[0]==='/',name=(closing?tag.slice(1):tag).split(/[\s/>]/)[0].toLowerCase();
|
|
261
|
+
if(!/^[a-z][a-z0-9-]*$/.test(name)){start=end;continue;}
|
|
262
|
+
if(closing){
|
|
263
|
+
// Close back to the matching tag, usually the top of the stack; an unmatched close is ignored.
|
|
264
|
+
let at=stack.length-1;while(at>=0&&stack.length-at<64&&stack[at].name!==name)at--;
|
|
265
|
+
if(at>=0&&stack[at].name!==name)at=-1;
|
|
266
|
+
if(at!==-1)while(stack.length>at){if(stack.pop().grouped)grouped--;}
|
|
267
|
+
start=end;continue;
|
|
268
|
+
}
|
|
269
|
+
const classes=/\bclass\s*=\s*(["'])([^"']*)\1/i.exec(tag)?.[2]||'',group=name==='fieldset'&&!/\bsm-choices\b/.test(classes)||SETTING_GROUPS.test(classes);
|
|
270
|
+
if(/(?:^|\s)sm-field(?:\s|$)/.test(classes)&&!grouped)loose++;
|
|
271
|
+
if(!VOID_TAGS.has(name)&&!tag.endsWith('/')){stack.push({name,grouped:group});if(group)grouped++;}
|
|
272
|
+
start=end;
|
|
273
|
+
}
|
|
274
|
+
return loose;
|
|
275
|
+
}
|
|
251
276
|
function jobWordWarnings(view,primary){
|
|
252
277
|
const warnings=[],lower=primary.toLowerCase();
|
|
253
278
|
for(let start=lower.indexOf('<button'),close=0;start!==-1;start=lower.indexOf('<button',close+9)){
|
|
@@ -261,6 +286,8 @@ function jobWordWarnings(view,primary){
|
|
|
261
286
|
if(fadedPrimary(view.css))warnings.push('Wording: the CSS gives .sm-primary the page\'s background, so the main button blends in. Keep the kit\'s solid ink button.');
|
|
262
287
|
if(hiddenPick(view.css))warnings.push('Wording: the CSS restyles the picked choice (.sm-choices label:has(input:checked)), so the pick no longer reads at a glance. Keep the kit\'s ink fill and its text colour.');
|
|
263
288
|
const kept=DEVICE_STATE.exec(primary.replace(/<[^<>]*>/g,' ')+'\n'+quotedText(String(view.javascript||'')));
|
|
289
|
+
const loose=looseFields(primary);
|
|
290
|
+
if(loose>=3)warnings.push(`Wording: ${loose} settings sit loose on the page. Put what the person sets in one <div class="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 apart.`);
|
|
264
291
|
if(kept)warnings.push(`Wording: the screen says where a file is kept ("${kept[0]}"). Show a picture or clip that cannot show here as a quiet <span class="sm-media-off"> tile with no words; StillMade says why once, as a notification.`);
|
|
265
292
|
return warnings;
|
|
266
293
|
}
|
|
@@ -282,8 +309,8 @@ export function reviewViewDesign(view){
|
|
|
282
309
|
const longText=[...primary.matchAll(/<p\b[^>]*>([\s\S]*?)<\/p>/gi)].filter(match=>words(match[1])>20).length;
|
|
283
310
|
if(longText)warnings.push('A visible paragraph runs past twenty words. Drop words nobody needs; put helpful explanation in a closed <details class="sm-hint">.');
|
|
284
311
|
if(/<select\b/i.test(primary)&&/<(?:img|video|audio)\b/i.test(primary)===false&&/\b(?:voice|font|look|style|color|colour|template|theme|music|sound)\b/i.test(primary))warnings.push('People choose something they see or hear from a word list. Show each option as a picture or sample in <fieldset class="sm-choices">.');
|
|
285
|
-
// Compact:
|
|
286
|
-
if(
|
|
312
|
+
// Compact: the view's own file picker and printed size limits each take rows nobody needs.
|
|
313
|
+
if(ownFileInput(primary))warnings.push('The view has a file input of its own. Show files as tiles: a thumbnail or chip for each saved file and the + tile, <button class="sm-add" data-stillmade-add="<input>" aria-label="Add photos">, which opens StillMade\'s picker and saves the files.');
|
|
287
314
|
if(/\bup to \d+\s*(?:mb|gb|kb)\b/i.test(primary.replace(/<[^<>]*>/g,' ')))warnings.push('A size limit is printed in view. Say it only when a file is too big.');
|
|
288
315
|
if(stackedLabel(primary,view.css))warnings.push('A short setting has its label above the control. Put the label beside it: <div class="sm-field"><label for="x">Label</label><input id="x"></div>, or several side by side in .sm-settings.');
|
|
289
316
|
const rareTool=rareToolSection(html);
|
|
@@ -302,12 +329,14 @@ export function reviewViewDesign(view){
|
|
|
302
329
|
|
|
303
330
|
// October 30, 2026: Chat presses a button on a Block's own screen only when it carries data-stillmade-action
|
|
304
331
|
// (naming what it does); an unmarked button is out of Chat's reach. The number of buttons in view.html without one.
|
|
332
|
+
// The + tile (data-stillmade-add) is not counted: StillMade's picker opens only from the person's own click.
|
|
305
333
|
export function unmarkedButtons(view){
|
|
306
334
|
const html=String(view?.html||''),lower=html.toLowerCase();let count=0;
|
|
307
335
|
for(let start=lower.indexOf('<button');start!==-1;start=lower.indexOf('<button',start+7)){
|
|
308
336
|
if(!/[\s>/]/.test(lower[start+7]||''))continue;
|
|
309
337
|
const end=lower.indexOf('>',start);if(end===-1)break;
|
|
310
|
-
|
|
338
|
+
const tag=lower.slice(start,end);
|
|
339
|
+
if(!/\sdata-stillmade-(?:action|add)\s*=/.test(tag))count++;
|
|
311
340
|
}
|
|
312
341
|
return count;
|
|
313
342
|
}
|
|
@@ -3,12 +3,12 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
3
3
|
// the search; the Block names only what to read and what to do with it. Both
|
|
4
4
|
// are paid with StillMade credits at the app's research prices.
|
|
5
5
|
export const webFetch={
|
|
6
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.summarize-page',version:'1.0.0',name:'Summarize a page',description:'Read a public web page and summarize it in a few production-ready bullet points.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{url:{type:'text',primary:true,semantic:'page_url',required:true,description:'Public http or https page to read.'}},outputs:{summary:{type:'text',primary:true,semantic:'page_summary',description:'Summary of the page.'}},permissions:{project:[],capabilities:['web.fetch'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'url'}]},
|
|
6
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.summarize-page',action:{label:'Summarize page',running:'Summarizing page…'},version:'1.0.0',name:'Summarize a page',description:'Read a public web page and summarize it in a few production-ready bullet points.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{url:{type:'text',primary:true,semantic:'page_url',required:true,description:'Public http or https page to read.'}},outputs:{summary:{type:'text',primary:true,semantic:'page_summary',description:'Summary of the page.'}},permissions:{project:[],capabilities:['web.fetch'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'url'}]},
|
|
7
7
|
capability:{schemaVersion:1,operation:'web.fetch',url:{$input:'url'},instruction:'Summarize the page in five short bullet points a video producer can use. Quote numbers exactly.',output:'summary'},
|
|
8
8
|
tests:[{name:'Read a public page',input:{url:'https://en.wikipedia.org/wiki/Tide'},expectations:{summary:{kind:'text',minLength:20,maxLength:4000}}}],
|
|
9
9
|
};
|
|
10
10
|
export const webResearch={
|
|
11
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.research-topic',version:'1.0.0',name:'Research a topic',description:'Search the web for a topic and return a cited summary.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{topic:{type:'text',primary:true,semantic:'research_topic',required:true,description:'What to research.'}},outputs:{findings:{type:'json',primary:true,semantic:'research_findings',description:'Summary and the sources it came from.'}},permissions:{project:[],capabilities:['web.research'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'topic'}]},
|
|
11
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.research-topic',action:{label:'Research topic',running:'Researching topic…'},version:'1.0.0',name:'Research a topic',description:'Search the web for a topic and return a cited summary.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{topic:{type:'text',primary:true,semantic:'research_topic',required:true,description:'What to research.'}},outputs:{findings:{type:'json',primary:true,semantic:'research_findings',description:'Summary and the sources it came from.',schema:{summary:'text',sources:'json[]'}}},permissions:{project:[],capabilities:['web.research'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'topic'}]},
|
|
12
12
|
capability:{schemaVersion:1,operation:'web.research',topic:{$input:'topic'},output:'findings'},
|
|
13
13
|
tests:[{name:'Tides',input:{topic:'How tides work'},expectations:{findings:{kind:'json',includes:['summary']}}}],
|
|
14
14
|
};
|