stillmade-sdk 0.1.42 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.stillmade/sdk-baseline.json +6 -8
- package/AGENTS.md +15 -10
- package/FRAME_VIEWS.md +113 -0
- package/INTERFACE_DESIGN.md +62 -22
- package/MOBILE_UI.md +4 -2
- package/MODULES.md +152 -6
- package/PASTE_TO_LLM.md +35 -25
- package/README.md +3 -2
- package/SDK.md +994 -550
- package/UI_AESTHETIC.md +22 -2
- package/blocks/generate/package.js +1 -0
- package/blocks/generate/versions/1.0.0/package.js +463 -0
- package/docs/build/audio-to-editor.md +36 -0
- package/docs/build/block-builder-contract.md +150 -0
- package/docs/build/comfyui.md +227 -0
- package/docs/build/concepts.md +40 -0
- package/docs/build/context.md +159 -0
- package/docs/build/cookbook.md +57 -0
- package/docs/build/custom-interface.md +617 -0
- package/docs/build/data-types.md +83 -0
- package/docs/build/frame-views.md +259 -0
- package/docs/build/generate-media.md +59 -0
- package/docs/build/github-applications.md +314 -0
- package/docs/build/hosted-images.md +214 -0
- package/docs/build/hosted-music.md +39 -0
- package/docs/build/hosted-speech.md +157 -0
- package/docs/build/hosted-text.md +246 -0
- package/docs/build/image-description.md +49 -0
- package/docs/build/interface-design.md +343 -0
- package/docs/build/javascript.md +63 -0
- package/docs/build/local-preview.md +61 -0
- package/docs/build/media-analysis.md +27 -0
- package/docs/build/media.md +400 -0
- package/docs/build/mobile-ui.md +106 -0
- package/docs/build/modules.md +487 -0
- package/docs/build/original-blocks.md +70 -0
- package/docs/build/outside-apis.md +128 -0
- package/docs/build/production-context-templates.md +12 -0
- package/docs/build/project-storage.md +94 -0
- package/docs/build/quickstart.md +48 -0
- package/docs/build/recipes.md +103 -0
- package/docs/build/script-handoff.md +45 -0
- package/docs/build/script-writer.md +34 -0
- package/docs/build/state.md +223 -0
- package/docs/build/testing.md +112 -0
- package/docs/build/timeline-edits.md +116 -0
- package/docs/build/timeline-proposals.md +76 -0
- package/docs/build/transcription.md +59 -0
- package/docs/build/triggers.md +59 -0
- package/docs/build/tutorial.md +52 -0
- package/docs/build/typescript.md +34 -0
- package/docs/build/video-generation.md +61 -0
- package/docs/build/web-reading.md +37 -0
- package/docs/build/with-your-ai.md +27 -0
- package/docs/changelog.md +9 -0
- package/docs/creators.md +21 -0
- package/docs/distribute/discovery.md +43 -0
- package/docs/distribute/imports.md +58 -0
- package/docs/distribute/publishing.md +29 -0
- package/docs/distribute/sdk-versions.md +36 -0
- package/docs/distribute/selling.md +14 -0
- package/docs/distribute/teams.md +60 -0
- package/docs/distribute/versioning.md +28 -0
- package/docs/examples.md +23 -0
- package/docs/gallery.md +26 -0
- package/docs/index.md +50 -0
- package/docs/native-mcp.md +307 -0
- package/docs/project-types/automation.md +43 -0
- package/docs/project-types/batches.md +50 -0
- package/docs/project-types/build.md +42 -0
- package/docs/project-types/completion.md +15 -0
- package/docs/project-types/conditions.md +21 -0
- package/docs/project-types/hosted-review.md +115 -0
- package/docs/project-types/onboarding.md +75 -0
- package/docs/project-types/running.md +309 -0
- package/docs/project-types/testing.md +72 -0
- package/docs/reference/application-artifacts.md +87 -0
- package/docs/reference/authoring-guide.md +134 -0
- package/docs/reference/browser-applications.md +267 -0
- package/docs/reference/capabilities.md +45 -0
- package/docs/reference/cli.md +71 -0
- package/docs/reference/connection-api.md +59 -0
- package/docs/reference/cooperative-jobs.md +13 -0
- package/docs/reference/desktop-capabilities.md +172 -0
- package/docs/reference/desktop-execution.md +77 -0
- package/docs/reference/errors.md +233 -0
- package/docs/reference/execution.md +800 -0
- package/docs/reference/first-party-blocks.md +40 -0
- package/docs/reference/generation-models.md +59 -0
- package/docs/reference/limits.md +306 -0
- package/docs/reference/manifest-kinds-controls.md +36 -0
- package/docs/reference/manifest.md +181 -0
- package/docs/reference/operations.md +36 -0
- package/docs/reference/package-jobs.md +35 -0
- package/docs/reference/parity.md +55 -0
- package/docs/reference/permissions.md +86 -0
- package/docs/reference/ports.md +26 -0
- package/docs/reference/types.md +268 -0
- package/docs/reference/universal-resolver.md +303 -0
- package/docs/reference/view-bridge.md +60 -0
- package/docs/reference/view-rules.md +15 -0
- package/docs/reference/workspace-sync.md +25 -0
- package/docs/sdk.md +34 -0
- package/docs/tools/block-remix.md +211 -0
- package/docs/tools/llm.md +2213 -0
- package/docs/tools/repository-adaptation.md +613 -0
- package/examples/check-clip.stillmade.json +4 -1
- package/examples/generate-video.stillmade.json +6 -2
- package/examples/research-topic.stillmade.json +5 -1
- package/examples/text-workflow.stillmade.json +3 -32
- package/examples/timeline-polish.stillmade.json +11 -3
- package/examples/transcribe-audio.stillmade.json +11 -3
- package/package.json +1 -1
- package/packages/block-cli/cli.js +13 -6
- package/packages/block-cli/dev-host/assets/host.js +76 -38
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-cli/format.js +1 -1
- package/packages/block-cli/templates.js +31 -13
- package/packages/block-cli/type-packages.js +119 -0
- package/packages/block-platform/block-readiness.js +5 -0
- package/packages/block-platform/canvas.js +2 -1
- package/packages/block-platform/catalog.js +27 -48
- package/packages/block-platform/native-host-inventory.js +0 -226
- package/packages/block-platform/plain-descriptions.js +1 -25
- package/packages/block-platform/project-type-starters.js +13 -53
- package/packages/block-platform/script-handoff.js +2 -4
- package/packages/block-platform/type-workspace.js +2 -0
- package/packages/block-sdk/appearance.js +3 -1
- package/packages/block-sdk/block-tier.js +1 -1
- package/packages/block-sdk/builder-contract.js +10 -8
- package/packages/block-sdk/capabilities.js +1 -1
- package/packages/block-sdk/contracts.js +6 -2
- package/packages/block-sdk/first-party-identities.js +1 -84
- package/packages/block-sdk/first-party-packages.js +3 -85
- package/packages/block-sdk/generation-catalog.js +15 -3
- package/packages/block-sdk/index.d.ts +2 -1
- package/packages/block-sdk/media-analysis-example.js +1 -1
- package/packages/block-sdk/mock-host.js +7 -1
- package/packages/block-sdk/module-host-calls.js +35 -10
- package/packages/block-sdk/module-hosted.js +15 -2
- package/packages/block-sdk/module-media-access.js +71 -0
- package/packages/block-sdk/module-node.js +1 -1
- package/packages/block-sdk/portable-media-actions.js +1 -1
- package/packages/block-sdk/runtime.js +1 -1
- package/packages/block-sdk/timeline-proposal-example.js +1 -1
- package/packages/block-sdk/transcription-example.js +1 -1
- package/packages/block-sdk/video-generation-example.js +1 -1
- package/packages/block-sdk/view-bridge.js +4 -1
- package/packages/block-sdk/view-design.js +17 -14
- package/packages/block-sdk/web-example.js +1 -1
- package/packages/design-system/block-kit.css +48 -12
- package/packages/design-system/block-kit.html +3 -2
- package/blocks/mcp-motion-planner/versions/1.1.0/package.js +0 -97
- package/blocks/motion-designer/versions/1.0.1/package.js +0 -173318
- package/blocks/motion-designer/versions/1.0.2/package.js +0 -173318
- package/blocks/motion-frame-review/versions/1.1.1/package.js +0 -465
- package/blocks/motion-frame-review/versions/1.1.2/package.snapshot.js +0 -572
- package/blocks/narration/package.js +0 -1
- package/blocks/narration/versions/1.0.0/package.js +0 -221
- package/blocks/narration/versions/1.0.1/package.js +0 -221
- package/blocks/native/sm.animated-scene/versions/2.0.0/package.js +0 -475
- package/blocks/native/sm.animated-scene/versions/2.1.0/package.js +0 -498
- package/blocks/native/sm.animated-scene/versions/2.2.0/package.js +0 -524
- package/blocks/native/sm.animated-scene/versions/2.3.0/package.js +0 -618
- package/blocks/native/sm.animated-scene/versions/2.3.1/package.js +0 -618
- package/blocks/native/sm.animated-scene/versions/2.3.2/package.js +0 -723
- package/blocks/native/sm.animated-scene/versions/2.3.3/package.js +0 -723
- package/blocks/native/sm.animated-scene/versions/2.3.4/package.js +0 -676
- package/blocks/native/sm.animated-scene/versions/2.3.5/package.js +0 -693
- package/blocks/native/sm.animated-scene/versions/2.3.6/package.js +0 -700
- package/blocks/native/sm.animated-scene/versions/2.3.7/package.js +0 -747
- package/blocks/native/sm.animated-scene/versions/2.3.8/package.js +0 -769
- package/blocks/native/sm.animated-scene/versions/2.3.9/package.js +0 -769
- package/blocks/native/sm.animated-scene-assistant/versions/1.0.0/package.js +0 -183
- package/blocks/native/sm.blueprint/versions/2.0.0/package.js +0 -358
- package/blocks/native/sm.blueprint/versions/2.1.0/package.js +0 -358
- package/blocks/native/sm.blueprint/versions/2.1.1/package.js +0 -358
- package/blocks/native/sm.blueprint/versions/2.1.2/package.js +0 -358
- package/blocks/native/sm.blueprint/versions/2.1.3/package.js +0 -381
- package/blocks/native/sm.blueprint/versions/2.1.4/package.js +0 -381
- package/blocks/native/sm.blueprint/versions/2.1.5/package.js +0 -339
- package/blocks/native/sm.blueprint/versions/2.1.6/package.js +0 -356
- package/blocks/native/sm.blueprint/versions/2.1.7/package.js +0 -363
- package/blocks/native/sm.blueprint/versions/2.1.8/package.js +0 -410
- package/blocks/native/sm.blueprint/versions/2.1.9/package.js +0 -422
- package/blocks/native/sm.blueprint-assistant/versions/1.0.0/package.js +0 -172
- package/blocks/native/sm.chat/versions/2.0.0/package.js +0 -144
- package/blocks/native/sm.chat/versions/2.0.1/package.js +0 -97
- package/blocks/native/sm.chat/versions/2.0.2/package.js +0 -100
- package/blocks/native/sm.chat/versions/2.0.3/package.js +0 -100
- package/blocks/native/sm.continue-if/versions/1.0.0/package.js +0 -168
- package/blocks/native/sm.describe-media/versions/2.0.0/package.js +0 -153
- package/blocks/native/sm.describe-media/versions/2.0.1/package.js +0 -156
- package/blocks/native/sm.describe-media/versions/2.1.0/package.js +0 -140
- package/blocks/native/sm.image-editor/versions/2.0.0/package.js +0 -183
- package/blocks/native/sm.image-editor/versions/2.0.1/package.js +0 -190
- package/blocks/native/sm.new-items/versions/1.0.0/package.js +0 -191
- package/blocks/native/sm.pick-photos/versions/1.0.0/package.js +0 -334
- package/blocks/native/sm.quality-audit/versions/1.0.0/package.js +0 -192
- package/blocks/native/sm.quality-audit/versions/1.0.1/package.js +0 -192
- package/blocks/native/sm.quality-audit/versions/1.0.2/package.js +0 -195
- package/blocks/native/sm.research/versions/2.0.0/package.js +0 -175
- package/blocks/native/sm.research/versions/2.0.1/package.js +0 -184
- package/blocks/native/sm.research/versions/2.0.2/package.js +0 -187
- package/blocks/native/sm.script/versions/2.0.0/package.js +0 -144
- package/blocks/native/sm.script/versions/2.1.0/package.js +0 -144
- package/blocks/native/sm.script/versions/2.1.1/package.js +0 -144
- package/blocks/native/sm.script/versions/2.1.2/package.js +0 -105
- package/blocks/native/sm.script/versions/2.1.3/package.js +0 -152
- package/blocks/native/sm.script/versions/2.1.4/package.js +0 -156
- package/blocks/native/sm.script/versions/2.1.5/package.js +0 -156
- package/blocks/native/sm.shot-plan/versions/2.0.0/package.js +0 -301
- package/blocks/native/sm.shot-plan/versions/2.1.0/package.js +0 -528
- package/blocks/native/sm.shot-plan/versions/2.1.1/package.js +0 -528
- package/blocks/native/sm.shot-plan/versions/2.1.2/package.js +0 -528
- package/blocks/native/sm.shot-plan/versions/2.1.3/package.js +0 -536
- package/blocks/native/sm.shot-plan/versions/2.2.0/package.js +0 -800
- package/blocks/native/sm.shot-plan/versions/2.2.1/package.js +0 -920
- package/blocks/native/sm.shot-plan-assistant/versions/1.0.0/package.js +0 -173
- package/blocks/native/sm.stock-search/versions/2.0.0/package.js +0 -174
- package/blocks/native/sm.thumbnail/versions/2.0.0/package.js +0 -137
- package/blocks/native/sm.thumbnail/versions/2.0.1/package.js +0 -140
- package/blocks/native/sm.transcribe/versions/2.0.0/package.js +0 -163
- package/blocks/native/sm.transcribe/versions/2.0.1/package.js +0 -168
- package/blocks/native/sm.whiteboard-assistant/versions/1.0.0/package.js +0 -175
- package/blocks/native/sm.youtube/versions/2.0.0/package.js +0 -217
- package/blocks/occasion-story/package.js +0 -1
- package/blocks/occasion-story/versions/1.0.0/package.js +0 -1658
- package/blocks/occasion-story/versions/1.0.1/package.js +0 -1699
- package/blocks/occasion-story/versions/1.0.2/package.js +0 -1682
- package/blocks/photo-reel-composer/versions/1.0.0/package.js +0 -3043
- package/blocks/photo-reel-composer/versions/1.0.1/package.js +0 -3026
- package/blocks/photo-reel-composer/versions/1.0.2/package.js +0 -4149
- package/blocks/product-ad-variants/versions/1.0.0/package.js +0 -2911
- package/blocks/product-launch-director/versions/1.0.0/package.js +0 -103
- package/blocks/release-visuals/package.js +0 -1
- package/blocks/release-visuals/versions/1.0.0/package.js +0 -1492
- package/blocks/release-visuals/versions/1.0.1/package.js +0 -1517
- package/blocks/release-visuals/versions/1.0.2/package.js +0 -1500
- package/blocks/screen-recorder/package.js +0 -1
- package/blocks/screen-recorder/versions/1.0.0/package.js +0 -223
- package/blocks/screen-recorder/versions/1.0.1/package.js +0 -223
- package/blocks/short-form-finish/package.js +0 -1
- package/blocks/short-form-finish/versions/1.0.0/package.js +0 -1043
- package/blocks/short-form-finish/versions/1.0.1/package.js +0 -1043
- package/blocks/short-form-finish/versions/1.0.2/package.js +0 -1043
- package/blocks/short-form-finish/versions/1.0.3/package.js +0 -1026
- package/examples/saas-product-launch-reviewed-source.stillmade.json +0 -383
- package/examples/saas-product-launch.stillmade.json +0 -304
- package/packages/block-platform/dealer-inventory-video-project-type.js +0 -38
- package/packages/block-platform/first-revenue-project-types.js +0 -46
- package/packages/block-platform/fixture-starters.js +0 -13
- package/packages/block-platform/launch-project-source.js +0 -10
- package/packages/block-platform/launch-project.js +0 -30
- package/packages/block-platform/motion-project-types.js +0 -55
- package/packages/block-platform/motion-starters.js +0 -14
- package/packages/block-platform/product-ad-variants-project-type.js +0 -30
- package/packages/block-platform/product-launch-updated.js +0 -90
- package/packages/block-platform/revenue-review-project-types.js +0 -36
- package/packages/block-platform/revenue-review-starters.js +0 -13
- package/packages/block-platform/script-writer.js +0 -29
- package/packages/block-platform/website-source-block.js +0 -14
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {default} from './versions/1.0.1/package.js';
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
// Generated from the owned versioned source by build-package.mjs.
|
|
2
|
-
export default {
|
|
3
|
-
"manifest": {
|
|
4
|
-
"schemaVersion": 1,
|
|
5
|
-
"sdkVersion": "0.1.0",
|
|
6
|
-
"id": "stillmade.narration",
|
|
7
|
-
"version": "1.0.0",
|
|
8
|
-
"name": "Narration",
|
|
9
|
-
"description": "Turn the facts Chat gathered into spoken words, hear them in a catalog voice or use your own recording, and hand the script and the voiceover on.",
|
|
10
|
-
"kind": "workspace",
|
|
11
|
-
"runtime": "module",
|
|
12
|
-
"license": "MIT",
|
|
13
|
-
"inputs": {
|
|
14
|
-
"brief": {
|
|
15
|
-
"type": "brief",
|
|
16
|
-
"primary": true,
|
|
17
|
-
"required": false,
|
|
18
|
-
"semantic": "project_brief",
|
|
19
|
-
"description": "The facts the voice speaks from: what is shown, its details, who it is for, how long, and how to reach you. Chat fills this in."
|
|
20
|
-
},
|
|
21
|
-
"script": {
|
|
22
|
-
"type": "script",
|
|
23
|
-
"required": false,
|
|
24
|
-
"semantic": "narration_script",
|
|
25
|
-
"sources": [
|
|
26
|
-
"upstream",
|
|
27
|
-
"user"
|
|
28
|
-
],
|
|
29
|
-
"description": "The words the voice says. Written from the facts when empty; change them before the voice runs."
|
|
30
|
-
},
|
|
31
|
-
"tone": {
|
|
32
|
-
"type": "text",
|
|
33
|
-
"required": false,
|
|
34
|
-
"default": "warm",
|
|
35
|
-
"semantic": "narration_tone",
|
|
36
|
-
"description": "How it sounds: warm, friendly, no-nonsense or luxury."
|
|
37
|
-
},
|
|
38
|
-
"rules": {
|
|
39
|
-
"type": "text",
|
|
40
|
-
"required": false,
|
|
41
|
-
"semantic": "writing_rules",
|
|
42
|
-
"description": "Your own writing rules, such as short sentences or no exclamation marks."
|
|
43
|
-
},
|
|
44
|
-
"voice": {
|
|
45
|
-
"type": "text",
|
|
46
|
-
"required": false,
|
|
47
|
-
"default": "onyx",
|
|
48
|
-
"semantic": "speech_voice",
|
|
49
|
-
"description": "The catalog voice that reads the script."
|
|
50
|
-
},
|
|
51
|
-
"speed": {
|
|
52
|
-
"type": "number",
|
|
53
|
-
"required": false,
|
|
54
|
-
"default": 1,
|
|
55
|
-
"min": 0.5,
|
|
56
|
-
"max": 2,
|
|
57
|
-
"semantic": "speech_speed",
|
|
58
|
-
"description": "How fast the voice reads, from half to double speed."
|
|
59
|
-
},
|
|
60
|
-
"take": {
|
|
61
|
-
"type": "audio",
|
|
62
|
-
"required": false,
|
|
63
|
-
"semantic": "narration_audio",
|
|
64
|
-
"sources": [
|
|
65
|
-
"user"
|
|
66
|
-
],
|
|
67
|
-
"description": "Your own recording or audio file, used instead of a generated voice."
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
"outputs": {
|
|
71
|
-
"script": {
|
|
72
|
-
"type": "script",
|
|
73
|
-
"required": false,
|
|
74
|
-
"semantic": "narration_script",
|
|
75
|
-
"role": "narration_script",
|
|
76
|
-
"description": "The narration, for shots, captions and the voice."
|
|
77
|
-
},
|
|
78
|
-
"audio": {
|
|
79
|
-
"type": "audio",
|
|
80
|
-
"primary": true,
|
|
81
|
-
"required": false,
|
|
82
|
-
"semantic": "narration_audio",
|
|
83
|
-
"role": "narration_audio",
|
|
84
|
-
"description": "The voiceover."
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
"permissions": {
|
|
88
|
-
"project": [],
|
|
89
|
-
"capabilities": [
|
|
90
|
-
"text.generate",
|
|
91
|
-
"audio.speech"
|
|
92
|
-
],
|
|
93
|
-
"network": [],
|
|
94
|
-
"filesystem": [],
|
|
95
|
-
"secrets": []
|
|
96
|
-
},
|
|
97
|
-
"ui": [],
|
|
98
|
-
"features": [
|
|
99
|
-
{
|
|
100
|
-
"id": "write-script",
|
|
101
|
-
"label": "Write the script",
|
|
102
|
-
"does": "Writes spoken narration from the facts in the brief, in the tone you pick, using only those facts.",
|
|
103
|
-
"asks": [
|
|
104
|
-
"write a script for my listing video",
|
|
105
|
-
"turn these car details into a 30 second narration",
|
|
106
|
-
"write the words for a product video"
|
|
107
|
-
],
|
|
108
|
-
"inputs": [
|
|
109
|
-
"brief",
|
|
110
|
-
"tone",
|
|
111
|
-
"rules"
|
|
112
|
-
],
|
|
113
|
-
"outputs": [
|
|
114
|
-
"script"
|
|
115
|
-
]
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
"id": "make-voiceover",
|
|
119
|
-
"label": "Make the voiceover",
|
|
120
|
-
"does": "Reads the script in a catalog voice at the speed you choose.",
|
|
121
|
-
"asks": [
|
|
122
|
-
"make a voiceover for this script",
|
|
123
|
-
"read this in a deep voice",
|
|
124
|
-
"narrate my video"
|
|
125
|
-
],
|
|
126
|
-
"inputs": [
|
|
127
|
-
"script",
|
|
128
|
-
"voice",
|
|
129
|
-
"speed"
|
|
130
|
-
],
|
|
131
|
-
"outputs": [
|
|
132
|
-
"audio"
|
|
133
|
-
]
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
"id": "own-recording",
|
|
137
|
-
"label": "Use your own recording",
|
|
138
|
-
"does": "Uses an audio file or recording you supply as the voiceover, with the script kept beside it.",
|
|
139
|
-
"asks": [
|
|
140
|
-
"use my own voice recording",
|
|
141
|
-
"I recorded the narration myself"
|
|
142
|
-
],
|
|
143
|
-
"inputs": [
|
|
144
|
-
"take"
|
|
145
|
-
],
|
|
146
|
-
"outputs": [
|
|
147
|
-
"audio"
|
|
148
|
-
]
|
|
149
|
-
}
|
|
150
|
-
],
|
|
151
|
-
"platforms": {
|
|
152
|
-
"phone": {
|
|
153
|
-
"supported": true
|
|
154
|
-
},
|
|
155
|
-
"browser": {
|
|
156
|
-
"supported": true
|
|
157
|
-
},
|
|
158
|
-
"desktop": {
|
|
159
|
-
"supported": true
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
"provenance": {
|
|
163
|
-
"notice": "MIT License\n\nCopyright (c) <year> <copyright holders>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
"module": {
|
|
167
|
-
"schemaVersion": 1,
|
|
168
|
-
"entry": "main.js",
|
|
169
|
-
"files": {
|
|
170
|
-
"main.js": {
|
|
171
|
-
"sha256": "a5eed2032d2ba5365d2dbb5b4bb4d483f6e7aee8604a004b69cdcacca1d20b32",
|
|
172
|
-
"bytes": 4703,
|
|
173
|
-
"mediaType": "text/javascript"
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
"moduleSource": {
|
|
178
|
-
"main.js": "// Narration: one job in two hosted steps. With no script, the facts in the\n// brief become spoken words (text.generate) and the run ends so the person can\n// read and change them. With a script, the words become a voice (audio.speech),\n// or the person's own take is used as it is. Every call is quoted and approved\n// by StillMade; this code never sees a provider, a key or a price.\nconst WORDS_PER_MINUTE = 150;\nconst SPEECH_LIMIT = 3800;\nconst VOICE = /^[a-z][a-z0-9_-]{0,63}$/;\nconst TONES = {\n warm: 'Warm and welcoming, like a trusted local who wants to help.',\n friendly: 'Upbeat and friendly, in plain words and short sentences.',\n 'no-nonsense': 'Direct and factual, with no filler and no hype.',\n luxury: 'Calm, polished and unhurried; confident, never pushy.',\n};\n\nconst text = value => typeof value === 'string' ? value.trim() : '';\n// A brief or script value is {schemaVersion: 1, text}; older briefs carry a topic.\nfunction words(value) {\n if (typeof value === 'string') return value.trim();\n if (!value || typeof value !== 'object') return '';\n for (const key of ['text', 'topic', 'summary']) if (text(value[key])) return text(value[key]);\n return Object.entries(value).filter(([key, item]) => key !== 'schemaVersion' && ['string', 'number'].includes(typeof item)).map(([key, item]) => `${key}: ${item}`).join('\\n');\n}\nfunction targetWords(brief) {\n const seconds = /\\b(\\d{1,3})\\s*(?:s|sec|secs|second|seconds)\\b/i.exec(brief);\n if (seconds) return Math.max(15, Math.round(Number(seconds[1]) / 60 * WORDS_PER_MINUTE));\n const minutes = /\\b(\\d{1,2}|one|two|three)\\s*(?:min|mins|minute|minutes)\\b/i.exec(brief);\n if (minutes) return Math.min(560, ({ one: 1, two: 2, three: 3 }[minutes[1].toLowerCase()] || Number(minutes[1])) * WORDS_PER_MINUTE);\n return 75;\n}\nfunction toneGuidance(tone, rules) {\n const preset = TONES[String(tone || 'warm').toLowerCase()];\n return [preset || text(tone) || TONES.warm, text(rules)].filter(Boolean).join(' ');\n}\n// What the model returns is the script and nothing else: no fences, labels or quotes.\nfunction cleanScript(reply) {\n let script = text(reply).replace(/^```[a-z]*\\s*|\\s*```$/g, '').trim();\n script = script.replace(/^(?:script|narration|voiceover)\\s*:\\s*/i, '').trim();\n if (/^[\"“].*[\"”]$/s.test(script)) script = script.slice(1, -1).trim();\n return script.replace(/[ \\t]+\\n/g, '\\n').replace(/\\n{3,}/g, '\\n\\n');\n}\nconst SYSTEM = 'You write spoken narration for short videos. Use only the facts you are given: never add a price, mileage, phone number, address, web address, award, inspection, warranty or history that is not in them, and never claim to have checked anything. Write for the ear: plain sentences that read well aloud, no headings, lists, stage directions, speaker labels, emojis, markdown or quotation marks. Return the narration only.';\n\nexport default async function run(input, stillmade) {\n const brief = words(input.brief), script = words(input.script), rules = text(input.rules);\n if (!script) {\n if (!brief) throw new Error('Add the facts the voice should speak from, or type the script.');\n stillmade.progress(0.2, 'Writing the script');\n const count = targetWords(brief);\n const prompt = `Facts (use only these):\\n${brief}\\n\\nWrite the spoken narration for a short video about this. Tone: ${toneGuidance(input.tone, rules)} Length: about ${count} words (${Math.round(count / WORDS_PER_MINUTE * 60)} seconds at ${WORDS_PER_MINUTE} words a minute). End with the contact or next step from the facts when there is one.`;\n const reply = await stillmade.hosted.run('text.generate', { prompt, system: SYSTEM, maxTokens: Math.min(1600, Math.max(300, count * 4)) });\n const written = cleanScript(reply);\n if (!written) throw new Error('No script came back. Add more facts and write again.');\n stillmade.progress(1, 'Script written');\n return { script: { schemaVersion: 1, text: written } };\n }\n const result = { script: { schemaVersion: 1, text: script } };\n if (input.take) {\n stillmade.log('Using the supplied recording.');\n return { ...result, audio: input.take };\n }\n if (script.length > SPEECH_LIMIT) throw new Error(`The script is longer than one voice take (${SPEECH_LIMIT.toLocaleString('en-US')} characters). Shorten it, or split it.`);\n const voice = VOICE.test(String(input.voice || '')) ? String(input.voice) : 'onyx';\n const speed = Number.isFinite(input.speed) && input.speed >= 0.5 && input.speed <= 2 ? input.speed : 1;\n stillmade.progress(0.4, 'Making the voice');\n const audio = await stillmade.hosted.run('audio.speech', { text: script, settings: { voice, speed } });\n stillmade.progress(1, 'Voiceover ready');\n return { ...result, audio };\n}\n"
|
|
179
|
-
},
|
|
180
|
-
"view": {
|
|
181
|
-
"runtime": "frame",
|
|
182
|
-
"html": "<main class=\"sm-block nr\">\n <section class=\"sm-stage nr-stage\" id=\"stage\" aria-live=\"polite\">\n <div class=\"sm-empty\" id=\"empty\">\n <span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span>\n <p>No narration yet</p>\n </div>\n <div class=\"nr-take\" id=\"take\" hidden>\n <audio id=\"player\" controls preload=\"metadata\"></audio>\n <p class=\"nr-meta\"><span id=\"length\"></span><span id=\"from\"></span></p>\n </div>\n </section>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"facts\">Facts</label><details class=\"sm-hint\"><summary aria-label=\"About the facts\" title=\"About the facts\"></summary><p>What the voice may say: the thing, its details, who it is for, how long, and how to reach you. Chat fills this from your conversation; change it here.</p></details><span class=\"sm-status\" id=\"facts-state\"></span></span>\n <textarea id=\"facts\" rows=\"3\" maxlength=\"6000\" data-stillmade-share=\"facts\" placeholder=\"What it is, its details, who it is for, and how to reach you.\"></textarea>\n </div>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"words\">Script</label><span class=\"sm-status\" id=\"words-state\"></span></span>\n <textarea id=\"words\" rows=\"7\" maxlength=\"3800\" data-stillmade-share=\"words\" placeholder=\"Written from the facts, or type what the voice says.\"></textarea>\n </div>\n <fieldset class=\"sm-choices\" data-compact id=\"voice\" data-stillmade-share=\"voice\">\n <legend>Voice</legend>\n <label><input type=\"radio\" name=\"voice\" value=\"onyx\" checked><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Onyx <small>deep</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"nova\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Nova <small>bright</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"alloy\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Alloy <small>even</small></span></label>\n </fieldset>\n <div class=\"sm-actions\">\n <button type=\"button\" class=\"sm-primary\" id=\"action\" data-stillmade-action=\"run\">Write script</button>\n <button type=\"button\" class=\"sm-quiet\" id=\"again\" hidden>Write again</button>\n <p class=\"sm-status\" id=\"status\" role=\"status\"></p>\n <details class=\"sm-hint\" id=\"why\" hidden><summary aria-label=\"Why\" title=\"Why\"></summary><p id=\"why-text\"></p></details>\n </div>\n <details class=\"sm-more\" id=\"more\">\n <summary>More</summary>\n <div class=\"sm-settings\">\n <fieldset class=\"sm-choices\" data-compact id=\"tone\" data-stillmade-share=\"tone\">\n <legend>Tone</legend>\n <label><input type=\"radio\" name=\"tone\" value=\"warm\" checked><span>Warm</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"friendly\"><span>Friendly</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"no-nonsense\"><span>No-nonsense</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"luxury\"><span>Luxury</span></label>\n </fieldset>\n <fieldset class=\"sm-choices\" data-segmented id=\"speed\" data-stillmade-share=\"speed\">\n <legend>Speed</legend>\n <label><input type=\"radio\" name=\"speed\" value=\"0.9\"><span>Slower</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1\" checked><span>Normal</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1.15\"><span>Faster</span></label>\n </fieldset>\n <div class=\"sm-field\"><label for=\"rules\">Own rules</label><input id=\"rules\" type=\"text\" maxlength=\"300\" data-stillmade-share=\"rules\" placeholder=\"Short sentences, no exclamation marks\"></div>\n </div>\n </details>\n</main>\n",
|
|
183
|
-
"css": "/* The Block kit (injected by the host) carries the layout; this file only draws\n the voiceover on the stage: the player and its two facts. */\n.nr [hidden] { display: none !important; }\n.nr-stage { display: grid; align-content: center; justify-items: stretch; gap: var(--s-2, 8px); min-height: 180px; }\n/* A voiceover is short: on a wide screen the stage hugs the player instead of stretching down the column. */\n@container (min-width: 860px) { .sm-block:not([data-layout]) > .nr-stage { align-self: start; min-height: 220px; } }\n.nr-stage > .sm-empty { justify-self: center; }\n.nr-take { display: grid; gap: var(--s-2, 8px); width: 100%; }\n.nr-take audio { width: 100%; }\n.nr-meta { display: flex; justify-content: space-between; gap: var(--s-3, 12px); margin: 0; color: var(--fg-muted); font-size: var(--fs-body-s, .875rem); font-variant-numeric: tabular-nums; }\n#voice { --sm-tile: 120px; }\n#voice small { color: var(--fg-subtle); font-size: var(--fs-caption, .8125rem); margin-inline-start: 4px; }\n#tone { --sm-tile: 110px; }\n",
|
|
184
|
-
"javascript": "(()=>{\n\"use strict\";\n// The Narration interface on the Block kit: the voiceover on the stage, the\n// facts and the script on the left, the voice as chips, and one button that\n// names the next step: Write script, then Make voiceover, or Use my recording.\n// The status beside it says the state in a few words; the price is StillMade's.\nconst $ = id => document.getElementById(id);\nconst state = { input: {}, outputs: {}, busy: false, previewUrl: null, previewFor: null, factsFromChat: false };\nconst message = error => String(error && error.message || error || 'Something went wrong.');\nconst say = (id, text) => { $(id).textContent = text || ''; };\nconst trimmed = value => String(value || '').trim();\nconst wordCount = text => trimmed(text) ? trimmed(text).split(/\\s+/).length : 0;\nconst clock = seconds => { const whole = Math.max(0, Math.round(seconds)); return Math.floor(whole / 60) + ':' + String(whole % 60).padStart(2, '0'); };\nconst spoken = text => 'about ' + clock(wordCount(text) / 150 * 60);\nconst picked = name => { const node = document.querySelector('input[name=\"' + name + '\"]:checked'); return node ? node.value : ''; };\nconst pick = (name, value) => { const node = document.querySelector('input[name=\"' + name + '\"][value=\"' + value + '\"]'); if (node) node.checked = true; };\n// A brief or script value is {schemaVersion: 1, text}; older briefs carry a topic.\nconst words = value => typeof value === 'string' ? value : value && typeof value === 'object' ? trimmed(value.text || value.topic || value.summary || '') : '';\nconst facts = () => trimmed($('facts').value);\nconst script = () => trimmed($('words').value);\nconst sameWords = (a, b) => trimmed(a).replace(/\\s+/g, ' ') === trimmed(b).replace(/\\s+/g, ' ');\n\nfunction status(text, tone, why) {\n const node = $('status');\n node.textContent = text || '';\n if (tone) node.dataset.state = tone; else delete node.dataset.state;\n $('why').hidden = !why; $('why').open = false; say('why-text', why || '');\n}\n// A field the host shares is written through its own input event, so the edit is kept.\nfunction setField(id, value) {\n const node = $(id);\n if (node.value === value) return;\n node.value = value;\n node.dispatchEvent(new Event('input', { bubbles: true }));\n node.dispatchEvent(new Event('change', { bubbles: true }));\n}\n\n// ── What the person is about to do ──────────────────────────────────────\nfunction phase() {\n if (!script()) return 'write';\n if (state.input.take) return 'take';\n const audio = state.outputs.audio, made = state.outputs.script;\n return audio && made && sameWords(words(made), script()) ? 'ready' : 'voice';\n}\nfunction render() {\n const p = phase(), count = wordCount(script());\n say('words-state', count ? count + ' words · ' + spoken(script()) : '');\n say('facts-state', facts() ? (state.factsFromChat ? 'From Chat' : '') : 'Needed');\n const labels = { write: 'Write script', voice: 'Make voiceover', ready: 'Make voiceover again', take: 'Use my recording' };\n say('action', labels[p]);\n $('again').hidden = p === 'write';\n $('action').disabled = state.busy || (p === 'write' && !facts());\n $('again').disabled = state.busy;\n if (state.busy) return;\n if (p === 'write') status(facts() ? 'Written from the facts' : 'Add the facts first', facts() ? '' : 'waiting', facts() ? 'The script uses only what the facts say; you can change every word before the voice runs.' : 'Chat usually fills the facts from your conversation. Type them here when it has not.');\n else if (p === 'take') status('Your recording will be used', '', 'The audio you supplied becomes the voiceover; nothing is generated.');\n else if (p === 'voice') status(state.outputs.audio ? 'Script changed, so the voice runs again' : 'Script is done, so only the voice runs', '', 'Only the voice is generated now; the words stay as written.');\n else status('Voiceover ready', 'success', '');\n}\n\n// ── The stage: the voiceover, when there is one ───────────────────────────\nasync function showAudio(reference, from) {\n if (!reference) { $('take').hidden = true; $('empty').hidden = false; return; }\n $('empty').hidden = true; $('take').hidden = false;\n say('from', from || '');\n say('length', Number.isFinite(reference.duration) ? clock(reference.duration) : spoken(script()));\n const key = reference.assetId + '/' + reference.versionId;\n if (state.previewFor === key) return;\n if (state.previewUrl) { try { StillMade.releaseMedia(state.previewUrl); } catch {} state.previewUrl = null; }\n state.previewFor = key;\n try {\n const preview = await StillMade.previewMedia(reference);\n if (state.previewFor !== key) { try { StillMade.releaseMedia(preview.url); } catch {} return; }\n state.previewUrl = preview.url; $('player').src = preview.url;\n } catch (error) {\n if (typeof reference.url === 'string' && /^https?:|^\\//.test(reference.url)) $('player').src = reference.url;\n else status(message(error), 'error', '');\n }\n}\n\n// ── Running ───────────────────────────────────────────────────────────────\nfunction values(forVoice) {\n const value = {};\n if (facts()) value.brief = { schemaVersion: 1, text: facts() };\n value.tone = picked('tone') || 'warm';\n const rules = trimmed($('rules').value); if (rules) value.rules = rules;\n if (forVoice) {\n value.script = { schemaVersion: 1, text: script() };\n value.voice = picked('voice') || 'onyx';\n value.speed = Number(picked('speed')) || 1;\n if (state.input.take) value.take = state.input.take;\n }\n return value;\n}\nasync function act(writeAgain) {\n if (state.busy) return;\n const p = writeAgain ? 'write' : phase();\n state.busy = true; render();\n status(p === 'write' ? 'Writing the script' : p === 'take' ? 'Keeping your recording' : 'Making the voice', 'loading', '');\n try {\n const result = await StillMade.run(values(p !== 'write'));\n state.outputs = { ...state.outputs, ...result };\n if (p === 'write') { setField('words', words(result.script)); await showAudio(null); }\n else await showAudio(result.audio, p === 'take' ? 'Your recording' : voiceName());\n } catch (error) {\n status(message(error), 'error', '');\n state.busy = false; render(); return;\n }\n state.busy = false; render();\n}\nconst voiceName = () => { const node = document.querySelector('input[name=\"voice\"]:checked'); return node ? node.parentElement.textContent.trim().split(/\\s+/)[0] : 'Voice'; };\n$('action').addEventListener('click', () => act(false));\n$('again').addEventListener('click', () => act(true));\nfor (const id of ['facts', 'words', 'rules']) $(id).addEventListener('input', () => { if (id === 'facts') state.factsFromChat = false; render(); });\nfor (const name of ['tone', 'voice', 'speed']) for (const node of document.querySelectorAll('input[name=\"' + name + '\"]')) node.addEventListener('change', render);\n\n// ── What the host gives: the Step's inputs and what was saved before ──────\nStillMade.onInput(input => {\n state.input = input || {};\n fillFromInput();\n if (typeof state.input.tone === 'string' && state.input.tone in { warm: 1, friendly: 1, 'no-nonsense': 1, luxury: 1 }) pick('tone', state.input.tone);\n if (typeof state.input.voice === 'string') pick('voice', state.input.voice);\n render();\n});\n// The host's shared fields start from the saved shared state, which can arrive after the\n// Step's input did and leave a bound field empty; what the input gave is put back then.\nfunction fillFromInput() {\n const brief = words(state.input.brief);\n if (brief && !facts()) { setField('facts', brief); state.factsFromChat = true; }\n const given = words(state.input.script);\n if (given && !script()) setField('words', given);\n}\nfunction adopt(snapshot) {\n const outputs = snapshot && snapshot.outputs || {};\n state.outputs = outputs;\n fillFromInput();\n if (outputs.script && !script()) setField('words', words(outputs.script));\n if (outputs.audio) showAudio(outputs.audio, state.input.take ? 'Your recording' : voiceName()); else showAudio(null);\n render();\n}\nif (typeof StillMade.onShared === 'function') StillMade.onShared(adopt);\ntry { if (typeof StillMade.getShared === 'function') { const shared = StillMade.getShared(); if (shared && typeof shared.then === 'function') shared.then(adopt, () => {}); else adopt(shared); } } catch {}\nrender();\n\n})();\n"
|
|
185
|
-
},
|
|
186
|
-
"tests": [
|
|
187
|
-
{
|
|
188
|
-
"name": "Uses the supplied recording as the voiceover and keeps the script beside it",
|
|
189
|
-
"input": {
|
|
190
|
-
"script": {
|
|
191
|
-
"schemaVersion": 1,
|
|
192
|
-
"text": "Looking for an SUV that does it all? Come see this 2021 Toyota RAV4 XLE at Sunrise Motors."
|
|
193
|
-
},
|
|
194
|
-
"take": {
|
|
195
|
-
"kind": "audio",
|
|
196
|
-
"assetId": "own-take-asset",
|
|
197
|
-
"versionId": "own-take-v1",
|
|
198
|
-
"url": "https://media.example/own-take.mp3",
|
|
199
|
-
"mimeType": "audio/mpeg",
|
|
200
|
-
"duration": 34.2,
|
|
201
|
-
"bytes": 548000
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
"expected": {
|
|
205
|
-
"script": {
|
|
206
|
-
"schemaVersion": 1,
|
|
207
|
-
"text": "Looking for an SUV that does it all? Come see this 2021 Toyota RAV4 XLE at Sunrise Motors."
|
|
208
|
-
},
|
|
209
|
-
"audio": {
|
|
210
|
-
"kind": "audio",
|
|
211
|
-
"assetId": "own-take-asset",
|
|
212
|
-
"versionId": "own-take-v1",
|
|
213
|
-
"url": "https://media.example/own-take.mp3",
|
|
214
|
-
"mimeType": "audio/mpeg",
|
|
215
|
-
"duration": 34.2,
|
|
216
|
-
"bytes": 548000
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
]
|
|
221
|
-
};
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
// Generated from the owned versioned source by build-package.mjs.
|
|
2
|
-
export default {
|
|
3
|
-
"manifest": {
|
|
4
|
-
"schemaVersion": 1,
|
|
5
|
-
"sdkVersion": "0.1.0",
|
|
6
|
-
"id": "stillmade.narration",
|
|
7
|
-
"version": "1.0.1",
|
|
8
|
-
"name": "Narration",
|
|
9
|
-
"description": "Turn the facts Chat gathered into spoken words, hear them in a catalog voice or use your own recording, and hand the script and the voiceover on.",
|
|
10
|
-
"kind": "workspace",
|
|
11
|
-
"runtime": "module",
|
|
12
|
-
"license": "MIT",
|
|
13
|
-
"inputs": {
|
|
14
|
-
"brief": {
|
|
15
|
-
"type": "brief",
|
|
16
|
-
"primary": true,
|
|
17
|
-
"required": false,
|
|
18
|
-
"semantic": "project_brief",
|
|
19
|
-
"description": "The facts the voice speaks from: what is shown, its details, who it is for, how long, and how to reach you. Chat fills this in."
|
|
20
|
-
},
|
|
21
|
-
"script": {
|
|
22
|
-
"type": "script",
|
|
23
|
-
"required": false,
|
|
24
|
-
"semantic": "narration_script",
|
|
25
|
-
"sources": [
|
|
26
|
-
"upstream",
|
|
27
|
-
"user"
|
|
28
|
-
],
|
|
29
|
-
"description": "The words the voice says. Written from the facts when empty; change them before the voice runs."
|
|
30
|
-
},
|
|
31
|
-
"tone": {
|
|
32
|
-
"type": "text",
|
|
33
|
-
"required": false,
|
|
34
|
-
"default": "warm",
|
|
35
|
-
"semantic": "narration_tone",
|
|
36
|
-
"description": "How it sounds: warm, friendly, no-nonsense or luxury."
|
|
37
|
-
},
|
|
38
|
-
"rules": {
|
|
39
|
-
"type": "text",
|
|
40
|
-
"required": false,
|
|
41
|
-
"semantic": "writing_rules",
|
|
42
|
-
"description": "Your own writing rules, such as short sentences or no exclamation marks."
|
|
43
|
-
},
|
|
44
|
-
"voice": {
|
|
45
|
-
"type": "text",
|
|
46
|
-
"required": false,
|
|
47
|
-
"default": "onyx",
|
|
48
|
-
"semantic": "speech_voice",
|
|
49
|
-
"description": "The catalog voice that reads the script."
|
|
50
|
-
},
|
|
51
|
-
"speed": {
|
|
52
|
-
"type": "number",
|
|
53
|
-
"required": false,
|
|
54
|
-
"default": 1,
|
|
55
|
-
"min": 0.5,
|
|
56
|
-
"max": 2,
|
|
57
|
-
"semantic": "speech_speed",
|
|
58
|
-
"description": "How fast the voice reads, from half to double speed."
|
|
59
|
-
},
|
|
60
|
-
"take": {
|
|
61
|
-
"type": "audio",
|
|
62
|
-
"required": false,
|
|
63
|
-
"semantic": "narration_audio",
|
|
64
|
-
"sources": [
|
|
65
|
-
"user"
|
|
66
|
-
],
|
|
67
|
-
"description": "Your own recording or audio file, used instead of a generated voice."
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
"outputs": {
|
|
71
|
-
"script": {
|
|
72
|
-
"type": "script",
|
|
73
|
-
"required": false,
|
|
74
|
-
"semantic": "narration_script",
|
|
75
|
-
"role": "narration_script",
|
|
76
|
-
"description": "The narration, for shots, captions and the voice."
|
|
77
|
-
},
|
|
78
|
-
"audio": {
|
|
79
|
-
"type": "audio",
|
|
80
|
-
"primary": true,
|
|
81
|
-
"required": false,
|
|
82
|
-
"semantic": "narration_audio",
|
|
83
|
-
"role": "narration_audio",
|
|
84
|
-
"description": "The voiceover."
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
"permissions": {
|
|
88
|
-
"project": [],
|
|
89
|
-
"capabilities": [
|
|
90
|
-
"text.generate",
|
|
91
|
-
"audio.speech"
|
|
92
|
-
],
|
|
93
|
-
"network": [],
|
|
94
|
-
"filesystem": [],
|
|
95
|
-
"secrets": []
|
|
96
|
-
},
|
|
97
|
-
"ui": [],
|
|
98
|
-
"features": [
|
|
99
|
-
{
|
|
100
|
-
"id": "write-script",
|
|
101
|
-
"label": "Write the script",
|
|
102
|
-
"does": "Writes spoken narration from the facts in the brief, in the tone you pick, using only those facts.",
|
|
103
|
-
"asks": [
|
|
104
|
-
"write a script for my listing video",
|
|
105
|
-
"turn these car details into a 30 second narration",
|
|
106
|
-
"write the words for a product video"
|
|
107
|
-
],
|
|
108
|
-
"inputs": [
|
|
109
|
-
"brief",
|
|
110
|
-
"tone",
|
|
111
|
-
"rules"
|
|
112
|
-
],
|
|
113
|
-
"outputs": [
|
|
114
|
-
"script"
|
|
115
|
-
]
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
"id": "make-voiceover",
|
|
119
|
-
"label": "Make the voiceover",
|
|
120
|
-
"does": "Reads the script in a catalog voice at the speed you choose.",
|
|
121
|
-
"asks": [
|
|
122
|
-
"make a voiceover for this script",
|
|
123
|
-
"read this in a deep voice",
|
|
124
|
-
"narrate my video"
|
|
125
|
-
],
|
|
126
|
-
"inputs": [
|
|
127
|
-
"script",
|
|
128
|
-
"voice",
|
|
129
|
-
"speed"
|
|
130
|
-
],
|
|
131
|
-
"outputs": [
|
|
132
|
-
"audio"
|
|
133
|
-
]
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
"id": "own-recording",
|
|
137
|
-
"label": "Use your own recording",
|
|
138
|
-
"does": "Uses an audio file or recording you supply as the voiceover, with the script kept beside it.",
|
|
139
|
-
"asks": [
|
|
140
|
-
"use my own voice recording",
|
|
141
|
-
"I recorded the narration myself"
|
|
142
|
-
],
|
|
143
|
-
"inputs": [
|
|
144
|
-
"take"
|
|
145
|
-
],
|
|
146
|
-
"outputs": [
|
|
147
|
-
"audio"
|
|
148
|
-
]
|
|
149
|
-
}
|
|
150
|
-
],
|
|
151
|
-
"platforms": {
|
|
152
|
-
"phone": {
|
|
153
|
-
"supported": true
|
|
154
|
-
},
|
|
155
|
-
"browser": {
|
|
156
|
-
"supported": true
|
|
157
|
-
},
|
|
158
|
-
"desktop": {
|
|
159
|
-
"supported": true
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
"provenance": {
|
|
163
|
-
"notice": "MIT License\n\nCopyright (c) <year> <copyright holders>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
"module": {
|
|
167
|
-
"schemaVersion": 1,
|
|
168
|
-
"entry": "main.js",
|
|
169
|
-
"files": {
|
|
170
|
-
"main.js": {
|
|
171
|
-
"sha256": "a5eed2032d2ba5365d2dbb5b4bb4d483f6e7aee8604a004b69cdcacca1d20b32",
|
|
172
|
-
"bytes": 4703,
|
|
173
|
-
"mediaType": "text/javascript"
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
"moduleSource": {
|
|
178
|
-
"main.js": "// Narration: one job in two hosted steps. With no script, the facts in the\n// brief become spoken words (text.generate) and the run ends so the person can\n// read and change them. With a script, the words become a voice (audio.speech),\n// or the person's own take is used as it is. Every call is quoted and approved\n// by StillMade; this code never sees a provider, a key or a price.\nconst WORDS_PER_MINUTE = 150;\nconst SPEECH_LIMIT = 3800;\nconst VOICE = /^[a-z][a-z0-9_-]{0,63}$/;\nconst TONES = {\n warm: 'Warm and welcoming, like a trusted local who wants to help.',\n friendly: 'Upbeat and friendly, in plain words and short sentences.',\n 'no-nonsense': 'Direct and factual, with no filler and no hype.',\n luxury: 'Calm, polished and unhurried; confident, never pushy.',\n};\n\nconst text = value => typeof value === 'string' ? value.trim() : '';\n// A brief or script value is {schemaVersion: 1, text}; older briefs carry a topic.\nfunction words(value) {\n if (typeof value === 'string') return value.trim();\n if (!value || typeof value !== 'object') return '';\n for (const key of ['text', 'topic', 'summary']) if (text(value[key])) return text(value[key]);\n return Object.entries(value).filter(([key, item]) => key !== 'schemaVersion' && ['string', 'number'].includes(typeof item)).map(([key, item]) => `${key}: ${item}`).join('\\n');\n}\nfunction targetWords(brief) {\n const seconds = /\\b(\\d{1,3})\\s*(?:s|sec|secs|second|seconds)\\b/i.exec(brief);\n if (seconds) return Math.max(15, Math.round(Number(seconds[1]) / 60 * WORDS_PER_MINUTE));\n const minutes = /\\b(\\d{1,2}|one|two|three)\\s*(?:min|mins|minute|minutes)\\b/i.exec(brief);\n if (minutes) return Math.min(560, ({ one: 1, two: 2, three: 3 }[minutes[1].toLowerCase()] || Number(minutes[1])) * WORDS_PER_MINUTE);\n return 75;\n}\nfunction toneGuidance(tone, rules) {\n const preset = TONES[String(tone || 'warm').toLowerCase()];\n return [preset || text(tone) || TONES.warm, text(rules)].filter(Boolean).join(' ');\n}\n// What the model returns is the script and nothing else: no fences, labels or quotes.\nfunction cleanScript(reply) {\n let script = text(reply).replace(/^```[a-z]*\\s*|\\s*```$/g, '').trim();\n script = script.replace(/^(?:script|narration|voiceover)\\s*:\\s*/i, '').trim();\n if (/^[\"“].*[\"”]$/s.test(script)) script = script.slice(1, -1).trim();\n return script.replace(/[ \\t]+\\n/g, '\\n').replace(/\\n{3,}/g, '\\n\\n');\n}\nconst SYSTEM = 'You write spoken narration for short videos. Use only the facts you are given: never add a price, mileage, phone number, address, web address, award, inspection, warranty or history that is not in them, and never claim to have checked anything. Write for the ear: plain sentences that read well aloud, no headings, lists, stage directions, speaker labels, emojis, markdown or quotation marks. Return the narration only.';\n\nexport default async function run(input, stillmade) {\n const brief = words(input.brief), script = words(input.script), rules = text(input.rules);\n if (!script) {\n if (!brief) throw new Error('Add the facts the voice should speak from, or type the script.');\n stillmade.progress(0.2, 'Writing the script');\n const count = targetWords(brief);\n const prompt = `Facts (use only these):\\n${brief}\\n\\nWrite the spoken narration for a short video about this. Tone: ${toneGuidance(input.tone, rules)} Length: about ${count} words (${Math.round(count / WORDS_PER_MINUTE * 60)} seconds at ${WORDS_PER_MINUTE} words a minute). End with the contact or next step from the facts when there is one.`;\n const reply = await stillmade.hosted.run('text.generate', { prompt, system: SYSTEM, maxTokens: Math.min(1600, Math.max(300, count * 4)) });\n const written = cleanScript(reply);\n if (!written) throw new Error('No script came back. Add more facts and write again.');\n stillmade.progress(1, 'Script written');\n return { script: { schemaVersion: 1, text: written } };\n }\n const result = { script: { schemaVersion: 1, text: script } };\n if (input.take) {\n stillmade.log('Using the supplied recording.');\n return { ...result, audio: input.take };\n }\n if (script.length > SPEECH_LIMIT) throw new Error(`The script is longer than one voice take (${SPEECH_LIMIT.toLocaleString('en-US')} characters). Shorten it, or split it.`);\n const voice = VOICE.test(String(input.voice || '')) ? String(input.voice) : 'onyx';\n const speed = Number.isFinite(input.speed) && input.speed >= 0.5 && input.speed <= 2 ? input.speed : 1;\n stillmade.progress(0.4, 'Making the voice');\n const audio = await stillmade.hosted.run('audio.speech', { text: script, settings: { voice, speed } });\n stillmade.progress(1, 'Voiceover ready');\n return { ...result, audio };\n}\n"
|
|
179
|
-
},
|
|
180
|
-
"view": {
|
|
181
|
-
"runtime": "frame",
|
|
182
|
-
"html": "<main class=\"sm-block nr\">\n <section class=\"sm-stage nr-stage\" id=\"stage\" aria-live=\"polite\">\n <div class=\"sm-empty\" id=\"empty\">\n <span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span>\n <p>No narration yet</p>\n </div>\n <div class=\"nr-take\" id=\"take\" hidden>\n <audio id=\"player\" controls preload=\"metadata\"></audio>\n <p class=\"nr-meta\"><span id=\"length\"></span><span id=\"from\"></span></p>\n </div>\n </section>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"facts\">Facts</label><details class=\"sm-hint\"><summary aria-label=\"About the facts\" title=\"About the facts\"></summary><p>What the voice may say: the thing, its details, who it is for, how long, and how to reach you. Chat fills this from your conversation; change it here.</p></details><span class=\"sm-status\" id=\"facts-state\"></span></span>\n <textarea id=\"facts\" rows=\"3\" maxlength=\"6000\" data-stillmade-share=\"facts\" placeholder=\"What it is, its details, who it is for, and how to reach you.\"></textarea>\n </div>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"words\">Script</label><span class=\"sm-status\" id=\"words-state\"></span></span>\n <textarea id=\"words\" rows=\"7\" maxlength=\"3800\" data-stillmade-share=\"words\" placeholder=\"Written from the facts, or type what the voice says.\"></textarea>\n </div>\n <fieldset class=\"sm-choices\" data-compact id=\"voice\" data-stillmade-share=\"voice\">\n <legend>Voice</legend>\n <label><input type=\"radio\" name=\"voice\" value=\"onyx\" checked><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Onyx <small>deep</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"nova\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Nova <small>bright</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"alloy\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Alloy <small>even</small></span></label>\n </fieldset>\n <div class=\"sm-actions\">\n <button type=\"button\" class=\"sm-primary\" id=\"action\" data-stillmade-action=\"run\">Write script</button>\n <button type=\"button\" class=\"sm-quiet\" id=\"again\" data-stillmade-action=\"write-again\" hidden>Write again</button>\n <p class=\"sm-status\" id=\"status\" role=\"status\"></p>\n <details class=\"sm-hint\" id=\"why\" hidden><summary aria-label=\"Why\" title=\"Why\"></summary><p id=\"why-text\"></p></details>\n </div>\n <details class=\"sm-more\" id=\"more\">\n <summary>More</summary>\n <div class=\"sm-settings\">\n <fieldset class=\"sm-choices\" data-compact id=\"tone\" data-stillmade-share=\"tone\">\n <legend>Tone</legend>\n <label><input type=\"radio\" name=\"tone\" value=\"warm\" checked><span>Warm</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"friendly\"><span>Friendly</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"no-nonsense\"><span>No-nonsense</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"luxury\"><span>Luxury</span></label>\n </fieldset>\n <fieldset class=\"sm-choices\" data-segmented id=\"speed\" data-stillmade-share=\"speed\">\n <legend>Speed</legend>\n <label><input type=\"radio\" name=\"speed\" value=\"0.9\"><span>Slower</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1\" checked><span>Normal</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1.15\"><span>Faster</span></label>\n </fieldset>\n <div class=\"sm-field\"><label for=\"rules\">Own rules</label><input id=\"rules\" type=\"text\" maxlength=\"300\" data-stillmade-share=\"rules\" placeholder=\"Short sentences, no exclamation marks\"></div>\n </div>\n </details>\n</main>\n",
|
|
183
|
-
"css": "/* The Block kit (injected by the host) carries the layout; this file only draws\n the voiceover on the stage: the player and its two facts. */\n.nr [hidden] { display: none !important; }\n.nr-stage { display: grid; align-content: center; justify-items: stretch; gap: var(--s-2, 8px); min-height: 180px; }\n/* A voiceover is short: on a wide screen the stage hugs the player instead of stretching down the column. */\n@container (min-width: 860px) { .sm-block:not([data-layout]) > .nr-stage { align-self: start; min-height: 220px; } }\n.nr-stage > .sm-empty { justify-self: center; }\n.nr-take { display: grid; gap: var(--s-2, 8px); width: 100%; }\n.nr-take audio { width: 100%; }\n.nr-meta { display: flex; justify-content: space-between; gap: var(--s-3, 12px); margin: 0; color: var(--fg-muted); font-size: var(--fs-body-s, .875rem); font-variant-numeric: tabular-nums; }\n#voice { --sm-tile: 120px; }\n#voice small { color: var(--fg-subtle); font-size: var(--fs-caption, .8125rem); margin-inline-start: 4px; }\n#tone { --sm-tile: 110px; }\n",
|
|
184
|
-
"javascript": "(()=>{\n\"use strict\";\n// The Narration interface on the Block kit: the voiceover on the stage, the\n// facts and the script on the left, the voice as chips, and one button that\n// names the next step: Write script, then Make voiceover, or Use my recording.\n// The status beside it says the state in a few words; the price is StillMade's.\nconst $ = id => document.getElementById(id);\nconst state = { input: {}, outputs: {}, busy: false, previewUrl: null, previewFor: null, factsFromChat: false };\nconst message = error => String(error && error.message || error || 'Something went wrong.');\nconst say = (id, text) => { $(id).textContent = text || ''; };\nconst trimmed = value => String(value || '').trim();\nconst wordCount = text => trimmed(text) ? trimmed(text).split(/\\s+/).length : 0;\nconst clock = seconds => { const whole = Math.max(0, Math.round(seconds)); return Math.floor(whole / 60) + ':' + String(whole % 60).padStart(2, '0'); };\nconst spoken = text => 'about ' + clock(wordCount(text) / 150 * 60);\nconst picked = name => { const node = document.querySelector('input[name=\"' + name + '\"]:checked'); return node ? node.value : ''; };\nconst pick = (name, value) => { const node = document.querySelector('input[name=\"' + name + '\"][value=\"' + value + '\"]'); if (node) node.checked = true; };\n// A brief or script value is {schemaVersion: 1, text}; older briefs carry a topic.\nconst words = value => typeof value === 'string' ? value : value && typeof value === 'object' ? trimmed(value.text || value.topic || value.summary || '') : '';\nconst facts = () => trimmed($('facts').value);\nconst script = () => trimmed($('words').value);\nconst sameWords = (a, b) => trimmed(a).replace(/\\s+/g, ' ') === trimmed(b).replace(/\\s+/g, ' ');\n\nfunction status(text, tone, why) {\n const node = $('status');\n node.textContent = text || '';\n if (tone) node.dataset.state = tone; else delete node.dataset.state;\n $('why').hidden = !why; $('why').open = false; say('why-text', why || '');\n}\n// A field the host shares is written through its own input event, so the edit is kept.\nfunction setField(id, value) {\n const node = $(id);\n if (node.value === value) return;\n node.value = value;\n node.dispatchEvent(new Event('input', { bubbles: true }));\n node.dispatchEvent(new Event('change', { bubbles: true }));\n}\n\n// ── What the person is about to do ──────────────────────────────────────\nfunction phase() {\n if (!script()) return 'write';\n if (state.input.take) return 'take';\n const audio = state.outputs.audio, made = state.outputs.script;\n return audio && made && sameWords(words(made), script()) ? 'ready' : 'voice';\n}\nfunction render() {\n const p = phase(), count = wordCount(script());\n say('words-state', count ? count + ' words · ' + spoken(script()) : '');\n say('facts-state', facts() ? (state.factsFromChat ? 'From Chat' : '') : 'Needed');\n const labels = { write: 'Write script', voice: 'Make voiceover', ready: 'Make voiceover again', take: 'Use my recording' };\n say('action', labels[p]);\n $('again').hidden = p === 'write';\n $('action').disabled = state.busy || (p === 'write' && !facts());\n $('again').disabled = state.busy;\n if (state.busy) return;\n if (p === 'write') status(facts() ? 'Written from the facts' : 'Add the facts first', facts() ? '' : 'waiting', facts() ? 'The script uses only what the facts say; you can change every word before the voice runs.' : 'Chat usually fills the facts from your conversation. Type them here when it has not.');\n else if (p === 'take') status('Your recording will be used', '', 'The audio you supplied becomes the voiceover; nothing is generated.');\n else if (p === 'voice') status(state.outputs.audio ? 'Script changed, so the voice runs again' : 'Script is done, so only the voice runs', '', 'Only the voice is generated now; the words stay as written.');\n else status('Voiceover ready', 'success', '');\n}\n\n// ── The stage: the voiceover, when there is one ───────────────────────────\nasync function showAudio(reference, from) {\n if (!reference) { $('take').hidden = true; $('empty').hidden = false; return; }\n $('empty').hidden = true; $('take').hidden = false;\n say('from', from || '');\n say('length', Number.isFinite(reference.duration) ? clock(reference.duration) : spoken(script()));\n const key = reference.assetId + '/' + reference.versionId;\n if (state.previewFor === key) return;\n if (state.previewUrl) { try { StillMade.releaseMedia(state.previewUrl); } catch {} state.previewUrl = null; }\n state.previewFor = key;\n try {\n const preview = await StillMade.previewMedia(reference);\n if (state.previewFor !== key) { try { StillMade.releaseMedia(preview.url); } catch {} return; }\n state.previewUrl = preview.url; $('player').src = preview.url;\n } catch (error) {\n if (typeof reference.url === 'string' && /^https?:|^\\//.test(reference.url)) $('player').src = reference.url;\n else status(message(error), 'error', '');\n }\n}\n\n// ── Running ───────────────────────────────────────────────────────────────\nfunction values(forVoice) {\n const value = {};\n if (facts()) value.brief = { schemaVersion: 1, text: facts() };\n value.tone = picked('tone') || 'warm';\n const rules = trimmed($('rules').value); if (rules) value.rules = rules;\n if (forVoice) {\n value.script = { schemaVersion: 1, text: script() };\n value.voice = picked('voice') || 'onyx';\n value.speed = Number(picked('speed')) || 1;\n if (state.input.take) value.take = state.input.take;\n }\n return value;\n}\nasync function act(writeAgain) {\n if (state.busy) return;\n const p = writeAgain ? 'write' : phase();\n state.busy = true; render();\n status(p === 'write' ? 'Writing the script' : p === 'take' ? 'Keeping your recording' : 'Making the voice', 'loading', '');\n try {\n const result = await StillMade.run(values(p !== 'write'));\n state.outputs = { ...state.outputs, ...result };\n if (p === 'write') { setField('words', words(result.script)); await showAudio(null); }\n else await showAudio(result.audio, p === 'take' ? 'Your recording' : voiceName());\n } catch (error) {\n status(message(error), 'error', '');\n state.busy = false; render(); return;\n }\n state.busy = false; render();\n}\nconst voiceName = () => { const node = document.querySelector('input[name=\"voice\"]:checked'); return node ? node.parentElement.textContent.trim().split(/\\s+/)[0] : 'Voice'; };\n$('action').addEventListener('click', () => act(false));\n$('again').addEventListener('click', () => act(true));\nfor (const id of ['facts', 'words', 'rules']) $(id).addEventListener('input', () => { if (id === 'facts') state.factsFromChat = false; render(); });\nfor (const name of ['tone', 'voice', 'speed']) for (const node of document.querySelectorAll('input[name=\"' + name + '\"]')) node.addEventListener('change', render);\n\n// ── What the host gives: the Step's inputs and what was saved before ──────\nStillMade.onInput(input => {\n state.input = input || {};\n fillFromInput();\n if (typeof state.input.tone === 'string' && state.input.tone in { warm: 1, friendly: 1, 'no-nonsense': 1, luxury: 1 }) pick('tone', state.input.tone);\n if (typeof state.input.voice === 'string') pick('voice', state.input.voice);\n render();\n});\n// The host's shared fields start from the saved shared state, which can arrive after the\n// Step's input did and leave a bound field empty; what the input gave is put back then.\nfunction fillFromInput() {\n const brief = words(state.input.brief);\n if (brief && !facts()) { setField('facts', brief); state.factsFromChat = true; }\n const given = words(state.input.script);\n if (given && !script()) setField('words', given);\n}\nfunction adopt(snapshot) {\n const outputs = snapshot && snapshot.outputs || {};\n state.outputs = outputs;\n fillFromInput();\n if (outputs.script && !script()) setField('words', words(outputs.script));\n if (outputs.audio) showAudio(outputs.audio, state.input.take ? 'Your recording' : voiceName()); else showAudio(null);\n render();\n}\nif (typeof StillMade.onShared === 'function') StillMade.onShared(adopt);\ntry { if (typeof StillMade.getShared === 'function') { const shared = StillMade.getShared(); if (shared && typeof shared.then === 'function') shared.then(adopt, () => {}); else adopt(shared); } } catch {}\nrender();\n\n})();\n"
|
|
185
|
-
},
|
|
186
|
-
"tests": [
|
|
187
|
-
{
|
|
188
|
-
"name": "Uses the supplied recording as the voiceover and keeps the script beside it",
|
|
189
|
-
"input": {
|
|
190
|
-
"script": {
|
|
191
|
-
"schemaVersion": 1,
|
|
192
|
-
"text": "Looking for an SUV that does it all? Come see this 2021 Toyota RAV4 XLE at Sunrise Motors."
|
|
193
|
-
},
|
|
194
|
-
"take": {
|
|
195
|
-
"kind": "audio",
|
|
196
|
-
"assetId": "own-take-asset",
|
|
197
|
-
"versionId": "own-take-v1",
|
|
198
|
-
"url": "https://media.example/own-take.mp3",
|
|
199
|
-
"mimeType": "audio/mpeg",
|
|
200
|
-
"duration": 34.2,
|
|
201
|
-
"bytes": 548000
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
"expected": {
|
|
205
|
-
"script": {
|
|
206
|
-
"schemaVersion": 1,
|
|
207
|
-
"text": "Looking for an SUV that does it all? Come see this 2021 Toyota RAV4 XLE at Sunrise Motors."
|
|
208
|
-
},
|
|
209
|
-
"audio": {
|
|
210
|
-
"kind": "audio",
|
|
211
|
-
"assetId": "own-take-asset",
|
|
212
|
-
"versionId": "own-take-v1",
|
|
213
|
-
"url": "https://media.example/own-take.mp3",
|
|
214
|
-
"mimeType": "audio/mpeg",
|
|
215
|
-
"duration": 34.2,
|
|
216
|
-
"bytes": 548000
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
]
|
|
221
|
-
};
|