stillmade-sdk 0.1.46 → 0.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.stillmade/sdk-baseline.json +1 -1
- package/AGENTS.md +6 -2
- package/FRAME_VIEWS.md +71 -5
- package/INTERFACE_DESIGN.md +105 -15
- package/MODULES.md +26 -3
- package/PASTE_TO_LLM.md +27 -10
- package/README.md +15 -0
- package/SDK.md +393 -64
- package/UI_AESTHETIC.md +13 -0
- package/blocks/native/sm.board-editor/versions/3.0.0/package.js +921 -0
- package/blocks/native/sm.editor/versions/3.0.0/package.js +751 -0
- package/blocks/native/sm.panel-strip/versions/3.0.0/package.js +350 -0
- package/blocks/native/sm.photo-editor/versions/1.0.0/package.js +6693 -0
- package/blocks/native/sm.voiceover/versions/3.0.0/package.js +513 -0
- package/docs/build/block-builder-contract.md +2 -2
- package/docs/build/custom-interface.md +23 -3
- package/docs/build/frame-views.md +71 -5
- package/docs/build/hosted-text.md +8 -3
- package/docs/build/interface-design.md +105 -15
- package/docs/build/media-analysis.md +18 -1
- package/docs/build/modules.md +26 -3
- package/docs/build/with-your-ai.md +14 -0
- package/docs/build/workspaces.md +42 -0
- package/docs/reference/authoring-guide.md +5 -4
- package/docs/reference/cli.md +8 -3
- package/docs/reference/execution.md +13 -5
- package/docs/reference/first-party-blocks.md +5 -4
- package/docs/reference/generation-models.md +4 -0
- package/docs/reference/limits.md +9 -2
- package/docs/reference/permissions.md +4 -0
- package/docs/reference/view-bridge.md +6 -3
- package/docs/reference/view-rules.md +2 -1
- package/docs/tools/llm.md +27 -10
- package/examples/custom-interface.stillmade.json +2 -2
- package/package.json +1 -1
- package/packages/block-cli/cli.js +124 -12
- package/packages/block-cli/dev-host/assets/host.js +63 -37
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-cli/dev-server.js +74 -23
- package/packages/block-cli/format.js +20 -1
- package/packages/block-cli/ready-gate.js +34 -2
- package/packages/block-cli/templates.js +61 -0
- package/packages/block-cli/type-packages.js +7 -4
- package/packages/block-cli/workspace-dev.js +124 -0
- package/packages/block-cli/workspace-pack.js +62 -0
- package/packages/block-cli/workspace.js +304 -0
- package/packages/block-platform/block-readiness.js +5 -4
- package/packages/block-platform/catalog.js +5 -3
- package/packages/block-platform/node-defaults.js +2 -2
- package/packages/block-platform/plain-descriptions.js +1 -0
- package/packages/block-platform/type-workspace.js +8 -0
- package/packages/block-sdk/admission.js +2 -2
- package/packages/block-sdk/builder-contract.js +3 -3
- package/packages/block-sdk/capabilities.js +32 -12
- package/packages/block-sdk/contracts.js +11 -2
- package/packages/block-sdk/examples.js +2 -2
- package/packages/block-sdk/first-party-identities.js +9 -0
- package/packages/block-sdk/first-party-packages.js +9 -1
- package/packages/block-sdk/folder-package.js +34 -2
- package/packages/block-sdk/generation-catalog.js +7 -0
- package/packages/block-sdk/index.d.ts +13 -5
- package/packages/block-sdk/index.js +1 -1
- package/packages/block-sdk/mock-host.js +7 -0
- package/packages/block-sdk/module-hosted.js +11 -1
- package/packages/block-sdk/module-media-access.js +5 -3
- package/packages/block-sdk/permission-summary.js +1 -1
- package/packages/block-sdk/portable-media-actions.d.ts +4 -1
- package/packages/block-sdk/portable-media-actions.js +16 -1
- package/packages/block-sdk/remix-policy.js +2 -2
- package/packages/block-sdk/staged-releases.js +5 -0
- package/packages/block-sdk/stillmade-pack.js +64 -0
- package/packages/block-sdk/testing.js +2 -2
- package/packages/block-sdk/view-bridge.js +6 -3
- package/packages/block-sdk/view-design.js +65 -17
- package/packages/block-sdk/view-renderer.js +17 -1
- package/packages/block-sdk/view.js +47 -9
- package/packages/design-system/block-kit.css +23 -0
- package/packages/design-system/theme.css +10 -7
|
@@ -6,11 +6,17 @@ import {validateConnectedCapability,prepareConnectedInvocation,validateConnected
|
|
|
6
6
|
import {IMAGE_GENERATION_MODELS,VIDEO_GENERATION_MODELS,GENERATION_LIMITS,imageSettings,videoSettings,validateSeed} from './generation-catalog.js';
|
|
7
7
|
|
|
8
8
|
export const CAPABILITY_LIMITS=Object.freeze({prompt:32000,contextBytes:24000,system:8000,output:32768,maxTokens:4096,fixtures:3,includes:8,includeLength:256,packageBytes:262144,metadataBytes:8192});
|
|
9
|
+
// text.generate takes long structured jobs too (November 6, 2026: a panel plan's instructions are 19,400 characters and its
|
|
10
|
+
// reply up to 24,000 tokens). Its price grows with the size a call may reach (packages/credit-pricing textCallCredits).
|
|
11
|
+
export const TEXT_GENERATE_LIMITS=Object.freeze({system:32000,prompt:64000,maxTokens:24000,output:131072});
|
|
9
12
|
export const SPEECH_CAPABILITY_LIMITS=Object.freeze({text:3800,duration:1400,bytes:67108864,sampleRate:24000,channels:1});
|
|
10
13
|
// Measured catalog audio (voices, music, sound effects): MP3 or WAV as generated.
|
|
11
14
|
// The host reads the page or searches; the Block only names what to read and why.
|
|
12
15
|
// media.analyze: up to four images, or one video with one closer-inspected range of at most 12 seconds.
|
|
13
16
|
export const MEDIA_ANALYZE_LIMITS=Object.freeze({images:4,question:2000,range:12});
|
|
17
|
+
// reply:"text" (November 6, 2026): the Block's own instructions and question over one to ten pictures, answered in one
|
|
18
|
+
// vision call as text (a panel's text placement, a style check across frames), priced per picture like an analysis.
|
|
19
|
+
export const MEDIA_REPLY_LIMITS=Object.freeze({images:10,question:8000,instructions:8000,maxTokens:4000});
|
|
14
20
|
// October 30, 2026: detail:"check" is a photo check (owner's option C): images only, at most four a call, each read
|
|
15
21
|
// smaller and answered in a short summary, priced per call rather than per image (server tiers.js 'media:check').
|
|
16
22
|
const MEDIA_ANALYZE_DETAILS=Object.freeze(['check']);
|
|
@@ -202,10 +208,16 @@ export function validateCapability(capability,manifest){
|
|
|
202
208
|
return true;
|
|
203
209
|
}
|
|
204
210
|
if(operation==='media.analyze'){
|
|
205
|
-
assert(only(capability,['schemaVersion','operation','media','question','range','detail','output'])&&['schemaVersion','operation','media','question','output'].every(key=>own(capability,key))&&capability.schemaVersion===1&&capability.operation===operation,'CAPABILITY_SCHEMA','Declare schema 1 media.analyze with media, question and output, and optionally a range {start,end} of number inputs for a video. Payment belongs to the host.');
|
|
211
|
+
assert(only(capability,['schemaVersion','operation','media','question','range','detail','reply','instructions','maxTokens','output'])&&['schemaVersion','operation','media','question','output'].every(key=>own(capability,key))&&capability.schemaVersion===1&&capability.operation===operation,'CAPABILITY_SCHEMA','Declare schema 1 media.analyze with media, question and output, and optionally a range {start,end} of number inputs for a video, or reply:"text" with instructions and maxTokens. Payment belongs to the host.');
|
|
212
|
+
const reply=own(capability,'reply');
|
|
213
|
+
if(reply){
|
|
214
|
+
assert(capability.reply==='text'&&['image','image[]'].includes(manifest.inputs[capability.media?.$input]?.type)&&!own(capability,'range')&&!own(capability,'detail')&&manifest.outputs[capability.output]?.type==='text','CAPABILITY_SCHEMA','reply:"text" asks about one image or image[] input (never a video) and returns one text output; it takes no range or detail.');
|
|
215
|
+
assert(!own(capability,'instructions')||typeof capability.instructions==='string'&&capability.instructions.length<=MEDIA_REPLY_LIMITS.instructions,'CAPABILITY_LIMIT','Reply instructions contain at most 8,000 characters');
|
|
216
|
+
assert(!own(capability,'maxTokens')||Number.isInteger(capability.maxTokens)&&capability.maxTokens>=1&&capability.maxTokens<=MEDIA_REPLY_LIMITS.maxTokens,'CAPABILITY_LIMIT','Choose a reply token limit from 1 to 4,000');
|
|
217
|
+
}else assert(!own(capability,'instructions')&&!own(capability,'maxTokens'),'CAPABILITY_SCHEMA','instructions and maxTokens belong to reply:"text"');
|
|
206
218
|
if(own(capability,'detail'))assert(MEDIA_ANALYZE_DETAILS.includes(capability.detail)&&['image','image[]'].includes(manifest.inputs[capability.media?.$input]?.type)&&!own(capability,'range'),'CAPABILITY_SCHEMA','detail:"check" is a short photo check: it takes one image or image[] input (never a video) and no range.');
|
|
207
219
|
assert(exact(capability.media,['$input'])&&['image','image[]','video'].includes(manifest.inputs[capability.media.$input]?.type),'CAPABILITY_BINDING','media must reference the declared video, image or image[] input');
|
|
208
|
-
assert(typeof capability.question==='string'&&capability.question.trim().length>0&&capability.question.length<=MEDIA_ANALYZE_LIMITS.question,'CAPABILITY_LIMIT','The analysis question must contain 1 to 2,000 characters');
|
|
220
|
+
assert(typeof capability.question==='string'&&capability.question.trim().length>0&&capability.question.length<=(reply?MEDIA_REPLY_LIMITS.question:MEDIA_ANALYZE_LIMITS.question),'CAPABILITY_LIMIT',reply?'The reply question must contain 1 to 8,000 characters':'The analysis question must contain 1 to 2,000 characters');
|
|
209
221
|
const numbers=Object.keys(manifest.inputs).filter(name=>manifest.inputs[name].type==='number');
|
|
210
222
|
if(own(capability,'range'))assert(plain(capability.range)&&exact(capability.range,['start','end'])&&['start','end'].every(key=>exact(capability.range[key],['$input'])&&manifest.inputs[capability.range[key].$input]?.type==='number')&&capability.range.start.$input!==capability.range.end.$input,'CAPABILITY_BINDING','range binds start and end to two declared number inputs');
|
|
211
223
|
assert(numbers.length===(own(capability,'range')?2:0),'CAPABILITY_BINDING','Declare number inputs only as the bound start and end of range');
|
|
@@ -250,15 +262,15 @@ export function validateCapability(capability,manifest){
|
|
|
250
262
|
// Context reaches the model only when the Block lists it; an unlisted input
|
|
251
263
|
// (for example a saved script a view only displays) is never sent.
|
|
252
264
|
if(own(capability,'context'))assert(Array.isArray(capability.context)&&capability.context.length>0&&capability.context.length<=3&&new Set(capability.context).size===capability.context.length&&capability.context.every(name=>typeof name==='string'&&name!==capability.prompt.$input&&own(manifest.inputs,name)),'CAPABILITY_BINDING','context lists one to three other declared input names to send to the model');
|
|
253
|
-
assert(typeof capability.system==='string'&&capability.system.length<=
|
|
254
|
-
assert(Number.isInteger(capability.maxTokens)&&capability.maxTokens>=1&&capability.maxTokens<=
|
|
265
|
+
assert(typeof capability.system==='string'&&capability.system.length<=TEXT_GENERATE_LIMITS.system,'CAPABILITY_LIMIT','System instructions must contain at most 32,000 characters');
|
|
266
|
+
assert(Number.isInteger(capability.maxTokens)&&capability.maxTokens>=1&&capability.maxTokens<=TEXT_GENERATE_LIMITS.maxTokens,'CAPABILITY_LIMIT','Choose an output token limit from 1 to 24,000');
|
|
255
267
|
assert(typeof capability.output==='string'&&own(manifest.outputs,capability.output),'CAPABILITY_BINDING','Output must name the exact declared text or script port');
|
|
256
268
|
// Earlier turns of a conversation: one json input holding [{role, content}], sent before the prompt.
|
|
257
269
|
if(own(capability,'history'))assert(exact(capability.history,['$input'])&&manifest.inputs[capability.history.$input]?.type==='json'&&capability.history.$input!==capability.prompt.$input&&!(capability.context||[]).includes(capability.history.$input),'CAPABILITY_BINDING','history references one declared json input holding earlier conversation turns');
|
|
258
270
|
// Structured replies: a json output port pairs with the JSON schema the reply must match.
|
|
259
271
|
const json=manifest.outputs[capability.output].type==='json';
|
|
260
272
|
assert(json===own(capability,'schema'),'CAPABILITY_SCHEMA',json?'A json output needs capability.schema, the JSON schema its reply must match':'capability.schema needs a json output port');
|
|
261
|
-
if(json){validateJsonSchema(capability.schema);assert(textSystem(capability).length<=
|
|
273
|
+
if(json){validateJsonSchema(capability.schema);assert(textSystem(capability).length<=TEXT_GENERATE_LIMITS.system,'CAPABILITY_LIMIT','System instructions and the JSON schema together must contain at most 32,000 characters');}
|
|
262
274
|
const out=manifest.outputs[capability.output];
|
|
263
275
|
// A structured answer (a decision, score, extracted fields) is one object
|
|
264
276
|
// port with a flat field schema the host asks the model to fill.
|
|
@@ -320,14 +332,15 @@ export function prepareCapabilityInvocation(pkg,input){
|
|
|
320
332
|
const values=validateValues(pkg.manifest.inputs,input),name=pkg.capability.media.$input,type=pkg.manifest.inputs[name].type,raw=values[name];
|
|
321
333
|
const saved=(value,kind)=>{assert(plain(value)&&value.kind===kind&&['assetId','versionId','url'].every(key=>typeof value[key]==='string'&&value[key].trim()),'CAPABILITY_INPUT',`Media analysis needs a saved ${kind} with a URL`);return {kind,assetId:value.assetId,versionId:value.versionId,url:value.url};};
|
|
322
334
|
const list=type==='image[]'?raw:[raw];
|
|
323
|
-
|
|
335
|
+
const most=pkg.capability.reply==='text'?MEDIA_REPLY_LIMITS.images:MEDIA_ANALYZE_LIMITS.images;
|
|
336
|
+
assert(Array.isArray(list)&&list.length>=1&&list.length<=most,'CAPABILITY_INPUT',most===4?'Provide one to four images to analyze':'Provide one to ten images to ask about');
|
|
324
337
|
const media=list.map(value=>saved(value,type==='video'?'video':'image'));
|
|
325
338
|
let range={};
|
|
326
339
|
if(own(pkg.capability,'range')){
|
|
327
340
|
const start=values[pkg.capability.range.start.$input],end=values[pkg.capability.range.end.$input];
|
|
328
341
|
if(start!==undefined||end!==undefined){assert(Number.isFinite(start)&&Number.isFinite(end)&&start>=0&&end>start&&end-start<=MEDIA_ANALYZE_LIMITS.range+1e-6,'CAPABILITY_INPUT','A video range needs a start and a later end, at most 12 seconds apart');range={range:{start,end}};}
|
|
329
342
|
}
|
|
330
|
-
return {operation:'media.analyze',media,question:pkg.capability.question,...range,...(own(pkg.capability,'detail')?{detail:pkg.capability.detail}:{})};
|
|
343
|
+
return {operation:'media.analyze',media,question:pkg.capability.question,...range,...(own(pkg.capability,'detail')?{detail:pkg.capability.detail}:{}),...(pkg.capability.reply==='text'?{reply:'text',...(own(pkg.capability,'instructions')?{instructions:pkg.capability.instructions}:{}),...(own(pkg.capability,'maxTokens')?{maxTokens:pkg.capability.maxTokens}:{})}:{})};
|
|
331
344
|
}
|
|
332
345
|
if(pkg.capability.operation==='web.fetch'){
|
|
333
346
|
const values=validateValues(pkg.manifest.inputs,input),url=values[pkg.capability.url.$input];
|
|
@@ -357,7 +370,8 @@ export function prepareCapabilityInvocation(pkg,input){
|
|
|
357
370
|
const values=validateValues(pkg.manifest.inputs,input),name=pkg.capability.prompt.$input;
|
|
358
371
|
assert(own(values,name),'CAPABILITY_INPUT','Provide the declared prompt input or a default');
|
|
359
372
|
const prompt=textValue(values[name],pkg.manifest.inputs[name]);
|
|
360
|
-
|
|
373
|
+
const promptLimit=pkg.capability.operation==='text.generate'?TEXT_GENERATE_LIMITS.prompt:CAPABILITY_LIMITS.prompt;
|
|
374
|
+
assert(typeof prompt==='string'&&prompt.trim().length>0&&prompt.length<=promptLimit,'CAPABILITY_LIMIT',`Provide a nonempty prompt of at most ${promptLimit.toLocaleString('en-US')} characters`);
|
|
361
375
|
if(pkg.capability.operation==='image.generate'){
|
|
362
376
|
const raw=own(pkg.capability,'references')?values[pkg.capability.references.$input]:undefined,list=raw===undefined?[]:Array.isArray(raw)?raw:[raw];
|
|
363
377
|
assert(list.length<=GENERATION_LIMITS.references,'CAPABILITY_LIMIT','Use at most 8 reference images');
|
|
@@ -373,9 +387,9 @@ export function prepareCapabilityInvocation(pkg,input){
|
|
|
373
387
|
const instructionsName=pkg.capability.prompt.$instructions,instructions=instructionsName&&typeof values[instructionsName]==='string'&&values[instructionsName].trim()?values[instructionsName].trim():null;
|
|
374
388
|
const full=instructions?`Instructions:\n${instructions}\n\nInput:\n${prompt}`:prompt;
|
|
375
389
|
const withContext=full+projectContextSection(pkg.capability.context||[],values);
|
|
376
|
-
assert(withContext.length<=
|
|
390
|
+
assert(withContext.length<=TEXT_GENERATE_LIMITS.prompt,'CAPABILITY_LIMIT','The prompt and its project context exceed 64,000 characters. Shorten the prompt or choose less project context.');
|
|
377
391
|
const history=own(pkg.capability,'history')?conversationHistory(values[pkg.capability.history.$input]):[];
|
|
378
|
-
assert(withContext.length+history.reduce((sum,turn)=>sum+turn.content.length,0)<=
|
|
392
|
+
assert(withContext.length+history.reduce((sum,turn)=>sum+turn.content.length,0)<=TEXT_GENERATE_LIMITS.prompt,'CAPABILITY_LIMIT','The conversation and prompt exceed 64,000 characters. Send fewer earlier turns.');
|
|
379
393
|
const out=pkg.manifest.outputs[pkg.capability.output],base=textSystem(pkg.capability),system=out.type==='object'?`${base}\n\nReply with exactly one JSON object and nothing else, with these fields: ${Object.entries(out.schema).map(([key,type])=>`"${key}" (${type==='boolean'?'true or false':type==='string'?'text':'number'})`).join(', ')}.`:base;
|
|
380
394
|
return {operation:'text.generate',system,prompt:withContext,maxTokens:pkg.capability.maxTokens,...(history.length?{history}:{})};
|
|
381
395
|
}
|
|
@@ -418,6 +432,12 @@ export function capabilityOutputs(pkg,text){
|
|
|
418
432
|
const outputs={[pkg.capability.output]:edit};validateValues(pkg.manifest.outputs,outputs,'output');return outputs;
|
|
419
433
|
}
|
|
420
434
|
if(pkg.capability.operation==='media.analyze'){
|
|
435
|
+
// reply:"text": the host returns {reply}, the model's answer as text.
|
|
436
|
+
if(pkg.capability.reply==='text'){
|
|
437
|
+
const answer=text?.reply;
|
|
438
|
+
assert(plain(text)&&exact(text,['reply'])&&typeof answer==='string'&&answer.trim()&&answer.length<=TEXT_GENERATE_LIMITS.output,'CAPABILITY_OUTPUT','A picture reply returns one bounded text answer');
|
|
439
|
+
const outputs={[pkg.capability.output]:answer};validateValues(pkg.manifest.outputs,outputs,'output');return outputs;
|
|
440
|
+
}
|
|
421
441
|
// The host returns {items:[{kind,summary,...evidence}]}; a json port keeps it all, a text port keeps the summaries.
|
|
422
442
|
const result=structuredClone(text),items=result?.items;
|
|
423
443
|
assert(plain(result)&&exact(result,['items'])&&Array.isArray(items)&&items.length>=1&&items.length<=MEDIA_ANALYZE_LIMITS.images&&items.every(item=>plain(item)&&['image','video'].includes(item.kind)&&typeof item.summary==='string'&&item.summary.trim()&&item.summary.length<=8000),'CAPABILITY_OUTPUT','Media analysis returns one bounded summary per analyzed item');
|
|
@@ -441,7 +461,7 @@ export function capabilityOutputs(pkg,text){
|
|
|
441
461
|
validateGeneratedImageReference(text);
|
|
442
462
|
return {[pkg.capability.output]:structuredClone(text)};
|
|
443
463
|
}
|
|
444
|
-
assert(typeof text==='string'&&text.trim().length>0&&text.length<=
|
|
464
|
+
assert(typeof text==='string'&&text.trim().length>0&&text.length<=TEXT_GENERATE_LIMITS.output,'CAPABILITY_OUTPUT','The host must return nonempty generated text of at most 131,072 characters');
|
|
445
465
|
const name=pkg.capability.output,port=pkg.manifest.outputs[name];
|
|
446
466
|
if(port.type==='object'){
|
|
447
467
|
let value;try{value=JSON.parse(text.trim().replace(/^```(?:json)?\s*/i,'').replace(/\s*```$/,''));}catch{assert(false,'CAPABILITY_OUTPUT','The AI did not return the requested JSON fields');}
|
|
@@ -684,7 +704,7 @@ export function testCapabilityOutputs(manifest,outputs,expectations){
|
|
|
684
704
|
}
|
|
685
705
|
if(manifest.outputs[name].type==='object')return {passed:true,checks:[{output:name,kind:'object',fields:Object.keys(values[name]).length}]};
|
|
686
706
|
const text=textValue(values[name],manifest.outputs[name],{output:true}),expected=expectations[name];
|
|
687
|
-
assert(typeof text==='string'&&text.trim().length>0&&text.length<=
|
|
707
|
+
assert(typeof text==='string'&&text.trim().length>0&&text.length<=TEXT_GENERATE_LIMITS.output,'CAPABILITY_OUTPUT','The host must return nonempty generated text of at most 131,072 characters');
|
|
688
708
|
assert(text.length>=expected.minLength&&text.length<=expected.maxLength,'CAPABILITY_EXPECTATION',`${name} did not match the expected text length`);
|
|
689
709
|
for(const literal of expected.includes||[])assert(text.includes(literal),'CAPABILITY_EXPECTATION',`${name} did not contain an expected literal phrase`);
|
|
690
710
|
return {passed:true,checks:[{output:name,kind:manifest.outputs[name].type,length:text.length,includesMatched:expected.includes?.length||0}]};
|
|
@@ -31,7 +31,7 @@ const networkProblem=entries=>{try{networkDeclarations(entries);return '';}catch
|
|
|
31
31
|
const networkValid=entries=>!networkProblem(entries);
|
|
32
32
|
export const ROOT_CHAT_ID = 'stillmade.starting-chat';
|
|
33
33
|
export const TYPES = Object.freeze(['text', 'number', 'integer', 'boolean', 'json', 'object', 'image', 'video', 'audio', 'asset', 'script', 'transcript', 'brief', 'bible', 'shot-plan', 'panel-document', 'board-document', 'scene-document', 'timeline', 'timeline-edit', 'file-bundle', 'workspace-edit', 'timeline-range', 'research', 'character', 'location', 'style', 'pipeline', 'approval', 'scene', 'shot', 'mask', 'depth', 'pose', 'metadata', 'project-context', 'file', 'document', 'table', 'url', 'date', 'color']);
|
|
34
|
-
export const PORTABLE_ACTIONS = Object.freeze(['final.render','stock.search','presenter.generate','quality.analyze','image.generate','image.generate.batch','media.generate','youtube.upload']);
|
|
34
|
+
export const PORTABLE_ACTIONS = Object.freeze(['final.render','stock.search','stock.select','stock.auto','gif.search','gif.save','presenter.generate','quality.analyze','image.generate','image.generate.batch','media.generate','youtube.upload']);
|
|
35
35
|
// The composite kind (a Block made of other Blocks) is off for now (October 20, 2026, owner):
|
|
36
36
|
// a Block is one thing with its own compact interface. No built-in uses it; a package that
|
|
37
37
|
// declares it fails validation with 'Unsupported block kind'.
|
|
@@ -97,6 +97,13 @@ export function assertStorableText(value, path = [], depth = 0) {
|
|
|
97
97
|
assertStorableText(child, [...path, key], depth + 1);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
// Whether a package is a published first-party package. A first-party module package also carries its files' text
|
|
101
|
+
// (moduleSource, checked against their digests before it runs) while its SDK folder holds the same files, so the
|
|
102
|
+
// comparison leaves that copy out (November 6, 2026: Voiceover 3.0.0 is the first first-party module Block with a folder).
|
|
103
|
+
export function sameFirstPartySource(published,candidate){
|
|
104
|
+
const sourceless=value=>{if(!value||typeof value!=='object'||!Object.hasOwn(value,'moduleSource'))return value;const {moduleSource,...rest}=value;return rest;};
|
|
105
|
+
return !!published&&stableStringify(sourceless(published))===stableStringify(sourceless(candidate));
|
|
106
|
+
}
|
|
100
107
|
export function stableStringify(value) {
|
|
101
108
|
safeDocument(value);
|
|
102
109
|
const visit = v => Array.isArray(v) ? v.map(visit) : plain(v) ? Object.fromEntries(Object.keys(v).sort().map(k => [k, visit(v[k])])) : v;
|
|
@@ -258,7 +265,9 @@ export function validateManifest(manifest, { builtIn = false } = {}) {
|
|
|
258
265
|
check(manifest.ui.filter(other => plain(other) && UI_OUTPUT_PRESENTATIONS.includes(other.control) && other.port === c.port).length === 1, 'ui', 'An output must have one unambiguous presentation');
|
|
259
266
|
continue;
|
|
260
267
|
}
|
|
261
|
-
|
|
268
|
+
// Says which control is wrong and why (November 6, 2026: a coding agent's textarea control got only "Control must
|
|
269
|
+
// bind to a declared input" and had to guess).
|
|
270
|
+
check(plain(c) && UI_INPUT_CONTROLS.includes(c.control) && Object.hasOwn(manifest.inputs || {}, c.port), 'ui', !plain(c)?'Control must bind to a declared input':!UI_INPUT_CONTROLS.includes(c.control)?`Control must bind to a declared input: ${JSON.stringify(c.control)} is not a control. Use one of ${UI_INPUT_CONTROLS.join(', ')} (a text box of several lines is text with multiline: true)`:`Control must bind to a declared input: the ${c.control} control names ${JSON.stringify(c.port)}, which is not one of the inputs`);
|
|
262
271
|
// A text input of several lines (one fact per line, a list) declares it, so every host draws a box that keeps its line breaks.
|
|
263
272
|
if (plain(c) && c.multiline !== undefined) check(c.multiline === true && c.control === 'text' && manifest.inputs?.[c.port]?.type === 'text', 'ui', 'multiline: true goes on a text control bound to a text input');
|
|
264
273
|
if (plain(c) && Object.hasOwn(UI_CONTROL_TYPES, c.control)) check(UI_CONTROL_TYPES[c.control].includes(manifest.inputs?.[c.port]?.type), 'ui', `The ${c.control} control binds a ${UI_CONTROL_TYPES[c.control].join(', ')} input`);
|
|
@@ -22,8 +22,8 @@ export const customInterface = {
|
|
|
22
22
|
...textCleanup,
|
|
23
23
|
manifest:{...textCleanup.manifest,id:'example.clean-text-interface',version:'1.1.0',name:'Clean text studio',description:'Edit text, run the cleanup, and compare the result in a responsive self-contained workspace.'},
|
|
24
24
|
view:{
|
|
25
|
-
html:'<main class="sm-block" data-layout="split"><
|
|
26
|
-
css:'#
|
|
25
|
+
html:'<main class="sm-block" data-layout="split"><section class="sm-stage" id="stage" aria-live="polite"><div class="sm-actions"><button class="sm-primary" id="run" data-stillmade-action="run">Clean text</button></div><output id="result" data-stillmade-state="empty">Your result shows here</output></section><div class="sm-field"><label for="source">Text</label><textarea id="source" data-stillmade-input="text" data-stillmade-share="text"></textarea></div></main>',
|
|
26
|
+
css:'#stage{min-height:160px}#result{justify-self:stretch;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--fg)}#result[data-stillmade-state="empty"]{justify-self:center;color:var(--fg-muted)}',
|
|
27
27
|
javascript:'const source=document.getElementById("source"), button=document.getElementById("run"), result=document.getElementById("result"); StillMade.onInput(input=>{source.value=input.text||"";document.querySelector("main").dataset.stillmadeState=source.value?"populated":"empty"}); button.addEventListener("click",async()=>{button.disabled=true;result.dataset.stillmadeState="loading";result.textContent="Cleaning…";try{const output=await StillMade.run({text:source.value});result.dataset.stillmadeState="success";result.textContent=output.text||"Empty result"}catch(error){result.dataset.stillmadeState="error";result.textContent=error.message}finally{button.disabled=false}});',
|
|
28
28
|
},
|
|
29
29
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
5
5
|
"sm.ai-decide@1.0.0",
|
|
6
6
|
"sm.ai-write@1.0.0",
|
|
7
|
+
"sm.board-editor.hosted@3.0.0",
|
|
7
8
|
"sm.board-editor@2.0.0",
|
|
8
9
|
"sm.board-editor@2.1.0",
|
|
9
10
|
"sm.board-editor@2.2.0",
|
|
@@ -18,6 +19,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
18
19
|
"sm.board-editor@2.2.7",
|
|
19
20
|
"sm.board-editor@2.2.8",
|
|
20
21
|
"sm.board-editor@2.2.9",
|
|
22
|
+
"sm.board-editor@3.0.0",
|
|
21
23
|
"sm.captions@2.0.0",
|
|
22
24
|
"sm.character@2.0.0",
|
|
23
25
|
"sm.character@2.0.1",
|
|
@@ -29,6 +31,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
29
31
|
"sm.edit-assistant@2.0.0",
|
|
30
32
|
"sm.edit-assistant@2.0.1",
|
|
31
33
|
"sm.edit-assistant@2.0.2",
|
|
34
|
+
"sm.editor.hosted@3.0.0",
|
|
32
35
|
"sm.editor@2.0.0",
|
|
33
36
|
"sm.editor@2.1.0",
|
|
34
37
|
"sm.editor@2.2.0",
|
|
@@ -44,6 +47,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
44
47
|
"sm.editor@2.2.7",
|
|
45
48
|
"sm.editor@2.2.8",
|
|
46
49
|
"sm.editor@2.2.9",
|
|
50
|
+
"sm.editor@3.0.0",
|
|
47
51
|
"sm.export-audio@2.0.0",
|
|
48
52
|
"sm.export-timeline@2.0.0",
|
|
49
53
|
"sm.export-video@2.0.0",
|
|
@@ -65,6 +69,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
65
69
|
"sm.location@2.0.1",
|
|
66
70
|
"sm.meta-creative@1.0.0",
|
|
67
71
|
"sm.motion-graphics@2.0.0",
|
|
72
|
+
"sm.panel-strip.hosted@3.0.0",
|
|
68
73
|
"sm.panel-strip@2.0.0",
|
|
69
74
|
"sm.panel-strip@2.1.0",
|
|
70
75
|
"sm.panel-strip@2.2.0",
|
|
@@ -79,6 +84,8 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
79
84
|
"sm.panel-strip@2.4.0",
|
|
80
85
|
"sm.panel-strip@2.4.1",
|
|
81
86
|
"sm.panel-strip@2.4.2",
|
|
87
|
+
"sm.panel-strip@3.0.0",
|
|
88
|
+
"sm.photo-editor@1.0.0",
|
|
82
89
|
"sm.photo-notes@1.0.0",
|
|
83
90
|
"sm.photo-notes@1.1.0",
|
|
84
91
|
"sm.pipeline@2.0.0",
|
|
@@ -105,6 +112,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
105
112
|
"sm.video-assets@2.1.2",
|
|
106
113
|
"sm.video-assets@2.1.3",
|
|
107
114
|
"sm.video-assets@2.1.4",
|
|
115
|
+
"sm.voiceover.hosted@3.0.0",
|
|
108
116
|
"sm.voiceover@2.0.0",
|
|
109
117
|
"sm.voiceover@2.1.0",
|
|
110
118
|
"sm.voiceover@2.1.1",
|
|
@@ -113,6 +121,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
113
121
|
"sm.voiceover@2.1.4",
|
|
114
122
|
"sm.voiceover@2.1.5",
|
|
115
123
|
"sm.voiceover@2.1.6",
|
|
124
|
+
"sm.voiceover@3.0.0",
|
|
116
125
|
"sm.when@1.0.0",
|
|
117
126
|
"sm.when@1.1.0",
|
|
118
127
|
"sm.write-summary@1.0.0",
|
|
@@ -5,6 +5,7 @@ import importMediaV202 from '../../blocks/native/sm.import-media/versions/2.0.2/
|
|
|
5
5
|
import editorV229 from '../../blocks/native/sm.editor/versions/2.2.9/package.js';
|
|
6
6
|
import editorV2210 from '../../blocks/native/sm.editor/versions/2.2.10/package.js';
|
|
7
7
|
import editorV2211 from '../../blocks/native/sm.editor/versions/2.2.11/package.js';
|
|
8
|
+
import editorV300 from '../../blocks/native/sm.editor/versions/3.0.0/package.js';
|
|
8
9
|
import editorV2212 from '../../blocks/native/sm.editor/versions/2.2.12/package.js';
|
|
9
10
|
import captions from '../../blocks/native/sm.captions/versions/2.0.0/package.js';
|
|
10
11
|
import exportTimeline from '../../blocks/native/sm.export-timeline/versions/2.0.0/package.js';
|
|
@@ -40,6 +41,8 @@ import voiceoverV213 from '../../blocks/native/sm.voiceover/versions/2.1.3/packa
|
|
|
40
41
|
import voiceoverV214 from '../../blocks/native/sm.voiceover/versions/2.1.4/package.js';
|
|
41
42
|
import voiceoverV215 from '../../blocks/native/sm.voiceover/versions/2.1.5/package.js';
|
|
42
43
|
import voiceoverV216 from '../../blocks/native/sm.voiceover/versions/2.1.6/package.js';
|
|
44
|
+
// 3.0.0 (November 6, 2026): the original Voiceover screen as an SDK module Block with a frame view.
|
|
45
|
+
import voiceoverV300 from '../../blocks/native/sm.voiceover/versions/3.0.0/package.js';
|
|
43
46
|
import generateVideo from '../../blocks/native/sm.generate-video/versions/2.0.0/package.js';
|
|
44
47
|
import generateVideoV201 from '../../blocks/native/sm.generate-video/versions/2.0.1/package.js';
|
|
45
48
|
import character from '../../blocks/native/sm.character/versions/2.0.0/package.js';
|
|
@@ -100,6 +103,8 @@ import panelStripV237 from '../../blocks/native/sm.panel-strip/versions/2.3.7/pa
|
|
|
100
103
|
import panelStripV240 from '../../blocks/native/sm.panel-strip/versions/2.4.0/package.js';
|
|
101
104
|
import panelStripV241 from '../../blocks/native/sm.panel-strip/versions/2.4.1/package.js';
|
|
102
105
|
import panelStripV242 from '../../blocks/native/sm.panel-strip/versions/2.4.2/package.js';
|
|
106
|
+
// 3.0.0 (November 6, 2026): the original Image Panels screen as an SDK module Block with a frame view.
|
|
107
|
+
import panelStripV300 from '../../blocks/native/sm.panel-strip/versions/3.0.0/package.js';
|
|
103
108
|
import boardEditorV210 from '../../blocks/native/sm.board-editor/versions/2.1.0/package.js';
|
|
104
109
|
import boardEditorV220 from '../../blocks/native/sm.board-editor/versions/2.2.0/package.js';
|
|
105
110
|
import boardEditorV221 from '../../blocks/native/sm.board-editor/versions/2.2.1/package.js';
|
|
@@ -113,12 +118,15 @@ import boardEditorV228 from '../../blocks/native/sm.board-editor/versions/2.2.8/
|
|
|
113
118
|
import boardEditorV229 from '../../blocks/native/sm.board-editor/versions/2.2.9/package.js';
|
|
114
119
|
import boardEditorV2210 from '../../blocks/native/sm.board-editor/versions/2.2.10/package.js';
|
|
115
120
|
import boardEditorV2211 from '../../blocks/native/sm.board-editor/versions/2.2.11/package.js';
|
|
121
|
+
import boardEditorV300 from '../../blocks/native/sm.board-editor/versions/3.0.0/package.js';
|
|
116
122
|
|
|
117
123
|
import photoNotes from '../../blocks/native/sm.photo-notes/versions/1.0.0/package.js';
|
|
118
124
|
import photoNotesV110 from '../../blocks/native/sm.photo-notes/versions/1.1.0/package.js';
|
|
125
|
+
// November 6, 2026: a photo editor made by StillMade, an app-mode Block (kind "workspace").
|
|
126
|
+
import photoEditor from '../../blocks/native/sm.photo-editor/versions/1.0.0/package.js';
|
|
119
127
|
|
|
120
128
|
// Exact, independently executable first-party releases. Host workspaces do not
|
|
121
129
|
// belong here. Changing implementation requires a new immutable version.
|
|
122
130
|
const freeze=value=>{if(value&&typeof value==='object'&&!Object.isFrozen(value)){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
|
|
123
|
-
export const FIRST_PARTY_SDK_PACKAGES = Object.freeze([freeze(generate),freeze(photoNotesV110),freeze(photoNotes),freeze(exportVideoV210),freeze(exportVideo),freeze(exportAudio),freeze(motionGraphics),freeze(audioScore),freeze(importMediaV202),freeze(importMediaV201),freeze(editorV2212),freeze(editorV2211),freeze(editorV2210),freeze(editorV229),freeze(editorV228),freeze(panelStripV242),freeze(panelStripV241),freeze(panelStripV240),freeze(panelStripV237),freeze(panelStripV236),freeze(boardEditorV2211),freeze(boardEditorV2210),freeze(boardEditorV229),freeze(pipelineV226),freeze(pipelineV225),freeze(pipelineV224),freeze(videoAssetsV214),freeze(videoAssetsV213),freeze(voiceoverV216),freeze(voiceoverV215),freeze(voiceoverV214),freeze(voiceoverV213),freeze(roughCutV202),freeze(roughCutV201),freeze(finalEditV202),freeze(finalEditV201),freeze(editAssistantV202),freeze(editAssistantV201),freeze(gradeV202),freeze(gradeV201),freeze(transitionV202),freeze(transitionV201),freeze(compareV202),freeze(compareV201),freeze(compare),freeze(grade),freeze(transition),freeze(exportTimeline),freeze(captions),freeze(importMedia),freeze(generateImageV210),freeze(generateImage),freeze(writeSummary),freeze(automationV110),freeze(automationWhen),freeze(automationAiDecide),freeze(connectApp),freeze(automationContactFields),freeze(automationAiWrite),freeze(automationMetaCreative),freeze(voiceover),freeze(voiceoverV210),freeze(voiceoverV211),freeze(voiceoverV212),freeze(generateVideoV201),freeze(generateVideo),freeze(characterV201),freeze(character),freeze(locationV201),freeze(location),freeze(styleV201),freeze(style),freeze(videoAssets),freeze(videoAssetsV210),freeze(videoAssetsV211),freeze(videoAssetsV212),freeze(roughCut),freeze(finalEdit),freeze(editAssistant),freeze(boardEditor),freeze(boardEditorV210),freeze(boardEditorV220),freeze(boardEditorV221),freeze(boardEditorV222),freeze(boardEditorV223),freeze(boardEditorV224),freeze(boardEditorV225),freeze(boardEditorV226),freeze(boardEditorV227),freeze(boardEditorV228),freeze(panelStripV200),freeze(panelStrip),freeze(panelStripV220),freeze(panelStripV230),freeze(panelStripV231),freeze(panelStripV232),freeze(panelStripV233),freeze(panelStripV234),freeze(panelStripV235),freeze(editor),freeze(editorV210),freeze(editorV220),freeze(editorV221),freeze(editorV222),freeze(editorV223),freeze(editorV224),freeze(editorV225),freeze(editorV226),freeze(editorV227),freeze(pipelineV200),freeze(pipeline),freeze(pipelineV220),freeze(pipelineV221),freeze(pipelineV223),freeze(pipelineV222) ]);
|
|
131
|
+
export const FIRST_PARTY_SDK_PACKAGES = Object.freeze([freeze(generate),freeze(photoEditor),freeze(photoNotesV110),freeze(photoNotes),freeze(exportVideoV210),freeze(exportVideo),freeze(exportAudio),freeze(motionGraphics),freeze(audioScore),freeze(importMediaV202),freeze(importMediaV201),freeze(editorV300),freeze(editorV2212),freeze(editorV2211),freeze(editorV2210),freeze(editorV229),freeze(editorV228),freeze(panelStripV300),freeze(panelStripV242),freeze(panelStripV241),freeze(panelStripV240),freeze(panelStripV237),freeze(panelStripV236),freeze(boardEditorV300),freeze(boardEditorV2211),freeze(boardEditorV2210),freeze(boardEditorV229),freeze(pipelineV226),freeze(pipelineV225),freeze(pipelineV224),freeze(videoAssetsV214),freeze(videoAssetsV213),freeze(voiceoverV300),freeze(voiceoverV216),freeze(voiceoverV215),freeze(voiceoverV214),freeze(voiceoverV213),freeze(roughCutV202),freeze(roughCutV201),freeze(finalEditV202),freeze(finalEditV201),freeze(editAssistantV202),freeze(editAssistantV201),freeze(gradeV202),freeze(gradeV201),freeze(transitionV202),freeze(transitionV201),freeze(compareV202),freeze(compareV201),freeze(compare),freeze(grade),freeze(transition),freeze(exportTimeline),freeze(captions),freeze(importMedia),freeze(generateImageV210),freeze(generateImage),freeze(writeSummary),freeze(automationV110),freeze(automationWhen),freeze(automationAiDecide),freeze(connectApp),freeze(automationContactFields),freeze(automationAiWrite),freeze(automationMetaCreative),freeze(voiceover),freeze(voiceoverV210),freeze(voiceoverV211),freeze(voiceoverV212),freeze(generateVideoV201),freeze(generateVideo),freeze(characterV201),freeze(character),freeze(locationV201),freeze(location),freeze(styleV201),freeze(style),freeze(videoAssets),freeze(videoAssetsV210),freeze(videoAssetsV211),freeze(videoAssetsV212),freeze(roughCut),freeze(finalEdit),freeze(editAssistant),freeze(boardEditor),freeze(boardEditorV210),freeze(boardEditorV220),freeze(boardEditorV221),freeze(boardEditorV222),freeze(boardEditorV223),freeze(boardEditorV224),freeze(boardEditorV225),freeze(boardEditorV226),freeze(boardEditorV227),freeze(boardEditorV228),freeze(panelStripV200),freeze(panelStrip),freeze(panelStripV220),freeze(panelStripV230),freeze(panelStripV231),freeze(panelStripV232),freeze(panelStripV233),freeze(panelStripV234),freeze(panelStripV235),freeze(editor),freeze(editorV210),freeze(editorV220),freeze(editorV221),freeze(editorV222),freeze(editorV223),freeze(editorV224),freeze(editorV225),freeze(editorV226),freeze(editorV227),freeze(pipelineV200),freeze(pipeline),freeze(pipelineV220),freeze(pipelineV221),freeze(pipelineV223),freeze(pipelineV222) ]);
|
|
124
132
|
export const firstPartySdkPackage = manifest => FIRST_PARTY_SDK_PACKAGES.find(pkg => pkg.manifest.id === manifest?.id && pkg.manifest.version === manifest?.version);
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// use it, so a folder made anywhere (the CLI, Codex, Claude Code, by hand) imports
|
|
5
5
|
// exactly as the CLI packs it. Pure JavaScript: the caller supplies the files.
|
|
6
6
|
import {VIEW_SOURCE_FILES,VIEW_CONFIG_FILE,viewFromSource} from './view-source.js';
|
|
7
|
-
import {describeViewFiles} from './view.js';
|
|
7
|
+
import {describeViewFiles,compileFrameViewCode,isFrameViewCodeFile} from './view.js';
|
|
8
8
|
import {describeModuleFiles,MODULE_LIMITS} from './module-runtime.js';
|
|
9
|
+
import {MOCKS_FILE} from './mocks.js';
|
|
9
10
|
|
|
10
11
|
export const BLOCK_FOLDER_MANIFEST='stillmade.block.json';
|
|
11
12
|
export const BLOCK_FOLDER_LIMITS=Object.freeze({jsonBytes:4000000,javascriptBytes:262144,viewScriptBytes:4194304,viewSourceBytes:262144});
|
|
@@ -14,7 +15,7 @@ const SOURCE=/^src\/(run\.js|recipe\.json|comfyui\.json|capability\.json|desktop
|
|
|
14
15
|
/** Whether a path relative to a Block folder belongs to the Block. Everything
|
|
15
16
|
* else (node_modules, build output, notes, .git) is left out. */
|
|
16
17
|
export function blockFolderPath(path){
|
|
17
|
-
return path===BLOCK_FOLDER_MANIFEST||path==='module.json'||path==='tests/fixtures.json'||SOURCE.test(path)||path.startsWith('module/')||path.startsWith('src/view-files/');
|
|
18
|
+
return path===BLOCK_FOLDER_MANIFEST||path==='module.json'||path==='tests/fixtures.json'||path===MOCKS_FILE||SOURCE.test(path)||path.startsWith('module/')||path.startsWith('src/view-files/');
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const missing=path=>Object.assign(new Error(`Missing ${path}`),{code:'ENOENT'});
|
|
@@ -61,9 +62,21 @@ export async function packageFromFolder({read,list}){
|
|
|
61
62
|
if(view?.runtime==='frame'){
|
|
62
63
|
const assets=await folder('src/view-files/');
|
|
63
64
|
if(Object.keys(assets).length){view.files=await describeViewFiles(assets);viewFiles=Object.fromEntries(Object.entries(assets).map(([name,value])=>[`view:${name}`,value]));}
|
|
65
|
+
// Code files (.js) are checked here as StillMade checks them before running one: one classic script each.
|
|
66
|
+
for(const [name,file] of Object.entries(view.files?.files||{}))if(isFrameViewCodeFile(file))compileFrameViewCode(new TextDecoder('utf-8',{fatal:true}).decode(assets[name]),name);
|
|
64
67
|
}
|
|
65
68
|
const desktop=await json('src/desktop.json',{optional:true});
|
|
66
69
|
const pkg={manifest,...source,...(desktop!==undefined?{desktop}:{}),...(view!==undefined?{view}:{}),tests:await json('tests/fixtures.json')};
|
|
70
|
+
// A module Block's tests run against tests/mocks.json on the creator's machine. Each fixture without its own mocks
|
|
71
|
+
// carries those recorded answers, so StillMade's import review runs the tests the same way (November 6, 2026: a Gmail
|
|
72
|
+
// connector Block passed the gate locally and failed its import review with "no recorded answer").
|
|
73
|
+
if(manifest.runtime==='module'&&Array.isArray(pkg.tests)&&pkg.tests.some(test=>test&&typeof test==='object'&&test.mocks===undefined)){
|
|
74
|
+
const shared=await json(MOCKS_FILE,{optional:true});
|
|
75
|
+
if(shared&&typeof shared==='object'&&!Array.isArray(shared)&&Object.keys(shared).length){
|
|
76
|
+
const carried=await portableMocks(shared,read);
|
|
77
|
+
pkg.tests=pkg.tests.map(test=>test&&typeof test==='object'&&test.mocks===undefined?{...test,mocks:carried}:test);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
67
80
|
return {pkg,files:moduleFiles||viewFiles?{...(moduleFiles||{}),...(viewFiles||{})}:null};
|
|
68
81
|
}
|
|
69
82
|
|
|
@@ -73,3 +86,22 @@ export function packageFilesBySha(pkg,files){
|
|
|
73
86
|
for(const [name,file] of [...Object.entries(pkg.module?.files||{}),...Object.entries(pkg.view?.files?.files||{}).map(([name,file])=>[`view:${name}`,file])])if(files?.[name])bySha[file.sha256]=files[name];
|
|
74
87
|
return bySha;
|
|
75
88
|
}
|
|
89
|
+
|
|
90
|
+
// tests/mocks.json as a fixture's mocks: a file it maps to a path in the Block folder travels as {base64} (up to 1 MB;
|
|
91
|
+
// a larger one stays on the creator's machine, and a test that reads it runs only there).
|
|
92
|
+
const MOCK_FILE_BYTES=1048576;
|
|
93
|
+
const safeMockPath=path=>typeof path==='string'&&path.length>0&&path.length<=512&&!path.startsWith('/')&&!path.startsWith('\\')&&!/^[A-Za-z]:/.test(path)&&!path.includes('\0')&&!path.split(/[\\/]/).includes('..');
|
|
94
|
+
async function portableMocks(mocks,read){
|
|
95
|
+
if(!mocks.files||typeof mocks.files!=='object')return mocks;
|
|
96
|
+
const files={};
|
|
97
|
+
for(const [name,value] of Object.entries(mocks.files)){
|
|
98
|
+
if(typeof value!=='string'){files[name]=value;continue;}
|
|
99
|
+
// Only a file inside the Block folder: relative, no .. part, no drive or NUL (validateMocks refuses the rest anyway).
|
|
100
|
+
if(!safeMockPath(value))continue;
|
|
101
|
+
let bytes;try{bytes=await read(value);}catch{bytes=undefined;}
|
|
102
|
+
if(bytes&&bytes.byteLength<=MOCK_FILE_BYTES)files[name]={base64:toBase64(bytes)};
|
|
103
|
+
}
|
|
104
|
+
const {files:_,...rest}=mocks;
|
|
105
|
+
return Object.keys(files).length?{...rest,files}:rest;
|
|
106
|
+
}
|
|
107
|
+
function toBase64(bytes){let text='';for(let index=0;index<bytes.length;index+=32768)text+=String.fromCharCode(...bytes.subarray(index,index+32768));return btoa(text);}
|
|
@@ -16,6 +16,13 @@ export const IMAGE_GENERATION_MODELS=Object.freeze({
|
|
|
16
16
|
'nano-banana-2-lite':image('Nano Banana 2 Lite',{aspectRatios:NANO_ASPECTS,resolutions:['1K','2K','4K'],references:8,seed:true}),
|
|
17
17
|
'nano-banana-pro':image('Nano Banana Pro',{aspectRatios:NANO_ASPECTS,resolutions:['1K','2K','4K'],references:8,seed:true}),
|
|
18
18
|
'gpt-image-2':image('GPT Image 2',{aspectRatios:['auto','16:9','9:16','1:1','4:3','3:4'],resolutions:['1K','2K','4K'],qualities:['low','medium','high'],references:8}),
|
|
19
|
+
// GPT Image 2.5 (November 2026): the same settings as GPT Image 2 at the same token rates, about a quarter of its price at
|
|
20
|
+
// medium and high. Flare is the everyday one, Sunburst the one for precise edits; both cost the same.
|
|
21
|
+
'gpt-image-2.5-flare':image('GPT Image 2.5 Flare',{aspectRatios:['auto','16:9','9:16','1:1','4:3','3:4'],resolutions:['1K','2K','4K'],qualities:['low','medium','high'],references:8}),
|
|
22
|
+
'gpt-image-2.5-sunburst':image('GPT Image 2.5 Sunburst',{aspectRatios:['auto','16:9','9:16','1:1','4:3','3:4'],resolutions:['1K','2K','4K'],qualities:['low','medium','high'],references:8}),
|
|
23
|
+
// GPT Image 2.6 (OpenAI, November 4, 2026): Flare and Sunburst again, about half the wait of 2.5, the same settings.
|
|
24
|
+
'gpt-image-2.6-flare':image('GPT Image 2.6 Flare',{aspectRatios:['auto','16:9','9:16','1:1','4:3','3:4'],resolutions:['1K','2K','4K'],qualities:['low','medium','high'],references:8}),
|
|
25
|
+
'gpt-image-2.6-sunburst':image('GPT Image 2.6 Sunburst',{aspectRatios:['auto','16:9','9:16','1:1','4:3','3:4'],resolutions:['1K','2K','4K'],qualities:['low','medium','high'],references:8}),
|
|
19
26
|
'gpt-image-1.5':image('GPT-Image 1.5',{aspectRatios:['16:9','9:16','1:1'],qualities:['low','medium','high']}),
|
|
20
27
|
'kling-image-v3':image('Kling Image V3',{aspectRatios:['21:9','16:9','9:16','1:1','4:3','3:4','3:2','2:3'],resolutions:['1K','2K']}),
|
|
21
28
|
'seedream-v4.5':image('Seedream 4.5',{aspectRatios:['16:9','9:16','1:1','4:3','3:4'],resolutions:['2K','4K'],seed:true}),
|
|
@@ -49,6 +49,8 @@ export function testPackage(pkg: BlockPackage, options?: { timeoutMs?: number; c
|
|
|
49
49
|
export const SDK_VERSION: '0.1.0';
|
|
50
50
|
export const ROOT_CHAT_ID: 'stillmade.starting-chat';
|
|
51
51
|
export function stableStringify(value: unknown): string;
|
|
52
|
+
/** Whether `candidate` is the published first-party package `published` (its carried module text, moduleSource, left out). */
|
|
53
|
+
export function sameFirstPartySource(published: unknown, candidate: unknown): boolean;
|
|
52
54
|
/** Throws when any text or field name in a package holds the NUL character (\u0000), which StillMade cannot store; the message names where it is (tests.1.mocks.files.tiny-1.text). Import review, validate, test, pack and ready run it. */
|
|
53
55
|
export function assertStorableText(value: unknown): void;
|
|
54
56
|
export function validateManifest(value: unknown): { valid: boolean; errors: { path: string; message: string }[] };
|
|
@@ -129,7 +131,7 @@ export function resolveContextInputs(manifest: Manifest, values: Record<string,
|
|
|
129
131
|
export function projectReadSnapshot(manifest: Manifest, context?: Record<string, unknown>, fields?: string[]): Record<string, unknown>;
|
|
130
132
|
|
|
131
133
|
export function validateView(view: unknown): true;
|
|
132
|
-
export const FRAME_VIEW_LIMITS: Readonly<{htmlBytes:262144;cssBytes:262144;javascriptBytes:4194304;assetFiles:32;assetBytes:8388608;taskMs:250;longTaskMs:1000}>;
|
|
134
|
+
export const FRAME_VIEW_LIMITS: Readonly<{htmlBytes:262144;cssBytes:262144;javascriptBytes:4194304;assetFiles:32;assetBytes:8388608;codeFiles:64;codeFileBytes:4194304;codeBytes:16777216;mediaPreviews:24;taskMs:250;longTaskMs:1000}>;
|
|
133
135
|
export const FRAME_VIEW_ASSET_TYPES: readonly string[];
|
|
134
136
|
export function isFrameView(view: unknown): view is FrameView;
|
|
135
137
|
export function validateFrameView(view: FrameView): true;
|
|
@@ -139,11 +141,13 @@ export function parseFrameViewJavaScript(source: string): {ast: unknown};
|
|
|
139
141
|
export function instrumentFrameViewJavaScript(source: string, guard: string): string;
|
|
140
142
|
export function describeViewFiles(files: Record<string, Uint8Array|ArrayBuffer|string>): Promise<NonNullable<FrameView['files']>>;
|
|
141
143
|
/** In a frame view, the StillMade object also offers its asset files. */
|
|
142
|
-
export interface FrameViewAssets { asset(path: string): string; assetBytes(path: string): ArrayBuffer; }
|
|
143
|
-
export const VIEW_LIMITS: Readonly<{htmlBytes:65536;cssBytes:32768;javascriptBytes:65536;messageBytes:4194304;runsPerMinute:
|
|
144
|
+
export interface FrameViewAssets { asset(path: string): string; assetBytes(path: string): ArrayBuffer; loadCode(path: string): Promise<void>; }
|
|
145
|
+
export const VIEW_LIMITS: Readonly<{htmlBytes:65536;cssBytes:32768;javascriptBytes:65536;messageBytes:4194304;runsPerMinute:120;imagePreviewsPerMinute:240;voicePreviewsPerMinute:30;allowanceCredits:2000;imagePreviewMs:15000;imagePreviewEdge:512;imagePreviewMaxEdge:2048}>;
|
|
144
146
|
|
|
145
147
|
/** A display-only copy. Never substitute this URL for the original run input. */
|
|
146
|
-
export interface BlockImagePreview { url: `data:image/png;base64,${string}`; width: number; height: number; }
|
|
148
|
+
export interface BlockImagePreview { url: `data:image/png;base64,${string}`|`data:image/jpeg;base64,${string}`; width: number; height: number; }
|
|
149
|
+
/** `edge`: the longest side of the display copy, 64 to 2048 pixels (default 512). */
|
|
150
|
+
export interface BlockImagePreviewOptions { edge?: number; }
|
|
147
151
|
export interface BlockImagePixels { width: number; height: number; data: number[]; role?: string; }
|
|
148
152
|
export interface BlockImageReference { kind: 'image'; assetId: string; versionId: string; url: string; role?: string; }
|
|
149
153
|
export type DesktopPermission = "screen.capture"|"cursor.track"|"camera.capture"|"microphone.capture"|"clipboard.read"|"clipboard.write"|"media.pick"|"notifications.show"|"power.keep-awake"|"native.tools"|"folder.read"|"folder.write";
|
|
@@ -171,7 +175,7 @@ export interface BlockViewNode { tag: BlockViewTag; text?: string; children?: Bl
|
|
|
171
175
|
export interface BlockViewAction { action: string; value: string; checked: boolean; event: 'click'|'input'|'change'; }
|
|
172
176
|
export interface BlockMediaReference { kind: 'video'|'audio'; assetId: string; versionId: string; url: string; role?: string; }
|
|
173
177
|
export interface BlockMediaPreview { kind: 'video'|'audio'; url: string; mimeType: string; bytes: number; }
|
|
174
|
-
export interface BlockViewBridge { useOutput(outputs: Record<string, BlockMediaReference|BlockMediaReference[]>): Promise<Record<string, unknown>>; previewMedia(value: BlockMediaReference): Promise<BlockMediaPreview>; releaseMedia(url: string): void; render(targetId: string, tree: BlockViewNode[]): void; onAction(callback: (event: BlockViewAction) => void|Promise<void>): () => void; desktop(operation: DesktopPermission|DesktopToolOperation|import('./block-storage.js').BlockStorageOperation|"record.start"|"record.stop"|"record.cancel"|"session.close"|"live.start"|"live.stop"|"live.status"|"folder.pick"|"folder.shared"|"folder.forget"|"folder.list", args?: Record<string,unknown>): Promise<Record<string,unknown>>; readonly input: Record<string, unknown>; onInput(callback: (input: Record<string, unknown>) => void): () => void; run(input: Record<string, unknown>): Promise<Record<string, unknown>>; previewImage(value: BlockImageReference|BlockImagePixels): Promise<BlockImagePreview>; }
|
|
178
|
+
export interface BlockViewBridge { useOutput(outputs: Record<string, BlockMediaReference|BlockMediaReference[]>): Promise<Record<string, unknown>>; previewMedia(value: BlockMediaReference): Promise<BlockMediaPreview>; releaseMedia(url: string): void; render(targetId: string, tree: BlockViewNode[]): void; onAction(callback: (event: BlockViewAction) => void|Promise<void>): () => void; desktop(operation: DesktopPermission|DesktopToolOperation|import('./block-storage.js').BlockStorageOperation|"record.start"|"record.stop"|"record.cancel"|"session.close"|"live.start"|"live.stop"|"live.status"|"folder.pick"|"folder.shared"|"folder.forget"|"folder.list", args?: Record<string,unknown>): Promise<Record<string,unknown>>; readonly input: Record<string, unknown>; onInput(callback: (input: Record<string, unknown>) => void): () => void; run(input: Record<string, unknown>): Promise<Record<string, unknown>>; previewImage(value: BlockImageReference|BlockImagePixels, options?: BlockImagePreviewOptions): Promise<BlockImagePreview>; }
|
|
175
179
|
|
|
176
180
|
/** Validate hosted UI colors independently of the preserved original stylesheet. */
|
|
177
181
|
export function validateViewAppearance(view?: BlockView, options?: {appearance?:'host'|'original'}): true;
|
|
@@ -254,6 +258,10 @@ export type CapabilityExecutor = (pkg: BlockPackage, input: Record<string, unkno
|
|
|
254
258
|
export const CAPABILITY_LIMITS: Readonly<{prompt:32000;system:8000;output:32768;maxTokens:4096;fixtures:3;includes:8;includeLength:256;packageBytes:262144;metadataBytes:8192}>;
|
|
255
259
|
/** media.analyze bounds: images per run, question length and the closer-inspected video range in seconds. */
|
|
256
260
|
export const MEDIA_ANALYZE_LIMITS: Readonly<{images:4;question:2000;range:12}>;
|
|
261
|
+
/** media.analyze reply:"text": the Block's own question and instructions about one to ten pictures, answered as text. */
|
|
262
|
+
export const MEDIA_REPLY_LIMITS: Readonly<{images:10;question:8000;instructions:8000;maxTokens:4000}>;
|
|
263
|
+
/** text.generate's limits: long structured jobs are priced by their size. */
|
|
264
|
+
export const TEXT_GENERATE_LIMITS: Readonly<{system:32000;prompt:64000;maxTokens:24000;output:131072}>;
|
|
257
265
|
/** web.fetch and web.research bounds: URL, instruction and topic lengths, sources kept and answer size. */
|
|
258
266
|
export const WEB_CAPABILITY_LIMITS: Readonly<{url:2048;instruction:4000;topic:500;sources:12;answer:32768}>;
|
|
259
267
|
/** image.describe with an image[] input sends one to four images together. */
|
|
@@ -27,7 +27,7 @@ export * from './admission.js';
|
|
|
27
27
|
export {CONTEXT_FIELDS,resolveContextInputs,projectReadSnapshot} from './context.js';
|
|
28
28
|
|
|
29
29
|
export {validateView,VIEW_LIMITS,FRAME_VIEW_LIMITS,FRAME_VIEW_ASSET_TYPES,isFrameView,validateFrameView,parseFrameViewJavaScript,instrumentFrameViewJavaScript,describeViewFiles} from './view.js';
|
|
30
|
-
export {reviewViewDesign,blockKitProblems,BLOCK_KIT_READINESS_RULES,BLOCK_INTERFACE_PRINCIPLES,BLOCK_KIT_CLASSES,INTERFACE_DESIGN_PRINCIPLES,INTERFACE_DESIGN_CONVENTIONS,INTERFACE_DESIGN_RULES,INTERFACE_AUTHORING_RULES} from './view-design.js';
|
|
30
|
+
export {reviewViewDesign,interfaceMode,interfacePrinciples,INTERFACE_MODES,SHARED_INTERFACE_PRINCIPLES,JOB_INTERFACE_PRINCIPLES,APP_INTERFACE_PRINCIPLES,blockKitProblems,BLOCK_KIT_READINESS_RULES,BLOCK_INTERFACE_PRINCIPLES,BLOCK_KIT_CLASSES,INTERFACE_DESIGN_PRINCIPLES,INTERFACE_DESIGN_CONVENTIONS,INTERFACE_DESIGN_RULES,INTERFACE_AUTHORING_RULES} from './view-design.js';
|
|
31
31
|
|
|
32
32
|
export {validateViewAppearance,APPEARANCE_TOKENS} from './appearance.js';
|
|
33
33
|
|
|
@@ -116,6 +116,8 @@ export function createMockHost({mocks:given={},readLocalFile,manifest,pickedMedi
|
|
|
116
116
|
if(operation==='audio.music'||operation==='audio.sfx'){const limits=AUDIO_CAPABILITY_LIMITS[operation==='audio.music'?'music':'sfx'];return reference.audio({seconds:Math.min(limits.max,Math.max(limits.min,Number(request.settings?.durationSec)||limits.default)),sampleRate:8000});}
|
|
117
117
|
if(operation==='audio.transcribe')return transcript(Number(request.audio?.duration));
|
|
118
118
|
if(operation==='web.research')return {summary:`Stand-in findings about ${clip(request.topic,200)||'the topic'}.`,sources:[{url:'https://example.com/source',title:'Example source'}]};
|
|
119
|
+
// reply:"text" (the Block's own question over its pictures) answers in words, as the host's picture reply does.
|
|
120
|
+
if(operation==='media.analyze'&&request.reply==='text')return {reply:'A stand-in answer: a dark frame with a light band across the middle.'};
|
|
119
121
|
if(operation==='media.analyze'){const list=Array.isArray(request.media)?request.media:[request.media];return {items:list.map(item=>({kind:item?.kind==='video'?'video':'image',summary:'A stand-in summary: a dark frame with a light band across the middle.',timeline:[],observations:[{observation:'A light band crosses the middle of the frame.',confidence:0.9}],detectedText:[],issues:[],confidence:0.9}))};}
|
|
120
122
|
if(operation==='image.describe')return 'Stand-in description: a dark frame with a light band across the middle.';
|
|
121
123
|
if(operation==='web.fetch')return `Stand-in answer from ${clip(request.url,200)}.`;
|
|
@@ -209,6 +211,11 @@ export function createMockHost({mocks:given={},readLocalFile,manifest,pickedMedi
|
|
|
209
211
|
return {key:item.key,kind:made.kind,assetId:made.assetId,versionId:made.versionId,url:made.url,...(Number.isFinite(seconds)?{duration:seconds}:{})};
|
|
210
212
|
}),failures:[]};
|
|
211
213
|
if(operation==='stock.search')return {results:[]};
|
|
214
|
+
if(operation==='stock.select')return {asset:{...reference.image(),license:structuredClone(action.request.result.license||null)}};
|
|
215
|
+
if(operation==='stock.auto')return {asset:null,noMatch:true};
|
|
216
|
+
if(operation==='gif.search')return {enabled:true,results:[]};
|
|
217
|
+
if(operation==='gif.save')return {asset:reference.image()};
|
|
218
|
+
if(operation==='final.render')return {asset:{...(action.request.kind==='audio'?reference.audio():reference.video()),duration:action.request.timeline.targetDuration},receipt:null};
|
|
212
219
|
fail(`There is no recorded stand-in for the ${operation} action. Add one under actions["${operation}"] in ${MOCKS_FILE}.`);
|
|
213
220
|
},
|
|
214
221
|
// The same request check and result validation as the app: a module's hosted call becomes a capability package.
|
|
@@ -15,7 +15,7 @@ const port=(type,description)=>({type,description,required:true});
|
|
|
15
15
|
// Each operation: its ports, its capability descriptor and the input values.
|
|
16
16
|
const SHAPES={
|
|
17
17
|
'text.generate':request=>{
|
|
18
|
-
if(!text(request.prompt,
|
|
18
|
+
if(!text(request.prompt,64000))fail('text.generate needs a prompt of up to 64,000 characters');
|
|
19
19
|
const schema=request.schema;
|
|
20
20
|
return {inputs:{prompt:port('text','Prompt from the module')},outputs:{result:port(schema?'json':'text','Generated result')},capability:{prompt:{$input:'prompt'},system:typeof request.system==='string'?request.system:'',maxTokens:Number.isInteger(request.maxTokens)?request.maxTokens:1200,output:'result',...(schema?{schema}:{})},input:{prompt:request.prompt}};
|
|
21
21
|
},
|
|
@@ -75,6 +75,16 @@ const SHAPES={
|
|
|
75
75
|
},
|
|
76
76
|
'media.analyze':request=>{
|
|
77
77
|
const list=Array.isArray(request.media)?request.media:[request.media],kind=list[0]?.kind;
|
|
78
|
+
// reply:'text' (November 6, 2026): the module's own instructions and question over one to ten saved pictures, one call.
|
|
79
|
+
if(request.reply!==undefined){
|
|
80
|
+
if(request.reply!=='text')fail('media.analyze reply is "text"');
|
|
81
|
+
if(!list.length||list.length>10||!list.every(value=>media(value,'image')))fail('media.analyze reply:"text" asks about one to ten saved images');
|
|
82
|
+
if(!text(request.question,8000))fail('media.analyze reply:"text" needs a question of up to 8,000 characters');
|
|
83
|
+
if(request.instructions!==undefined&&!(typeof request.instructions==='string'&&request.instructions.length<=8000))fail('media.analyze instructions are text of at most 8,000 characters');
|
|
84
|
+
if(request.maxTokens!==undefined&&!(Number.isInteger(request.maxTokens)&&request.maxTokens>=1&&request.maxTokens<=4000))fail('media.analyze maxTokens is 1 to 4,000');
|
|
85
|
+
const many=Array.isArray(request.media);
|
|
86
|
+
return {inputs:{media:port(many?'image[]':'image','Pictures to ask about')},outputs:{result:port('text','The answer')},capability:{media:{$input:'media'},question:request.question,reply:'text',...(request.instructions?{instructions:request.instructions}:{}),...(request.maxTokens?{maxTokens:request.maxTokens}:{}),output:'result'},input:{media:request.media}};
|
|
87
|
+
}
|
|
78
88
|
if(!list.length||!['image','video'].includes(kind)||!list.every(value=>media(value,kind))||(kind==='video'&&list.length!==1)||list.length>4)fail('media.analyze needs one saved video or one to four saved images');
|
|
79
89
|
if(!text(request.question,2000))fail('media.analyze needs a question');
|
|
80
90
|
// October 30, 2026: detail:'check' is the short photo check, priced per call of up to four photos.
|
|
@@ -58,12 +58,14 @@ const FIX='Pass the reference the Block received as input, or the one StillMade
|
|
|
58
58
|
* vouches for (project media it read). The checks throw MODULE_FILE_READ.
|
|
59
59
|
*/
|
|
60
60
|
export function moduleMediaAccess(input){
|
|
61
|
-
const usable=new Set(mediaReferences(input).map(mediaReferenceKey));
|
|
61
|
+
const usable=new Set(mediaReferences(input).map(mediaReferenceKey)),addresses=new Set(mediaReferences(input).map(ref=>ref.url));
|
|
62
62
|
const allowed=ref=>!!ref&&typeof ref==='object'&&usable.has(mediaReferenceKey(ref));
|
|
63
63
|
return {
|
|
64
64
|
allowed,
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// A render plan names media by address (final.render's timeline): only addresses of the run's usable files count.
|
|
66
|
+
allowsUrl:url=>typeof url==='string'&&addresses.has(url),
|
|
67
|
+
add(ref){usable.add(mediaReferenceKey(ref));if(typeof ref?.url==='string')addresses.add(ref.url);},
|
|
68
|
+
remember(value){if(value&&typeof value==='object'&&mediaReferences(value)[0]===value){usable.add(mediaReferenceKey(value));if(typeof value.url==='string')addresses.add(value.url);}return value;},
|
|
67
69
|
// Every reference in a request, and inside its references, must be usable.
|
|
68
70
|
check(value,what){if(!mediaReferences(value,{strict:true,within:true}).every(allowed))throw fail(`${what} use only files ${RULE}. ${FIX}`,'MODULE_FILE_READ');},
|
|
69
71
|
checkFile(ref,call){if(!allowed(ref))throw fail(`${call} only files ${RULE}. ${FIX}`,'MODULE_FILE_READ');},
|
|
@@ -35,7 +35,7 @@ const HOSTED={
|
|
|
35
35
|
'workspace.propose':'suggests workspace edits','connection.execute':'uses an app you connected','comfyui.execute':'runs a ComfyUI workflow',
|
|
36
36
|
};
|
|
37
37
|
const ACTIONS={
|
|
38
|
-
'final.render':'renders your final video','stock.search':'searches stock media','presenter.generate':'makes a presenter video',
|
|
38
|
+
'final.render':'renders your final video','stock.search':'searches stock media','stock.select':'keeps stock media you pick','stock.auto':'finds stock media for a description','gif.search':'searches GIFs','gif.save':'saves GIFs you pick','presenter.generate':'makes a presenter video',
|
|
39
39
|
'quality.analyze':'checks quality','image.generate':'generates images','image.generate.batch':'generates images in batches',
|
|
40
40
|
'media.generate':'generates media','youtube.upload':'uploads to your YouTube channel after you confirm',
|
|
41
41
|
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export type PortableMediaActionOperation='final.render'|'stock.search'|'presenter.generate'|'quality.analyze'|'image.generate';
|
|
1
|
+
export type PortableMediaActionOperation='final.render'|'stock.search'|'stock.select'|'stock.auto'|'gif.search'|'gif.save'|'presenter.generate'|'quality.analyze'|'image.generate';
|
|
2
2
|
export interface PortableMediaAction {schemaVersion:1;kind:'stillmade.media-action';operation:PortableMediaActionOperation;request:Record<string,unknown>;resultPort:string|null}
|
|
3
3
|
export declare const PORTABLE_MEDIA_ACTION_KIND:'stillmade.media-action';
|
|
4
4
|
export declare function portableMediaAction(value:unknown,permissions?:string[]):Readonly<PortableMediaAction>;
|
|
5
|
+
export const STOCK_AUTO_LIMITS:Readonly<{queries:10;excludeUrls:300}>;
|
|
6
|
+
export const GIF_LIMITS:Readonly<{results:24}>;
|
|
7
|
+
export function isGiphyMediaUrl(url:unknown):boolean;
|