stillmade-sdk 0.1.49 → 0.1.50
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 +6 -2
- package/AGENT_CONTROL.md +15 -0
- package/FRAME_VIEWS.md +24 -0
- package/INTERFACE_DESIGN.md +40 -5
- package/MODULES.md +2 -2
- package/PASTE_TO_LLM.md +26 -19
- package/SDK.md +126 -39
- package/UI_AESTHETIC.md +15 -0
- package/blocks/generate/package.js +1 -1
- package/blocks/generate/versions/1.1.0/package.js +471 -0
- package/blocks/generate/versions/1.2.0/package.js +555 -0
- package/blocks/native/sm.board-editor/versions/3.0.0/package.js +50 -17
- package/blocks/native/sm.board-editor/versions/3.1.0/package.js +1037 -0
- package/blocks/native/sm.editor/versions/3.0.0/package.js +128 -83
- package/blocks/native/sm.editor/versions/3.1.0/package.js +1299 -0
- package/blocks/native/sm.panel-strip/versions/3.0.0/package.js +45 -12
- package/blocks/native/sm.panel-strip/versions/3.1.0/package.js +479 -0
- package/blocks/native/sm.voiceover/versions/3.0.0/package.js +9 -3
- package/docs/build/block-builder-contract.md +6 -2
- package/docs/build/frame-views.md +24 -0
- package/docs/build/interface-design.md +40 -5
- package/docs/build/modules.md +2 -2
- package/docs/reference/authoring-guide.md +4 -4
- package/docs/reference/execution.md +15 -1
- package/docs/reference/first-party-blocks.md +4 -4
- package/docs/reference/parity.md +1 -1
- package/docs/reference/ports.md +1 -1
- package/docs/reference/view-bridge.md +3 -0
- package/docs/tools/llm.md +26 -19
- package/package.json +1 -1
- package/packages/block-cli/dev-host/assets/host.js +161 -33
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-platform/block-readiness.js +16 -3
- package/packages/block-platform/canvas.js +4 -1
- package/packages/block-platform/catalog.js +32 -5
- package/packages/block-platform/flow-inspection.js +16 -2
- package/packages/block-platform/listings.js +16 -2
- package/packages/block-platform/steps.js +4 -1
- package/packages/block-sdk/admission.js +1 -1
- package/packages/block-sdk/agent-inputs.js +4 -2
- package/packages/block-sdk/builder-contract.js +7 -4
- package/packages/block-sdk/first-party-identities.js +9 -1
- package/packages/block-sdk/first-party-packages.js +11 -1
- package/packages/block-sdk/generation-catalog.js +55 -5
- package/packages/block-sdk/generation-models.js +60 -0
- package/packages/block-sdk/index.d.ts +15 -1
- package/packages/block-sdk/index.js +2 -1
- package/packages/block-sdk/input-source-policy.js +2 -1
- package/packages/block-sdk/module-hosted.js +13 -5
- package/packages/block-sdk/module-worker.js +1 -1
- package/packages/block-sdk/port-types.js +6 -2
- package/packages/block-sdk/semantic-contracts.d.ts +1 -1
- package/packages/block-sdk/semantic-relationships.js +3 -0
- package/packages/block-sdk/semantic-schema.js +4 -1
- package/packages/block-sdk/shared-controls.js +4 -1
- package/packages/block-sdk/staged-releases.js +2 -1
- package/packages/block-sdk/view-bridge.js +3 -0
- package/packages/block-sdk/view-design.js +54 -10
- package/packages/block-sdk/view-renderer.js +19 -6
- package/packages/design-system/block-kit.css +12 -3
package/UI_AESTHETIC.md
CHANGED
|
@@ -75,6 +75,10 @@ listing previews read its raw palette. Do not maintain a parallel theme.
|
|
|
75
75
|
- Use the 4px spacing scale. Keep breathing room around primary tasks and compact,
|
|
76
76
|
legible secondary metadata. Existing surfaces use fine tonal borders and small
|
|
77
77
|
corner radii (`--r-2`, `--r-3`); buttons may use the established pill affordance.
|
|
78
|
+
- Never a circle (owner, November 7, 2026, of round shot tiles: "keep it a square rectangle ... make sure it never
|
|
79
|
+
comes out like that unless explicitly said"). A picture, or something picked by its picture (a thumbnail, a shot, a
|
|
80
|
+
swatch, a photo tile), and every square control on media are rounded rectangles in `--r-2` or `--r-3`, unless the owner
|
|
81
|
+
asks for a circle. Block readiness notes a circle in a Block's own CSS.
|
|
78
82
|
- Primary actions invert foreground/background. Secondary controls use the
|
|
79
83
|
existing quiet outlined/ghost treatment. Use shared focus, hover and disabled
|
|
80
84
|
states; do not add feature-specific bright colors or grey browser-default buttons.
|
|
@@ -275,6 +279,10 @@ before/after examples from an Image Panels project (Voiceover 1,210 px to about
|
|
|
275
279
|
(`sm-actions` and `sm-status` inside `sm-stage`), and every Step page
|
|
276
280
|
StillMade draws does the same (the one-page job, a single Block's page and the
|
|
277
281
|
focused page; owner: "this is for everything, it should all be shared").
|
|
282
|
+
Under a rendered video the stage shows its shots with their times, the one
|
|
283
|
+
playing marked, and the line being said, on a band of the lighter surface;
|
|
284
|
+
pressing a shot plays from it, and the status carries the video's length and
|
|
285
|
+
size (November 7, 2026, owner: "i want like this option 7"; `VideoParts.jsx`).
|
|
278
286
|
- Settings sit side by side next to what they change (a grid of short
|
|
279
287
|
segmented controls, not rows of big tiles). Lists fill the width at a small
|
|
280
288
|
size: photos are small tiles, shots are one line each and open in place.
|
|
@@ -332,6 +340,13 @@ accessible name and a tooltip; retain visible text when the meaning would be unc
|
|
|
332
340
|
paid review, or when the run fails and needs a decision.
|
|
333
341
|
- Steps use `StepRunControls` and `client/src/blocks/runAction.js` for the verb
|
|
334
342
|
and price; reuse them instead of writing new run buttons.
|
|
343
|
+
- A Block's own screen gets the same price on its own run button (November 8,
|
|
344
|
+
2026, owner, of Generate: "Where's the credit cost?"): the screen says what its
|
|
345
|
+
next run makes (`StillMade.nextRun`) and StillMade writes "· 4 credits" after
|
|
346
|
+
the button's words, in the words above (`runPriceWords`). The button stays where
|
|
347
|
+
it is, the price leaves while the run works, and a viewer sees none. A stage's
|
|
348
|
+
Download (`data-stillmade-action="download"`) saves what the stage shows through
|
|
349
|
+
StillMade's own download, under the export policy.
|
|
335
350
|
|
|
336
351
|
## Speak the job (November 2, 2026)
|
|
337
352
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {default} from './versions/1.
|
|
1
|
+
export {default} from './versions/1.2.0/package.js';
|
|
@@ -0,0 +1,471 @@
|
|
|
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.generate",
|
|
7
|
+
"version": "1.1.0",
|
|
8
|
+
"name": "Generate",
|
|
9
|
+
"description": "Generate images and videos with any model StillMade runs: pictures from a prompt or your own photos, clips from a start and end frame. Pick the model, shape, size and length, and make up to four at once.",
|
|
10
|
+
"kind": "task",
|
|
11
|
+
"runtime": "module",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"inputs": {
|
|
14
|
+
"prompt": {
|
|
15
|
+
"type": "text",
|
|
16
|
+
"primary": true,
|
|
17
|
+
"required": true,
|
|
18
|
+
"semantic": "generation_prompt",
|
|
19
|
+
"accepts": [
|
|
20
|
+
"written_message",
|
|
21
|
+
"summary_report",
|
|
22
|
+
"narration_script"
|
|
23
|
+
],
|
|
24
|
+
"sources": [
|
|
25
|
+
"upstream",
|
|
26
|
+
"user"
|
|
27
|
+
],
|
|
28
|
+
"description": "What to make: the picture, or the shot and how it moves."
|
|
29
|
+
},
|
|
30
|
+
"mode": {
|
|
31
|
+
"type": "text",
|
|
32
|
+
"required": false,
|
|
33
|
+
"semantic": "generation_mode",
|
|
34
|
+
"description": "Image or video."
|
|
35
|
+
},
|
|
36
|
+
"model": {
|
|
37
|
+
"type": "text",
|
|
38
|
+
"required": false,
|
|
39
|
+
"semantic": "generation_model",
|
|
40
|
+
"description": "The model that makes it, from the models StillMade runs. Leave it empty and StillMade picks one that makes what you asked for."
|
|
41
|
+
},
|
|
42
|
+
"aspectRatio": {
|
|
43
|
+
"type": "text",
|
|
44
|
+
"required": false,
|
|
45
|
+
"semantic": "aspect_ratio",
|
|
46
|
+
"description": "The shape, such as 16:9 for wide or 9:16 for a phone screen."
|
|
47
|
+
},
|
|
48
|
+
"size": {
|
|
49
|
+
"type": "text",
|
|
50
|
+
"required": false,
|
|
51
|
+
"semantic": "render_resolution",
|
|
52
|
+
"description": "How large it is, such as 2K for a picture or 1080p for a video."
|
|
53
|
+
},
|
|
54
|
+
"quality": {
|
|
55
|
+
"type": "text",
|
|
56
|
+
"required": false,
|
|
57
|
+
"semantic": "render_quality",
|
|
58
|
+
"description": "Low, medium or high, for the models that offer it. Higher costs more."
|
|
59
|
+
},
|
|
60
|
+
"duration": {
|
|
61
|
+
"type": "integer",
|
|
62
|
+
"required": false,
|
|
63
|
+
"semantic": "duration_seconds",
|
|
64
|
+
"description": "How long the video is, in seconds."
|
|
65
|
+
},
|
|
66
|
+
"sound": {
|
|
67
|
+
"type": "boolean",
|
|
68
|
+
"required": false,
|
|
69
|
+
"semantic": "generate_audio",
|
|
70
|
+
"description": "Whether the video comes with its own sound, for the models that make it."
|
|
71
|
+
},
|
|
72
|
+
"settings": {
|
|
73
|
+
"type": "json",
|
|
74
|
+
"schema": "json",
|
|
75
|
+
"required": false,
|
|
76
|
+
"semantic": "generation_settings",
|
|
77
|
+
"sources": [
|
|
78
|
+
"view"
|
|
79
|
+
],
|
|
80
|
+
"description": "Other settings StillMade lists for the chosen model, by name. The screen fills this in."
|
|
81
|
+
},
|
|
82
|
+
"count": {
|
|
83
|
+
"type": "integer",
|
|
84
|
+
"required": false,
|
|
85
|
+
"semantic": "generation_count",
|
|
86
|
+
"description": "How many to make at once, from 1 to 4. Each one is priced."
|
|
87
|
+
},
|
|
88
|
+
"avoid": {
|
|
89
|
+
"type": "text",
|
|
90
|
+
"required": false,
|
|
91
|
+
"semantic": "negative_prompt",
|
|
92
|
+
"description": "What to leave out of it."
|
|
93
|
+
},
|
|
94
|
+
"seed": {
|
|
95
|
+
"type": "integer",
|
|
96
|
+
"required": false,
|
|
97
|
+
"semantic": "generation_seed",
|
|
98
|
+
"description": "A number that makes the same choices again. Leave it empty for a new result each time."
|
|
99
|
+
},
|
|
100
|
+
"references": {
|
|
101
|
+
"type": "image[]",
|
|
102
|
+
"required": false,
|
|
103
|
+
"semantic": "reference_image",
|
|
104
|
+
"sources": [
|
|
105
|
+
"upstream",
|
|
106
|
+
"user"
|
|
107
|
+
],
|
|
108
|
+
"description": "Your own photos the picture keeps, such as a product, a person or a place."
|
|
109
|
+
},
|
|
110
|
+
"startFrame": {
|
|
111
|
+
"type": "image",
|
|
112
|
+
"required": false,
|
|
113
|
+
"semantic": "first_frame",
|
|
114
|
+
"sources": [
|
|
115
|
+
"upstream",
|
|
116
|
+
"user"
|
|
117
|
+
],
|
|
118
|
+
"description": "The picture the video starts on."
|
|
119
|
+
},
|
|
120
|
+
"endFrame": {
|
|
121
|
+
"type": "image",
|
|
122
|
+
"required": false,
|
|
123
|
+
"semantic": "last_frame",
|
|
124
|
+
"sources": [
|
|
125
|
+
"upstream",
|
|
126
|
+
"user"
|
|
127
|
+
],
|
|
128
|
+
"description": "The picture the video ends on. It needs a start frame."
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"outputs": {
|
|
132
|
+
"media": {
|
|
133
|
+
"type": "asset[]",
|
|
134
|
+
"primary": true,
|
|
135
|
+
"semantic": "generated_media",
|
|
136
|
+
"description": "The pictures or clips that were made, in order."
|
|
137
|
+
},
|
|
138
|
+
"notMade": {
|
|
139
|
+
"type": "text",
|
|
140
|
+
"required": false,
|
|
141
|
+
"semantic": "generation_failures",
|
|
142
|
+
"description": "What could not be made, and why. Nothing is charged for those."
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"permissions": {
|
|
146
|
+
"project": [],
|
|
147
|
+
"actions": [
|
|
148
|
+
"media.generate"
|
|
149
|
+
],
|
|
150
|
+
"network": [],
|
|
151
|
+
"filesystem": [],
|
|
152
|
+
"secrets": []
|
|
153
|
+
},
|
|
154
|
+
"features": [
|
|
155
|
+
{
|
|
156
|
+
"id": "generate-image",
|
|
157
|
+
"label": "Generate pictures",
|
|
158
|
+
"does": "Makes up to four pictures with the image model you pick, in its shapes and sizes, keeping up to eight of your own photos.",
|
|
159
|
+
"asks": [
|
|
160
|
+
"make a picture of my product on a beach",
|
|
161
|
+
"generate four images of a cozy cafe",
|
|
162
|
+
"make a 9:16 poster with my logo"
|
|
163
|
+
],
|
|
164
|
+
"inputs": [
|
|
165
|
+
"prompt",
|
|
166
|
+
"model",
|
|
167
|
+
"aspectRatio",
|
|
168
|
+
"size",
|
|
169
|
+
"quality",
|
|
170
|
+
"settings",
|
|
171
|
+
"count",
|
|
172
|
+
"references",
|
|
173
|
+
"avoid",
|
|
174
|
+
"seed"
|
|
175
|
+
],
|
|
176
|
+
"outputs": [
|
|
177
|
+
"media"
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"id": "generate-video",
|
|
182
|
+
"label": "Generate video clips",
|
|
183
|
+
"does": "Makes up to four clips with the video model you pick, at its lengths, sizes and shapes, from a start and end frame you add, with sound where offered.",
|
|
184
|
+
"asks": [
|
|
185
|
+
"make a 5 second clip of waves at sunset",
|
|
186
|
+
"animate this photo",
|
|
187
|
+
"make a vertical video from this first frame"
|
|
188
|
+
],
|
|
189
|
+
"inputs": [
|
|
190
|
+
"prompt",
|
|
191
|
+
"model",
|
|
192
|
+
"aspectRatio",
|
|
193
|
+
"size",
|
|
194
|
+
"duration",
|
|
195
|
+
"sound",
|
|
196
|
+
"settings",
|
|
197
|
+
"count",
|
|
198
|
+
"startFrame",
|
|
199
|
+
"endFrame",
|
|
200
|
+
"avoid",
|
|
201
|
+
"seed"
|
|
202
|
+
],
|
|
203
|
+
"outputs": [
|
|
204
|
+
"media"
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
"platforms": {
|
|
209
|
+
"phone": {
|
|
210
|
+
"supported": true
|
|
211
|
+
},
|
|
212
|
+
"browser": {
|
|
213
|
+
"supported": true
|
|
214
|
+
},
|
|
215
|
+
"desktop": {
|
|
216
|
+
"supported": true
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"ui": [
|
|
220
|
+
{
|
|
221
|
+
"port": "prompt",
|
|
222
|
+
"control": "text",
|
|
223
|
+
"multiline": true
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"port": "mode",
|
|
227
|
+
"control": "select",
|
|
228
|
+
"options": [
|
|
229
|
+
"image",
|
|
230
|
+
"video"
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"port": "model",
|
|
235
|
+
"control": "text"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"port": "aspectRatio",
|
|
239
|
+
"control": "text"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"port": "size",
|
|
243
|
+
"control": "text"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"port": "quality",
|
|
247
|
+
"control": "select",
|
|
248
|
+
"options": [
|
|
249
|
+
"low",
|
|
250
|
+
"medium",
|
|
251
|
+
"high"
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"port": "duration",
|
|
256
|
+
"control": "number"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"port": "sound",
|
|
260
|
+
"control": "checkbox"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"port": "count",
|
|
264
|
+
"control": "number"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"port": "avoid",
|
|
268
|
+
"control": "text"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"port": "seed",
|
|
272
|
+
"control": "number"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"port": "references",
|
|
276
|
+
"control": "asset"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"port": "startFrame",
|
|
280
|
+
"control": "asset"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"port": "endFrame",
|
|
284
|
+
"control": "asset"
|
|
285
|
+
}
|
|
286
|
+
],
|
|
287
|
+
"provenance": {
|
|
288
|
+
"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"
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
"module": {
|
|
292
|
+
"schemaVersion": 1,
|
|
293
|
+
"entry": "main.js",
|
|
294
|
+
"files": {
|
|
295
|
+
"main.js": {
|
|
296
|
+
"sha256": "eedb3d827f7fc91766ce2a2840a58650afc4564b1a49995c070c464cc1a95a6e",
|
|
297
|
+
"bytes": 3445,
|
|
298
|
+
"mediaType": "text/javascript"
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
"moduleSource": {
|
|
303
|
+
"main.js": "// Generate: every image and video model StillMade runs, in one Block. The view sets the choices; this run turns\n// them into one media.generate batch. StillMade prices the whole batch, the person approves it once, and the\n// pictures or clips come back here in order.\nconst clean = value => typeof value === 'string' && value && value !== 'default' ? value : undefined;\n// 1.1.0: a setting StillMade lists for a model beyond the ones named below arrives in `settings` by its key and\n// goes to StillMade as it is, so a setting added later works without a new version. The named inputs win.\nconst otherSettings = value => value && typeof value === 'object' && !Array.isArray(value)\n ? Object.fromEntries(Object.entries(value).filter(([key, item]) => key !== 'model' && /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(key) && ['string', 'number', 'boolean'].includes(typeof item)).slice(0, 20))\n : {};\n\nexport function generationItems(input) {\n const mode = input.mode === 'video' ? 'video' : 'image';\n const prompt = String(input.prompt || '').trim();\n if (!prompt) throw new Error(mode === 'video' ? 'Describe the shot first.' : 'Describe the picture first.');\n if (mode === 'video' && input.endFrame && !input.startFrame) throw new Error('Add a start frame before an end frame.');\n const count = Math.max(1, Math.min(4, Math.floor(Number(input.count) || 1)));\n const settings = otherSettings(input.settings);\n if (clean(input.model)) settings.model = input.model;\n if (clean(input.aspectRatio)) settings.aspectRatio = input.aspectRatio;\n if (clean(input.size)) settings.resolution = input.size;\n if (mode === 'image' && clean(input.quality)) settings.quality = input.quality;\n if (mode === 'video') {\n const seconds = Math.floor(Number(input.duration));\n if (Number.isFinite(seconds) && seconds > 0) settings.duration = seconds;\n if (typeof input.sound === 'boolean') settings.generateAudio = input.sound;\n }\n const references = Array.isArray(input.references) ? input.references.filter(Boolean).slice(0, 8) : [];\n const avoid = String(input.avoid || '').trim();\n const seed = Number.isInteger(input.seed) && input.seed >= 0 ? input.seed : undefined;\n const items = [];\n for (let index = 0; index < count; index++) {\n const item = { key: `${mode}-${index + 1}`, operation: mode, prompt };\n if (Object.keys(settings).length) item.settings = settings;\n if (mode === 'image' && references.length) item.references = references;\n if (mode === 'video' && input.startFrame) item.firstFrame = input.startFrame;\n if (mode === 'video' && input.startFrame && input.endFrame) item.lastFrame = input.endFrame;\n if (avoid) item.negativePrompt = avoid;\n if (seed !== undefined) item.seed = Math.min(2147483647, seed + index);\n items.push(item);\n }\n return items;\n}\n\nexport default async function run(input, stillmade) {\n const items = generationItems(input);\n stillmade.progress(0.05, items.length > 1 ? `Making ${items.length}` : 'Making it');\n const { value, failures } = await stillmade.actions.run({\n schemaVersion: 1, kind: 'stillmade.media-action', operation: 'media.generate', resultPort: null, request: { items },\n });\n const media = value.map(({ key, ...made }) => made);\n const outputs = { media };\n if (failures && failures.length) outputs.notMade = `${failures.length} of ${items.length} were not made: ${failures.map(failure => failure.message).join(' ')}`.slice(0, 2000);\n return outputs;\n}\n"
|
|
304
|
+
},
|
|
305
|
+
"view": {
|
|
306
|
+
"runtime": "frame",
|
|
307
|
+
"html": "<main class=\"sm-block gen\" id=\"root\" data-mode=\"image\" data-models=\"loading\">\n <div class=\"gen-layout\">\n <section class=\"gen-controls\" aria-label=\"What to make\">\n <div class=\"sm-fields\">\n <fieldset class=\"sm-choices gen-mode\" data-segmented id=\"mode\" data-stillmade-share=\"mode\">\n <legend class=\"sm-sr\">Make</legend>\n <label><input type=\"radio\" name=\"mode\" value=\"image\" checked><span class=\"sm-glyph\" data-glyph=\"image\" aria-hidden=\"true\"></span><span>Image</span></label>\n <label><input type=\"radio\" name=\"mode\" value=\"video\"><span class=\"sm-glyph\" data-glyph=\"video\" aria-hidden=\"true\"></span><span>Video</span></label>\n </fieldset>\n\n <div class=\"sm-field gen-prompt\">\n <label class=\"sm-sr\" for=\"prompt\">Describe it</label>\n <textarea id=\"prompt\" rows=\"4\" maxlength=\"4000\" placeholder=\"Describe the picture\" data-stillmade-share=\"prompt\"></textarea>\n </div>\n </div>\n\n <div class=\"sm-field gen-only-image\" id=\"references-row\">\n <span class=\"sm-label\">Your photos <small class=\"gen-muted\" id=\"references-limit\"></small></span>\n <ul class=\"sm-gallery\" data-size=\"small\" id=\"references\" aria-label=\"Your photos\">\n <li><button type=\"button\" class=\"sm-add\" data-stillmade-add=\"references\" aria-label=\"Add photos\" title=\"Add photos\"></button></li>\n </ul>\n </div>\n\n <div class=\"sm-field gen-only-video gen-frames\" id=\"frames-row\">\n <span class=\"sm-label\">Frames</span>\n <ul class=\"sm-gallery\" data-size=\"small\" aria-label=\"Start and end frames\">\n <li id=\"start-frame\"><button type=\"button\" class=\"sm-add\" data-stillmade-add=\"startFrame\" aria-label=\"Add a start frame\" title=\"Add a start frame\"></button><small>Start</small></li>\n <li id=\"end-frame\"><button type=\"button\" class=\"sm-add\" data-stillmade-add=\"endFrame\" aria-label=\"Add an end frame\" title=\"Add an end frame\"></button><small>End</small></li>\n </ul>\n </div>\n\n <div class=\"sm-settings\" id=\"settings\">\n <div class=\"gen-models-row\" id=\"models-row\">\n <p class=\"sm-status\" id=\"models-status\" role=\"status\" data-state=\"loading\">Loading models</p>\n <button type=\"button\" class=\"sm-quiet\" id=\"models-retry\" data-stillmade-action=\"load-models\" hidden>Try again</button>\n </div>\n <fieldset class=\"sm-choices gen-row\" data-segmented id=\"count\" data-stillmade-share=\"count\">\n <legend>How many</legend>\n <label><input type=\"radio\" name=\"count\" value=\"1\" checked><span>1</span></label>\n <label><input type=\"radio\" name=\"count\" value=\"2\"><span>2</span></label>\n <label><input type=\"radio\" name=\"count\" value=\"3\"><span>3</span></label>\n <label><input type=\"radio\" name=\"count\" value=\"4\"><span>4</span></label>\n </fieldset>\n </div>\n\n <details class=\"sm-more\" id=\"more\">\n <summary>More</summary>\n <div class=\"sm-fields\">\n <div class=\"sm-field\"><label for=\"avoid\">Leave out</label><input id=\"avoid\" type=\"text\" maxlength=\"2000\" placeholder=\"Blur, text, extra fingers\" data-stillmade-share=\"avoid\"></div>\n <div class=\"sm-field\" id=\"seed-row\"><label for=\"seed\">Seed</label><input id=\"seed\" type=\"number\" min=\"0\" max=\"2147483647\" step=\"1\" inputmode=\"numeric\" placeholder=\"Random\" data-stillmade-share=\"seed\"></div>\n </div>\n </details>\n\n <div class=\"sm-actions gen-actions\">\n <details class=\"gen-model\" id=\"model-picker\">\n <summary class=\"gen-chip\" data-stillmade-action=\"choose-model\"><span id=\"model-name\">Loading models</span><span class=\"sm-glyph\" data-glyph=\"chevron-down\" aria-hidden=\"true\"></span></summary>\n <div class=\"gen-models\">\n <fieldset class=\"sm-choices gen-only-image\" data-size=\"wide\" id=\"imageModel\" data-stillmade-share=\"imageModel\">\n <legend>Image model</legend>\n </fieldset>\n <fieldset class=\"sm-choices gen-only-video\" data-size=\"wide\" id=\"videoModel\" data-stillmade-share=\"videoModel\">\n <legend>Video model</legend>\n </fieldset>\n </div>\n </details>\n <button type=\"button\" class=\"sm-primary\" id=\"generate\" data-stillmade-action=\"generate\">Generate image</button>\n </div>\n <p class=\"sm-status\" id=\"status\" role=\"status\"></p>\n </section>\n\n <section class=\"gen-results\" aria-label=\"What was made\" aria-live=\"polite\">\n <div class=\"sm-stage gen-place\" id=\"place\">\n <span class=\"sm-shape\" id=\"place-shape\" style=\"--r:1/1\"></span>\n <p class=\"sm-empty\" id=\"place-text\">Your picture appears here</p>\n </div>\n <ul class=\"sm-gallery gen-gallery\" id=\"gallery\" aria-label=\"Made in this Step\" hidden></ul>\n </section>\n </div>\n</main>\n",
|
|
308
|
+
"css": "/* The Block kit (injected by the host) carries fields, choices, galleries and buttons; this file only lays the\n page out (choices on the left, what was made on the right) and draws the model picker. The setting rows and the\n models in the picker are drawn by view.ui.js from StillMade.models(). */\n.gen [hidden], .gen [data-off=\"true\"] { display: none !important; }\n.gen[data-mode=\"image\"] .gen-only-video, .gen[data-mode=\"video\"] .gen-only-image { display: none !important; }\n.gen-layout { display: grid; gap: var(--s-4, 16px); align-items: start; }\n@container (min-width: 860px) { .gen-layout { grid-template-columns: minmax(300px, 380px) minmax(0, 1fr); } }\n@media (min-width: 860px) { .gen-layout { grid-template-columns: minmax(300px, 380px) minmax(0, 1fr); } }\n.gen-controls { display: grid; gap: var(--s-3, 12px); min-width: 0; }\n.gen-mode { --sm-tile: 120px; }\n.gen-prompt textarea { width: 100%; box-sizing: border-box; resize: vertical; min-height: 96px; }\n.gen-muted { color: var(--fg-muted); font-weight: 400; }\n.gen .sm-settings { grid-template-columns: minmax(0, 1fr); }\n/* Shape and length have many options: small chips that wrap, the pick in ink as in every choice. */\n.gen-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n.gen-chips > legend { width: 100%; }\n.gen-chips > label { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 4px 10px; border-radius: 999px; font-size: var(--fs-body-s, .875rem); white-space: nowrap; }\n.gen-row .sm-shape { width: 14px; }\n/* While StillMade's model list loads, or when it did not: a few words and Try again, in the settings card. */\n.gen-models-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s-2, 8px); }\n.gen-frames li { display: grid; justify-items: center; gap: 4px; }\n.gen-frames small { color: var(--fg-muted); }\n.gen-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2, 8px); }\n.gen-actions .sm-primary { flex: 1 1 auto; }\n.gen-model { position: relative; }\n.gen-chip { display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 6px 12px; border: 1px solid var(--border-strong); border-radius: 999px; background: var(--bg-elev-1); color: var(--fg); cursor: pointer; list-style: none; white-space: nowrap; }\n.gen-chip::-webkit-details-marker { display: none; }\n.gen-chip:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }\n.gen-models { position: absolute; z-index: 2; bottom: calc(100% + 6px); left: 0; width: min(360px, 90vw); max-height: 360px; overflow: auto; padding: var(--s-3, 12px); border: 1px solid var(--border-strong); border-radius: var(--r-3, 8px); background: var(--bg-elev-1); box-shadow: 0 8px 24px rgba(0, 0, 0, .12); }\n.gen-models .sm-choices > label { justify-items: start; text-align: left; }\n.gen-models strong { font-weight: 500; }\n.gen-tags { display: flex; flex-wrap: wrap; gap: 4px; }\n.gen-tags span { font-size: var(--fs-body-xs, .75rem); color: var(--fg-muted); border: 1px solid var(--border-tok); border-radius: 999px; padding: 1px 6px; }\n.gen-models label:has(input:disabled) { opacity: .5; }\n.gen-why { font-size: var(--fs-body-xs, .75rem); color: var(--fg-muted); }\n.gen-results { display: grid; gap: var(--s-3, 12px); min-width: 0; }\n.gen-place { display: grid; place-items: center; min-height: 240px; }\n.gen-place .sm-shape { width: min(100%, 420px); }\n.gen-place > .sm-empty { position: absolute; }\n.gen-place { position: relative; }\n.gen-gallery { --sm-tile: 220px; }\n.gen-gallery li { display: grid; gap: 4px; }\n.gen-gallery img, .gen-gallery video { width: 100%; border-radius: var(--r-2, 4px); background: var(--bg-inset); }\n.gen-gallery small { color: var(--fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n",
|
|
309
|
+
"javascript": "(()=>{\n\"use strict\";\n// Generate's one page. 1.1.0 (November 8, 2026, owner: \"as I add more models, it all gets updated without updating\n// the Block\"): the models, and what each one takes, come from StillMade.models() when the page opens, never from a copy\n// in this code. Each model lists its settings as data, so the page draws every setting it finds (a row of picks per\n// setting) and offers only what the chosen model can make: its shapes, sizes, lengths, sound, your photos and frames.\n// A setting StillMade adds later draws here and reaches the run with no new version. Every choice is a shared field\n// (data-stillmade-share), so people editing the Step together see the same page. The button runs the Block once;\n// StillMade shows the price and asks before anything is made.\nconst $ = id => document.getElementById(id);\nconst root = $('root');\nconst state = { input: {}, busy: false, history: [], shownKeys: '', started: false, lists: { image: null, video: null }, failed: { image: false, video: false }, drawn: '' };\nconst KINDS = { image: 'image.generate', video: 'video.generate' };\n// The settings this page has inputs for, and the shared field each one's picks live in (the names 1.0.0 used).\n// Any other setting a model lists goes to the run in `settings` under its own key.\nconst INPUTS = { aspectRatio: 'aspectRatio', resolution: 'size', quality: 'quality', duration: 'duration', generateAudio: 'sound' };\nconst FIELDS = { aspectRatio: 'shape', resolution: 'size', quality: 'quality', duration: 'length', generateAudio: 'sound' };\n// The page's own controls: a setting by one of these names is not drawn twice, and one named like another row's field\n// gets a field of its own.\nconst OWN = new Set(['mode', 'count', 'imageModel', 'videoModel', 'prompt', 'avoid', 'seed', 'model', 'settings', 'references', 'startFrame', 'endFrame']);\nconst field = key => FIELDS[key] || (Object.values(FIELDS).includes(key) ? `setting-${key}` : key);\nconst groupFor = key => [...document.querySelectorAll('[data-setting]')].find(group => group.dataset.setting === key);\n\nconst checked = name => { const el = document.querySelector(`input[name=\"${name}\"]:checked`); return el ? el.value : undefined; };\nconst visibleChecked = name => { const el = document.querySelector(`input[name=\"${name}\"]:checked`); return el && !el.disabled && !el.closest('label').hidden ? el.value : undefined; };\nfunction pick(name, value) {\n const el = [...document.querySelectorAll(`input[name=\"${name}\"]`)].find(option => option.value === String(value));\n if (!el || el.checked) return;\n el.checked = true;\n el.dispatchEvent(new Event('input', { bubbles: true }));\n el.dispatchEvent(new Event('change', { bubbles: true }));\n}\nconst mode = () => checked('mode') === 'video' ? 'video' : 'image';\nconst listFor = kind => state.lists[kind];\n// The chosen model of this mode, as StillMade listed it: [id, model], or [] before the list arrives or when it did not.\nfunction model() {\n const kind = mode(), list = listFor(kind);\n if (!list || !list.models.length) return [];\n const id = checked(kind === 'video' ? 'videoModel' : 'imageModel'), found = list.models.find(item => item.id === id) || list.models.find(item => item.id === list.default) || list.models[0];\n return [found.id, found];\n}\nconst message = error => (error && error.message) || 'That did not work. Try again.';\nfunction status(text, tone) { const el = $('status'); el.textContent = text || ''; if (tone) el.dataset.state = tone; else delete el.dataset.state; }\n\n// How a setting's value reads on its pick. Shapes get their frame; anything StillMade adds later reads as it is.\nconst words = value => typeof value === 'boolean' ? (value ? 'On' : 'Off') : /^[a-z]+$/.test(String(value)) ? String(value)[0].toUpperCase() + String(value).slice(1) : String(value);\nfunction valueLabel(key, value) {\n if (key === 'aspectRatio') return value === 'auto' ? 'Auto' : String(value);\n if (key === 'resolution') return String(value).replace(/k$/, 'K');\n if (key === 'duration') return `${value}s`;\n return words(value);\n}\nconst settingOf = (entry, key) => entry && Array.isArray(entry.settings) ? entry.settings.find(item => item.key === key) : undefined;\nconst valueOf = (setting, text) => setting.values.find(value => String(value) === text);\n\n// One row per setting any model lists, holding every value any model takes; refresh() shows the chosen model's.\nfunction settingRows() {\n const rows = new Map();\n for (const kind of ['image', 'video']) for (const entry of (listFor(kind) || { models: [] }).models) for (const setting of entry.settings || []) {\n if (!setting || typeof setting.key !== 'string' || !/^[a-zA-Z][a-zA-Z0-9_]{0,40}$/.test(setting.key) || OWN.has(setting.key) || !Array.isArray(setting.values) || !setting.values.length) continue;\n const row = rows.get(setting.key) || { key: setting.key, label: setting.label || setting.key, values: [], most: 0 };\n for (const value of setting.values) if (['string', 'number', 'boolean'].includes(typeof value) && !row.values.some(seen => String(seen) === String(value))) row.values.push(value);\n row.most = Math.max(row.most, setting.values.length);\n rows.set(setting.key, row);\n }\n for (const row of rows.values()) if (row.values.every(value => typeof value === 'number')) row.values.sort((a, b) => a - b);\n return [...rows.values()];\n}\nfunction drawSettings() {\n const rows = settingRows(), signature = JSON.stringify(rows.map(row => [row.key, row.label, row.values]));\n if (signature === state.drawn) return;\n state.drawn = signature;\n const before = new Map([...document.querySelectorAll('[data-setting]')].map(group => [group.dataset.setting, checked(field(group.dataset.setting))]));\n for (const group of [...document.querySelectorAll('[data-setting]')]) group.remove();\n const anchor = $('count');\n for (const row of rows) {\n const name = field(row.key), group = document.createElement('fieldset');\n // Many picks wrap as small chips (shapes, lengths); a few sit side by side.\n group.className = row.most > 4 ? 'sm-choices gen-row gen-chips' : 'sm-choices gen-row';\n if (row.most <= 4) group.dataset.segmented = '';\n group.id = name; group.dataset.setting = row.key; group.dataset.stillmadeShare = name;\n group.append(Object.assign(document.createElement('legend'), { textContent: row.label }));\n for (const value of row.values) {\n const label = document.createElement('label'), radio = document.createElement('input');\n radio.type = 'radio'; radio.name = name; radio.value = String(value); radio.checked = before.get(row.key) === String(value);\n label.append(radio);\n if (row.key === 'aspectRatio' && /^\\d+:\\d+$/.test(String(value))) {\n const frame = document.createElement('span'); frame.className = 'sm-shape'; frame.setAttribute('aria-hidden', 'true');\n frame.style.setProperty('--r', String(value).replace(':', '/'));\n label.append(frame);\n }\n label.append(Object.assign(document.createElement('span'), { textContent: valueLabel(row.key, value) }));\n group.append(label);\n }\n anchor.before(group);\n }\n}\n\nconst tagsFor = (kind, entry) => {\n const size = settingOf(entry, 'resolution'), length = settingOf(entry, 'duration'), quality = settingOf(entry, 'quality');\n const top = size && size.values.length ? `up to ${valueLabel('resolution', size.values[size.values.length - 1])}` : '';\n return kind === 'image'\n ? [top, entry.references ? `keeps ${entry.references} photos` : '', quality && quality.values.length > 1 ? 'quality levels' : '']\n : [length && length.values.some(value => typeof value === 'number') ? `up to ${Math.max(...length.values.filter(value => typeof value === 'number'))}s` : '', top, entry.sound ? 'sound' : '', entry.lastFrame ? 'end frame' : '', entry.needsFirstFrame ? 'starts from a frame' : ''];\n};\n// The model picker for one mode, from StillMade's list: its default is picked until someone picks another.\nfunction drawModels(kind) {\n const group = $(kind === 'video' ? 'videoModel' : 'imageModel'), name = group.id, list = listFor(kind), was = checked(name);\n for (const label of [...group.querySelectorAll('label')]) label.remove();\n for (const entry of list ? list.models : []) {\n const label = document.createElement('label'), radio = document.createElement('input');\n radio.type = 'radio'; radio.name = name; radio.value = entry.id; radio.checked = (was || list.default) === entry.id;\n const tags = document.createElement('span'); tags.className = 'gen-tags';\n for (const tag of tagsFor(kind, entry).filter(Boolean)) tags.append(Object.assign(document.createElement('span'), { textContent: tag }));\n label.append(radio, Object.assign(document.createElement('strong'), { textContent: entry.label }), tags, Object.assign(document.createElement('span'), { className: 'gen-why' }));\n group.append(label);\n }\n if (list && list.models.length && !checked(name)) pick(name, list.default || list.models[0].id);\n}\n\n// One axis: show the values the model takes, keep the pick when it still fits, otherwise move to the model's own\n// default. A setting the model does not list hides its row.\nfunction axis(group, setting) {\n const options = [...group.querySelectorAll('input')], allowed = setting ? setting.values.map(String) : [];\n for (const option of options) option.closest('label').hidden = !allowed.includes(option.value);\n group.dataset.off = String(!allowed.length);\n if (!allowed.length) return;\n const current = checked(group.id), fallback = setting.default !== undefined ? String(setting.default) : allowed[0];\n if (!allowed.includes(current)) pick(group.id, allowed.includes(fallback) ? fallback : allowed[0]);\n}\n\nfunction refresh() {\n const kind = mode(), [id, entry] = model(), list = listFor(kind);\n root.dataset.mode = kind;\n root.dataset.models = list ? 'ready' : state.failed[kind] ? 'failed' : 'loading';\n $('prompt').placeholder = kind === 'video' ? 'Describe the shot: who or what, how it moves, the camera' : 'Describe the picture';\n $('model-name').textContent = entry ? entry.label : 'Loading models';\n // With no list the picker is gone and StillMade runs its default model: Generate still works.\n $('model-picker').dataset.off = String(!entry && state.failed[kind]);\n $('models-row').dataset.off = String(!!list);\n const note = $('models-status');\n if (state.failed[kind]) { note.textContent = 'Models did not load. StillMade picks the model.'; note.dataset.state = 'error'; }\n else if (!list) { note.textContent = 'Loading models'; note.dataset.state = 'loading'; }\n else { note.textContent = ''; delete note.dataset.state; }\n $('models-retry').hidden = !state.failed[kind];\n for (const group of document.querySelectorAll('[data-setting]')) axis(group, settingOf(entry, group.dataset.setting));\n const references = state.input.references || [];\n $('references-row').dataset.off = String(!!entry && !entry.references && !references.length);\n $('references-limit').textContent = !entry ? '' : entry.references ? `up to ${entry.references}` : 'this model takes none';\n $('end-frame').dataset.off = String(!!entry && kind === 'video' && !entry.lastFrame);\n // StillMade's list does not say yet which models take a seed; a model that says it takes none hides it.\n $('seed-row').dataset.off = String(!!entry && entry.seed === false);\n // Models that cannot use what is already here are not offered: photos need a model that keeps them, a video model\n // that needs a start frame waits for one, an end frame needs a model that takes it, and StillMade says which models\n // it cannot run now.\n const refs = references.length, start = !!state.input.startFrame, end = !!state.input.endFrame;\n for (const [kindName, name] of [['image', 'imageModel'], ['video', 'videoModel']]) for (const option of document.querySelectorAll(`input[name=\"${name}\"]`)) {\n const item = (listFor(kindName) || { models: [] }).models.find(each => each.id === option.value);\n if (!item) continue;\n const reason = item.available === false ? (item.reason || 'Not available now')\n : kindName === 'image' ? (refs > 0 && !item.references ? 'Does not keep your photos' : '')\n : item.needsFirstFrame && !start ? 'Needs a start frame' : end && !item.lastFrame ? 'Takes no end frame' : '';\n option.disabled = !!reason;\n option.closest('label').querySelector('.gen-why').textContent = reason;\n }\n const count = Number(checked('count')) || 1, noun = kind === 'video' ? 'video' : 'image';\n $('generate').textContent = count > 1 ? `Generate ${count} ${noun}s` : `Generate ${noun}`;\n const shape = visibleChecked(field('aspectRatio'));\n $('place-shape').style.setProperty('--r', shape && shape !== 'auto' ? shape.replace(':', '/') : kind === 'video' ? '16/9' : '1/1');\n $('place-text').textContent = kind === 'video' ? 'Your video appears here' : 'Your picture appears here';\n return id;\n}\n\ndocument.addEventListener('change', event => { if (event.target.closest('fieldset')) refresh(); });\n$('model-picker').addEventListener('change', () => { $('model-picker').open = false; });\n$('model-picker').addEventListener('toggle', () => { if ($('model-picker').open && !model()[1]) $('model-picker').open = false; });\n\n// The lists StillMade runs now, asked for once when the page has its input (StillMade answers only then).\nasync function loadModels(kinds) {\n for (const kind of kinds) { state.failed[kind] = false; }\n refresh();\n await Promise.all(kinds.map(async kind => {\n try {\n if (typeof StillMade.models !== 'function') throw new Error('This StillMade has no model list.');\n const list = await StillMade.models(KINDS[kind]);\n if (!list || !Array.isArray(list.models) || !list.models.length) throw new Error('The model list was empty.');\n state.lists[kind] = list;\n } catch { state.lists[kind] = null; state.failed[kind] = true; }\n }));\n drawSettings();\n for (const kind of kinds) drawModels(kind);\n applyGiven(state.input);\n refresh();\n}\n$('models-retry').addEventListener('click', () => { loadModels(['image', 'video'].filter(kind => state.failed[kind])); });\n\n// Choices that arrive as inputs (from Chat or an earlier Step) set the page; the person can change them.\nfunction applyGiven(given) {\n const kind = given.mode === 'video' ? 'video' : given.mode === 'image' ? 'image' : undefined;\n if (kind) pick('mode', kind);\n for (const [kindName, name] of [['image', 'imageModel'], ['video', 'videoModel']]) {\n const list = listFor(kindName);\n if (given.model && list && list.models.some(item => item.id === given.model)) { pick('mode', kindName); pick(name, given.model); }\n }\n refresh();\n const named = { aspectRatio: given.aspectRatio, resolution: given.size, quality: given.quality, duration: Number.isInteger(given.duration) ? given.duration : undefined, generateAudio: typeof given.sound === 'boolean' ? given.sound : undefined };\n const others = given.settings && typeof given.settings === 'object' && !Array.isArray(given.settings) ? given.settings : {};\n for (const group of document.querySelectorAll('[data-setting]')) {\n const key = group.dataset.setting, value = named[key] !== undefined ? named[key] : others[key];\n if (value !== undefined && value !== null) pick(group.id, String(value));\n }\n}\n\nasync function preview(value) {\n const shown = value.kind === 'video' ? await StillMade.previewMedia(value) : await StillMade.previewImage(value);\n return typeof shown === 'string' ? shown : shown && shown.url;\n}\n\nasync function tile(value, { label, list, index }) {\n const li = document.createElement('li');\n try {\n const url = await preview(value);\n const img = document.createElement('img'); img.src = url; img.alt = label; li.append(img);\n } catch { li.append(Object.assign(document.createElement('span'), { className: 'sm-empty', textContent: label })); }\n if (list) {\n const tools = document.createElement('span'); tools.className = 'sm-tile-tools';\n const remove = document.createElement('button');\n remove.type = 'button'; remove.dataset.stillmadeRemove = list; remove.dataset.index = String(index);\n remove.setAttribute('aria-label', 'Remove'); remove.title = 'Remove';\n const glyph = document.createElement('span'); glyph.className = 'sm-glyph'; glyph.dataset.glyph = 'close'; glyph.setAttribute('aria-hidden', 'true');\n remove.append(glyph);\n tools.append(remove); li.append(tools);\n }\n return li;\n}\n\nasync function drawInputs(input) {\n const list = $('references'), add = list.lastElementChild;\n for (const li of [...list.children]) if (li !== add) li.remove();\n const photos = Array.isArray(input.references) ? input.references : [];\n for (let index = 0; index < photos.length; index++) list.insertBefore(await tile(photos[index], { label: `Photo ${index + 1}`, list: 'references', index }), add);\n for (const [id, name, label] of [['start-frame', 'startFrame', 'Start'], ['end-frame', 'endFrame', 'End']]) {\n const slot = $(id), value = input[name];\n for (const child of [...slot.querySelectorAll('img, .sm-empty')]) child.remove();\n if (value) { const shown = await tile(value, { label }); const img = shown.querySelector('img, .sm-empty'); if (img) slot.prepend(img); }\n }\n}\n\nStillMade.onInput(async input => {\n state.input = input || {};\n if (typeof state.input.prompt === 'string' && state.input.prompt && !$('prompt').value) $('prompt').value = state.input.prompt;\n if (typeof state.input.avoid === 'string' && state.input.avoid && !$('avoid').value) $('avoid').value = state.input.avoid;\n if (Number.isInteger(state.input.seed) && !$('seed').value) $('seed').value = String(state.input.seed);\n if (Number.isInteger(state.input.count)) pick('count', String(Math.max(1, Math.min(4, state.input.count))));\n applyGiven(state.input);\n if (!state.started) { state.started = true; loadModels(['image', 'video']); }\n try { await drawInputs(state.input); } catch (error) { status(message(error), 'error'); }\n refresh();\n});\n\n// What was made: newest first, one tile each. Earlier batches stay in the shared history (the last 24).\nasync function drawResults() {\n const items = state.history, gallery = $('gallery'), keys = items.map(item => item.assetId + ':' + item.versionId).join('|');\n if (keys === state.shownKeys) return;\n state.shownKeys = keys;\n gallery.replaceChildren();\n for (const item of items) {\n const li = document.createElement('li');\n try {\n const url = await preview(item);\n if (item.kind === 'video') {\n const video = document.createElement('video'); video.src = url; video.controls = true; video.playsInline = true; video.preload = 'metadata';\n li.append(video);\n } else {\n const img = document.createElement('img'); img.src = url; img.alt = item.prompt || 'Generated picture'; img.dataset.stillmadeExpand = 'made';\n li.append(img);\n }\n } catch { li.append(Object.assign(document.createElement('span'), { className: 'sm-empty', textContent: 'Could not show this one' })); }\n li.append(Object.assign(document.createElement('small'), { textContent: [item.model, item.prompt].filter(Boolean).join(' · '), title: item.prompt || '' }));\n gallery.append(li);\n }\n gallery.hidden = !items.length;\n $('place').hidden = !!items.length;\n}\n\nfunction remember(made, details) {\n const fresh = made.filter(item => item && item.url && ['image', 'video'].includes(item.kind))\n .map(item => ({ kind: item.kind, assetId: item.assetId, versionId: item.versionId, url: item.url, ...(item.duration ? { duration: item.duration } : {}), ...details }));\n if (!fresh.length) return;\n const seen = new Set(fresh.map(item => item.assetId + ':' + item.versionId));\n state.history = [...fresh, ...state.history.filter(item => !seen.has(item.assetId + ':' + item.versionId))].slice(0, 24);\n StillMade.updateShared({ history: state.history }).catch(() => {});\n drawResults();\n}\n\nStillMade.onShared(shared => {\n if (shared && shared.state && Array.isArray(shared.state.history)) state.history = shared.state.history;\n const made = shared && shared.outputs && Array.isArray(shared.outputs.media) ? shared.outputs.media : [];\n if (made.length) remember(made, {});\n else drawResults();\n // A pick someone else made arrives without a change event: show what their model takes.\n queueMicrotask(refresh);\n});\n\nfunction busy(on) {\n state.busy = on;\n $('generate').disabled = on;\n root.toggleAttribute('aria-busy', on);\n}\n\nasync function generate() {\n if (state.busy) return;\n const kind = mode(), [id, entry] = model(), list = listFor(kind), prompt = $('prompt').value.trim();\n if (!prompt) { status(kind === 'video' ? 'Describe the shot first.' : 'Describe the picture first.', 'error'); $('prompt').focus(); return; }\n if (kind === 'video' && entry && entry.needsFirstFrame && !state.input.startFrame) { status(`${entry.label} needs a start frame.`, 'error'); return; }\n const count = Number(checked('count')) || 1;\n const values = { prompt, mode: kind, count };\n // The model goes only when someone picked one other than StillMade's default; with none named StillMade runs its\n // default, so a model StillMade makes its default later is the one used.\n if (entry && list && id !== list.default) values.model = id;\n const others = {};\n for (const setting of entry ? entry.settings || [] : []) {\n const group = groupFor(setting.key), picked = group ? visibleChecked(group.id) : undefined, value = picked === undefined ? undefined : valueOf(setting, picked);\n if (value === undefined) continue;\n if (INPUTS[setting.key]) values[INPUTS[setting.key]] = value; else others[setting.key] = value;\n }\n if (Object.keys(others).length) values.settings = others;\n const photos = Array.isArray(state.input.references) ? state.input.references : [];\n if (kind === 'image' && photos.length && (!entry || entry.references)) values.references = photos.slice(0, entry ? entry.references : 8);\n if (kind === 'video') {\n if (state.input.startFrame) values.startFrame = state.input.startFrame;\n if (state.input.startFrame && state.input.endFrame && (!entry || entry.lastFrame)) values.endFrame = state.input.endFrame;\n }\n const avoid = $('avoid').value.trim();\n if (avoid) values.avoid = avoid;\n const seed = Number($('seed').value);\n if ((!entry || entry.seed !== false) && $('seed').value !== '' && Number.isInteger(seed) && seed >= 0) values.seed = seed;\n busy(true);\n status(count > 1 ? `Generating ${count} ${kind}s…` : `Generating ${kind === 'video' ? 'the video' : 'the picture'}…`, 'loading');\n try {\n const outputs = await StillMade.run(values);\n const made = outputs && Array.isArray(outputs.media) ? outputs.media : [];\n remember(made, { prompt, ...(entry ? { model: entry.label } : {}) });\n status(made.length ? `${made.length} made` : '', made.length ? 'success' : undefined);\n } catch (error) { status(message(error), 'error'); }\n finally { busy(false); }\n}\n$('generate').addEventListener('click', generate);\nrefresh();\n\n})();\n"
|
|
310
|
+
},
|
|
311
|
+
"tests": [
|
|
312
|
+
{
|
|
313
|
+
"name": "Makes two pictures that keep your photo",
|
|
314
|
+
"input": {
|
|
315
|
+
"prompt": "The bottle on a wet rock at dawn",
|
|
316
|
+
"mode": "image",
|
|
317
|
+
"aspectRatio": "4:5",
|
|
318
|
+
"size": "2K",
|
|
319
|
+
"count": 2,
|
|
320
|
+
"references": [
|
|
321
|
+
{
|
|
322
|
+
"kind": "image",
|
|
323
|
+
"assetId": "photo-asset",
|
|
324
|
+
"versionId": "photo-v1",
|
|
325
|
+
"url": "https://media.example/photo.png",
|
|
326
|
+
"mimeType": "image/png",
|
|
327
|
+
"width": 1024,
|
|
328
|
+
"height": 1024,
|
|
329
|
+
"bytes": 200000
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
},
|
|
333
|
+
"mocks": {
|
|
334
|
+
"actions": {
|
|
335
|
+
"media.generate": {
|
|
336
|
+
"value": [
|
|
337
|
+
{
|
|
338
|
+
"key": "image-1",
|
|
339
|
+
"kind": "image",
|
|
340
|
+
"assetId": "made-image-1",
|
|
341
|
+
"versionId": "made-image-1-v1",
|
|
342
|
+
"url": "https://media.example/made-image-1.png"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"key": "image-2",
|
|
346
|
+
"kind": "image",
|
|
347
|
+
"assetId": "made-image-2",
|
|
348
|
+
"versionId": "made-image-2-v1",
|
|
349
|
+
"url": "https://media.example/made-image-2.png"
|
|
350
|
+
}
|
|
351
|
+
],
|
|
352
|
+
"failures": []
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
"expected": {
|
|
357
|
+
"media": [
|
|
358
|
+
{
|
|
359
|
+
"kind": "image",
|
|
360
|
+
"assetId": "made-image-1",
|
|
361
|
+
"versionId": "made-image-1-v1",
|
|
362
|
+
"url": "https://media.example/made-image-1.png"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"kind": "image",
|
|
366
|
+
"assetId": "made-image-2",
|
|
367
|
+
"versionId": "made-image-2-v1",
|
|
368
|
+
"url": "https://media.example/made-image-2.png"
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"name": "Makes a vertical clip from a start and end frame with its own sound",
|
|
375
|
+
"input": {
|
|
376
|
+
"prompt": "Slow push in as the door opens",
|
|
377
|
+
"mode": "video",
|
|
378
|
+
"aspectRatio": "9:16",
|
|
379
|
+
"size": "720p",
|
|
380
|
+
"duration": 6,
|
|
381
|
+
"sound": true,
|
|
382
|
+
"startFrame": {
|
|
383
|
+
"kind": "image",
|
|
384
|
+
"assetId": "photo-asset",
|
|
385
|
+
"versionId": "photo-v1",
|
|
386
|
+
"url": "https://media.example/photo.png",
|
|
387
|
+
"mimeType": "image/png",
|
|
388
|
+
"width": 1024,
|
|
389
|
+
"height": 1024,
|
|
390
|
+
"bytes": 200000
|
|
391
|
+
},
|
|
392
|
+
"endFrame": {
|
|
393
|
+
"kind": "image",
|
|
394
|
+
"assetId": "frame-asset",
|
|
395
|
+
"versionId": "frame-v1",
|
|
396
|
+
"url": "https://media.example/frame.png",
|
|
397
|
+
"mimeType": "image/png",
|
|
398
|
+
"width": 1024,
|
|
399
|
+
"height": 1024,
|
|
400
|
+
"bytes": 200000
|
|
401
|
+
},
|
|
402
|
+
"avoid": "blur",
|
|
403
|
+
"seed": 7
|
|
404
|
+
},
|
|
405
|
+
"mocks": {
|
|
406
|
+
"actions": {
|
|
407
|
+
"media.generate": {
|
|
408
|
+
"value": [
|
|
409
|
+
{
|
|
410
|
+
"key": "video-1",
|
|
411
|
+
"kind": "video",
|
|
412
|
+
"assetId": "made-video-1",
|
|
413
|
+
"versionId": "made-video-1-v1",
|
|
414
|
+
"url": "https://media.example/made-video-1.mp4",
|
|
415
|
+
"duration": 6
|
|
416
|
+
}
|
|
417
|
+
],
|
|
418
|
+
"failures": []
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
"expected": {
|
|
423
|
+
"media": [
|
|
424
|
+
{
|
|
425
|
+
"kind": "video",
|
|
426
|
+
"assetId": "made-video-1",
|
|
427
|
+
"versionId": "made-video-1-v1",
|
|
428
|
+
"url": "https://media.example/made-video-1.mp4",
|
|
429
|
+
"duration": 6
|
|
430
|
+
}
|
|
431
|
+
]
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
"name": "Passes a setting it has no input for to StillMade",
|
|
436
|
+
"input": {
|
|
437
|
+
"prompt": "A paper lantern over a dark lake",
|
|
438
|
+
"mode": "image",
|
|
439
|
+
"settings": {
|
|
440
|
+
"quality": "high"
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
"mocks": {
|
|
444
|
+
"actions": {
|
|
445
|
+
"media.generate": {
|
|
446
|
+
"value": [
|
|
447
|
+
{
|
|
448
|
+
"key": "image-1",
|
|
449
|
+
"kind": "image",
|
|
450
|
+
"assetId": "made-image-1",
|
|
451
|
+
"versionId": "made-image-1-v1",
|
|
452
|
+
"url": "https://media.example/made-image-1.png"
|
|
453
|
+
}
|
|
454
|
+
],
|
|
455
|
+
"failures": []
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
"expected": {
|
|
460
|
+
"media": [
|
|
461
|
+
{
|
|
462
|
+
"kind": "image",
|
|
463
|
+
"assetId": "made-image-1",
|
|
464
|
+
"versionId": "made-image-1-v1",
|
|
465
|
+
"url": "https://media.example/made-image-1.png"
|
|
466
|
+
}
|
|
467
|
+
]
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
]
|
|
471
|
+
};
|