stillmade-sdk 0.1.13 → 0.1.15

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/SDK.md CHANGED
@@ -12208,6 +12208,8 @@ The latest version of each Block bundled with the SDK, generated from the packag
12208
12208
  | `sm.motion-graphics` 2.0.0 | Motion graphics | editor-extension | `timeline` timeline (timeline) · primary; `projectTimeline` timeline (timeline) · project context only; `plan` json (motion_graphics_plan) | `timeline` timeline (timeline) · primary; `edit` workspace-edit (editor_workspace_edit) |
12209
12209
  | `sm.new-items` 1.0.0 | Only new items | task | `items` object (external_data) · primary; `id_field` text (item_id_field) | `item` object (external_data) · primary; `waiting` integer (pending_item_count) |
12210
12210
  | `sm.panel-strip` 2.4.1 | Image Panels | workspace | `panels` panel-document (image_panel_document) · project context only · primary; `transcript` transcript (timed_transcript) · project context only; `script` script (narration_script) · project context only; `narrationScript` script (narration_script); `narrationTiming` transcript (timed_transcript); `blueprint` bible (production_blueprint) · project context only; `updatedPanels` panel-document (image_panel_document); `commit` boolean (apply_changes); `generatedImage` image (image); `targetPanelId` text (panel_id); `attachGeneratedImage` boolean (boolean); `action` text (host_action); `stockQuery` text (stock_query); `generatedImages` image[] (image_list) | `panels` panel-document (image_panel_document) · primary; `edit` workspace-edit (workspace_edit); `stockAction` approval (portable_media_action); `qualityAction` approval (portable_media_action); `renderAction` approval (portable_media_action); `stockAsset` asset (licensed_asset); `stockImage` image (licensed_stock_image); `qualityReport` metadata (quality_report); `generationPlan` metadata (generation_plan); `generateAction` approval (portable_media_action); `generatedImage` image (generated_image); `batchGenerateAction` approval (portable_media_action); `generatedImages` image[] (generated_image_list) |
12211
+ | `sm.photo-notes` 1.0.0 | Photo notes | task | `photos` image[] (photo_set) · primary | `notes` text (photo_notes) · primary |
12212
+ | `sm.pick-photos` 1.0.0 | Pick best photos | task | `photos` image[] (image) · primary; `keep` integer (photo_keep_count); `instruction` text (photo_selection_instruction); `notes` json (photo_notes) | `photos` image[] (image) · primary; `reasons` text (plain_text) |
12211
12213
  | `sm.pipeline` 2.2.5 | Canvas | workspace | `canvas` pipeline (canvas_document) · project context only · primary; `assets` asset[] (project_assets) · project context only; `versions` metadata[] (asset_versions) · project context only; `updatedCanvas` pipeline (canvas_document); `commit` boolean (canvas_commit_intent); `generatedImage` image (generated_image_version); `addGeneratedImage` boolean (attach_generated_image); `generatedImageTitle` text (generated_image_title); `generatedVideo` video (generated_video_version); `addGeneratedVideo` boolean (attach_generated_video); `generatedVideoTitle` text (generated_video_title); `generatedAudio` audio (generated_audio_version); `addGeneratedAudio` boolean (attach_generated_audio); `generatedAudioTitle` text (generated_audio_title); `connectionProposal` json (context_connection_proposal); `applyContextConnections` boolean (context_connection_intent) | `canvas` pipeline (canvas_document) · primary; `edit` workspace-edit (canvas_edit_proposal) |
12212
12214
  | `sm.quality-audit` 1.0.2 | Quality review plan | task | `assets` asset[] (saved_asset_versions) · project context only · primary | `plan` metadata (visual_quality_review_plan) · primary |
12213
12215
  | `sm.research` 2.0.2 | Research | task | `input` text (research_question) · primary; `sourceTitle` text (source_title); `sourceUrl` text (source_url); `sourceNotes` text (source_notes) | `output` research (research_dossier) · primary |
@@ -12228,7 +12230,7 @@ The latest version of each Block bundled with the SDK, generated from the packag
12228
12230
  | `sm.write-summary` 1.0.0 | Write summary | task | `data` text (external_data) · primary | `summary` text (summary_report) · primary |
12229
12231
  | `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) |
12230
12232
  | `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) |
12231
- | `stillmade.photo-reel-composer` 1.0.1 | 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) |
12233
+ | `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) |
12232
12234
  | `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) |
