stillmade-sdk 0.1.24 → 0.1.26
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/AGENTS.md +4 -0
- package/PASTE_TO_LLM.md +4 -0
- package/SDK.md +10 -2
- package/blocks/narration/package.js +1 -0
- package/blocks/narration/versions/1.0.0/package.js +221 -0
- package/package.json +1 -1
- package/packages/block-cli/cli.js +2 -2
- package/packages/block-platform/block-readiness.js +2 -2
- package/packages/block-platform/catalog.js +24 -7
- package/packages/block-platform/dealer-inventory-video-project-type.js +33 -0
- package/packages/block-platform/plain-descriptions.js +1 -0
- package/packages/block-platform/project-type-starters.js +7 -7
- package/packages/block-sdk/builder-contract.js +4 -1
- package/packages/block-sdk/contracts.js +4 -1
- package/packages/block-sdk/first-party-identities.js +1 -0
- package/packages/block-sdk/first-party-packages.js +2 -1
- package/packages/block-sdk/local-admission.js +22 -5
- package/packages/block-sdk/testing.js +6 -3
package/AGENTS.md
CHANGED
|
@@ -16,6 +16,10 @@ Treat the current SDK (`stillmade.block.json` manifest, `packages/block-sdk` con
|
|
|
16
16
|
|
|
17
17
|
Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator's source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.
|
|
18
18
|
|
|
19
|
+
## Three sizes of Block
|
|
20
|
+
|
|
21
|
+
Blocks come in three sizes and the manifest decides which (packages/block-sdk/block-tier.js): an app is a workspace a person works in (Editor, Canvas, Shots), placed in a Type and never wired automatically; a job is one deliverable a person would name (the narration, the captioned clip), built from the hosted operations it needs (`permissions.capabilities`) and reviewed in one place, the unit Chat picks for a Step; an ingredient is one hosted operation with nothing of its own (a declarative capability Block), what a job is made of. When two ingredients would stand in a row to make one deliverable (writing the words, then voicing them), build or pick the job that does both and keep the review point inside it. A job declares a second output when another Block consumes the intermediate (the script beside the voice); split into two jobs only when the intermediate has a life of its own or the halves have different owners or payment.
|
|
22
|
+
|
|
19
23
|
## One job per Step
|
|
20
24
|
|
|
21
25
|
A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.
|
package/PASTE_TO_LLM.md
CHANGED
|
@@ -14,6 +14,10 @@ Treat the current SDK (`stillmade.block.json` manifest, `packages/block-sdk` con
|
|
|
14
14
|
|
|
15
15
|
Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator's source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.
|
|
16
16
|
|
|
17
|
+
### Three sizes of Block
|
|
18
|
+
|
|
19
|
+
Blocks come in three sizes and the manifest decides which (packages/block-sdk/block-tier.js): an app is a workspace a person works in (Editor, Canvas, Shots), placed in a Type and never wired automatically; a job is one deliverable a person would name (the narration, the captioned clip), built from the hosted operations it needs (`permissions.capabilities`) and reviewed in one place, the unit Chat picks for a Step; an ingredient is one hosted operation with nothing of its own (a declarative capability Block), what a job is made of. When two ingredients would stand in a row to make one deliverable (writing the words, then voicing them), build or pick the job that does both and keep the review point inside it. A job declares a second output when another Block consumes the intermediate (the script beside the voice); split into two jobs only when the intermediate has a life of its own or the halves have different owners or payment.
|
|
20
|
+
|
|
17
21
|
### One job per Step
|
|
18
22
|
|
|
19
23
|
A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.
|
package/SDK.md
CHANGED
|
@@ -192,6 +192,10 @@ Treat the current SDK (`stillmade.block.json` manifest, `packages/block-sdk` con
|
|
|
192
192
|
|
|
193
193
|
Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator's source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.
|
|
194
194
|
|
|
195
|
+
### Three sizes of Block
|
|
196
|
+
|
|
197
|
+
Blocks come in three sizes and the manifest decides which (packages/block-sdk/block-tier.js): an app is a workspace a person works in (Editor, Canvas, Shots), placed in a Type and never wired automatically; a job is one deliverable a person would name (the narration, the captioned clip), built from the hosted operations it needs (`permissions.capabilities`) and reviewed in one place, the unit Chat picks for a Step; an ingredient is one hosted operation with nothing of its own (a declarative capability Block), what a job is made of. When two ingredients would stand in a row to make one deliverable (writing the words, then voicing them), build or pick the job that does both and keep the review point inside it. A job declares a second output when another Block consumes the intermediate (the script beside the voice); split into two jobs only when the intermediate has a life of its own or the halves have different owners or payment.
|
|
198
|
+
|
|
195
199
|
### One job per Step
|
|
196
200
|
|
|
197
201
|
A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.
|
|
@@ -6908,7 +6912,7 @@ Generated from the SDK source (197 codes). The message is one example; the actua
|
|
|
6908
6912
|
| REQUIREMENT | Choose a declared input requirement. | `interactive-requirements`, `package-jobs`, `sandbox-node` and 1 more |
|
|
6909
6913
|
| RESOLVER | Choose an exact asset version for scoped resolution. | `universal-resolver` |
|
|
6910
6914
|
| RUNTIME | Sandbox memory configuration was not applied | `javascript` |
|
|
6911
|
-
| RUNTIME_RESTRICTED |
|
|
6915
|
+
| RUNTIME_RESTRICTED | Portable Blocks support recipe, isolated JavaScript, declarative host capabilities and reviewed ComfyUI workflows. Arbitrary Python and native code are restricted; do not package a substitute that pretends to provide them. | `portable` |
|
|
6912
6916
|
| RUNTIME_UNAVAILABLE | This host does not provide interactive Block questions. Supply this input before running. | `javascript`, `recipe`, `runtime` |
|
|
6913
6917
|
| SCHEMA | Numbers must be finite | `contracts`, `semantic-contracts` |
|
|
6914
6918
|
| SEMANTIC | Resolve meanings from at most 200 pinned Block manifests. | `package-semantics`, `semantic-contracts` |
|
|
@@ -7249,6 +7253,10 @@ Treat the current SDK (`stillmade.block.json` manifest, `packages/block-sdk` con
|
|
|
7249
7253
|
|
|
7250
7254
|
Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator's source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.
|
|
7251
7255
|
|
|
7256
|
+
### Three sizes of Block
|
|
7257
|
+
|
|
7258
|
+
Blocks come in three sizes and the manifest decides which (packages/block-sdk/block-tier.js): an app is a workspace a person works in (Editor, Canvas, Shots), placed in a Type and never wired automatically; a job is one deliverable a person would name (the narration, the captioned clip), built from the hosted operations it needs (`permissions.capabilities`) and reviewed in one place, the unit Chat picks for a Step; an ingredient is one hosted operation with nothing of its own (a declarative capability Block), what a job is made of. When two ingredients would stand in a row to make one deliverable (writing the words, then voicing them), build or pick the job that does both and keep the review point inside it. A job declares a second output when another Block consumes the intermediate (the script beside the voice); split into two jobs only when the intermediate has a life of its own or the halves have different owners or payment.
|
|
7259
|
+
|
|
7252
7260
|
### One job per Step
|
|
7253
7261
|
|
|
7254
7262
|
A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.
|
|
@@ -12184,7 +12192,6 @@ Music, sound effects and catalog voices are stored as the provider's measured MP
|
|
|
12184
12192
|
| `task` | A Project Type Step. Its primary input and output connect automatically to neighbouring Steps. Use this for almost every Block. |
|
|
12185
12193
|
| `workspace` | A complete project workspace that owns a project document (like Shots or the Editor). Never connected automatically: neighbours only reach it through project context or explicit connections. |
|
|
12186
12194
|
| `editor-extension` | Accepted by the validator; no host behaviour depends on it today. Use `task`. |
|
|
12187
|
-
| `composite` | Accepted by the validator; no host behaviour depends on it today. Use `task`. |
|
|
12188
12195
|
|
|
12189
12196
|
### Generated controls
|
|
12190
12197
|
`manifest.ui` binds generated controls to inputs. A custom view replaces them in the interface, but keep them as the fallback schema.
|
|
@@ -12273,6 +12280,7 @@ The latest version of each Block bundled with the SDK, generated from the packag
|
|
|
12273
12280
|
| `sm.whiteboard-assistant` 1.0.0 | Build Whiteboard | task | `board` board-document · project context only; `request` text · primary | `edit` workspace-edit · primary |
|
|
12274
12281
|
| `sm.write-summary` 1.0.0 | Write summary | task | `data` text (external_data) · primary | `summary` text (summary_report) · primary |
|
|
12275
12282
|
| `sm.youtube` 2.0.0 | YouTube publication | task | `video` video (finished_video) · primary; `title` text; `description` text; `tags` text; `visibility` text; `publishAt` text; `madeForKids` boolean; `aiGenerated` boolean | `uploadAction` approval (portable_media_action); `publication` json (youtube_publication) |
|
|
12283
|
+
| `stillmade.narration` 1.0.0 | Narration | workspace | `brief` brief (project_brief) · primary; `script` script (narration_script); `tone` text (narration_tone); `rules` text (writing_rules); `voice` text (speech_voice); `speed` number (speech_speed); `take` audio (narration_audio) | `script` script (narration_script); `audio` audio (narration_audio) · primary |
|
|
12276
12284
|
| `stillmade.occasion-story` 1.0.2 | Occasion Story | task | `assets` asset[] (legacy.asset-list.assets) · primary; `occasion` text (stillmade.occasion_story.occasion); `title` text (stillmade.occasion_story.title); `ending` text (stillmade.occasion_story.ending); `notes` text (stillmade.occasion_story.notes); `selections` text (stillmade.occasion_story.selections); `music` audio[] (stillmade.occasion_story.music); `musicVolume` number (stillmade.occasion_story.musicvolume); `muteVideoAudio` boolean (stillmade.occasion_story.mutevideoaudio); `aspect` text (stillmade.occasion_story.aspect); `disclosure` text (stillmade.occasion_story.disclosure) | `timeline` timeline (legacy.timeline.updatededitor) · primary; `review` json (stillmade.occasion_story.review) |
|
|
12277
12285
|
| `stillmade.photo-reel-composer` 1.0.2 | Photo Reel Composer | task | `assets` asset[] (legacy.asset-list.assets) · primary; `purpose` text (stillmade.photo_reel.purpose); `title` text (stillmade.photo_reel.title); `captions` text (stillmade.photo_reel.captions); `cta` text (stillmade.photo_reel.cta); `contact` text (stillmade.photo_reel.contact); `demoDisclosure` text (stillmade.photo_reel.demodisclosure); `duration` number (stillmade.photo_reel.duration); `aspect` text (stillmade.photo_reel.aspect); `logo` asset[] (stillmade.photo_reel.logo); `music` asset[] (stillmade.photo_reel.music) | `timeline` timeline (legacy.timeline.updatededitor) · primary; `review` json (stillmade.photo_reel.review); `provenance` json (stillmade.photo_reel.provenance) |
|
|
12278
12286
|
| `stillmade.product-ad-variants` 1.0.0 | Product Ad Variants | task | `timeline` timeline (legacy.timeline.updatededitor) · primary; `hookA` text (stillmade.product_ad_variants.hooka); `hookB` text (stillmade.product_ad_variants.hookb); `hookC` text (stillmade.product_ad_variants.hookc); `openingA` number (stillmade.product_ad_variants.openinga); `openingB` number (stillmade.product_ad_variants.openingb); `openingC` number (stillmade.product_ad_variants.openingc); `openingDuration` number (stillmade.product_ad_variants.openingduration); `copyApproved` boolean (stillmade.product_ad_variants.copyapproved) | `variantA` timeline (legacy.timeline.updatededitor) · primary; `variantB` timeline (legacy.timeline.updatededitor); `variantC` timeline (legacy.timeline.updatededitor); `review` json (stillmade.product_ad_variants.review) |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {default} from './versions/1.0.0/package.js';
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Generated from the owned versioned source by build-package.mjs.
|
|
2
|
+
export default {
|
|
3
|
+
"manifest": {
|
|
4
|
+
"schemaVersion": 1,
|
|
5
|
+
"sdkVersion": "0.1.0",
|
|
6
|
+
"id": "stillmade.narration",
|
|
7
|
+
"version": "1.0.0",
|
|
8
|
+
"name": "Narration",
|
|
9
|
+
"description": "Turn the facts Chat gathered into spoken words, hear them in a catalog voice or use your own recording, and hand the script and the voiceover on.",
|
|
10
|
+
"kind": "workspace",
|
|
11
|
+
"runtime": "module",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"inputs": {
|
|
14
|
+
"brief": {
|
|
15
|
+
"type": "brief",
|
|
16
|
+
"primary": true,
|
|
17
|
+
"required": false,
|
|
18
|
+
"semantic": "project_brief",
|
|
19
|
+
"description": "The facts the voice speaks from: what is shown, its details, who it is for, how long, and how to reach you. Chat fills this in."
|
|
20
|
+
},
|
|
21
|
+
"script": {
|
|
22
|
+
"type": "script",
|
|
23
|
+
"required": false,
|
|
24
|
+
"semantic": "narration_script",
|
|
25
|
+
"sources": [
|
|
26
|
+
"upstream",
|
|
27
|
+
"user"
|
|
28
|
+
],
|
|
29
|
+
"description": "The words the voice says. Written from the facts when empty; change them before the voice runs."
|
|
30
|
+
},
|
|
31
|
+
"tone": {
|
|
32
|
+
"type": "text",
|
|
33
|
+
"required": false,
|
|
34
|
+
"default": "warm",
|
|
35
|
+
"semantic": "narration_tone",
|
|
36
|
+
"description": "How it sounds: warm, friendly, no-nonsense or luxury."
|
|
37
|
+
},
|
|
38
|
+
"rules": {
|
|
39
|
+
"type": "text",
|
|
40
|
+
"required": false,
|
|
41
|
+
"semantic": "writing_rules",
|
|
42
|
+
"description": "Your own writing rules, such as short sentences or no exclamation marks."
|
|
43
|
+
},
|
|
44
|
+
"voice": {
|
|
45
|
+
"type": "text",
|
|
46
|
+
"required": false,
|
|
47
|
+
"default": "onyx",
|
|
48
|
+
"semantic": "speech_voice",
|
|
49
|
+
"description": "The catalog voice that reads the script."
|
|
50
|
+
},
|
|
51
|
+
"speed": {
|
|
52
|
+
"type": "number",
|
|
53
|
+
"required": false,
|
|
54
|
+
"default": 1,
|
|
55
|
+
"min": 0.5,
|
|
56
|
+
"max": 2,
|
|
57
|
+
"semantic": "speech_speed",
|
|
58
|
+
"description": "How fast the voice reads, from half to double speed."
|
|
59
|
+
},
|
|
60
|
+
"take": {
|
|
61
|
+
"type": "audio",
|
|
62
|
+
"required": false,
|
|
63
|
+
"semantic": "narration_audio",
|
|
64
|
+
"sources": [
|
|
65
|
+
"user"
|
|
66
|
+
],
|
|
67
|
+
"description": "Your own recording or audio file, used instead of a generated voice."
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"outputs": {
|
|
71
|
+
"script": {
|
|
72
|
+
"type": "script",
|
|
73
|
+
"required": false,
|
|
74
|
+
"semantic": "narration_script",
|
|
75
|
+
"role": "narration_script",
|
|
76
|
+
"description": "The narration, for shots, captions and the voice."
|
|
77
|
+
},
|
|
78
|
+
"audio": {
|
|
79
|
+
"type": "audio",
|
|
80
|
+
"primary": true,
|
|
81
|
+
"required": false,
|
|
82
|
+
"semantic": "narration_audio",
|
|
83
|
+
"role": "narration_audio",
|
|
84
|
+
"description": "The voiceover."
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"permissions": {
|
|
88
|
+
"project": [],
|
|
89
|
+
"capabilities": [
|
|
90
|
+
"text.generate",
|
|
91
|
+
"audio.speech"
|
|
92
|
+
],
|
|
93
|
+
"network": [],
|
|
94
|
+
"filesystem": [],
|
|
95
|
+
"secrets": []
|
|
96
|
+
},
|
|
97
|
+
"ui": [],
|
|
98
|
+
"features": [
|
|
99
|
+
{
|
|
100
|
+
"id": "write-script",
|
|
101
|
+
"label": "Write the script",
|
|
102
|
+
"does": "Writes spoken narration from the facts in the brief, in the tone you pick, using only those facts.",
|
|
103
|
+
"asks": [
|
|
104
|
+
"write a script for my listing video",
|
|
105
|
+
"turn these car details into a 30 second narration",
|
|
106
|
+
"write the words for a product video"
|
|
107
|
+
],
|
|
108
|
+
"inputs": [
|
|
109
|
+
"brief",
|
|
110
|
+
"tone",
|
|
111
|
+
"rules"
|
|
112
|
+
],
|
|
113
|
+
"outputs": [
|
|
114
|
+
"script"
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "make-voiceover",
|
|
119
|
+
"label": "Make the voiceover",
|
|
120
|
+
"does": "Reads the script in a catalog voice at the speed you choose.",
|
|
121
|
+
"asks": [
|
|
122
|
+
"make a voiceover for this script",
|
|
123
|
+
"read this in a deep voice",
|
|
124
|
+
"narrate my video"
|
|
125
|
+
],
|
|
126
|
+
"inputs": [
|
|
127
|
+
"script",
|
|
128
|
+
"voice",
|
|
129
|
+
"speed"
|
|
130
|
+
],
|
|
131
|
+
"outputs": [
|
|
132
|
+
"audio"
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "own-recording",
|
|
137
|
+
"label": "Use your own recording",
|
|
138
|
+
"does": "Uses an audio file or recording you supply as the voiceover, with the script kept beside it.",
|
|
139
|
+
"asks": [
|
|
140
|
+
"use my own voice recording",
|
|
141
|
+
"I recorded the narration myself"
|
|
142
|
+
],
|
|
143
|
+
"inputs": [
|
|
144
|
+
"take"
|
|
145
|
+
],
|
|
146
|
+
"outputs": [
|
|
147
|
+
"audio"
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"platforms": {
|
|
152
|
+
"phone": {
|
|
153
|
+
"supported": true
|
|
154
|
+
},
|
|
155
|
+
"browser": {
|
|
156
|
+
"supported": true
|
|
157
|
+
},
|
|
158
|
+
"desktop": {
|
|
159
|
+
"supported": true
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"provenance": {
|
|
163
|
+
"notice": "MIT License\n\nCopyright (c) <year> <copyright holders>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"module": {
|
|
167
|
+
"schemaVersion": 1,
|
|
168
|
+
"entry": "main.js",
|
|
169
|
+
"files": {
|
|
170
|
+
"main.js": {
|
|
171
|
+
"sha256": "a5eed2032d2ba5365d2dbb5b4bb4d483f6e7aee8604a004b69cdcacca1d20b32",
|
|
172
|
+
"bytes": 4703,
|
|
173
|
+
"mediaType": "text/javascript"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"moduleSource": {
|
|
178
|
+
"main.js": "// Narration: one job in two hosted steps. With no script, the facts in the\n// brief become spoken words (text.generate) and the run ends so the person can\n// read and change them. With a script, the words become a voice (audio.speech),\n// or the person's own take is used as it is. Every call is quoted and approved\n// by StillMade; this code never sees a provider, a key or a price.\nconst WORDS_PER_MINUTE = 150;\nconst SPEECH_LIMIT = 3800;\nconst VOICE = /^[a-z][a-z0-9_-]{0,63}$/;\nconst TONES = {\n warm: 'Warm and welcoming, like a trusted local who wants to help.',\n friendly: 'Upbeat and friendly, in plain words and short sentences.',\n 'no-nonsense': 'Direct and factual, with no filler and no hype.',\n luxury: 'Calm, polished and unhurried; confident, never pushy.',\n};\n\nconst text = value => typeof value === 'string' ? value.trim() : '';\n// A brief or script value is {schemaVersion: 1, text}; older briefs carry a topic.\nfunction words(value) {\n if (typeof value === 'string') return value.trim();\n if (!value || typeof value !== 'object') return '';\n for (const key of ['text', 'topic', 'summary']) if (text(value[key])) return text(value[key]);\n return Object.entries(value).filter(([key, item]) => key !== 'schemaVersion' && ['string', 'number'].includes(typeof item)).map(([key, item]) => `${key}: ${item}`).join('\\n');\n}\nfunction targetWords(brief) {\n const seconds = /\\b(\\d{1,3})\\s*(?:s|sec|secs|second|seconds)\\b/i.exec(brief);\n if (seconds) return Math.max(15, Math.round(Number(seconds[1]) / 60 * WORDS_PER_MINUTE));\n const minutes = /\\b(\\d{1,2}|one|two|three)\\s*(?:min|mins|minute|minutes)\\b/i.exec(brief);\n if (minutes) return Math.min(560, ({ one: 1, two: 2, three: 3 }[minutes[1].toLowerCase()] || Number(minutes[1])) * WORDS_PER_MINUTE);\n return 75;\n}\nfunction toneGuidance(tone, rules) {\n const preset = TONES[String(tone || 'warm').toLowerCase()];\n return [preset || text(tone) || TONES.warm, text(rules)].filter(Boolean).join(' ');\n}\n// What the model returns is the script and nothing else: no fences, labels or quotes.\nfunction cleanScript(reply) {\n let script = text(reply).replace(/^```[a-z]*\\s*|\\s*```$/g, '').trim();\n script = script.replace(/^(?:script|narration|voiceover)\\s*:\\s*/i, '').trim();\n if (/^[\"“].*[\"”]$/s.test(script)) script = script.slice(1, -1).trim();\n return script.replace(/[ \\t]+\\n/g, '\\n').replace(/\\n{3,}/g, '\\n\\n');\n}\nconst SYSTEM = 'You write spoken narration for short videos. Use only the facts you are given: never add a price, mileage, phone number, address, web address, award, inspection, warranty or history that is not in them, and never claim to have checked anything. Write for the ear: plain sentences that read well aloud, no headings, lists, stage directions, speaker labels, emojis, markdown or quotation marks. Return the narration only.';\n\nexport default async function run(input, stillmade) {\n const brief = words(input.brief), script = words(input.script), rules = text(input.rules);\n if (!script) {\n if (!brief) throw new Error('Add the facts the voice should speak from, or type the script.');\n stillmade.progress(0.2, 'Writing the script');\n const count = targetWords(brief);\n const prompt = `Facts (use only these):\\n${brief}\\n\\nWrite the spoken narration for a short video about this. Tone: ${toneGuidance(input.tone, rules)} Length: about ${count} words (${Math.round(count / WORDS_PER_MINUTE * 60)} seconds at ${WORDS_PER_MINUTE} words a minute). End with the contact or next step from the facts when there is one.`;\n const reply = await stillmade.hosted.run('text.generate', { prompt, system: SYSTEM, maxTokens: Math.min(1600, Math.max(300, count * 4)) });\n const written = cleanScript(reply);\n if (!written) throw new Error('No script came back. Add more facts and write again.');\n stillmade.progress(1, 'Script written');\n return { script: { schemaVersion: 1, text: written } };\n }\n const result = { script: { schemaVersion: 1, text: script } };\n if (input.take) {\n stillmade.log('Using the supplied recording.');\n return { ...result, audio: input.take };\n }\n if (script.length > SPEECH_LIMIT) throw new Error(`The script is longer than one voice take (${SPEECH_LIMIT.toLocaleString('en-US')} characters). Shorten it, or split it.`);\n const voice = VOICE.test(String(input.voice || '')) ? String(input.voice) : 'onyx';\n const speed = Number.isFinite(input.speed) && input.speed >= 0.5 && input.speed <= 2 ? input.speed : 1;\n stillmade.progress(0.4, 'Making the voice');\n const audio = await stillmade.hosted.run('audio.speech', { text: script, settings: { voice, speed } });\n stillmade.progress(1, 'Voiceover ready');\n return { ...result, audio };\n}\n"
|
|
179
|
+
},
|
|
180
|
+
"view": {
|
|
181
|
+
"runtime": "frame",
|
|
182
|
+
"html": "<main class=\"sm-block nr\">\n <section class=\"sm-stage nr-stage\" id=\"stage\" aria-live=\"polite\">\n <div class=\"sm-empty\" id=\"empty\">\n <span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span>\n <p>No narration yet</p>\n </div>\n <div class=\"nr-take\" id=\"take\" hidden>\n <audio id=\"player\" controls preload=\"metadata\"></audio>\n <p class=\"nr-meta\"><span id=\"length\"></span><span id=\"from\"></span></p>\n </div>\n </section>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"facts\">Facts</label><details class=\"sm-hint\"><summary aria-label=\"About the facts\" title=\"About the facts\"></summary><p>What the voice may say: the thing, its details, who it is for, how long, and how to reach you. Chat fills this from your conversation; change it here.</p></details><span class=\"sm-status\" id=\"facts-state\"></span></span>\n <textarea id=\"facts\" rows=\"3\" maxlength=\"6000\" data-stillmade-share=\"facts\" placeholder=\"2021 Toyota RAV4 XLE, $27,995, 31,200 miles, one owner. Sunrise Motors, (555) 201-7788.\"></textarea>\n </div>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"words\">Script</label><span class=\"sm-status\" id=\"words-state\"></span></span>\n <textarea id=\"words\" rows=\"7\" maxlength=\"3800\" data-stillmade-share=\"words\" placeholder=\"Written from the facts, or type what the voice says.\"></textarea>\n </div>\n <fieldset class=\"sm-choices\" data-compact id=\"voice\" data-stillmade-share=\"voice\">\n <legend>Voice</legend>\n <label><input type=\"radio\" name=\"voice\" value=\"onyx\" checked><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Onyx <small>deep</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"nova\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Nova <small>bright</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"alloy\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Alloy <small>even</small></span></label>\n </fieldset>\n <div class=\"sm-actions\">\n <button type=\"button\" class=\"sm-primary\" id=\"action\" data-stillmade-action=\"run\">Write script</button>\n <button type=\"button\" class=\"sm-quiet\" id=\"again\" hidden>Write again</button>\n <p class=\"sm-status\" id=\"status\" role=\"status\"></p>\n <details class=\"sm-hint\" id=\"why\" hidden><summary aria-label=\"Why\" title=\"Why\"></summary><p id=\"why-text\"></p></details>\n </div>\n <details class=\"sm-more\" id=\"more\">\n <summary>More</summary>\n <div class=\"sm-settings\">\n <fieldset class=\"sm-choices\" data-compact id=\"tone\" data-stillmade-share=\"tone\">\n <legend>Tone</legend>\n <label><input type=\"radio\" name=\"tone\" value=\"warm\" checked><span>Warm</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"friendly\"><span>Friendly</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"no-nonsense\"><span>No-nonsense</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"luxury\"><span>Luxury</span></label>\n </fieldset>\n <fieldset class=\"sm-choices\" data-segmented id=\"speed\" data-stillmade-share=\"speed\">\n <legend>Speed</legend>\n <label><input type=\"radio\" name=\"speed\" value=\"0.9\"><span>Slower</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1\" checked><span>Normal</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1.15\"><span>Faster</span></label>\n </fieldset>\n <div class=\"sm-field\"><label for=\"rules\">Own rules</label><input id=\"rules\" type=\"text\" maxlength=\"300\" data-stillmade-share=\"rules\" placeholder=\"Short sentences, no exclamation marks\"></div>\n </div>\n </details>\n</main>\n",
|
|
183
|
+
"css": "/* The Block kit (injected by the host) carries the layout; this file only draws\n the voiceover on the stage: the player and its two facts. */\n.nr [hidden] { display: none !important; }\n.nr-stage { display: grid; align-content: center; justify-items: stretch; gap: var(--s-2, 8px); min-height: 180px; }\n/* A voiceover is short: on a wide screen the stage hugs the player instead of stretching down the column. */\n@container (min-width: 860px) { .sm-block:not([data-layout]) > .nr-stage { align-self: start; min-height: 220px; } }\n.nr-stage > .sm-empty { justify-self: center; }\n.nr-take { display: grid; gap: var(--s-2, 8px); width: 100%; }\n.nr-take audio { width: 100%; }\n.nr-meta { display: flex; justify-content: space-between; gap: var(--s-3, 12px); margin: 0; color: var(--fg-muted); font-size: var(--fs-body-s, .875rem); font-variant-numeric: tabular-nums; }\n#voice { --sm-tile: 120px; }\n#voice small { color: var(--fg-subtle); font-size: var(--fs-caption, .8125rem); margin-inline-start: 4px; }\n#tone { --sm-tile: 110px; }\n",
|
|
184
|
+
"javascript": "(()=>{\n\"use strict\";\n// The Narration interface on the Block kit: the voiceover on the stage, the\n// facts and the script on the left, the voice as chips, and one button that\n// names the next step: Write script, then Make voiceover, or Use my recording.\n// The status beside it says the state in a few words; the price is StillMade's.\nconst $ = id => document.getElementById(id);\nconst state = { input: {}, outputs: {}, busy: false, previewUrl: null, previewFor: null, factsFromChat: false };\nconst message = error => String(error && error.message || error || 'Something went wrong.');\nconst say = (id, text) => { $(id).textContent = text || ''; };\nconst trimmed = value => String(value || '').trim();\nconst wordCount = text => trimmed(text) ? trimmed(text).split(/\\s+/).length : 0;\nconst clock = seconds => { const whole = Math.max(0, Math.round(seconds)); return Math.floor(whole / 60) + ':' + String(whole % 60).padStart(2, '0'); };\nconst spoken = text => 'about ' + clock(wordCount(text) / 150 * 60);\nconst picked = name => { const node = document.querySelector('input[name=\"' + name + '\"]:checked'); return node ? node.value : ''; };\nconst pick = (name, value) => { const node = document.querySelector('input[name=\"' + name + '\"][value=\"' + value + '\"]'); if (node) node.checked = true; };\n// A brief or script value is {schemaVersion: 1, text}; older briefs carry a topic.\nconst words = value => typeof value === 'string' ? value : value && typeof value === 'object' ? trimmed(value.text || value.topic || value.summary || '') : '';\nconst facts = () => trimmed($('facts').value);\nconst script = () => trimmed($('words').value);\nconst sameWords = (a, b) => trimmed(a).replace(/\\s+/g, ' ') === trimmed(b).replace(/\\s+/g, ' ');\n\nfunction status(text, tone, why) {\n const node = $('status');\n node.textContent = text || '';\n if (tone) node.dataset.state = tone; else delete node.dataset.state;\n $('why').hidden = !why; $('why').open = false; say('why-text', why || '');\n}\n// A field the host shares is written through its own input event, so the edit is kept.\nfunction setField(id, value) {\n const node = $(id);\n if (node.value === value) return;\n node.value = value;\n node.dispatchEvent(new Event('input', { bubbles: true }));\n node.dispatchEvent(new Event('change', { bubbles: true }));\n}\n\n// ── What the person is about to do ──────────────────────────────────────\nfunction phase() {\n if (!script()) return 'write';\n if (state.input.take) return 'take';\n const audio = state.outputs.audio, made = state.outputs.script;\n return audio && made && sameWords(words(made), script()) ? 'ready' : 'voice';\n}\nfunction render() {\n const p = phase(), count = wordCount(script());\n say('words-state', count ? count + ' words · ' + spoken(script()) : '');\n say('facts-state', facts() ? (state.factsFromChat ? 'From Chat' : '') : 'Needed');\n const labels = { write: 'Write script', voice: 'Make voiceover', ready: 'Make voiceover again', take: 'Use my recording' };\n say('action', labels[p]);\n $('again').hidden = p === 'write';\n $('action').disabled = state.busy || (p === 'write' && !facts());\n $('again').disabled = state.busy;\n if (state.busy) return;\n if (p === 'write') status(facts() ? 'Written from the facts' : 'Add the facts first', facts() ? '' : 'waiting', facts() ? 'The script uses only what the facts say; you can change every word before the voice runs.' : 'Chat usually fills the facts from your conversation. Type them here when it has not.');\n else if (p === 'take') status('Your recording will be used', '', 'The audio you supplied becomes the voiceover; nothing is generated.');\n else if (p === 'voice') status(state.outputs.audio ? 'Script changed, so the voice runs again' : 'Script is done, so only the voice runs', '', 'Only the voice is generated now; the words stay as written.');\n else status('Voiceover ready', 'success', '');\n}\n\n// ── The stage: the voiceover, when there is one ───────────────────────────\nasync function showAudio(reference, from) {\n if (!reference) { $('take').hidden = true; $('empty').hidden = false; return; }\n $('empty').hidden = true; $('take').hidden = false;\n say('from', from || '');\n say('length', Number.isFinite(reference.duration) ? clock(reference.duration) : spoken(script()));\n const key = reference.assetId + '/' + reference.versionId;\n if (state.previewFor === key) return;\n if (state.previewUrl) { try { StillMade.releaseMedia(state.previewUrl); } catch {} state.previewUrl = null; }\n state.previewFor = key;\n try {\n const preview = await StillMade.previewMedia(reference);\n if (state.previewFor !== key) { try { StillMade.releaseMedia(preview.url); } catch {} return; }\n state.previewUrl = preview.url; $('player').src = preview.url;\n } catch (error) {\n if (typeof reference.url === 'string' && /^https?:|^\\//.test(reference.url)) $('player').src = reference.url;\n else status(message(error), 'error', '');\n }\n}\n\n// ── Running ───────────────────────────────────────────────────────────────\nfunction values(forVoice) {\n const value = {};\n if (facts()) value.brief = { schemaVersion: 1, text: facts() };\n value.tone = picked('tone') || 'warm';\n const rules = trimmed($('rules').value); if (rules) value.rules = rules;\n if (forVoice) {\n value.script = { schemaVersion: 1, text: script() };\n value.voice = picked('voice') || 'onyx';\n value.speed = Number(picked('speed')) || 1;\n if (state.input.take) value.take = state.input.take;\n }\n return value;\n}\nasync function act(writeAgain) {\n if (state.busy) return;\n const p = writeAgain ? 'write' : phase();\n state.busy = true; render();\n status(p === 'write' ? 'Writing the script' : p === 'take' ? 'Keeping your recording' : 'Making the voice', 'loading', '');\n try {\n const result = await StillMade.run(values(p !== 'write'));\n state.outputs = { ...state.outputs, ...result };\n if (p === 'write') { setField('words', words(result.script)); await showAudio(null); }\n else await showAudio(result.audio, p === 'take' ? 'Your recording' : voiceName());\n } catch (error) {\n status(message(error), 'error', '');\n state.busy = false; render(); return;\n }\n state.busy = false; render();\n}\nconst voiceName = () => { const node = document.querySelector('input[name=\"voice\"]:checked'); return node ? node.parentElement.textContent.trim().split(/\\s+/)[0] : 'Voice'; };\n$('action').addEventListener('click', () => act(false));\n$('again').addEventListener('click', () => act(true));\nfor (const id of ['facts', 'words', 'rules']) $(id).addEventListener('input', () => { if (id === 'facts') state.factsFromChat = false; render(); });\nfor (const name of ['tone', 'voice', 'speed']) for (const node of document.querySelectorAll('input[name=\"' + name + '\"]')) node.addEventListener('change', render);\n\n// ── What the host gives: the Step's inputs and what was saved before ──────\nStillMade.onInput(input => {\n state.input = input || {};\n fillFromInput();\n if (typeof state.input.tone === 'string' && state.input.tone in { warm: 1, friendly: 1, 'no-nonsense': 1, luxury: 1 }) pick('tone', state.input.tone);\n if (typeof state.input.voice === 'string') pick('voice', state.input.voice);\n render();\n});\n// The host's shared fields start from the saved shared state, which can arrive after the\n// Step's input did and leave a bound field empty; what the input gave is put back then.\nfunction fillFromInput() {\n const brief = words(state.input.brief);\n if (brief && !facts()) { setField('facts', brief); state.factsFromChat = true; }\n const given = words(state.input.script);\n if (given && !script()) setField('words', given);\n}\nfunction adopt(snapshot) {\n const outputs = snapshot && snapshot.outputs || {};\n state.outputs = outputs;\n fillFromInput();\n if (outputs.script && !script()) setField('words', words(outputs.script));\n if (outputs.audio) showAudio(outputs.audio, state.input.take ? 'Your recording' : voiceName()); else showAudio(null);\n render();\n}\nif (typeof StillMade.onShared === 'function') StillMade.onShared(adopt);\ntry { if (typeof StillMade.getShared === 'function') { const shared = StillMade.getShared(); if (shared && typeof shared.then === 'function') shared.then(adopt, () => {}); else adopt(shared); } } catch {}\nrender();\n\n})();\n"
|
|
185
|
+
},
|
|
186
|
+
"tests": [
|
|
187
|
+
{
|
|
188
|
+
"name": "Uses the supplied recording as the voiceover and keeps the script beside it",
|
|
189
|
+
"input": {
|
|
190
|
+
"script": {
|
|
191
|
+
"schemaVersion": 1,
|
|
192
|
+
"text": "Looking for an SUV that does it all? Come see this 2021 Toyota RAV4 XLE at Sunrise Motors."
|
|
193
|
+
},
|
|
194
|
+
"take": {
|
|
195
|
+
"kind": "audio",
|
|
196
|
+
"assetId": "own-take-asset",
|
|
197
|
+
"versionId": "own-take-v1",
|
|
198
|
+
"url": "https://media.example/own-take.mp3",
|
|
199
|
+
"mimeType": "audio/mpeg",
|
|
200
|
+
"duration": 34.2,
|
|
201
|
+
"bytes": 548000
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
"expected": {
|
|
205
|
+
"script": {
|
|
206
|
+
"schemaVersion": 1,
|
|
207
|
+
"text": "Looking for an SUV that does it all? Come see this 2021 Toyota RAV4 XLE at Sunrise Motors."
|
|
208
|
+
},
|
|
209
|
+
"audio": {
|
|
210
|
+
"kind": "audio",
|
|
211
|
+
"assetId": "own-take-asset",
|
|
212
|
+
"versionId": "own-take-v1",
|
|
213
|
+
"url": "https://media.example/own-take.mp3",
|
|
214
|
+
"mimeType": "audio/mpeg",
|
|
215
|
+
"duration": 34.2,
|
|
216
|
+
"bytes": 548000
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
};
|
package/package.json
CHANGED
|
@@ -297,9 +297,9 @@ export async function main(rawArgs) {
|
|
|
297
297
|
const scan=scanPackage(pkg);if(!scan.passed)throw Object.assign(new Error('Portable admission failed'),{report:scan});
|
|
298
298
|
const evidence=['validate','pack'].includes(command)?await verifyPortableSources(pkg):null;
|
|
299
299
|
if(command==='validate')return {valid:true,format:pkg.portable.format,id:pkg.manifest.id,security:scan,execution:'not-run',repositoryEvidence:evidence,...(args.includes('--mobile')?{mobileProfile:mobileProfile()}:{}),...(hosted?{tests:0,fixtureContracts:pkg.tests.length,liveVerified:false,reviewRequired:true}:{})};
|
|
300
|
-
if(command==='test'){const report=await admitLocalPackage(pkg);if(!report.passed)throw Object.assign(new Error('Portable admission failed'),{report});return report;}
|
|
300
|
+
if(command==='test'){const report=await admitLocalPackage(pkg,{files:moduleFiles.get(pkg),moduleExecution:true});if(!report.passed)throw Object.assign(new Error('Portable admission failed'),{report});return report;}
|
|
301
301
|
if(command==='pack'){
|
|
302
|
-
const report=await admitLocalPackage(pkg);if(!report.passed)throw Object.assign(new Error('Portable admission failed'),{report});
|
|
302
|
+
const report=await admitLocalPackage(pkg,{files:moduleFiles.get(pkg),moduleExecution:true});if(!report.passed)throw Object.assign(new Error('Portable admission failed'),{report});
|
|
303
303
|
const output=resolve(extra||`${pkg.manifest.id}-${pkg.manifest.version}.stillmade-block`);
|
|
304
304
|
if(!output.endsWith('.stillmade-block'))throw new Error('Canonical output must end in .stillmade-block');
|
|
305
305
|
await writeFile(output,writePortableArchive(pkg,archiveCodec()),{flag:'wx'});return {file:output,digest:report.digest,tests:hosted?0:pkg.tests.length,format:pkg.portable.format,admission:report,repositoryEvidence:evidence,...(hosted?{fixtureContracts:pkg.tests.length,execution:'not-run',liveVerified:false,reviewRequired:true}:{})};
|
|
@@ -81,7 +81,7 @@ function rehearsalContext(packages){
|
|
|
81
81
|
* @param options.liveReceipt {digest} of a reviewed live hosted run for this exact package
|
|
82
82
|
* @param options.unsupported {reason,gapClass,primitive} when authoring reported a platform gap
|
|
83
83
|
*/
|
|
84
|
-
export async function assessBlockReadiness(pkg,{catalog=FIRST_PARTY_SDK_PACKAGES,admission,platformChecks,sharedEditing,liveReceipt,unsupported,budgetMs=20000,signal}={}){
|
|
84
|
+
export async function assessBlockReadiness(pkg,{catalog=FIRST_PARTY_SDK_PACKAGES,admission,platformChecks,sharedEditing,liveReceipt,unsupported,budgetMs=20000,signal,moduleExecution=false}={}){
|
|
85
85
|
const deadline=Date.now()+budgetMs,left=()=>deadline-Date.now(),items=new Map(),composition={};
|
|
86
86
|
const set=(id,state,detail={})=>{if(!items.has(id)||items.get(id).state!=='failed')items.set(id,{id,title:TITLES[id],state,...detail});};
|
|
87
87
|
const verified=(id,evidence)=>set(id,'verified',{evidence});
|
|
@@ -103,7 +103,7 @@ export async function assessBlockReadiness(pkg,{catalog=FIRST_PARTY_SDK_PACKAGES
|
|
|
103
103
|
|
|
104
104
|
const manifest=pkg.manifest,sourceDigest=await digest(pkg),hosted=!offline(pkg);
|
|
105
105
|
let report=admission;
|
|
106
|
-
if(!report){try{report=await admitLocalPackage(pkg,{timeoutMs:Math.max(1000,Math.min(15000,left()))});}catch(error){
|
|
106
|
+
if(!report){try{report=await admitLocalPackage(pkg,{timeoutMs:Math.max(1000,Math.min(15000,left())),moduleExecution:moduleExecution===true});}catch(error){
|
|
107
107
|
// Admission running out of this check's own time budget is not a Block defect.
|
|
108
108
|
const exhausted=error.code==='TIMEOUT'&&/shared budget/.test(error.message||'');
|
|
109
109
|
report={passed:false,results:[{name:'Admission',passed:false,code:exhausted?'READINESS_BUDGET':error.code,message:message(error)}]};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {REVENUE_REVIEW_PROJECT_TYPES} from './revenue-review-project-types.js';
|
|
2
|
-
import {PRODUCT_AD_VARIANTS_PROJECT_TYPE,PRODUCT_AD_VARIANTS_PROJECT_TYPE_1_0_3,PRIOR_PRODUCT_AD_VARIANTS_PROJECT_TYPE,PREVIOUS_PRODUCT_AD_VARIANTS_PROJECT_TYPE} from './product-ad-variants-project-type.js';
|
|
3
1
|
import {FIRST_PARTY_SDK_PACKAGES} from '../block-sdk/first-party-packages.js';
|
|
4
|
-
import {
|
|
2
|
+
import {DEALER_INVENTORY_VIDEO_PROJECT_TYPE} from './dealer-inventory-video-project-type.js';
|
|
5
3
|
import { SDK_VERSION, ROOT_CHAT_ID } from '../block-sdk/contracts.js';
|
|
6
4
|
import {SCRIPT_WRITER_BLOCK} from './script-writer.js';
|
|
7
5
|
import {VIDEO_ASSETS_BLOCK} from './video-assets.js';
|
|
@@ -133,16 +131,24 @@ const currentPortableById=new Map();
|
|
|
133
131
|
// An unset app connector is a project placeholder, not something to find or install.
|
|
134
132
|
// Photo notes is the photo check inside Pick best photos, not a Step of its own.
|
|
135
133
|
export const UNDISCOVERABLE_SDK_BLOCKS=new Set(['sm.connect-app','sm.photo-notes']);
|
|
134
|
+
// Hidden from discovery (October 20, 2026, owner decision): fragments a job now covers (Script
|
|
135
|
+
// Writer and Voiceover are Narration), a second Chat, tools that belong inside their app (the
|
|
136
|
+
// assistants, the Editor's AI edits, references, compare, image editor, scoring) and thin results.
|
|
137
|
+
// Every release stays in BUILTIN_BLOCKS: saved projects and built-in Types keep their pins and run.
|
|
138
|
+
export const HIDDEN_BLOCKS=new Set(['sm.script','sm.voiceover','sm.chat','sm.thumbnail','sm.shot-plan-assistant','sm.blueprint-assistant','sm.animated-scene-assistant','sm.whiteboard-assistant','sm.final-edit','sm.edit-assistant','sm.character','sm.location','sm.style','sm.compare','sm.image-editor','sm.score','sm.research','sm.quality-audit','sm.rough-cut']);
|
|
139
|
+
// Exact releases to hide (id@version) when one release of a shown Block should not be offered; none today.
|
|
140
|
+
export const HIDDEN_RELEASES=new Set([]);
|
|
141
|
+
export const isHiddenBlock=block=>HIDDEN_BLOCKS.has(block.manifest.id)||HIDDEN_RELEASES.has(`${block.manifest.id}@${block.manifest.version}`);
|
|
136
142
|
// Export video is offered as the original Editor opened at its Export (1.0.0), so
|
|
137
143
|
// people can still change the cut before rendering (the owner, Oct 15). The
|
|
138
144
|
// render-only 2.0.0 stays in BUILTIN_BLOCKS for exact project pins and for
|
|
139
145
|
// planning with explicit candidates; a prepared timeline goes to the portable Editor.
|
|
140
146
|
const EXPORT_VIDEO_EDITOR=LEGACY_BLOCKS.find(block=>block.manifest.id==='sm.export-video'&&block.manifest.version==='1.0.0');
|
|
141
147
|
export const DISCOVERED_AS_HOST=new Map([['sm.export-video',EXPORT_VIDEO_EDITOR]]);
|
|
142
|
-
for(const block of SDK_BUILTIN_BLOCKS){if(UNDISCOVERABLE_SDK_BLOCKS.has(block.manifest.id)||DISCOVERED_AS_HOST.has(block.manifest.id))continue;const prior=currentPortableById.get(block.manifest.id);if(!prior||newer(block.manifest.version,prior.manifest.version))currentPortableById.set(block.manifest.id,block);}
|
|
148
|
+
for(const block of SDK_BUILTIN_BLOCKS){if(UNDISCOVERABLE_SDK_BLOCKS.has(block.manifest.id)||DISCOVERED_AS_HOST.has(block.manifest.id)||isHiddenBlock(block))continue;const prior=currentPortableById.get(block.manifest.id);if(!prior||newer(block.manifest.version,prior.manifest.version))currentPortableById.set(block.manifest.id,block);}
|
|
143
149
|
export const DISCOVERABLE_SDK_BUILTIN_BLOCKS = Object.freeze([...currentPortableById.values()]);
|
|
144
150
|
const ACCEPTED_PORTABLE_RELEASES=[...MAIN_HOST_WORKSPACES].map(id=>FIRST_PARTY_SDK_PACKAGES.find(pkg=>pkg.manifest.id===id&&pkg.manifest.version===CURRENT_PORTABLE_WORKSPACE_VERSIONS[id])).filter(Boolean);
|
|
145
|
-
export const DISCOVERABLE_BLOCKS = [STARTING_CHAT_BLOCK,...WORKSPACE_BLOCKS,...ACCEPTED_PORTABLE_RELEASES,...DISCOVERED_AS_HOST.values(),...DISCOVERABLE_SDK_BUILTIN_BLOCKS.filter(block=>!ACCEPTED_PORTABLE_RELEASES.includes(block)).filter(block=>!WORKSPACE_BLOCKS.some(current=>current.manifest.id===block.manifest.id&¤t.manifest.version===block.manifest.version))];
|
|
151
|
+
export const DISCOVERABLE_BLOCKS = [STARTING_CHAT_BLOCK,...WORKSPACE_BLOCKS,...ACCEPTED_PORTABLE_RELEASES,...DISCOVERED_AS_HOST.values(),...DISCOVERABLE_SDK_BUILTIN_BLOCKS.filter(block=>!ACCEPTED_PORTABLE_RELEASES.includes(block)).filter(block=>!WORKSPACE_BLOCKS.some(current=>current.manifest.id===block.manifest.id&¤t.manifest.version===block.manifest.version))].filter(block=>!isHiddenBlock(block));
|
|
146
152
|
// Planners pin a finished file to a Block's output port, which the Editor alias has
|
|
147
153
|
// none of, so planning keeps the newest render-only release in its place. Chat
|
|
148
154
|
// builds then fold it into the Editor (compact-project.js), so people still get
|
|
@@ -179,8 +185,19 @@ const PRIOR_CURRENT_TYPES = currentBuiltinTypes('1.13.0','2.2.3',SCRIPT_WRITER_B
|
|
|
179
185
|
const PREVIOUS_PORTABLE_TYPES=currentBuiltinTypes('1.14.0','2.2.4',SCRIPT_WRITER_BLOCK.manifest.version,{'blueprint':'2.1.8','panel-strip':'2.3.6','board-editor':'2.2.9','animated-scene':'2.3.7','editor':'2.2.8'});
|
|
180
186
|
// 1.16.0: every built-in type ends in the original Editor (1.0), the full video editor.
|
|
181
187
|
const PORTABLE_EDITOR_TYPES=currentBuiltinTypes('1.15.0','2.2.4','2.1.4',{'voiceover':'2.1.4','shot-plan':'2.1.3','blueprint':'2.1.9','panel-strip':'2.3.7','board-editor':'2.2.10','animated-scene':'2.3.8','editor':'2.2.9'});
|
|
182
|
-
export const CURRENT_BUILTIN_TYPES = [...currentBuiltinTypes('1.16.0','2.2.4','2.1.4',{'voiceover':'2.1.4','shot-plan':'2.1.3','blueprint':'2.1.9','panel-strip':'2.3.7','board-editor':'2.2.10','animated-scene':'2.3.8','editor':'1.0.0'})
|
|
183
|
-
const HISTORICAL_CURRENT_TYPES=[...PORTABLE_EDITOR_TYPES
|
|
188
|
+
export const CURRENT_BUILTIN_TYPES = [...currentBuiltinTypes('1.16.0','2.2.4','2.1.4',{'voiceover':'2.1.4','shot-plan':'2.1.3','blueprint':'2.1.9','panel-strip':'2.3.7','board-editor':'2.2.10','animated-scene':'2.3.8','editor':'1.0.0'}),DEALER_INVENTORY_VIDEO_PROJECT_TYPE];
|
|
189
|
+
const HISTORICAL_CURRENT_TYPES=[...PORTABLE_EDITOR_TYPES,...PREVIOUS_PORTABLE_TYPES,...PRIOR_CURRENT_TYPES,...SHARED_WORKSPACE_TYPES,...SHARED_CANVAS_TYPES,...SHARED_BLUEPRINT_TYPES,...PRIOR_SHARED_TYPES,...MEDIA_LIBRARY_TYPES,...EDITOR_AUDIO_TYPES,...PLAYABLE_EDITOR_TYPES,...ADVANCED_PORTABLE_TYPES,...PORTABLE_MVP_TYPES,...PRE_PROMOTION_CURRENT_TYPES,...PREVIOUS_BUILTIN_TYPES.filter(type=>flows[type.legacyMode].includes('shot-plan')),...currentBuiltinTypes('1.1.0','1.1.0','1.1.0',PRIOR_HOST_WORKSPACE_VERSIONS)];
|
|
184
190
|
export const BUILTIN_TYPES = [...CURRENT_BUILTIN_TYPES,...HISTORICAL_CURRENT_TYPES,...LEGACY_TYPES];
|
|
191
|
+
// Hidden from discovery (October 20, 2026, owner decision: keep only Guided, Image Panels, Blank
|
|
192
|
+
// Canvas and White Board Motion; the rest start again on the updated SDK). Animated Studio and the
|
|
193
|
+
// Dealer Type keep their releases here, so saved projects keep their pins and run and every lookup
|
|
194
|
+
// by id still resolves; the Type shelf, Chat's Types-first check, the marketplace, the sitemap,
|
|
195
|
+
// search and the catalog read DISCOVERABLE_TYPES. The eight revenue-review Types, Product Footage
|
|
196
|
+
// Ad Variants and the two photo reels are no longer built-in Types at all: their definitions stay
|
|
197
|
+
// in the repository only as acceptance fixtures (revenue-review-project-types.js and friends), a
|
|
198
|
+
// saved project carries its own copy of its Type, and the Blocks they used remain Blocks.
|
|
199
|
+
export const HIDDEN_TYPES=new Set(['sm.animated','stillmade.dealer-inventory-video']);
|
|
200
|
+
export const isHiddenType=type=>HIDDEN_TYPES.has((type?.manifest||type)?.id);
|
|
201
|
+
export const DISCOVERABLE_TYPES=Object.freeze(CURRENT_BUILTIN_TYPES.filter(type=>!isHiddenType(type)));
|
|
185
202
|
export const workspaceBlock = (id,version)=>DISCOVERABLE_BLOCKS.find(block=>block.manifest.id===id && (!version || block.manifest.version===version));
|
|
186
203
|
export const discoverableBuiltin = item=>!!workspaceBlock((item.manifest || item).id,(item.manifest || item).version);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Dealer Inventory Video (October 20, 2026): one vehicle listing and its photos
|
|
2
|
+
// become a narrated inventory video in two Steps after Chat. Chat gathers the
|
|
3
|
+
// facts into Narration's brief; Narration writes the words and voices them (one
|
|
4
|
+
// Block, two hosted operations); the photos are cut to the words in one Video
|
|
5
|
+
// Step made of Import media, Shots and Export video. Each release is pinned:
|
|
6
|
+
// adopt a newer Block in a new Type version, never by editing this one.
|
|
7
|
+
import narration from '../../blocks/narration/versions/1.0.0/package.js';
|
|
8
|
+
import mediaImport from '../../blocks/native/sm.import-media/versions/2.0.2/package.js';
|
|
9
|
+
import shots from '../../blocks/native/sm.shot-plan/versions/2.2.1/package.js';
|
|
10
|
+
import exportVideo from '../../blocks/native/sm.export-video/versions/2.1.0/package.js';
|
|
11
|
+
|
|
12
|
+
const description='Turn one vehicle listing and its photos into a narrated inventory video: Chat gathers the facts, Narration writes and voices them, and the photos are cut to the words.';
|
|
13
|
+
export const DEALER_INVENTORY_VIDEO_PROJECT_TYPE=Object.freeze({
|
|
14
|
+
schemaVersion:1,id:'stillmade.dealer-inventory-video',version:'1.0.0',name:'Dealer Inventory Video',description,license:'MIT',startingChat:'native',
|
|
15
|
+
summary:{text:description,origin:'human',generation:'authored',status:'ready'},
|
|
16
|
+
defaults:{aspectRatio:'16:9',brief:'Tell Chat the vehicle: year, make, model and trim, price, mileage, the features worth saying, the dealership name and how to reach it. Narration writes the words from those facts only and reads them in the voice you pick, or uses your own recording. Import the vehicle photos, check the shots, and make the video.'},
|
|
17
|
+
onboarding:[],
|
|
18
|
+
packages:[narration,mediaImport,shots,exportVideo].map(pkg=>structuredClone(pkg)),
|
|
19
|
+
stages:[
|
|
20
|
+
{id:'narration',blockId:narration.manifest.id,version:narration.manifest.version,label:'Narration'},
|
|
21
|
+
{id:'photos',blockId:mediaImport.manifest.id,version:mediaImport.manifest.version,label:'Vehicle photos',config:{include:'Images',duplicates:'Remove duplicates'}},
|
|
22
|
+
{id:'shots',blockId:shots.manifest.id,version:shots.manifest.version,label:'Shots'},
|
|
23
|
+
{id:'video',blockId:exportVideo.manifest.id,version:exportVideo.manifest.version,label:'Video'},
|
|
24
|
+
],
|
|
25
|
+
connections:[
|
|
26
|
+
{from:{stage:'narration',port:'script'},to:{stage:'shots',port:'narrationScript'}},
|
|
27
|
+
{from:{stage:'photos',port:'assets'},to:{stage:'video',port:'media'}},
|
|
28
|
+
{from:{stage:'narration',port:'audio'},to:{stage:'video',port:'narration'}},
|
|
29
|
+
],
|
|
30
|
+
// Photos, shots and the render are one Step a person opens: the video.
|
|
31
|
+
groups:[{id:'make-video',label:'Video',stages:['photos','shots','video'],result:{stage:'video',port:'output'}}],
|
|
32
|
+
outcome:{type:'export',dedupe:'material_project_state',minimumDurationSec:5},
|
|
33
|
+
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// immutable, so the plain wording for first-party Blocks lives here, by Block id.
|
|
4
4
|
// A Block whose own description is already plain needs no entry.
|
|
5
5
|
export const PLAIN_BLOCK_DESCRIPTIONS=Object.freeze({
|
|
6
|
+
'stillmade.narration':'Turn the facts from Chat into a script, hear it in a catalog voice or use your own recording, and pass the script and the voiceover on.',
|
|
6
7
|
'stillmade.screen-recorder':'Record your screen or a window, keep it on your device, cut out the parts you do not want, and pass it on.',
|
|
7
8
|
'sm.script':'Write and edit a script with AI. Set a length, add notes, and go back to earlier versions.',
|
|
8
9
|
'sm.shot-plan':'Turn a script into a list of timed shots, then edit what each shot shows and how long it lasts.',
|
|
@@ -7,11 +7,10 @@ import {PROPERTY_LISTING_REEL_PROJECT_TYPE,PRODUCT_PROMO_REEL_PROJECT_TYPE} from
|
|
|
7
7
|
|
|
8
8
|
// These are editable starting configurations of the existing production Blocks,
|
|
9
9
|
// not separate generators or new marketplace identities.
|
|
10
|
+
// October 20, 2026 (owner): the revenue-review, ad-variant and photo-reel starters are off the
|
|
11
|
+
// Create page; their definitions stay as acceptance fixtures, so newProjectTypeFromStarter still
|
|
12
|
+
// resolves them by id for those drivers and for the creator experiments.
|
|
10
13
|
export const PROJECT_TYPE_STARTERS=Object.freeze([
|
|
11
|
-
...REVENUE_REVIEW_STARTERS,
|
|
12
|
-
{id:'product-ad-variants',name:'Product Footage Ad Variants',description:'Make three distinct, editable opening variants from your real product demonstration and approved hooks.'},
|
|
13
|
-
{id:'property-listing-reel',name:'Property Listing Reel',description:'Turn 3–8 original property photos into an editable vertical listing reel with your approved facts and contact details.'},
|
|
14
|
-
{id:'product-promo-reel',name:'Product Promo Reel',description:'Turn original product photos and approved copy into an editable vertical promo, while preserving the real product.'},
|
|
15
14
|
{id:'faceless-youtube',name:'Faceless YouTube',description:'Write a narrated story, plan the visuals, and assemble a complete video.',blocks:['script','voiceover','shot-plan','blueprint','pipeline','editor'],brief:'Create a faceless YouTube video. Start with a clear hook, develop the story with concrete visual beats, and end with a useful conclusion. Ask for the topic, audience, target duration and sources. Do not invent factual claims.'},
|
|
16
15
|
{id:'short-form',name:'Short-form vertical',description:'Build a concise story with a strong opening and visuals for a vertical screen.',blocks:['script','voiceover','shot-plan','pipeline','editor'],brief:'Create a short-form vertical video. Ask for the subject and intended platform. Keep narration concise, place the hook immediately, and plan readable 9:16 compositions with room for captions. Confirm the target length before writing.'},
|
|
17
16
|
{id:'ai-commercial',name:'AI commercial',description:'Develop a campaign idea, maintain its visual direction, and edit the final spot.',blocks:['script','voiceover','shot-plan','blueprint','pipeline','editor'],brief:'Develop an AI commercial from the supplied campaign brief. Establish the audience, promise, tone, product proof and call to action. Ask for brand assets and target duration. Use only supported product claims; preserve a consistent visual direction.'},
|
|
@@ -50,9 +49,10 @@ export function newProjectTypeFromStarter(starter,id){
|
|
|
50
49
|
const selected=PROJECT_TYPE_STARTERS.find(item=>item.id===starter);
|
|
51
50
|
const reviewed=REVENUE_REVIEW_PROJECT_TYPES.find(type=>type.id==='stillmade.'+starter);
|
|
52
51
|
if(reviewed)return {...structuredClone(reviewed),id};
|
|
53
|
-
|
|
54
|
-
if(
|
|
55
|
-
if(
|
|
52
|
+
// Fixture Types (no longer on the Create page) still resolve by id for the acceptance drivers.
|
|
53
|
+
if(starter==='product-ad-variants')return {...structuredClone(PRODUCT_AD_VARIANTS_PROJECT_TYPE),id};
|
|
54
|
+
if(starter==='property-listing-reel')return {...structuredClone(PROPERTY_LISTING_REEL_PROJECT_TYPE),id};
|
|
55
|
+
if(starter==='product-promo-reel')return {...structuredClone(PRODUCT_PROMO_REEL_PROJECT_TYPE),id};
|
|
56
56
|
if(selected?.id==='saas-launch')return {...structuredClone(PRODUCT_LAUNCH_UPDATED),id};
|
|
57
57
|
if(selected?.id==='screenshot-motion')return {...structuredClone(SCREENSHOT_TO_MOTION_PROJECT_TYPE),id};
|
|
58
58
|
if(selected?.id==='product-photo-ad')return {...structuredClone(PRODUCT_PHOTO_AD_PROJECT_TYPE),id};
|
|
@@ -37,6 +37,9 @@ const SECTIONS=Object.freeze([
|
|
|
37
37
|
rule('reuse-first','both',
|
|
38
38
|
'Reuse before building: use, remix or chain an existing Block when one fits; build only what is missing.',
|
|
39
39
|
'Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator\'s source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.'),
|
|
40
|
+
rule('block-tier','both',
|
|
41
|
+
'Three sizes: an app a person works in, a job that makes one deliverable from the hosted operations it needs (what Chat picks), an ingredient (one hosted operation) that only a job is made of.',
|
|
42
|
+
'Blocks come in three sizes and the manifest decides which (packages/block-sdk/block-tier.js): an app is a workspace a person works in (Editor, Canvas, Shots), placed in a Type and never wired automatically; a job is one deliverable a person would name (the narration, the captioned clip), built from the hosted operations it needs (`permissions.capabilities`) and reviewed in one place, the unit Chat picks for a Step; an ingredient is one hosted operation with nothing of its own (a declarative capability Block), what a job is made of. When two ingredients would stand in a row to make one deliverable (writing the words, then voicing them), build or pick the job that does both and keep the review point inside it. A job declares a second output when another Block consumes the intermediate (the script beside the voice); split into two jobs only when the intermediate has a life of its own or the halves have different owners or payment.'),
|
|
40
43
|
rule('block-scope','both',
|
|
41
44
|
'One Step per job: start one only where a person works with the result, where finding new items turns into handling each, or where the result leaves StillMade; all else is a part.',
|
|
42
45
|
'A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.\n\n1. **A person works with the result**: reads, edits, picks or approves it (a workspace Block such as a script, a Canvas or an Editor, or an approval). That is why Chat, Bible, Canvas and Editor are four Steps.\n2. **Finding new things turns into handling each one**: the trigger (a schedule or an app event), the app read, its filter and "only new" are the first job; what is done with each item is the next job. A trigger is a setting of the first job, never a Step of its own.\n3. **The result leaves StillMade**: it is sent, posted, published, paid or written into another app. Consecutive writes to the same app, with the glue that shapes data for them, are one job.\n\nIn a production the Steps are the workspaces a person opens: work feeding the next workspace is its part, else it joins the one before; neighbouring workspaces with one main result type are one Step unless another part feeds the second. A video ends in the Editor (1.0), which has captions and export built in, so Export video is never a Step beside it.\n\nA part that only runs in the desktop app cannot share a Step with cloud parts. Before adding a Step, name which of the three reasons makes it separate; if none applies, make it a part of a neighbor. A Step nobody would ever open must not exist on its own. Prefer an app task\'s own filter (a Gmail query such as `has:attachment filename:png`) to an AI check, and use AI decide only for judgement.\n\nInside one job, each capability stays its own part: a capability Block performs exactly one hosted operation and a connector one app task, so every part keeps its own permission, approval and credit rules, and StillMade groups the parts of one job into one Step (`groups` in the Project Type). Never invent a combined runtime. When one Block can do the whole job with documented primitives (a JavaScript Block doing several local transforms, a workspace owning its whole document), build it as one Block.\n\nExamples: "every new email with an image becomes a generated image" is two Steps, "Gmail · New emails with images" (When, fetch with an image filter, only new) and "Image from email" (AI write, Generate image); listing When, Fetch, Only new, AI decide, Continue only if, AI write and Generate image as seven Steps splits one job into its parts. "A new Meta lead goes to HubSpot" is three Steps: "Meta Ads · New leads", "Qualify lead" and "HubSpot · Add contact".'),
|
|
@@ -133,7 +136,7 @@ export const BUILDER_CONTRACT=Object.freeze({
|
|
|
133
136
|
gapClasses:GAP_CLASSES,
|
|
134
137
|
});
|
|
135
138
|
|
|
136
|
-
const SECTION_TITLES=Object.freeze({'grounded-text':'Only supplied facts','source-of-truth':'Source of truth','reuse-first':'Reuse first','block-scope':'One job per Step','capability-contract':'Define the capability first','feature-map':'Feature map','compose':'Blocks compose','platform-primitives':'Use platform primitives','no-workarounds':'No hidden workarounds','naming':'Name it briefly','ui':'Interface','multiplayer':'Shared editing','external-services':'External services','dependencies':'Dependencies','security':'Security','lifecycle':'Execution lifecycle','errors':'Useful failures','adapt-to-platform':'Adapt to the platform','verification':'What StillMade verifies','project-types':'Project Types','readiness-report':'Readiness report'});
|
|
139
|
+
const SECTION_TITLES=Object.freeze({'grounded-text':'Only supplied facts','source-of-truth':'Source of truth','reuse-first':'Reuse first','block-tier':'Three sizes of Block','block-scope':'One job per Step','capability-contract':'Define the capability first','feature-map':'Feature map','compose':'Blocks compose','platform-primitives':'Use platform primitives','no-workarounds':'No hidden workarounds','naming':'Name it briefly','ui':'Interface','multiplayer':'Shared editing','external-services':'External services','dependencies':'Dependencies','security':'Security','lifecycle':'Execution lifecycle','errors':'Useful failures','adapt-to-platform':'Adapt to the platform','verification':'What StillMade verifies','project-types':'Project Types','readiness-report':'Readiness report'});
|
|
137
140
|
const inScope=(scope,wanted)=>wanted==='all'||scope==='both'||scope===wanted;
|
|
138
141
|
|
|
139
142
|
/** Render the contract for a model prompt ('compact'), a guide ('full') or an SDK folder ('agents-md'). */
|
|
@@ -32,7 +32,10 @@ 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
34
|
export const PORTABLE_ACTIONS = Object.freeze(['final.render','stock.search','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
|
+
// a Block is one thing with its own compact interface. No built-in uses it; a package that
|
|
37
|
+
// declares it fails validation with 'Unsupported block kind'.
|
|
38
|
+
export const KINDS = ['task', 'workspace', 'editor-extension'];
|
|
36
39
|
// Generated manifest.ui controls (inputs) and output presentations; the docs list these.
|
|
37
40
|
export const UI_INPUT_CONTROLS = Object.freeze(['text', 'number', 'slider', 'checkbox', 'asset', 'select', 'multiselect', 'file', 'date', 'color', 'url', 'richtext', 'list', 'grid']);
|
|
38
41
|
export const UI_OUTPUT_PRESENTATIONS = Object.freeze(['gallery', 'before-after', 'table', 'document', 'chart', 'code', 'player', 'download']);
|
|
@@ -175,6 +175,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
175
175
|
"sm.whiteboard-assistant@1.0.0",
|
|
176
176
|
"sm.write-summary@1.0.0",
|
|
177
177
|
"sm.youtube@2.0.0",
|
|
178
|
+
"stillmade.narration@1.0.0",
|
|
178
179
|
"stillmade.occasion-story@1.0.0",
|
|
179
180
|
"stillmade.occasion-story@1.0.1",
|
|
180
181
|
"stillmade.occasion-story@1.0.2",
|
|
@@ -3,6 +3,7 @@ import shortFormFinishV101 from '../../blocks/short-form-finish/versions/1.0.1/p
|
|
|
3
3
|
import shortFormFinishV102 from '../../blocks/short-form-finish/versions/1.0.2/package.js';
|
|
4
4
|
import shortFormFinish from '../../blocks/short-form-finish/package.js';
|
|
5
5
|
import screenRecorder from '../../blocks/screen-recorder/package.js';
|
|
6
|
+
import narration from '../../blocks/narration/package.js';
|
|
6
7
|
import occasionStoryV100 from '../../blocks/occasion-story/versions/1.0.0/package.js';
|
|
7
8
|
import releaseVisualsV100 from '../../blocks/release-visuals/versions/1.0.0/package.js';
|
|
8
9
|
import occasionStoryV101 from '../../blocks/occasion-story/versions/1.0.1/package.js';
|
|
@@ -190,5 +191,5 @@ import pickPhotos from '../../blocks/native/sm.pick-photos/versions/1.0.0/packag
|
|
|
190
191
|
// Exact, independently executable first-party releases. Host workspaces do not
|
|
191
192
|
// belong here. Changing implementation requires a new immutable version.
|
|
192
193
|
const freeze=value=>{if(value&&typeof value==='object'&&!Object.isFrozen(value)){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
|
|
193
|
-
export const FIRST_PARTY_SDK_PACKAGES = Object.freeze([freeze(productAdVariants),freeze(pickPhotos),freeze(photoNotes),freeze(youtubePublication),freeze(stockSearch),freeze(exportVideoV210),freeze(exportVideo),freeze(exportAudio),freeze(motionGraphics),freeze(audioScore),freeze(shortFormFinish),freeze(screenRecorder),freeze(shortFormFinishV102),freeze(shortFormFinishV101),freeze(shortFormFinishV100),freeze(occasionStory),freeze(occasionStoryV101),freeze(occasionStoryV100),freeze(releaseVisuals),freeze(releaseVisualsV101),freeze(releaseVisualsV100),freeze(photoReelComposer),freeze(photoReelComposerV101),freeze(photoReelComposerV100),freeze(importMediaV202),freeze(importMediaV201),freeze(editorV2212),freeze(editorV2211),freeze(editorV2210),freeze(editorV229),freeze(editorV228),freeze(panelStripV241),freeze(panelStripV240),freeze(panelStripV237),freeze(panelStripV236),freeze(boardEditorV2210),freeze(boardEditorV229),freeze(animatedSceneV238),freeze(animatedSceneV237),freeze(pipelineV225),freeze(pipelineV224),freeze(blueprintV219),freeze(blueprintV218),freeze(videoAssetsV214),freeze(videoAssetsV213),freeze(chatV203),freeze(chatV202),freeze(chatV201),freeze(shotPlanV221),freeze(shotPlanV220),freeze(shotPlanV213),freeze(shotPlanV212),freeze(voiceoverV216),freeze(voiceoverV215),freeze(voiceoverV214),freeze(voiceoverV213),freeze(scriptV215),freeze(scriptV214),freeze(scriptV213),freeze(roughCutV202),freeze(roughCutV201),freeze(finalEditV202),freeze(finalEditV201),freeze(editAssistantV202),freeze(editAssistantV201),freeze(gradeV202),freeze(gradeV201),freeze(transitionV202),freeze(transitionV201),freeze(qualityAuditV102),freeze(qualityAuditV101),freeze(compareV202),freeze(compareV201),freeze(compare),freeze(grade),freeze(transition),freeze(exportTimeline),freeze(captions),freeze(importMedia),freeze(generateImageV210),freeze(generateImage),freeze(thumbnailV201),freeze(thumbnail),freeze(writeSummary),freeze(automationWhen),freeze(automationNewItems),freeze(automationAiDecide),freeze(automationContinueIf),freeze(connectApp),freeze(automationContactFields),freeze(automationAiWrite),freeze(automationMetaCreative),freeze(voiceover),freeze(voiceoverV210),freeze(voiceoverV211),freeze(voiceoverV212),freeze(script),freeze(scriptV210),freeze(scriptV211),freeze(scriptV212),freeze(generateVideoV201),freeze(generateVideo),freeze(chat),freeze(characterV201),freeze(character),freeze(locationV201),freeze(location),freeze(styleV201),freeze(style),freeze(researchV202),freeze(researchV201),freeze(research),freeze(imageEditorV201),freeze(imageEditor),freeze(describeMediaV201),freeze(describeMedia),freeze(videoAssets),freeze(videoAssetsV210),freeze(videoAssetsV211),freeze(videoAssetsV212),freeze(roughCut),freeze(finalEdit),freeze(editAssistant),freeze(transcribeV201),freeze(transcribe),freeze(blueprint),freeze(blueprintV210),freeze(blueprintV211),freeze(blueprintV212),freeze(blueprintV213),freeze(blueprintV214),freeze(blueprintV215),freeze(blueprintV216),freeze(blueprintV217),freeze(shotPlan),freeze(shotPlanV210),freeze(shotPlanV211),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(animatedSceneV200),freeze(animatedScene),freeze(animatedSceneV220),freeze(animatedSceneV230),freeze(animatedSceneV231),freeze(animatedSceneV232),freeze(animatedSceneV233),freeze(animatedSceneV234),freeze(animatedSceneV235),freeze(animatedSceneV236),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),freeze(shotPlanAssistant),freeze(animatedSceneAssistant),freeze(blueprintAssistant),freeze(whiteboardAssistant),freeze(qualityAudit)]);
|
|
194
|
+
export const FIRST_PARTY_SDK_PACKAGES = Object.freeze([freeze(productAdVariants),freeze(pickPhotos),freeze(photoNotes),freeze(youtubePublication),freeze(stockSearch),freeze(exportVideoV210),freeze(exportVideo),freeze(exportAudio),freeze(motionGraphics),freeze(audioScore),freeze(shortFormFinish),freeze(screenRecorder),freeze(narration),freeze(shortFormFinishV102),freeze(shortFormFinishV101),freeze(shortFormFinishV100),freeze(occasionStory),freeze(occasionStoryV101),freeze(occasionStoryV100),freeze(releaseVisuals),freeze(releaseVisualsV101),freeze(releaseVisualsV100),freeze(photoReelComposer),freeze(photoReelComposerV101),freeze(photoReelComposerV100),freeze(importMediaV202),freeze(importMediaV201),freeze(editorV2212),freeze(editorV2211),freeze(editorV2210),freeze(editorV229),freeze(editorV228),freeze(panelStripV241),freeze(panelStripV240),freeze(panelStripV237),freeze(panelStripV236),freeze(boardEditorV2210),freeze(boardEditorV229),freeze(animatedSceneV238),freeze(animatedSceneV237),freeze(pipelineV225),freeze(pipelineV224),freeze(blueprintV219),freeze(blueprintV218),freeze(videoAssetsV214),freeze(videoAssetsV213),freeze(chatV203),freeze(chatV202),freeze(chatV201),freeze(shotPlanV221),freeze(shotPlanV220),freeze(shotPlanV213),freeze(shotPlanV212),freeze(voiceoverV216),freeze(voiceoverV215),freeze(voiceoverV214),freeze(voiceoverV213),freeze(scriptV215),freeze(scriptV214),freeze(scriptV213),freeze(roughCutV202),freeze(roughCutV201),freeze(finalEditV202),freeze(finalEditV201),freeze(editAssistantV202),freeze(editAssistantV201),freeze(gradeV202),freeze(gradeV201),freeze(transitionV202),freeze(transitionV201),freeze(qualityAuditV102),freeze(qualityAuditV101),freeze(compareV202),freeze(compareV201),freeze(compare),freeze(grade),freeze(transition),freeze(exportTimeline),freeze(captions),freeze(importMedia),freeze(generateImageV210),freeze(generateImage),freeze(thumbnailV201),freeze(thumbnail),freeze(writeSummary),freeze(automationWhen),freeze(automationNewItems),freeze(automationAiDecide),freeze(automationContinueIf),freeze(connectApp),freeze(automationContactFields),freeze(automationAiWrite),freeze(automationMetaCreative),freeze(voiceover),freeze(voiceoverV210),freeze(voiceoverV211),freeze(voiceoverV212),freeze(script),freeze(scriptV210),freeze(scriptV211),freeze(scriptV212),freeze(generateVideoV201),freeze(generateVideo),freeze(chat),freeze(characterV201),freeze(character),freeze(locationV201),freeze(location),freeze(styleV201),freeze(style),freeze(researchV202),freeze(researchV201),freeze(research),freeze(imageEditorV201),freeze(imageEditor),freeze(describeMediaV201),freeze(describeMedia),freeze(videoAssets),freeze(videoAssetsV210),freeze(videoAssetsV211),freeze(videoAssetsV212),freeze(roughCut),freeze(finalEdit),freeze(editAssistant),freeze(transcribeV201),freeze(transcribe),freeze(blueprint),freeze(blueprintV210),freeze(blueprintV211),freeze(blueprintV212),freeze(blueprintV213),freeze(blueprintV214),freeze(blueprintV215),freeze(blueprintV216),freeze(blueprintV217),freeze(shotPlan),freeze(shotPlanV210),freeze(shotPlanV211),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(animatedSceneV200),freeze(animatedScene),freeze(animatedSceneV220),freeze(animatedSceneV230),freeze(animatedSceneV231),freeze(animatedSceneV232),freeze(animatedSceneV233),freeze(animatedSceneV234),freeze(animatedSceneV235),freeze(animatedSceneV236),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),freeze(shotPlanAssistant),freeze(animatedSceneAssistant),freeze(blueprintAssistant),freeze(whiteboardAssistant),freeze(qualityAudit)]);
|
|
194
195
|
export const firstPartySdkPackage = manifest => FIRST_PARTY_SDK_PACKAGES.find(pkg => pkg.manifest.id === manifest?.id && pkg.manifest.version === manifest?.version);
|
|
@@ -7,8 +7,20 @@ import {requiresDesktopExecution} from './desktop-execution.js';
|
|
|
7
7
|
|
|
8
8
|
// Shared by external portable packaging and managed import. Repository evidence,
|
|
9
9
|
// account permissions and current quarantine are additional host-side checks.
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// A module Block's files (October 20, 2026): the folder's files when the caller has
|
|
11
|
+
// them, else the text a repo-shipped package carries beside its descriptor.
|
|
12
|
+
function moduleFilesOf(pkg,files){
|
|
13
|
+
if(files&&typeof files==='object'&&Object.keys(files).length)return files;
|
|
14
|
+
if(pkg?.moduleSource&&typeof pkg.moduleSource==='object')return Object.fromEntries(Object.entries(pkg.moduleSource).map(([path,text])=>[path,new TextEncoder().encode(String(text))]));
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
// Running a module here means running its code in a Node worker, which is no
|
|
18
|
+
// isolation boundary. Only a caller that owns the code asks for it: the SDK CLI
|
|
19
|
+
// on the creator's own machine (`moduleExecution:true`), never a server path
|
|
20
|
+
// admitting someone else's upload (imports, AI authoring, readiness from the API).
|
|
21
|
+
export async function admitLocalPackage(pkg,{timeoutMs=15000,files,moduleExecution=false}={}){
|
|
22
|
+
const moduleFiles=moduleExecution===true&&pkg?.manifest?.runtime==='module'?moduleFilesOf(pkg,files):null;
|
|
23
|
+
assert(['recipe','javascript','capability','comfyui'].includes(pkg?.manifest?.runtime)||moduleFiles,'RUNTIME_RESTRICTED',pkg?.manifest?.runtime==='module'?'Local admission runs a module Block only on your own machine (the SDK CLI); StillMade runs it in the browser sandbox after review.':'Local admission supports recipes, isolated JavaScript, declarative capabilities and ComfyUI workflows. Hosted execution still requires account review.');
|
|
12
24
|
assert(Number.isFinite(timeoutMs)&&timeoutMs>0&&timeoutMs<=15000,'TIMEOUT','Local admission needs a positive budget of at most 15 seconds');
|
|
13
25
|
const deadline=Date.now()+timeoutMs,controller=new AbortController();
|
|
14
26
|
const remaining=()=>{const value=deadline-Date.now();assert(value>0&&!controller.signal.aborted,'TIMEOUT','Local admission exceeded its shared budget');return value;};
|
|
@@ -20,11 +32,16 @@ export async function admitLocalPackage(pkg,{timeoutMs=15000}={}){
|
|
|
20
32
|
remaining();
|
|
21
33
|
return {...scan,digest:sourceDigest,platformChecks,execution:'not-run',liveVerified:false,reviewRequired:true,tests:0,fixtureContracts:scan.passed?pkg.tests.length:0};
|
|
22
34
|
}
|
|
35
|
+
// A module runs in the SDK's own Node executor with the stand-in host answering its
|
|
36
|
+
// hosted calls, media and files; the digests are checked before anything runs.
|
|
37
|
+
// Loaded only for a module package: the Node executor needs worker threads, which the
|
|
38
|
+
// App's bundle (this file is part of it) must never pull in statically.
|
|
39
|
+
const module=moduleFiles?await (async()=>{const [{createNodeModuleExecutor},{createMockHost}]=await Promise.all([import('./module-node.js'),import('./mock-host.js')]);return createNodeModuleExecutor({files:moduleFiles,host:createMockHost({manifest:pkg.manifest}).host});})():undefined;
|
|
23
40
|
const report=await admitPackage(pkg,{
|
|
24
|
-
test:content=>testPackage(content,{timeoutMs:remaining()}),
|
|
25
|
-
run:(content,input,options)=>{remaining();return runPackageAsync(content,input,{...options,signal:controller.signal});},
|
|
41
|
+
test:content=>testPackage(content,{timeoutMs:remaining(),...(module?{module}:{})}),
|
|
42
|
+
run:(content,input,options)=>{remaining();return runPackageAsync(content,input,{...options,signal:controller.signal,...(module?{module}:{})});},
|
|
26
43
|
});
|
|
27
44
|
remaining();
|
|
28
|
-
return report;
|
|
45
|
+
return module?{...report,execution:'local-stand-ins',liveVerified:false,reviewRequired:true}:report;
|
|
29
46
|
}finally{clearTimeout(timer);}
|
|
30
47
|
}
|
|
@@ -2,7 +2,7 @@ import {firstPartySdkPackage} from './first-party-packages.js';
|
|
|
2
2
|
import {requiresDesktopExecution,validateDesktopExecution} from './desktop-execution.js';
|
|
3
3
|
import {validatePortableMetadata} from './portable.js';
|
|
4
4
|
import {initialBlockState,validateBlockState} from './state.js';
|
|
5
|
-
import { assert, digest, safeDocument, stableStringify, validateManifest, validateValues } from './contracts.js';
|
|
5
|
+
import { assert, digest, plain, safeDocument, stableStringify, validateManifest, validateValues } from './contracts.js';
|
|
6
6
|
import { validateRecipe } from './recipe.js';
|
|
7
7
|
import { validateJavaScript } from './javascript.js';
|
|
8
8
|
import { runPackageAsync } from './runtime.js';
|
|
@@ -14,7 +14,7 @@ import {validateModuleDescriptor,MODULE_LIMITS} from './module-runtime.js';
|
|
|
14
14
|
export function validatePackage(pkg) {
|
|
15
15
|
safeDocument(pkg);
|
|
16
16
|
if(pkg?.manifest?.runtime==='native-application')return validateNativeApplicationPackage(pkg);
|
|
17
|
-
assert(pkg && Object.keys(pkg).every(k => ['manifest', 'recipe', 'code', 'module', 'comfyui', 'capability', 'tests', 'view', 'portable', 'desktop'].includes(k)), 'PACKAGE', 'Unknown package field');
|
|
17
|
+
assert(pkg && Object.keys(pkg).every(k => ['manifest', 'recipe', 'code', 'module', 'moduleSource', 'comfyui', 'capability', 'tests', 'view', 'portable', 'desktop'].includes(k)), 'PACKAGE', 'Unknown package field');
|
|
18
18
|
if(typeof pkg.manifest?.id==='string'&&pkg.manifest.id.startsWith('sm.'))assert(stableStringify(firstPartySdkPackage(pkg.manifest)??null)===stableStringify(pkg),'FIRST_PARTY_SOURCE','Use the exact published first-party package, or create a remix with a new identity.');
|
|
19
19
|
const result = validateManifest(pkg.manifest);
|
|
20
20
|
assert(result.valid, 'MANIFEST', JSON.stringify(result.errors));
|
|
@@ -32,8 +32,11 @@ export function validatePackage(pkg) {
|
|
|
32
32
|
// Module files are named by digest; StillMade fetches and verifies the bytes before they run.
|
|
33
33
|
assert(['code','recipe','comfyui','capability'].every(key=>!Object.hasOwn(pkg,key)),'PACKAGE','Module packages contain a module descriptor, not inline code, recipes, ComfyUI workflows or host requests');
|
|
34
34
|
validateModuleDescriptor(pkg.module);
|
|
35
|
+
// A first-party package may carry its text files beside the descriptor so the app
|
|
36
|
+
// runs it without the content-addressed store; each is still checked against its digest.
|
|
37
|
+
if(Object.hasOwn(pkg,'moduleSource'))assert(plain(pkg.moduleSource)&&Object.entries(pkg.moduleSource).every(([path,text])=>typeof text==='string'&&Object.hasOwn(pkg.module.files,path)&&pkg.module.files[path].mediaType==='text/javascript'),'PACKAGE','moduleSource carries the text of files named in the module descriptor');
|
|
35
38
|
}
|
|
36
|
-
else if (pkg.manifest.runtime === 'javascript') { assert(!Object.hasOwn(pkg,'comfyui')&&!Object.hasOwn(pkg,'capability')&&!Object.hasOwn(pkg,'module'),'PACKAGE','Only matching runtimes can contain ComfyUI workflows, host requests or module files'); validateJavaScript(pkg); }
|
|
39
|
+
else if (pkg.manifest.runtime === 'javascript') { assert(!Object.hasOwn(pkg,'comfyui')&&!Object.hasOwn(pkg,'capability')&&!Object.hasOwn(pkg,'module')&&!Object.hasOwn(pkg,'moduleSource'),'PACKAGE','Only matching runtimes can contain ComfyUI workflows, host requests or module files'); validateJavaScript(pkg); }
|
|
37
40
|
else { assert(!Object.hasOwn(pkg, 'code')&&!Object.hasOwn(pkg,'comfyui')&&!Object.hasOwn(pkg,'capability')&&!Object.hasOwn(pkg,'module'), 'PACKAGE', 'Recipe packages cannot contain code, ComfyUI workflows or host requests'); validateRecipe(pkg.recipe); }
|
|
38
41
|
if(pkg.view !== undefined)validateView(pkg.view);
|
|
39
42
|
assert(Array.isArray(pkg.tests) && pkg.tests.length > 0 && pkg.tests.length <= 50, 'TESTS_REQUIRED', 'Include 1–50 fixture tests');
|