@mevdragon/vidfarm-devcli 0.2.11 → 0.2.13
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/.env.example +37 -1
- package/README.md +27 -183
- package/SKILL.director.md +293 -0
- package/SKILL.platform.md +312 -0
- package/auto-create-hyperframe-templates/AUTO_CREATE_HYPERFRAME_TEMPLATES.md +275 -0
- package/auto-create-hyperframe-templates/extractor-system-prompt.md +128 -0
- package/auto-create-hyperframe-templates/input-manifest.schema.json +56 -0
- package/auto-create-hyperframe-templates/lambda-memory-estimator.md +118 -0
- package/auto-create-hyperframe-templates/production-graph.schema.json +314 -0
- package/auto-create-hyperframe-templates/runbook.md +198 -0
- package/auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs +368 -0
- package/auto-create-hyperframe-templates/template-plan.schema.json +311 -0
- package/auto-create-hyperframe-templates/template-planner-prompt.md +144 -0
- package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +124 -0
- package/auto-create-templates/extractor-system-prompt.md +8 -2
- package/auto-create-templates/template-plan.schema.json +73 -2
- package/auto-create-templates/template-planner-prompt.md +63 -1
- package/dist/src/account-pages-legacy.js +361 -89
- package/dist/src/account-pages-legacy.js.map +1 -0
- package/dist/src/account-pages.js +13 -3
- package/dist/src/account-pages.js.map +1 -0
- package/dist/src/app.js +7060 -946
- package/dist/src/app.js.map +1 -0
- package/dist/src/cli.js +111 -29
- package/dist/src/cli.js.map +1 -0
- package/dist/src/composition-runtime.js +581 -0
- package/dist/src/composition-runtime.js.map +1 -0
- package/dist/src/config.js +69 -11
- package/dist/src/config.js.map +1 -0
- package/dist/src/context.js +280 -21
- package/dist/src/context.js.map +1 -0
- package/dist/src/dev-app-legacy.js +18 -17
- package/dist/src/dev-app-legacy.js.map +1 -0
- package/dist/src/dev-app.js +1 -0
- package/dist/src/dev-app.js.map +1 -0
- package/dist/src/domain.js +2 -1
- package/dist/src/domain.js.map +1 -0
- package/dist/src/editor-chat-history.js +10 -0
- package/dist/src/editor-chat-history.js.map +1 -0
- package/dist/src/editor-chat.js +133 -24
- package/dist/src/editor-chat.js.map +1 -0
- package/dist/src/frontend/flockposter-cache-store.js +10 -2
- package/dist/src/frontend/flockposter-cache-store.js.map +1 -0
- package/dist/src/frontend/homepage-client.js +6 -3
- package/dist/src/frontend/homepage-client.js.map +1 -0
- package/dist/src/frontend/homepage-shared.js +1 -0
- package/dist/src/frontend/homepage-shared.js.map +1 -0
- package/dist/src/frontend/homepage-store.js +1 -0
- package/dist/src/frontend/homepage-store.js.map +1 -0
- package/dist/src/frontend/homepage-view.js +131 -18
- package/dist/src/frontend/homepage-view.js.map +1 -0
- package/dist/src/frontend/page-runtime-client.js +5 -2
- package/dist/src/frontend/page-runtime-client.js.map +1 -0
- package/dist/src/frontend/page-runtime-store.js +1 -0
- package/dist/src/frontend/page-runtime-store.js.map +1 -0
- package/dist/src/frontend/sentry.js +42 -0
- package/dist/src/frontend/sentry.js.map +1 -0
- package/dist/src/frontend/template-editor-chat.js +1719 -213
- package/dist/src/frontend/template-editor-chat.js.map +1 -0
- package/dist/src/help-page.js +333 -0
- package/dist/src/help-page.js.map +1 -0
- package/dist/src/homepage.js +169 -47
- package/dist/src/homepage.js.map +1 -0
- package/dist/src/hyperframes/composition.js +180 -0
- package/dist/src/hyperframes/composition.js.map +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/instrument.js +30 -0
- package/dist/src/instrument.js.map +1 -0
- package/dist/src/lib/crypto.js +1 -0
- package/dist/src/lib/crypto.js.map +1 -0
- package/dist/src/lib/dev-log.js +54 -0
- package/dist/src/lib/dev-log.js.map +1 -0
- package/dist/src/lib/display-name.js +11 -0
- package/dist/src/lib/display-name.js.map +1 -0
- package/dist/src/lib/ids.js +21 -1
- package/dist/src/lib/ids.js.map +1 -0
- package/dist/src/lib/images.js +1 -0
- package/dist/src/lib/images.js.map +1 -0
- package/dist/src/lib/json.js +1 -0
- package/dist/src/lib/json.js.map +1 -0
- package/dist/src/lib/template-dna.js +1 -0
- package/dist/src/lib/template-dna.js.map +1 -0
- package/dist/src/lib/template-paths.js +1 -0
- package/dist/src/lib/template-paths.js.map +1 -0
- package/dist/src/lib/template-style-options.js +40 -2
- package/dist/src/lib/template-style-options.js.map +1 -0
- package/dist/src/lib/time.js +1 -0
- package/dist/src/lib/time.js.map +1 -0
- package/dist/src/lib/video-quality-harness.js +60 -0
- package/dist/src/lib/video-quality-harness.js.map +1 -0
- package/dist/src/page-runtime.js +1 -0
- package/dist/src/page-runtime.js.map +1 -0
- package/dist/src/page-shell.js +1501 -49
- package/dist/src/page-shell.js.map +1 -0
- package/dist/src/primitive-context.js +144 -8
- package/dist/src/primitive-context.js.map +1 -0
- package/dist/src/primitive-registry.js +1919 -78
- package/dist/src/primitive-registry.js.map +1 -0
- package/dist/src/primitive-sdk.js +1 -0
- package/dist/src/primitive-sdk.js.map +1 -0
- package/dist/src/primitives/remotion/html-image.js +3 -1
- package/dist/src/primitives/remotion/html-image.js.map +1 -0
- package/dist/src/primitives/remotion/media-slideshow.js +60 -0
- package/dist/src/primitives/remotion/media-slideshow.js.map +1 -0
- package/dist/src/react-page-shell.js +1 -0
- package/dist/src/react-page-shell.js.map +1 -0
- package/dist/src/ready-post-schedule-component.js +1 -0
- package/dist/src/ready-post-schedule-component.js.map +1 -0
- package/dist/src/registry.js +237 -24
- package/dist/src/registry.js.map +1 -0
- package/dist/src/runtime.js +3 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/services/api-call-history.js +4 -0
- package/dist/src/services/api-call-history.js.map +1 -0
- package/dist/src/services/auth.js +40 -50
- package/dist/src/services/auth.js.map +1 -0
- package/dist/src/services/billing.js +20 -44
- package/dist/src/services/billing.js.map +1 -0
- package/dist/src/services/chat-threads.js +10 -6
- package/dist/src/services/chat-threads.js.map +1 -0
- package/dist/src/services/fork-access.js +94 -0
- package/dist/src/services/fork-access.js.map +1 -0
- package/dist/src/services/ghostcut.js +132 -0
- package/dist/src/services/ghostcut.js.map +1 -0
- package/dist/src/services/hyperframes.js +1014 -0
- package/dist/src/services/hyperframes.js.map +1 -0
- package/dist/src/services/job-capacity.js +14 -0
- package/dist/src/services/job-capacity.js.map +1 -0
- package/dist/src/services/job-logs.js +4 -0
- package/dist/src/services/job-logs.js.map +1 -0
- package/dist/src/services/jobs.js +99 -91
- package/dist/src/services/jobs.js.map +1 -0
- package/dist/src/services/media-processing.js +743 -0
- package/dist/src/services/media-processing.js.map +1 -0
- package/dist/src/services/primitive-media-lambda.js +280 -0
- package/dist/src/services/primitive-media-lambda.js.map +1 -0
- package/dist/src/services/providers.js +1560 -178
- package/dist/src/services/providers.js.map +1 -0
- package/dist/src/services/rate-limits.js +3 -2
- package/dist/src/services/rate-limits.js.map +1 -0
- package/dist/src/services/remotion.js +495 -92
- package/dist/src/services/remotion.js.map +1 -0
- package/dist/src/services/serverless-auth.js +374 -0
- package/dist/src/services/serverless-auth.js.map +1 -0
- package/dist/src/services/serverless-jobs.js +1074 -0
- package/dist/src/services/serverless-jobs.js.map +1 -0
- package/dist/src/services/serverless-provider-keys.js +401 -0
- package/dist/src/services/serverless-provider-keys.js.map +1 -0
- package/dist/src/services/serverless-records.js +1086 -0
- package/dist/src/services/serverless-records.js.map +1 -0
- package/dist/src/services/serverless-template-configs.js +67 -0
- package/dist/src/services/serverless-template-configs.js.map +1 -0
- package/dist/src/services/storage.js +170 -35
- package/dist/src/services/storage.js.map +1 -0
- package/dist/src/services/template-certification.js +295 -3
- package/dist/src/services/template-certification.js.map +1 -0
- package/dist/src/services/template-loader.js +45 -1
- package/dist/src/services/template-loader.js.map +1 -0
- package/dist/src/services/template-runtime-bundles.js +217 -0
- package/dist/src/services/template-runtime-bundles.js.map +1 -0
- package/dist/src/services/template-sources.js +452 -87
- package/dist/src/services/template-sources.js.map +1 -0
- package/dist/src/services/video-normalization.js +2 -0
- package/dist/src/services/video-normalization.js.map +1 -0
- package/dist/src/services/webhooks.js +7 -6
- package/dist/src/services/webhooks.js.map +1 -0
- package/dist/src/template-editor-pages.js +5637 -1322
- package/dist/src/template-editor-pages.js.map +1 -0
- package/dist/src/template-editor-shell.js +1376 -170
- package/dist/src/template-editor-shell.js.map +1 -0
- package/dist/src/template-sdk.js +1 -0
- package/dist/src/template-sdk.js.map +1 -0
- package/dist/src/worker.js +11 -226
- package/dist/src/worker.js.map +1 -0
- package/package.json +47 -12
- package/public/assets/homepage-client-app.js +34 -8
- package/public/assets/page-runtime-client-app.js +44 -18
- package/readme.secret.md +89 -0
- package/templates/vidfarm_template_0000/README.md +106 -0
- package/templates/vidfarm_template_0000/SKILL.md +266 -0
- package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
- package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
- package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/composition.json +11 -0
- package/templates/vidfarm_template_0000/src/lib/images.js +221 -0
- package/templates/vidfarm_template_0000/src/remotion/Root.js +33 -0
- package/templates/vidfarm_template_0000/src/remotion/index.js +3 -0
- package/templates/vidfarm_template_0000/src/sdk.js +3 -0
- package/templates/vidfarm_template_0000/src/style-options.js +200 -0
- package/templates/vidfarm_template_0000/src/template-dna.js +9 -0
- package/templates/vidfarm_template_0000/src/template.js +1566 -0
- package/templates/vidfarm_template_0000/template.config.json +21 -0
- package/GETTING_STARTED.developers.md +0 -87
- package/SKILL.developer.md +0 -1177
- package/dist/src/db.js +0 -2007
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Auto-Create HyperFrame Template Runbook
|
|
2
|
+
|
|
3
|
+
Use this runbook when a simple MP4 or slideshow should become an editable HyperFrames composition and production Lambda site.
|
|
4
|
+
|
|
5
|
+
## 1. Create A Run Folder
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
RUN_ID="<format-slug>-$(date -u +%Y%m%dT%H%M%SZ)"
|
|
9
|
+
mkdir -p "auto-create-hyperframe-templates/runs/${RUN_ID}"/{source,frames,audio,transcripts,ocr,motion,extraction,plan,output}
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Copy source media and notes into `source/`.
|
|
13
|
+
|
|
14
|
+
If running through automation, create `manifest.json` matching `input-manifest.schema.json` before copying media:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mode": "autocreate-hyperframe-template",
|
|
19
|
+
"input_path": "drafts/example/preview/source.mp4",
|
|
20
|
+
"output_path": "drafts/example",
|
|
21
|
+
"slug_id": "example",
|
|
22
|
+
"source_mode": "auto",
|
|
23
|
+
"render_defaults": {
|
|
24
|
+
"width": 720,
|
|
25
|
+
"height": 1280,
|
|
26
|
+
"fps": 30
|
|
27
|
+
},
|
|
28
|
+
"lambda": {
|
|
29
|
+
"stack_name": "hyperframes-vidfarm-prod",
|
|
30
|
+
"region": "us-east-1",
|
|
31
|
+
"memory_strategy": "estimate"
|
|
32
|
+
},
|
|
33
|
+
"publish": {
|
|
34
|
+
"prestage_site": false,
|
|
35
|
+
"output_url_policy": "signed_https_final_output_only"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 2. Extract Media Artifacts
|
|
41
|
+
|
|
42
|
+
For MP4:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
ffprobe -v error -show_entries format=duration:stream=codec_type,width,height,avg_frame_rate -of json source/source.mp4 \
|
|
46
|
+
> "auto-create-hyperframe-templates/runs/${RUN_ID}/source/ffprobe.json"
|
|
47
|
+
|
|
48
|
+
ffmpeg -y -i source/source.mp4 \
|
|
49
|
+
-vf "fps=1/2,scale=240:-1,tile=3x4" \
|
|
50
|
+
-frames:v 1 "auto-create-hyperframe-templates/runs/${RUN_ID}/frames/contact-sheet.jpg"
|
|
51
|
+
|
|
52
|
+
ffmpeg -y -i source/source.mp4 \
|
|
53
|
+
-vn -ac 1 -ar 16000 "auto-create-hyperframe-templates/runs/${RUN_ID}/audio/source.wav"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For slideshows, probe each image and create a contact sheet:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ffprobe -v error -show_entries stream=width,height -of json source/slide-01.png \
|
|
60
|
+
> "auto-create-hyperframe-templates/runs/${RUN_ID}/source/slide-01.ffprobe.json"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Add scene detection, OCR, STT, and layout/subject-region detection with whichever local or provider tool is available. The important part is to write concrete timing, text, and bounding boxes into the run folder before planning.
|
|
64
|
+
|
|
65
|
+
## 3. Decide Source Mode
|
|
66
|
+
|
|
67
|
+
Classify the input:
|
|
68
|
+
|
|
69
|
+
- `single_video`: one MP4 drives the full timeline
|
|
70
|
+
- `scene_split_video`: one MP4 should be split into editable timed scene clips
|
|
71
|
+
- `slideshow`: images become timed slides
|
|
72
|
+
- `mixed_media`: video, images, audio, and HTML overlays need explicit composition
|
|
73
|
+
|
|
74
|
+
Record whether source media contains baked text that should be extracted as deterministic captions/HTML overlays.
|
|
75
|
+
|
|
76
|
+
## 4. Generate Production Graph
|
|
77
|
+
|
|
78
|
+
Send `extractor-system-prompt.md`, source notes, artifacts, contact sheet, transcript, OCR, and scene cuts to a multimodal model.
|
|
79
|
+
|
|
80
|
+
Write:
|
|
81
|
+
|
|
82
|
+
```txt
|
|
83
|
+
auto-create-hyperframe-templates/runs/<run_id>/extraction/production_graph.json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Validate against:
|
|
87
|
+
|
|
88
|
+
```txt
|
|
89
|
+
auto-create-hyperframe-templates/production-graph.schema.json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 5. Generate HyperFrame Template Plan
|
|
93
|
+
|
|
94
|
+
Send `template-planner-prompt.md` plus `production_graph.json`.
|
|
95
|
+
|
|
96
|
+
Write:
|
|
97
|
+
|
|
98
|
+
```txt
|
|
99
|
+
auto-create-hyperframe-templates/runs/<run_id>/plan/template_plan.json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Validate against:
|
|
103
|
+
|
|
104
|
+
```txt
|
|
105
|
+
auto-create-hyperframe-templates/template-plan.schema.json
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 6. Write Composition Artifacts
|
|
109
|
+
|
|
110
|
+
Create or update the target source folder:
|
|
111
|
+
|
|
112
|
+
```txt
|
|
113
|
+
drafts/<format_slug>/
|
|
114
|
+
composition.html
|
|
115
|
+
composition.json
|
|
116
|
+
lambda-site.json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For simple MP4/slideshow inputs, the local bridge can do this step directly:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
node auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs drafts/<format_slug>/preview/source.mp4
|
|
123
|
+
node auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs drafts/<format_slug>/preview --stage-site
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Implementation order:
|
|
127
|
+
|
|
128
|
+
1. write `composition.json` from `template_plan.layers`
|
|
129
|
+
2. write `composition.html` from the same layer manifest
|
|
130
|
+
3. include root `data-composition-id`, `data-width`, `data-height`, `data-duration`, and `data-source-folder`
|
|
131
|
+
4. write `lambda-site.json` with stack, region, memory, render defaults, and site upload status
|
|
132
|
+
5. run local editor smoke test
|
|
133
|
+
|
|
134
|
+
## 7. HyperFrames Layer Rules
|
|
135
|
+
|
|
136
|
+
Use these data attributes in `composition.html`:
|
|
137
|
+
|
|
138
|
+
- `data-hf-id`
|
|
139
|
+
- `data-layer-kind`
|
|
140
|
+
- `data-layer-mode`
|
|
141
|
+
- `data-start`
|
|
142
|
+
- `data-duration`
|
|
143
|
+
- `data-track-index`
|
|
144
|
+
- `data-label`
|
|
145
|
+
- `data-media-start` for timed media slices
|
|
146
|
+
- `data-playback-start` for editor/publish compatibility
|
|
147
|
+
- `data-src` when using a timeline proxy
|
|
148
|
+
|
|
149
|
+
For video scene clips, prefer timed real `<video>` elements or a timeline proxy that publish normalization rewrites into a real `<video>`. A preview-only `data-vf-playback-source` may help editor scrubbing, but it must not be the publish source of truth.
|
|
150
|
+
|
|
151
|
+
## 8. Estimate Lambda Memory
|
|
152
|
+
|
|
153
|
+
Use `lambda-memory-estimator.md`.
|
|
154
|
+
|
|
155
|
+
Recommended baseline:
|
|
156
|
+
|
|
157
|
+
- `2048` MB: static images, simple slideshows, minimal HTML, no video decode pressure
|
|
158
|
+
- `3072` MB: default for vertical social video with one decoded video stream and deterministic overlays
|
|
159
|
+
- `4096` MB: 1080p video, multiple streams, heavy captions, large shadows, masks, or long audio
|
|
160
|
+
- `6144+` MB: high-resolution, many active layers, complex filters, or measured failures
|
|
161
|
+
|
|
162
|
+
Write both the recommendation and the reasoning into `composition.json` and `lambda-site.json`.
|
|
163
|
+
|
|
164
|
+
## 9. Stage Production Lambda Site
|
|
165
|
+
|
|
166
|
+
After the project folder is valid, pre-stage it for repeat publishing:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
hyperframes lambda sites create drafts/<format_slug> \
|
|
170
|
+
--stack-name hyperframes-vidfarm-prod \
|
|
171
|
+
--region us-east-1
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
If using the SDK or demo API instead of the CLI, write the returned content-addressed site id into `lambda-site.json`.
|
|
175
|
+
|
|
176
|
+
The frontend should show the final signed output URL after publish. Do not expose S3 Console or Step Functions Console links as user-facing CTAs.
|
|
177
|
+
|
|
178
|
+
## 10. Validate Publish
|
|
179
|
+
|
|
180
|
+
Run one local render and one Lambda publish:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
hyperframes lambda render drafts/<format_slug> \
|
|
184
|
+
--stack-name hyperframes-vidfarm-prod \
|
|
185
|
+
--region us-east-1 \
|
|
186
|
+
--site-id <site_id> \
|
|
187
|
+
--width 720 \
|
|
188
|
+
--height 1280 \
|
|
189
|
+
--wait
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Check:
|
|
193
|
+
|
|
194
|
+
- final output reflects deleted/hidden scene layers
|
|
195
|
+
- caption timing matches the editor timeline
|
|
196
|
+
- blank gaps remain blank when no media layer covers them
|
|
197
|
+
- audio does not silently disappear after scene splitting
|
|
198
|
+
- output URL is accessible without AWS Console login when surfaced in the product
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { cp, copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const harnessRoot = path.resolve(scriptDir, "..");
|
|
9
|
+
const repoRoot = path.resolve(harnessRoot, "..");
|
|
10
|
+
const defaultStackName = "hyperframes-vidfarm-prod";
|
|
11
|
+
const defaultRegion = "us-east-1";
|
|
12
|
+
const memoryBuckets = [2048, 3072, 4096, 6144, 8192, 10240];
|
|
13
|
+
const videoExts = new Set([".mp4", ".mov", ".m4v", ".webm"]);
|
|
14
|
+
const imageExts = new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const stageSite = args.includes("--stage-site");
|
|
18
|
+
const inputArg = args.find((arg) => !arg.startsWith("--"));
|
|
19
|
+
|
|
20
|
+
if (!inputArg) {
|
|
21
|
+
console.error("Usage: node auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs <media-path|input-manifest.json> [--stage-site]");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const manifest = await loadInputManifest(inputArg);
|
|
26
|
+
const outputDir = path.resolve(repoRoot, manifest.output_path);
|
|
27
|
+
const decomposeInput = await prepareInputForDecomposer(manifest);
|
|
28
|
+
|
|
29
|
+
await runCommand(process.execPath, [
|
|
30
|
+
path.join(repoRoot, "demo", "scripts", "decompose-static.mjs"),
|
|
31
|
+
path.relative(repoRoot, decomposeInput)
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const decompositionPath = path.join(outputDir, "composition.json");
|
|
35
|
+
const decomposition = JSON.parse(await readFile(decompositionPath, "utf8"));
|
|
36
|
+
const memoryEstimate = estimateLambdaMemory(decomposition, manifest);
|
|
37
|
+
const lambdaSite = await buildLambdaSiteManifest(manifest, outputDir, memoryEstimate, stageSite);
|
|
38
|
+
|
|
39
|
+
decomposition.schema = "vidfarm.hyperframe-decomposition.v1";
|
|
40
|
+
decomposition.lambda_memory_estimate = memoryEstimate;
|
|
41
|
+
decomposition.lambda_site = lambdaSite;
|
|
42
|
+
|
|
43
|
+
await writeFile(decompositionPath, `${JSON.stringify(decomposition, null, 2)}\n`);
|
|
44
|
+
await writeFile(path.join(outputDir, "lambda-site.json"), `${JSON.stringify(lambdaSite, null, 2)}\n`);
|
|
45
|
+
|
|
46
|
+
console.log(`Wrote ${path.relative(repoRoot, path.join(outputDir, "composition.html"))}`);
|
|
47
|
+
console.log(`Wrote ${path.relative(repoRoot, decompositionPath)}`);
|
|
48
|
+
console.log(`Wrote ${path.relative(repoRoot, path.join(outputDir, "lambda-site.json"))}`);
|
|
49
|
+
console.log(`Lambda memory estimate: ${memoryEstimate.recommended_memory_mb} MB (${memoryEstimate.confidence} confidence)`);
|
|
50
|
+
if (lambdaSite.site_id) {
|
|
51
|
+
console.log(`Lambda site id: ${lambdaSite.site_id}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function loadInputManifest(input) {
|
|
55
|
+
const inputPath = path.resolve(repoRoot, input);
|
|
56
|
+
const inputStats = await stat(inputPath);
|
|
57
|
+
if (inputStats.isFile() && path.extname(inputPath).toLowerCase() === ".json") {
|
|
58
|
+
const parsed = JSON.parse(await readFile(inputPath, "utf8"));
|
|
59
|
+
if (parsed.mode === "autocreate-hyperframe-template") {
|
|
60
|
+
return normalizeManifest(parsed, inputPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const outputPath = inputStats.isDirectory() ? inputPath : path.dirname(inputPath);
|
|
65
|
+
return normalizeManifest({
|
|
66
|
+
mode: "autocreate-hyperframe-template",
|
|
67
|
+
input_path: path.relative(repoRoot, inputPath),
|
|
68
|
+
output_path: path.relative(repoRoot, outputPath),
|
|
69
|
+
slug_id: path.basename(outputPath),
|
|
70
|
+
source_mode: "auto",
|
|
71
|
+
render_defaults: {
|
|
72
|
+
width: 720,
|
|
73
|
+
height: 1280,
|
|
74
|
+
fps: 30
|
|
75
|
+
},
|
|
76
|
+
lambda: {
|
|
77
|
+
stack_name: defaultStackName,
|
|
78
|
+
region: defaultRegion,
|
|
79
|
+
memory_strategy: "estimate"
|
|
80
|
+
},
|
|
81
|
+
publish: {
|
|
82
|
+
prestage_site: false,
|
|
83
|
+
output_url_policy: "signed_https_final_output_only"
|
|
84
|
+
}
|
|
85
|
+
}, inputPath);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeManifest(manifest, manifestPath) {
|
|
89
|
+
if (manifest.mode !== "autocreate-hyperframe-template") {
|
|
90
|
+
throw new Error(`Invalid manifest mode in ${manifestPath}`);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...manifest,
|
|
94
|
+
output_path: manifest.output_path || path.dirname(manifest.input_path),
|
|
95
|
+
source_mode: manifest.source_mode || "auto",
|
|
96
|
+
render_defaults: {
|
|
97
|
+
width: Number(manifest.render_defaults?.width || 720),
|
|
98
|
+
height: Number(manifest.render_defaults?.height || 1280),
|
|
99
|
+
fps: Number(manifest.render_defaults?.fps || 30)
|
|
100
|
+
},
|
|
101
|
+
lambda: {
|
|
102
|
+
stack_name: manifest.lambda?.stack_name || defaultStackName,
|
|
103
|
+
region: manifest.lambda?.region || defaultRegion,
|
|
104
|
+
memory_strategy: manifest.lambda?.memory_strategy || "estimate",
|
|
105
|
+
fixed_memory_mb: manifest.lambda?.fixed_memory_mb
|
|
106
|
+
},
|
|
107
|
+
publish: {
|
|
108
|
+
prestage_site: Boolean(manifest.publish?.prestage_site),
|
|
109
|
+
output_url_policy: "signed_https_final_output_only"
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function prepareInputForDecomposer(manifest) {
|
|
115
|
+
const inputPath = path.resolve(repoRoot, manifest.input_path);
|
|
116
|
+
const outputPath = path.resolve(repoRoot, manifest.output_path);
|
|
117
|
+
const inputStats = await stat(inputPath);
|
|
118
|
+
|
|
119
|
+
if (inputStats.isDirectory()) {
|
|
120
|
+
if (path.resolve(inputPath) !== path.resolve(outputPath)) {
|
|
121
|
+
await mkdir(outputPath, { recursive: true });
|
|
122
|
+
await cp(inputPath, outputPath, { recursive: true, force: true });
|
|
123
|
+
return outputPath;
|
|
124
|
+
}
|
|
125
|
+
return inputPath;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const ext = path.extname(inputPath).toLowerCase();
|
|
129
|
+
if (!videoExts.has(ext) && !imageExts.has(ext)) {
|
|
130
|
+
throw new Error(`Unsupported input media: ${inputPath}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (path.resolve(path.dirname(inputPath)) !== path.resolve(outputPath)) {
|
|
134
|
+
await mkdir(outputPath, { recursive: true });
|
|
135
|
+
const target = path.join(outputPath, path.basename(inputPath));
|
|
136
|
+
await copyFile(inputPath, target);
|
|
137
|
+
return target;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return inputPath;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function estimateLambdaMemory(decomposition, manifest) {
|
|
144
|
+
const renderDefaults = manifest.render_defaults;
|
|
145
|
+
const layers = Array.isArray(decomposition.editor_layers) ? decomposition.editor_layers : [];
|
|
146
|
+
const assets = Array.isArray(decomposition.source_assets) ? decomposition.source_assets : [];
|
|
147
|
+
const firstAsset = assets[0] || {};
|
|
148
|
+
const width = Number(renderDefaults?.width || firstAsset.width || 720);
|
|
149
|
+
const height = Number(renderDefaults?.height || firstAsset.height || 1280);
|
|
150
|
+
const outputPixels = width * height;
|
|
151
|
+
const durationSeconds = layers.reduce((max, layer) => {
|
|
152
|
+
return Math.max(max, Number(layer.start || 0) + Number(layer.duration || 0));
|
|
153
|
+
}, Number(firstAsset.duration || firstAsset.duration_seconds || 0));
|
|
154
|
+
const videoLayers = layers.filter((layer) => layer.type === "video" || layer.kind === "video");
|
|
155
|
+
const audioLayers = layers.filter((layer) => layer.type === "audio" || layer.kind === "audio");
|
|
156
|
+
const textLayers = layers.filter((layer) => ["caption", "text", "shape", "html"].includes(layer.type || layer.kind));
|
|
157
|
+
const uniqueVideoSources = new Set(videoLayers.map((layer) => layer.src || layer.media?.src).filter(Boolean));
|
|
158
|
+
const maxSimultaneousVideos = maxOverlap(videoLayers);
|
|
159
|
+
|
|
160
|
+
const outputPixelScoreMb = outputPixels <= 1_000_000 ? 0 : outputPixels <= 2_100_000 ? 512 : outputPixels <= 4_000_000 ? 1024 : 2048;
|
|
161
|
+
let mediaScoreMb = 0;
|
|
162
|
+
if (videoLayers.length > 0 || assets.some((asset) => asset.type === "video")) {
|
|
163
|
+
mediaScoreMb += 768;
|
|
164
|
+
mediaScoreMb += Math.max(0, maxSimultaneousVideos - 1) * 512;
|
|
165
|
+
mediaScoreMb += Math.max(0, uniqueVideoSources.size - 1) * 256;
|
|
166
|
+
if (assets.some((asset) => Number(asset.width || 0) * Number(asset.height || 0) > outputPixels * 1.75)) {
|
|
167
|
+
mediaScoreMb += 512;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let domCaptionScoreMb = layers.length <= 10 ? 0 : layers.length <= 30 ? 256 : 512;
|
|
172
|
+
if (textLayers.length >= 4) domCaptionScoreMb += 256;
|
|
173
|
+
if (textLayers.some((layer) => String(layer.textBackgroundStyle || layer.background_style || "").includes("highlight"))) {
|
|
174
|
+
domCaptionScoreMb += 256;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let effectScoreMb = 0;
|
|
178
|
+
if (textLayers.some((layer) => ["outline", "radial_halo"].includes(layer.textBackgroundStyle || layer.background_style))) {
|
|
179
|
+
effectScoreMb += 256;
|
|
180
|
+
}
|
|
181
|
+
if (layers.some((layer) => layer.filter || layer.mask || layer.mixBlendMode || layer.blur)) {
|
|
182
|
+
effectScoreMb += 512;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let durationAudioScoreMb = durationSeconds <= 30 ? 0 : durationSeconds <= 60 ? 256 : durationSeconds <= 180 ? 512 : 1024;
|
|
186
|
+
if (audioLayers.length > 0 || assets.some((asset) => asset.type === "audio" || asset.type === "video")) {
|
|
187
|
+
durationAudioScoreMb += 256;
|
|
188
|
+
}
|
|
189
|
+
if (audioLayers.length > 1) durationAudioScoreMb += 256;
|
|
190
|
+
|
|
191
|
+
const rawEstimateMb = 1536 + outputPixelScoreMb + mediaScoreMb + domCaptionScoreMb + effectScoreMb + durationAudioScoreMb;
|
|
192
|
+
let roundedBucketMb = memoryBuckets.find((bucket) => bucket >= rawEstimateMb) || 10240;
|
|
193
|
+
if (videoLayers.length === 0 && !assets.some((asset) => asset.type === "video")) {
|
|
194
|
+
roundedBucketMb = Math.min(roundedBucketMb, 2048);
|
|
195
|
+
}
|
|
196
|
+
if ((videoLayers.length > 0 || assets.some((asset) => asset.type === "video")) && maxSimultaneousVideos <= 1 && roundedBucketMb < 3072) {
|
|
197
|
+
roundedBucketMb = 3072;
|
|
198
|
+
}
|
|
199
|
+
if (roundedBucketMb > 4096 && rawEstimateMb < 6144) {
|
|
200
|
+
roundedBucketMb = 4096;
|
|
201
|
+
}
|
|
202
|
+
const fixedMemory = manifest.lambda?.memory_strategy === "fixed"
|
|
203
|
+
? Number(manifest.lambda?.fixed_memory_mb || 0)
|
|
204
|
+
: 0;
|
|
205
|
+
const recommendedMemoryMb = memoryBuckets.includes(fixedMemory) ? fixedMemory : roundedBucketMb;
|
|
206
|
+
const estimatedBilledSeconds = estimateBilledSeconds(durationSeconds, layers.length, videoLayers.length);
|
|
207
|
+
const estimatedLambdaGbSeconds = (recommendedMemoryMb / 1024) * estimatedBilledSeconds;
|
|
208
|
+
const estimatedCostUsd = estimatedLambdaGbSeconds * 0.0000166667 + 40 * 0.000025;
|
|
209
|
+
|
|
210
|
+
const riskFlags = [];
|
|
211
|
+
if (videoLayers.length > 0 || assets.some((asset) => asset.type === "video")) riskFlags.push("source_video_decode");
|
|
212
|
+
if (maxSimultaneousVideos > 1) riskFlags.push("multiple_simultaneous_video_streams");
|
|
213
|
+
if (textLayers.length >= 4) riskFlags.push("caption_or_html_overlay_density");
|
|
214
|
+
if (audioLayers.length > 0 || assets.some((asset) => asset.type === "video")) riskFlags.push("continuous_audio");
|
|
215
|
+
if (outputPixels > 2_100_000) riskFlags.push("high_output_resolution");
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
recommended_memory_mb: recommendedMemoryMb,
|
|
219
|
+
confidence: riskFlags.length >= 3 ? "medium" : "high",
|
|
220
|
+
complexity_scores: {
|
|
221
|
+
output_pixels: outputPixels,
|
|
222
|
+
output_pixel_score_mb: outputPixelScoreMb,
|
|
223
|
+
max_simultaneous_video_streams: maxSimultaneousVideos,
|
|
224
|
+
unique_video_sources: uniqueVideoSources.size,
|
|
225
|
+
media_score_mb: mediaScoreMb,
|
|
226
|
+
layer_count: layers.length,
|
|
227
|
+
text_layer_count: textLayers.length,
|
|
228
|
+
dom_caption_score_mb: domCaptionScoreMb,
|
|
229
|
+
effect_score_mb: effectScoreMb,
|
|
230
|
+
duration_seconds: Number(durationSeconds.toFixed(3)),
|
|
231
|
+
duration_audio_score_mb: durationAudioScoreMb,
|
|
232
|
+
raw_estimate_mb: rawEstimateMb,
|
|
233
|
+
rounded_bucket_mb: roundedBucketMb,
|
|
234
|
+
estimated_billed_seconds: estimatedBilledSeconds
|
|
235
|
+
},
|
|
236
|
+
risk_flags: riskFlags,
|
|
237
|
+
rationale: rationaleForEstimate(recommendedMemoryMb, maxSimultaneousVideos, videoLayers.length, outputPixels, textLayers.length),
|
|
238
|
+
benchmark_override: null,
|
|
239
|
+
estimated_lambda_gb_seconds: Number(estimatedLambdaGbSeconds.toFixed(3)),
|
|
240
|
+
estimated_cost_usd: Number(estimatedCostUsd.toFixed(4))
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function maxOverlap(layers) {
|
|
245
|
+
if (layers.length === 0) return 0;
|
|
246
|
+
const points = [];
|
|
247
|
+
for (const layer of layers) {
|
|
248
|
+
const start = Number(layer.start || 0);
|
|
249
|
+
const end = start + Number(layer.duration || 0);
|
|
250
|
+
if (end > start) {
|
|
251
|
+
points.push(start + (end - start) / 2);
|
|
252
|
+
points.push(start + 0.001);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return points.reduce((max, point) => {
|
|
256
|
+
const active = layers.filter((layer) => {
|
|
257
|
+
const start = Number(layer.start || 0);
|
|
258
|
+
const end = start + Number(layer.duration || 0);
|
|
259
|
+
return point >= start && point < end;
|
|
260
|
+
}).length;
|
|
261
|
+
return Math.max(max, active);
|
|
262
|
+
}, 0);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function estimateBilledSeconds(durationSeconds, layerCount, videoLayerCount) {
|
|
266
|
+
const base = videoLayerCount > 0 ? 90 : 20;
|
|
267
|
+
const durationFactor = durationSeconds * (videoLayerCount > 0 ? 8 : 4);
|
|
268
|
+
const layerFactor = layerCount * 2;
|
|
269
|
+
return Math.ceil(Math.max(base, durationFactor + layerFactor));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function rationaleForEstimate(memoryMb, maxVideos, videoLayerCount, outputPixels, textLayerCount) {
|
|
273
|
+
if (videoLayerCount === 0) {
|
|
274
|
+
return "No active video decode pressure; simple static/slideshow output fits the 2GB bucket.";
|
|
275
|
+
}
|
|
276
|
+
if (memoryMb === 3072) {
|
|
277
|
+
return "One active social-video stream with deterministic overlays fits the measured 3GB production baseline.";
|
|
278
|
+
}
|
|
279
|
+
if (memoryMb === 4096) {
|
|
280
|
+
return `Raised to 4GB for ${maxVideos} simultaneous video stream(s), ${Math.round(outputPixels / 1_000_000 * 10) / 10}MP output, and ${textLayerCount} text/html layer(s).`;
|
|
281
|
+
}
|
|
282
|
+
return "Raised above 4GB because composition complexity crosses the normal social-video range.";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function buildLambdaSiteManifest(manifest, outputDir, memoryEstimate, shouldStageSite) {
|
|
286
|
+
const relativeOutput = path.relative(repoRoot, outputDir);
|
|
287
|
+
const stackName = manifest.lambda.stack_name || defaultStackName;
|
|
288
|
+
const region = manifest.lambda.region || defaultRegion;
|
|
289
|
+
const siteUploadCommand = `demo/node_modules/.bin/hyperframes lambda sites create ${shellQuote(relativeOutput)} --stack-name ${shellQuote(stackName)} --region ${shellQuote(region)}`;
|
|
290
|
+
const site = {
|
|
291
|
+
schema: "vidfarm.hyperframe.lambda-site.v1",
|
|
292
|
+
stack_name: stackName,
|
|
293
|
+
region,
|
|
294
|
+
site_id: null,
|
|
295
|
+
project_dir: relativeOutput,
|
|
296
|
+
composition_file: "composition.html",
|
|
297
|
+
memory_mb: memoryEstimate.recommended_memory_mb,
|
|
298
|
+
render_defaults: manifest.render_defaults,
|
|
299
|
+
output_url_policy: "signed_https_final_output_only",
|
|
300
|
+
prestage_site_requested: Boolean(manifest.publish?.prestage_site),
|
|
301
|
+
site_upload_command: siteUploadCommand,
|
|
302
|
+
estimated_lambda_gb_seconds: memoryEstimate.estimated_lambda_gb_seconds,
|
|
303
|
+
estimated_cost_usd: memoryEstimate.estimated_cost_usd
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
if (!shouldStageSite) {
|
|
307
|
+
return site;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const hyperframesBin = path.join(repoRoot, "demo", "node_modules", ".bin", "hyperframes");
|
|
312
|
+
const stdout = await runCommand(hyperframesBin, [
|
|
313
|
+
"lambda",
|
|
314
|
+
"sites",
|
|
315
|
+
"create",
|
|
316
|
+
relativeOutput,
|
|
317
|
+
"--stack-name",
|
|
318
|
+
stackName,
|
|
319
|
+
"--region",
|
|
320
|
+
region
|
|
321
|
+
]);
|
|
322
|
+
site.site_id = parseSiteId(stdout);
|
|
323
|
+
site.site_upload_stdout = stdout.trim();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
site.site_upload_error = error instanceof Error ? error.message : String(error);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return site;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseSiteId(stdout) {
|
|
332
|
+
const text = String(stdout || "");
|
|
333
|
+
const labeled = text.match(/Site ID:\s*([A-Za-z0-9_-]+)/i);
|
|
334
|
+
if (labeled) return labeled[1];
|
|
335
|
+
const generic = text.match(/\b([a-f0-9]{12,64}|[A-Za-z0-9_-]{12,64})\b/);
|
|
336
|
+
return generic ? generic[1] : null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function runCommand(command, commandArgs) {
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
const child = spawn(command, commandArgs, {
|
|
342
|
+
cwd: repoRoot,
|
|
343
|
+
env: process.env,
|
|
344
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
345
|
+
});
|
|
346
|
+
let stdout = "";
|
|
347
|
+
let stderr = "";
|
|
348
|
+
child.stdout.on("data", (chunk) => {
|
|
349
|
+
stdout += String(chunk);
|
|
350
|
+
process.stdout.write(chunk);
|
|
351
|
+
});
|
|
352
|
+
child.stderr.on("data", (chunk) => {
|
|
353
|
+
stderr += String(chunk);
|
|
354
|
+
process.stderr.write(chunk);
|
|
355
|
+
});
|
|
356
|
+
child.on("error", reject);
|
|
357
|
+
child.on("close", (code) => {
|
|
358
|
+
if (code === 0) resolve(stdout);
|
|
359
|
+
else reject(new Error(`${command} exited ${code}: ${stderr}`));
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function shellQuote(value) {
|
|
365
|
+
const text = String(value);
|
|
366
|
+
if (/^[A-Za-z0-9_./:=@-]+$/.test(text)) return text;
|
|
367
|
+
return `'${text.replaceAll("'", "'\\''")}'`;
|
|
368
|
+
}
|