12233
12235
  | `stillmade.release-visuals` 1.0.2 | Release Visuals | task | `assets` asset[] (legacy.asset-list.assets) · primary; `purpose` text (stillmade.release_visuals.purpose); `title` text (stillmade.release_visuals.title); `artist` text (stillmade.release_visuals.artist); `cta` text (stillmade.release_visuals.cta); `duration` number (stillmade.release_visuals.duration); `songVolume` number (stillmade.release_visuals.songvolume); `offset` number (stillmade.release_visuals.offset); `lyrics` text (stillmade.release_visuals.lyrics); `aspect` text (stillmade.release_visuals.aspect); `disclosure` text (stillmade.release_visuals.disclosure) | `timeline` timeline (legacy.timeline.updatededitor) · primary; `review` json (stillmade.release_visuals.review) |
12234
12236
  | `stillmade.short-form-finish` 1.0.3 | Short-form Finish | task | `assets` video[] (legacy.asset-list.assets) · primary; `music` audio[] (legacy.asset-list.assets); `audioMode` text (stillmade.short_form.audiomode); `speechVolume` number (stillmade.short_form.speechvolume); `purpose` text (stillmade.short_form.purpose); `cuts` text (stillmade.short_form.cuts); `subtitles` text (stillmade.short_form.subtitles); `title` text (stillmade.short_form.title); `byline` text (stillmade.short_form.byline); `cta` text (stillmade.short_form.cta); `disclosure` text (stillmade.short_form.disclosure); `captionStyle` text (stillmade.short_form.captionstyle); `aspect` text (stillmade.short_form.aspect); `fit` text (stillmade.short_form.fit); `cropX` number (stillmade.short_form.cropx) | `timeline` timeline (legacy.timeline.updatededitor) · primary; `review` json (stillmade.short_form.review) |
@@ -0,0 +1,180 @@
1
+ // Generated by scripts/build-owned-blocks.mjs. Edit the owned source files.
2
+ export default {
3
+ "manifest": {
4
+ "schemaVersion": 1,
5
+ "sdkVersion": "0.1.0",
6
+ "id": "sm.photo-notes",
7
+ "version": "1.0.0",
8
+ "name": "Photo notes",
9
+ "description": "Rate up to four owned photos for sharpness, light, people and faces through StillMade's approved vision capability, one line per photo.",
10
+ "kind": "task",
11
+ "runtime": "capability",
12
+ "entry": "src/capability.json",
13
+ "license": "MIT",
14
+ "provenance": {
15
+ "notice": "Original source (authorship declared by the packager)\nLicense: MIT\nChanges: Original implementation declared by the packager; no reused repository or dependency code.\nFiles: LICENSE, src/capability.json\n\nMIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
16
+ "portableSources": [
17
+ {
18
+ "kind": "original",
19
+ "license": "MIT",
20
+ "paths": [
21
+ "LICENSE",
22
+ "src/capability.json"
23
+ ],
24
+ "evidence": [
25
+ "upstream/0/LICENSE",
26
+ "upstream/0/src/capability.json"
27
+ ],
28
+ "changes": "Original implementation declared by the packager; no reused repository or dependency code."
29
+ }
30
+ ]
31
+ },
32
+ "inputs": {
33
+ "photos": {
34
+ "type": "image[]",
35
+ "primary": true,
36
+ "description": "One to four persistent photos from the user's StillMade library.",
37
+ "semantic": "photo_set",
38
+ "required": true
39
+ }
40
+ },
41
+ "outputs": {
42
+ "notes": {
43
+ "type": "text",
44
+ "primary": true,
45
+ "description": "One line per photo, in order: sharpness, light, people, faces, smiles, closed eyes, tags and a short description.",
46
+ "semantic": "photo_notes"
47
+ }
48
+ },
49
+ "permissions": {
50
+ "project": [],
51
+ "capabilities": [
52
+ "image.describe"
53
+ ],
54
+ "network": [],
55
+ "filesystem": [],
56
+ "secrets": []
57
+ },
58
+ "ui": [
59
+ {
60
+ "port": "photos",
61
+ "control": "asset"
62
+ }
63
+ ],
64
+ "platforms": {
65
+ "phone": {
66
+ "supported": true
67
+ },
68
+ "browser": {
69
+ "supported": true
70
+ },
71
+ "desktop": {
72
+ "supported": true
73
+ }
74
+ }
75
+ },
76
+ "capability": {
77
+ "schemaVersion": 1,
78
+ "operation": "image.describe",
79
+ "image": {
80
+ "$input": "photos"
81
+ },
82
+ "instruction": "Rate each supplied photo, in the order given, so a person can choose the best photos for a video. Return exactly one line per photo and nothing else, in this format:\nPhoto 1 | sharp 8 | light 7 | people 2 | faces clear yes | smiling yes | eyes closed no | tags: beach, family, sunset | Two people laughing on a beach at sunset.\nsharp is focus and blur from 0 (unusable) to 10 (crisp). light is exposure and color from 0 (too dark or blown out) to 10 (well lit). people is how many people are clearly visible. faces clear is yes when at least one face is visible and in focus. smiling is yes when a visible person smiles or laughs. eyes closed is yes when the main person blinks. tags are up to six plain lowercase words for the subject, setting, occasion and mood. End with one short plain sentence describing only what is visible. Never identify a real person by name, never guess age, ethnicity, health or other sensitive traits, and never invent anything outside the frame.",
83
+ "maxTokens": 700,
84
+ "output": "notes"
85
+ },
86
+ "tests": [
87
+ {
88
+ "name": "One blue photo",
89
+ "input": {
90
+ "photos": [
91
+ {
92
+ "width": 1,
93
+ "height": 1,
94
+ "data": [
95
+ 20,
96
+ 80,
97
+ 200,
98
+ 255
99
+ ]
100
+ }
101
+ ]
102
+ },
103
+ "expectations": {
104
+ "notes": {
105
+ "kind": "text",
106
+ "minLength": 20,
107
+ "maxLength": 32768,
108
+ "includes": [
109
+ "Photo 1"
110
+ ]
111
+ }
112
+ }
113
+ },
114
+ {
115
+ "name": "Two plain photos",
116
+ "input": {
117
+ "photos": [
118
+ {
119
+ "width": 1,
120
+ "height": 1,
121
+ "data": [
122
+ 128,
123
+ 128,
124
+ 128,
125
+ 255
126
+ ]
127
+ },
128
+ {
129
+ "width": 1,
130
+ "height": 1,
131
+ "data": [
132
+ 240,
133
+ 200,
134
+ 40,
135
+ 255
136
+ ]
137
+ }
138
+ ]
139
+ },
140
+ "expectations": {
141
+ "notes": {
142
+ "kind": "text",
143
+ "minLength": 40,
144
+ "maxLength": 32768,
145
+ "includes": [
146
+ "Photo 1",
147
+ "Photo 2"
148
+ ]
149
+ }
150
+ }
151
+ }
152
+ ],
153
+ "portable": {
154
+ "format": "stillmade-block/1",
155
+ "dependencies": [],
156
+ "sources": [
157
+ {
158
+ "kind": "original",
159
+ "license": "MIT",
160
+ "paths": [
161
+ "LICENSE",
162
+ "src/capability.json"
163
+ ],
164
+ "evidence": [
165
+ "upstream/0/LICENSE",
166
+ "upstream/0/src/capability.json"
167
+ ],
168
+ "changes": "Original implementation declared by the packager; no reused repository or dependency code."
169
+ }
170
+ ],
171
+ "files": {
172
+ "upstream/0/LICENSE": "MIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
173
+ "upstream/0/src/capability.json": "{\n \"schemaVersion\": 1,\n \"operation\": \"image.describe\",\n \"image\": {\n \"$input\": \"photos\"\n },\n \"instruction\": \"Rate each supplied photo, in the order given, so a person can choose the best photos for a video. Return exactly one line per photo and nothing else, in this format:\\nPhoto 1 | sharp 8 | light 7 | people 2 | faces clear yes | smiling yes | eyes closed no | tags: beach, family, sunset | Two people laughing on a beach at sunset.\\nsharp is focus and blur from 0 (unusable) to 10 (crisp). light is exposure and color from 0 (too dark or blown out) to 10 (well lit). people is how many people are clearly visible. faces clear is yes when at least one face is visible and in focus. smiling is yes when a visible person smiles or laughs. eyes closed is yes when the main person blinks. tags are up to six plain lowercase words for the subject, setting, occasion and mood. End with one short plain sentence describing only what is visible. Never identify a real person by name, never guess age, ethnicity, health or other sensitive traits, and never invent anything outside the frame.\",\n \"maxTokens\": 700,\n \"output\": \"notes\"\n}"
174
+ },
175
+ "lineage": {
176
+ "parents": [],
177
+ "changes": "Packaged original Block source for StillMade."
178
+ }
179
+ }
180
+ };
@@ -0,0 +1,334 @@
1
+ // Generated by scripts/build-owned-blocks.mjs. Edit the owned source files.
2
+ export default {
3
+ "manifest": {
4
+ "schemaVersion": 1,
5
+ "sdkVersion": "0.1.0",
6
+ "id": "sm.pick-photos",
7
+ "version": "1.0.0",
8
+ "name": "Pick best photos",
9
+ "description": "Check every photo with StillMade's vision capability and keep only the best ones: sharp, well lit, clear faces, varied, and true to your note.",
10
+ "kind": "task",
11
+ "runtime": "javascript",
12
+ "entry": "src/run.js",
13
+ "license": "MIT",
14
+ "provenance": {
15
+ "notice": "Original source (authorship declared by the packager)\nLicense: MIT\nChanges: Original implementation declared by the packager; no reused repository or dependency code.\nFiles: LICENSE, src/run.js\n\nMIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
16
+ "portableSources": [
17
+ {
18
+ "kind": "original",
19
+ "license": "MIT",
20
+ "paths": [
21
+ "LICENSE",
22
+ "src/run.js"
23
+ ],
24
+ "evidence": [
25
+ "upstream/0/LICENSE",
26
+ "upstream/0/src/run.js"
27
+ ],
28
+ "changes": "Original implementation declared by the packager; no reused repository or dependency code."
29
+ }
30
+ ]
31
+ },
32
+ "inputs": {
33
+ "photos": {
34
+ "type": "image[]",
35
+ "primary": true,
36
+ "required": true,
37
+ "imageMode": "reference",
38
+ "description": "The photos to choose from, up to 100.",
39
+ "semantic": "image",
40
+ "accepts": [
41
+ "source_image"
42
+ ]
43
+ },
44
+ "keep": {
45
+ "type": "integer",
46
+ "default": 12,
47
+ "min": 1,
48
+ "max": 50,
49
+ "required": false,
50
+ "description": "How many photos to keep.",
51
+ "semantic": "photo_keep_count"
52
+ },
53
+ "instruction": {
54
+ "type": "text",
55
+ "default": "",
56
+ "required": false,
57
+ "description": "What the photos are for, such as: a memorial tribute for Grandma; prefer clear faces and happy moments.",
58
+ "semantic": "photo_selection_instruction"
59
+ },
60
+ "notes": {
61
+ "type": "json",
62
+ "required": false,
63
+ "description": "One note per photo from the photo check. StillMade fills this in when the Step runs; leave it empty.",
64
+ "semantic": "photo_notes",
65
+ "schema": [
66
+ {
67
+ "ok": "boolean?",
68
+ "sharp": "number?",
69
+ "light": "number?",
70
+ "people": "integer?",
71
+ "facesClear": "boolean?",
72
+ "smiling": "boolean?",
73
+ "eyesClosed": "boolean?",
74
+ "tags": "string[]?",
75
+ "text": "string?"
76
+ }
77
+ ]
78
+ }
79
+ },
80
+ "outputs": {
81
+ "photos": {
82
+ "type": "image[]",
83
+ "primary": true,
84
+ "description": "Only the chosen photos, unchanged, in the order they were taken when dates are known, otherwise best first.",
85
+ "semantic": "image"
86
+ },
87
+ "reasons": {
88
+ "type": "text",
89
+ "description": "Why each photo was kept.",
90
+ "semantic": "plain_text"
91
+ }
92
+ },
93
+ "permissions": {
94
+ "project": [],
95
+ "network": [],
96
+ "filesystem": [],
97
+ "secrets": []
98
+ },
99
+ "ui": [
100
+ {
101
+ "port": "photos",
102
+ "control": "asset"
103
+ },
104
+ {
105
+ "port": "keep",
106
+ "control": "number",
107
+ "label": "How many to keep"
108
+ },
109
+ {
110
+ "port": "instruction",
111
+ "control": "text",
112
+ "label": "What they are for"
113
+ },
114
+ {
115
+ "port": "photos",
116
+ "control": "gallery"
117
+ }
118
+ ],
119
+ "platforms": {
120
+ "phone": {
121
+ "supported": true
122
+ },
123
+ "browser": {
124
+ "supported": true
125
+ },
126
+ "desktop": {
127
+ "supported": true
128
+ }
129
+ }
130
+ },
131
+ "code": "// Pick best photos: rank the photos from their notes (one per photo, made by\n// Photo notes when the Step runs) and return only the chosen ones, unchanged.\nconst MAX_PHOTOS=100;\nconst photos=Array.isArray(input.photos)?input.photos:[];\nif(!photos.length)throw new Error('Add photos to choose from.');\nif(photos.length>MAX_PHOTOS)throw new Error('Pick best photos checks up to 100 photos at a time. You added '+String(photos.length)+'. Remove some, or split them into groups of 100 or fewer.');\nconst notes=Array.isArray(input.notes)?input.notes:null;\nif(!notes||notes.length!==photos.length)throw new Error('Pick best photos needs its photo check. Run this Step in your project to check the photos.');\nconst keep=Math.max(1,Math.min(50,Math.round(Number(input.keep)||12)));\nconst instruction=typeof input.instruction==='string'?input.instruction.toLowerCase():'';\n\nconst STOP=new Set(['the','and','for','with','that','this','from','into','our','her','his','their','them','who','are','was','were','has','have','had','want','like','prefer','some','more','most','very','only','photos','photo','pictures','picture','images','image','video','please','make','show','use','good','best','nice','tribute','about']);\nconst words=text=>String(text||'').toLowerCase().split(/[^a-z0-9]+/).filter(word=>word.length>=3&&!STOP.has(word));\nconst stem=word=>word.slice(0,5);\nconst wanted=[...new Set(words(instruction).map(stem))];\nconst wantsFaces=/face|people|person|portrait|family|everyone|group/.test(instruction);\nconst wantsHappy=/happy|smil|joy|laugh|fun|cheer/.test(instruction);\nconst wantsSharp=/clear|sharp|focus|quality/.test(instruction);\nconst level=value=>{const number=Number(value);return Number.isFinite(number)?Math.max(0,Math.min(10,number)):5;};\n\nconst notesFor=(note)=>{\n const ok=!!note&&typeof note==='object'&&note.ok!==false;\n const tags=Array.isArray(note&&note.tags)?note.tags.map(tag=>String(tag).toLowerCase()).slice(0,12):[];\n const text=String(note&&note.text||'');\n return {ok,sharp:level(note&&note.sharp),light:level(note&&note.light),people:Math.max(0,Math.min(50,Math.round(Number(note&&note.people)||0))),facesClear:note&&note.facesClear===true,smiling:note&&note.smiling===true,eyesClosed:note&&note.eyesClosed===true,tags,text,signature:new Set([...tags.flatMap(words),...words(text)].map(stem))};\n};\nconst fitOf=note=>{\n if(!wanted.length)return 0;\n let hits=0;for(const word of wanted)if(note.signature.has(word))hits++;\n return hits/wanted.length;\n};\nconst scoreOf=note=>{\n if(!note.ok)return -100;\n const quality=(note.sharp*.6+note.light*.4)/10;\n let score=3*quality+(note.people>0?1:0)+(note.facesClear?1.5:0)+(note.smiling?1:0)-(note.eyesClosed?1.5:0)+3*fitOf(note);\n if(wantsFaces)score+=(note.facesClear?1.5:0)+(note.people>0?1:0);\n if(wantsHappy)score+=note.smiling?2:0;\n if(wantsSharp)score+=2*quality;\n if(note.sharp<=2)score-=2;\n return Math.round(score*1000)/1000;\n};\nconst similarity=(a,b)=>{\n if(!a.signature.size||!b.signature.size)return 0;\n let shared=0;for(const word of a.signature)if(b.signature.has(word))shared++;\n return shared/(a.signature.size+b.signature.size-shared);\n};\nconst dateOf=photo=>{\n for(const key of ['takenAt','capturedAt','createdAt','date']){\n const value=photo&&photo[key];\n const time=typeof value==='number'?value:typeof value==='string'?Date.parse(value):NaN;\n if(Number.isFinite(time))return time;\n }\n return null;\n};\n\n// Exact repeats of one saved version count once.\nconst seen=new Set();\nconst rows=[];\nphotos.forEach((photo,index)=>{\n const key=photo&&photo.assetId&&photo.versionId?photo.assetId+':'+photo.versionId:String(photo&&photo.url||index);\n if(seen.has(key))return;\n seen.add(key);\n const note=notesFor(notes[index]);\n rows.push({photo,index,note,score:scoreOf(note),fit:fitOf(note)});\n});\nconst ranked=rows.filter(row=>row.note.ok).sort((a,b)=>b.score-a.score||a.index-b.index);\nif(!ranked.length)throw new Error('None of these photos could be checked. Try clearer photos, or run the Step again.');\n\n// Near-duplicates (the same scene and people) give way to the best of them,\n// unless there are not enough different photos to fill the count.\nconst NEAR=.7;\nconst chosen=[],skipped=[];\nfor(const row of ranked){\n if(chosen.length>=keep)break;\n const twin=chosen.find(other=>Math.abs(other.note.people-row.note.people)<=1&&similarity(other.note,row.note)>=NEAR);\n if(twin){row.twin=twin;skipped.push(row);}else chosen.push(row);\n}\nfor(const row of skipped){if(chosen.length>=keep)break;chosen.push(row);}\n\nconst dated=chosen.every(row=>dateOf(row.photo)!==null);\nconst ordered=dated?chosen.slice().sort((a,b)=>dateOf(a.photo)-dateOf(b.photo)||a.index-b.index):chosen;\nconst nameOf=row=>row.photo&&typeof row.photo.name==='string'&&row.photo.name.trim()?row.photo.name.trim():'Photo '+String(row.index+1);\nconst why=row=>{\n const parts=[];\n const quality=(row.note.sharp*.6+row.note.light*.4);\n parts.push(quality>=7.5?'sharp and well lit':quality>=5?'clear enough':'the best of a weak set');\n if(row.note.people)parts.push(String(row.note.people)+(row.note.people===1?' person':' people'));\n if(row.note.facesClear)parts.push('clear faces');\n if(row.note.smiling)parts.push('smiling');\n if(row.fit>0)parts.push('fits your note');\n if(row.twin)parts.push('similar to another pick, kept to fill the count');\n return parts.join(', ');\n};\nconst lines=ordered.map((row,at)=>String(at+1)+'. '+nameOf(row)+': '+why(row)+'.');\nconst left=rows.length-chosen.length;\nconst header='Kept '+String(chosen.length)+' of '+String(photos.length)+' photos'+(dated?', in the order they were taken':', best first')+'.'+(left>0?' '+String(left)+' left out'+(skipped.length>chosen.filter(row=>row.twin).length?', some as near-duplicates':'')+'.':'');\nreturn {photos:ordered.map(row=>row.photo),reasons:[header,...lines].join('\\n')};\n",
132
+ "tests": [
133
+ {
134
+ "name": "keeps the sharpest varied photos and drops a near-duplicate",
135
+ "input": {
136
+ "photos": [
137
+ {
138
+ "kind": "image",
139
+ "assetId": "photo-1",
140
+ "versionId": "v1",
141
+ "url": "/api/media/photo-1"
142
+ },
143
+ {
144
+ "kind": "image",
145
+ "assetId": "photo-2",
146
+ "versionId": "v1",
147
+ "url": "/api/media/photo-2"
148
+ },
149
+ {
150
+ "kind": "image",
151
+ "assetId": "photo-3",
152
+ "versionId": "v1",
153
+ "url": "/api/media/photo-3"
154
+ },
155
+ {
156
+ "kind": "image",
157
+ "assetId": "photo-4",
158
+ "versionId": "v1",
159
+ "url": "/api/media/photo-4"
160
+ }
161
+ ],
162
+ "keep": 2,
163
+ "instruction": "Memorial tribute for Grandma; prefer clear faces",
164
+ "notes": [
165
+ {
166
+ "sharp": 9,
167
+ "light": 8,
168
+ "people": 2,
169
+ "facesClear": true,
170
+ "smiling": true,
171
+ "eyesClosed": false,
172
+ "tags": [
173
+ "grandma",
174
+ "garden",
175
+ "family"
176
+ ],
177
+ "text": "Two people smiling in a garden."
178
+ },
179
+ {
180
+ "sharp": 2,
181
+ "light": 3,
182
+ "people": 0,
183
+ "facesClear": false,
184
+ "smiling": false,
185
+ "eyesClosed": false,
186
+ "tags": [
187
+ "blur"
188
+ ],
189
+ "text": "A blurred hallway."
190
+ },
191
+ {
192
+ "sharp": 9,
193
+ "light": 8,
194
+ "people": 2,
195
+ "facesClear": true,
196
+ "smiling": true,
197
+ "eyesClosed": false,
198
+ "tags": [
199
+ "grandma",
200
+ "garden",
201
+ "family"
202
+ ],
203
+ "text": "Two people smiling in a garden."
204
+ },
205
+ {
206
+ "sharp": 8,
207
+ "light": 8,
208
+ "people": 1,
209
+ "facesClear": true,
210
+ "smiling": false,
211
+ "eyesClosed": false,
212
+ "tags": [
213
+ "kitchen",
214
+ "baking",
215
+ "grandma"
216
+ ],
217
+ "text": "A woman baking in a kitchen."
218
+ }
219
+ ]
220
+ },
221
+ "expected": {
222
+ "photos": [
223
+ {
224
+ "kind": "image",
225
+ "assetId": "photo-1",
226
+ "versionId": "v1",
227
+ "url": "/api/media/photo-1"
228
+ },
229
+ {
230
+ "kind": "image",
231
+ "assetId": "photo-4",
232
+ "versionId": "v1",
233
+ "url": "/api/media/photo-4"
234
+ }
235
+ ],
236
+ "reasons": "Kept 2 of 4 photos, best first. 2 left out, some as near-duplicates.\n1. Photo 1: sharp and well lit, 2 people, clear faces, smiling, fits your note.\n2. Photo 4: sharp and well lit, 1 person, clear faces, fits your note."
237
+ }
238
+ },
239
+ {
240
+ "name": "orders dated picks by when they were taken",
241
+ "input": {
242
+ "photos": [
243
+ {
244
+ "kind": "image",
245
+ "assetId": "photo-1",
246
+ "versionId": "v1",
247
+ "url": "/api/media/photo-1",
248
+ "takenAt": "2020-05-01T10:00:00Z"
249
+ },
250
+ {
251
+ "kind": "image",
252
+ "assetId": "photo-2",
253
+ "versionId": "v1",
254
+ "url": "/api/media/photo-2",
255
+ "takenAt": "2019-01-01T10:00:00Z"
256
+ }
257
+ ],
258
+ "keep": 2,
259
+ "notes": [
260
+ {
261
+ "sharp": 9,
262
+ "light": 9,
263
+ "people": 1,
264
+ "facesClear": true,
265
+ "smiling": true,
266
+ "eyesClosed": false,
267
+ "tags": [
268
+ "park"
269
+ ],
270
+ "text": "A child in a park."
271
+ },
272
+ {
273
+ "sharp": 6,
274
+ "light": 6,
275
+ "people": 0,
276
+ "facesClear": false,
277
+ "smiling": false,
278
+ "eyesClosed": false,
279
+ "tags": [
280
+ "lake"
281
+ ],
282
+ "text": "A quiet lake."
283
+ }
284
+ ]
285
+ },
286
+ "expected": {
287
+ "photos": [
288
+ {
289
+ "kind": "image",
290
+ "assetId": "photo-2",
291
+ "versionId": "v1",
292
+ "url": "/api/media/photo-2",
293
+ "takenAt": "2019-01-01T10:00:00Z"
294
+ },
295
+ {
296
+ "kind": "image",
297
+ "assetId": "photo-1",
298
+ "versionId": "v1",
299
+ "url": "/api/media/photo-1",
300
+ "takenAt": "2020-05-01T10:00:00Z"
301
+ }
302
+ ],
303
+ "reasons": "Kept 2 of 2 photos, in the order they were taken.\n1. Photo 2: clear enough.\n2. Photo 1: sharp and well lit, 1 person, clear faces, smiling."
304
+ }
305
+ }
306
+ ],
307
+ "portable": {
308
+ "format": "stillmade-block/1",
309
+ "dependencies": [],
310
+ "sources": [
311
+ {
312
+ "kind": "original",
313
+ "license": "MIT",
314
+ "paths": [
315
+ "LICENSE",
316
+ "src/run.js"
317
+ ],
318
+ "evidence": [
319
+ "upstream/0/LICENSE",
320
+ "upstream/0/src/run.js"
321
+ ],
322
+ "changes": "Original implementation declared by the packager; no reused repository or dependency code."
323
+ }
324
+ ],
325
+ "files": {
326
+ "upstream/0/LICENSE": "MIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
327
+ "upstream/0/src/run.js": "// Pick best photos: rank the photos from their notes (one per photo, made by\n// Photo notes when the Step runs) and return only the chosen ones, unchanged.\nconst MAX_PHOTOS=100;\nconst photos=Array.isArray(input.photos)?input.photos:[];\nif(!photos.length)throw new Error('Add photos to choose from.');\nif(photos.length>MAX_PHOTOS)throw new Error('Pick best photos checks up to 100 photos at a time. You added '+String(photos.length)+'. Remove some, or split them into groups of 100 or fewer.');\nconst notes=Array.isArray(input.notes)?input.notes:null;\nif(!notes||notes.length!==photos.length)throw new Error('Pick best photos needs its photo check. Run this Step in your project to check the photos.');\nconst keep=Math.max(1,Math.min(50,Math.round(Number(input.keep)||12)));\nconst instruction=typeof input.instruction==='string'?input.instruction.toLowerCase():'';\n\nconst STOP=new Set(['the','and','for','with','that','this','from','into','our','her','his','their','them','who','are','was','were','has','have','had','want','like','prefer','some','more','most','very','only','photos','photo','pictures','picture','images','image','video','please','make','show','use','good','best','nice','tribute','about']);\nconst words=text=>String(text||'').toLowerCase().split(/[^a-z0-9]+/).filter(word=>word.length>=3&&!STOP.has(word));\nconst stem=word=>word.slice(0,5);\nconst wanted=[...new Set(words(instruction).map(stem))];\nconst wantsFaces=/face|people|person|portrait|family|everyone|group/.test(instruction);\nconst wantsHappy=/happy|smil|joy|laugh|fun|cheer/.test(instruction);\nconst wantsSharp=/clear|sharp|focus|quality/.test(instruction);\nconst level=value=>{const number=Number(value);return Number.isFinite(number)?Math.max(0,Math.min(10,number)):5;};\n\nconst notesFor=(note)=>{\n const ok=!!note&&typeof note==='object'&&note.ok!==false;\n const tags=Array.isArray(note&&note.tags)?note.tags.map(tag=>String(tag).toLowerCase()).slice(0,12):[];\n const text=String(note&&note.text||'');\n return {ok,sharp:level(note&&note.sharp),light:level(note&&note.light),people:Math.max(0,Math.min(50,Math.round(Number(note&&note.people)||0))),facesClear:note&&note.facesClear===true,smiling:note&&note.smiling===true,eyesClosed:note&&note.eyesClosed===true,tags,text,signature:new Set([...tags.flatMap(words),...words(text)].map(stem))};\n};\nconst fitOf=note=>{\n if(!wanted.length)return 0;\n let hits=0;for(const word of wanted)if(note.signature.has(word))hits++;\n return hits/wanted.length;\n};\nconst scoreOf=note=>{\n if(!note.ok)return -100;\n const quality=(note.sharp*.6+note.light*.4)/10;\n let score=3*quality+(note.people>0?1:0)+(note.facesClear?1.5:0)+(note.smiling?1:0)-(note.eyesClosed?1.5:0)+3*fitOf(note);\n if(wantsFaces)score+=(note.facesClear?1.5:0)+(note.people>0?1:0);\n if(wantsHappy)score+=note.smiling?2:0;\n if(wantsSharp)score+=2*quality;\n if(note.sharp<=2)score-=2;\n return Math.round(score*1000)/1000;\n};\nconst similarity=(a,b)=>{\n if(!a.signature.size||!b.signature.size)return 0;\n let shared=0;for(const word of a.signature)if(b.signature.has(word))shared++;\n return shared/(a.signature.size+b.signature.size-shared);\n};\nconst dateOf=photo=>{\n for(const key of ['takenAt','capturedAt','createdAt','date']){\n const value=photo&&photo[key];\n const time=typeof value==='number'?value:typeof value==='string'?Date.parse(value):NaN;\n if(Number.isFinite(time))return time;\n }\n return null;\n};\n\n// Exact repeats of one saved version count once.\nconst seen=new Set();\nconst rows=[];\nphotos.forEach((photo,index)=>{\n const key=photo&&photo.assetId&&photo.versionId?photo.assetId+':'+photo.versionId:String(photo&&photo.url||index);\n if(seen.has(key))return;\n seen.add(key);\n const note=notesFor(notes[index]);\n rows.push({photo,index,note,score:scoreOf(note),fit:fitOf(note)});\n});\nconst ranked=rows.filter(row=>row.note.ok).sort((a,b)=>b.score-a.score||a.index-b.index);\nif(!ranked.length)throw new Error('None of these photos could be checked. Try clearer photos, or run the Step again.');\n\n// Near-duplicates (the same scene and people) give way to the best of them,\n// unless there are not enough different photos to fill the count.\nconst NEAR=.7;\nconst chosen=[],skipped=[];\nfor(const row of ranked){\n if(chosen.length>=keep)break;\n const twin=chosen.find(other=>Math.abs(other.note.people-row.note.people)<=1&&similarity(other.note,row.note)>=NEAR);\n if(twin){row.twin=twin;skipped.push(row);}else chosen.push(row);\n}\nfor(const row of skipped){if(chosen.length>=keep)break;chosen.push(row);}\n\nconst dated=chosen.every(row=>dateOf(row.photo)!==null);\nconst ordered=dated?chosen.slice().sort((a,b)=>dateOf(a.photo)-dateOf(b.photo)||a.index-b.index):chosen;\nconst nameOf=row=>row.photo&&typeof row.photo.name==='string'&&row.photo.name.trim()?row.photo.name.trim():'Photo '+String(row.index+1);\nconst why=row=>{\n const parts=[];\n const quality=(row.note.sharp*.6+row.note.light*.4);\n parts.push(quality>=7.5?'sharp and well lit':quality>=5?'clear enough':'the best of a weak set');\n if(row.note.people)parts.push(String(row.note.people)+(row.note.people===1?' person':' people'));\n if(row.note.facesClear)parts.push('clear faces');\n if(row.note.smiling)parts.push('smiling');\n if(row.fit>0)parts.push('fits your note');\n if(row.twin)parts.push('similar to another pick, kept to fill the count');\n return parts.join(', ');\n};\nconst lines=ordered.map((row,at)=>String(at+1)+'. '+nameOf(row)+': '+why(row)+'.');\nconst left=rows.length-chosen.length;\nconst header='Kept '+String(chosen.length)+' of '+String(photos.length)+' photos'+(dated?', in the order they were taken':', best first')+'.'+(left>0?' '+String(left)+' left out'+(skipped.length>chosen.filter(row=>row.twin).length?', some as near-duplicates':'')+'.':'');\nreturn {photos:ordered.map(row=>row.photo),reasons:[header,...lines].join('\\n')};\n"
328
+ },
329
+ "lineage": {
330
+ "parents": [],
331
+ "changes": "Packaged original Block source for StillMade."
332
+ }
333
+ }
334
+ };