@1agh/maude 0.42.0 → 0.44.0
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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +1451 -324
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +568 -24
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +258 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +656 -12
- package/apps/studio/canvas-lib.tsx +637 -10
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +2385 -699
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +18 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +432 -0
- package/apps/studio/client/styles/3-shell-maude.css +376 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +42 -8
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +5572 -21
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +287 -0
- package/apps/studio/footage/schema.ts +662 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +199 -21
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +1039 -22
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +20 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +18 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +154 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +49 -21
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +40 -1
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +149 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +67 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +13 -11
- package/plugins/design/dependencies.json +18 -0
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file footage/schema.ts — Footage-analysis + EDL sidecar data model
|
|
3
|
+
* (feature-footage-analysis-director, Task 1).
|
|
4
|
+
* @scope apps/studio/footage/schema.ts
|
|
5
|
+
* @purpose The single source of truth for the two artifacts the footage
|
|
6
|
+
* "director" pipeline persists (DDR-115 taxonomy: both VERSIONED,
|
|
7
|
+
* alongside the footage they describe — same bucket as `.photo.json`
|
|
8
|
+
* / `.meta.json` / `.annotations.svg`):
|
|
9
|
+
*
|
|
10
|
+
* 1. `assets/<sha8>.footage.json` — a `FootageAnalysis`: what the
|
|
11
|
+
* `footage-analyst` agent SAW when it watched the keyframes of
|
|
12
|
+
* one clip (shots, good-moment ranges, tags, a summary). Per-clip.
|
|
13
|
+
* 2. `<designRoot>/<slug>.edl.json` — an `Edl`: what the
|
|
14
|
+
* `footage-director` agent decided (an ordered beat list — the
|
|
15
|
+
* cut). Per-project-cut.
|
|
16
|
+
*
|
|
17
|
+
* Both are *metadata only* — never pixels, never re-encoded video.
|
|
18
|
+
* The `/design:reel` codegen reads the `Edl` and emits a
|
|
19
|
+
* Timeline-parseable `<TransitionSeries>` video-comp TSX (video-comp
|
|
20
|
+
* SKILL.md is the authoring contract).
|
|
21
|
+
*
|
|
22
|
+
* @invariant DEPENDENCY-FREE. Imported by BOTH the server (`footage-store.ts`,
|
|
23
|
+
* the `/_api/footage` route) and any client/agent surface. MUST NOT
|
|
24
|
+
* import any browser / native / heavy lib. Pure TS types + plain
|
|
25
|
+
* helpers + a hand-rolled structural validator (no Ajv) — the exact
|
|
26
|
+
* house style of `photo/schema.ts`.
|
|
27
|
+
*
|
|
28
|
+
* @time-model Footage time is in **SECONDS** (source-relative), NOT frames: an
|
|
29
|
+
* HTML5 `<video>` (the probe surface) can report `duration` but not a
|
|
30
|
+
* reliable native fps, and the analyst reasons about seconds. The
|
|
31
|
+
* `Edl` composes at a chosen OUTPUT fps and lengths there ARE in
|
|
32
|
+
* frames (Remotion's clock); the director converts source-seconds →
|
|
33
|
+
* output-frames at codegen. Keeping the two clocks separate is
|
|
34
|
+
* deliberate — see the plan's Task-1 note.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Schema version of a `FootageAnalysis`. Bump on an incompatible shape change. */
|
|
38
|
+
export const FOOTAGE_ANALYSIS_VERSION = 1 as const;
|
|
39
|
+
/** Schema version of an `Edl`. */
|
|
40
|
+
export const EDL_VERSION = 1 as const;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The six transition presentations Maude bundles from `@remotion/transitions`
|
|
44
|
+
* (video-comp SKILL.md — the ONLY ones that resolve on an end-user install).
|
|
45
|
+
* `none` is a hard cut. The director may only choose from these.
|
|
46
|
+
*/
|
|
47
|
+
export type TransitionPresentation = 'none' | 'fade' | 'slide' | 'wipe' | 'flip' | 'clock-wipe';
|
|
48
|
+
export const TRANSITION_PRESENTATIONS: readonly TransitionPresentation[] = [
|
|
49
|
+
'none',
|
|
50
|
+
'fade',
|
|
51
|
+
'slide',
|
|
52
|
+
'wipe',
|
|
53
|
+
'flip',
|
|
54
|
+
'clock-wipe',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/** Coarse shot vocabulary the analyst tags each segment with (advisory). */
|
|
58
|
+
export type ShotKind =
|
|
59
|
+
| 'wide'
|
|
60
|
+
| 'medium'
|
|
61
|
+
| 'close'
|
|
62
|
+
| 'detail'
|
|
63
|
+
| 'establishing'
|
|
64
|
+
| 'action'
|
|
65
|
+
| 'portrait'
|
|
66
|
+
| 'product'
|
|
67
|
+
| 'transition'
|
|
68
|
+
| 'other';
|
|
69
|
+
export const SHOT_KINDS: readonly ShotKind[] = [
|
|
70
|
+
'wide',
|
|
71
|
+
'medium',
|
|
72
|
+
'close',
|
|
73
|
+
'detail',
|
|
74
|
+
'establishing',
|
|
75
|
+
'action',
|
|
76
|
+
'portrait',
|
|
77
|
+
'product',
|
|
78
|
+
'transition',
|
|
79
|
+
'other',
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/** Dominant camera/subject motion the analyst reads across the keyframe run. */
|
|
83
|
+
export type MotionKind =
|
|
84
|
+
| 'static'
|
|
85
|
+
| 'pan'
|
|
86
|
+
| 'tilt'
|
|
87
|
+
| 'push-in'
|
|
88
|
+
| 'pull-out'
|
|
89
|
+
| 'handheld'
|
|
90
|
+
| 'tracking'
|
|
91
|
+
| 'fast'
|
|
92
|
+
| 'other';
|
|
93
|
+
export const MOTION_KINDS: readonly MotionKind[] = [
|
|
94
|
+
'static',
|
|
95
|
+
'pan',
|
|
96
|
+
'tilt',
|
|
97
|
+
'push-in',
|
|
98
|
+
'pull-out',
|
|
99
|
+
'handheld',
|
|
100
|
+
'tracking',
|
|
101
|
+
'fast',
|
|
102
|
+
'other',
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* One shot the analyst found inside a clip. `start`/`end` are SECONDS in the
|
|
107
|
+
* SOURCE clip (0 ≤ start < end ≤ clip duration). `quality` is the analyst's
|
|
108
|
+
* 0..1 confidence that this is a *usable, good* moment; `usable` is the boolean
|
|
109
|
+
* gate the director filters on first.
|
|
110
|
+
*/
|
|
111
|
+
export interface FootageShot {
|
|
112
|
+
/** Source in-point, seconds. */
|
|
113
|
+
start: number;
|
|
114
|
+
/** Source out-point, seconds (> start). */
|
|
115
|
+
end: number;
|
|
116
|
+
kind?: ShotKind;
|
|
117
|
+
motion?: MotionKind;
|
|
118
|
+
/** Free-text: who/what is on screen ("drone push over the river"). */
|
|
119
|
+
subject?: string;
|
|
120
|
+
/** Free-text lighting note ("golden hour, backlit"). */
|
|
121
|
+
lighting?: string;
|
|
122
|
+
/** Free-text mood ("calm, aspirational"). */
|
|
123
|
+
mood?: string;
|
|
124
|
+
/** 0..1 — how strong a shot this is for a reel. */
|
|
125
|
+
quality?: number;
|
|
126
|
+
/** The director only considers `usable !== false` shots. */
|
|
127
|
+
usable?: boolean;
|
|
128
|
+
/** One-line director-facing note ("hero shot; hold on the logo reveal"). */
|
|
129
|
+
note?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The full analysis of one clip. `asset` is the `assets/<sha8>.<ext>` it
|
|
134
|
+
* describes (redundant with the sidecar filename, but self-describing for the
|
|
135
|
+
* director + audit). `durationSec` / `width` / `height` come from the probe
|
|
136
|
+
* (`<video>` metadata). `keyframes` is the count of frames the analyst actually
|
|
137
|
+
* watched (provenance — so a downstream reader knows the sampling density).
|
|
138
|
+
*/
|
|
139
|
+
export interface FootageAnalysis {
|
|
140
|
+
version?: number;
|
|
141
|
+
/** `assets/<sha8>.<ext>` — the source this analysis targets. */
|
|
142
|
+
asset?: string;
|
|
143
|
+
durationSec?: number;
|
|
144
|
+
width?: number;
|
|
145
|
+
height?: number;
|
|
146
|
+
/** How many keyframes were sampled to produce this analysis. */
|
|
147
|
+
keyframes?: number;
|
|
148
|
+
shots?: FootageShot[];
|
|
149
|
+
/** One-paragraph character-of-the-clip summary. */
|
|
150
|
+
summary?: string;
|
|
151
|
+
/** Cross-cutting tags ("rebrand", "exterior", "people"). */
|
|
152
|
+
tags?: string[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** An optional graphic overlay laid over a beat (title card / lower-third / logo). */
|
|
156
|
+
export interface EdlOverlay {
|
|
157
|
+
kind: 'title' | 'lower-third' | 'caption' | 'logo';
|
|
158
|
+
/** Display text (absent for a bare logo). */
|
|
159
|
+
text?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The transition BETWEEN this beat and the previous one (null on beat 0 / hard cut). */
|
|
163
|
+
export interface EdlTransition {
|
|
164
|
+
presentation: TransitionPresentation;
|
|
165
|
+
/** Overlap length in OUTPUT frames. */
|
|
166
|
+
frames: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* One beat of the cut. `clip` is the `assets/<sha8>.<ext>` it draws from;
|
|
171
|
+
* `startSec` is the SOURCE in-point (seconds), `durationFrames` the beat length
|
|
172
|
+
* in OUTPUT frames (at the Edl's `fps`). Two beats may share one `clip` with
|
|
173
|
+
* different `startSec` — pulling multiple shots from one clip is first-class.
|
|
174
|
+
*/
|
|
175
|
+
export interface EdlBeat {
|
|
176
|
+
/** `assets/<sha8>.<ext>` — the clip this beat draws from. */
|
|
177
|
+
clip: string;
|
|
178
|
+
/** Source in-point, seconds. */
|
|
179
|
+
startSec: number;
|
|
180
|
+
/** Beat length in OUTPUT frames. */
|
|
181
|
+
durationFrames: number;
|
|
182
|
+
/** Why this shot, here — the director's reasoning (audit + regen). */
|
|
183
|
+
why?: string;
|
|
184
|
+
/** Transition INTO this beat from the previous one (null ⇒ hard cut). */
|
|
185
|
+
transition?: EdlTransition | null;
|
|
186
|
+
/** Optional graphic over the beat. */
|
|
187
|
+
overlay?: EdlOverlay | null;
|
|
188
|
+
/** Stable Timeline identity ("beat-1", "hero", "logo"). */
|
|
189
|
+
name?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** A plain user-dropped music bed laid under the whole cut (NOT analysis — see the plan's scope note). */
|
|
193
|
+
export interface EdlMusic {
|
|
194
|
+
/** `assets/<sha8>.<ext>` audio track. */
|
|
195
|
+
asset: string;
|
|
196
|
+
/** Fade the bed out over the last N output frames. */
|
|
197
|
+
fadeOutFrames?: number;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** feature-ai-media-generation Phase 2 — the role an audio track plays in the cut. */
|
|
201
|
+
export type EdlAudioKind = 'music' | 'voiceover' | 'sfx';
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* One audio track in the cut (DDR-164 Phase 2). Generalizes the single `music`
|
|
205
|
+
* bed into a LIST so a reel can layer generated music + voiceover + per-beat
|
|
206
|
+
* SFX. `startFrame`/`durationFrames` place + trim it on the OUTPUT timeline (at
|
|
207
|
+
* the Edl's `fps`); omit both for a whole-reel bed. The codegen emits an
|
|
208
|
+
* `<Audio src="assets/…">` inside a `<Sequence from={startFrame}>`.
|
|
209
|
+
*/
|
|
210
|
+
export interface EdlAudioTrack {
|
|
211
|
+
/** `assets/<sha8>.<ext>` audio asset. */
|
|
212
|
+
asset: string;
|
|
213
|
+
/** Role — drives default gain/ducking guidance in the codegen. Default 'music'. */
|
|
214
|
+
kind?: EdlAudioKind;
|
|
215
|
+
/** Where the track enters, OUTPUT frames from the start. Default 0. */
|
|
216
|
+
startFrame?: number;
|
|
217
|
+
/** Trim length in OUTPUT frames. Omit ⇒ play to the end of the source. */
|
|
218
|
+
durationFrames?: number;
|
|
219
|
+
/** Volume adjustment in dB (negative = quieter; e.g. a −12 dB music bed under VO). */
|
|
220
|
+
gainDb?: number;
|
|
221
|
+
fadeInFrames?: number;
|
|
222
|
+
fadeOutFrames?: number;
|
|
223
|
+
/** Stable Timeline identity ("vo", "bed", "whoosh-3"). */
|
|
224
|
+
name?: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** One rendered caption cue on the OUTPUT timeline — SECONDS (matches captions.ts). */
|
|
228
|
+
export interface EdlCaptionCue {
|
|
229
|
+
startSec: number;
|
|
230
|
+
endSec: number;
|
|
231
|
+
text: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The caption/subtitle track (DDR-164 Phase 2), fed by `generation/captions.ts`
|
|
236
|
+
* (whisper.cpp local or ElevenLabs Scribe / Groq cloud). Stored as parsed cues
|
|
237
|
+
* (seconds) rather than a raw SRT file so the codegen can render them as overlays
|
|
238
|
+
* without re-parsing; the codegen converts seconds → frames at the Edl's `fps`.
|
|
239
|
+
*/
|
|
240
|
+
export interface EdlCaptions {
|
|
241
|
+
cues: EdlCaptionCue[];
|
|
242
|
+
/** Where the caption sits on screen. Default 'lower-third'. */
|
|
243
|
+
style?: 'lower-third' | 'centered' | 'top';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The edit decision list — the director's output, one per cut. `beats` is the
|
|
248
|
+
* ordered timeline; the composition's total length = sum(durationFrames) −
|
|
249
|
+
* sum(transition overlaps).
|
|
250
|
+
*/
|
|
251
|
+
export interface Edl {
|
|
252
|
+
version?: number;
|
|
253
|
+
title?: string;
|
|
254
|
+
/** Output composition fps (default 30). */
|
|
255
|
+
fps?: number;
|
|
256
|
+
/** Output composition dimensions. */
|
|
257
|
+
width?: number;
|
|
258
|
+
height?: number;
|
|
259
|
+
beats?: EdlBeat[];
|
|
260
|
+
/** A single whole-reel music bed (kept for backwards compatibility). */
|
|
261
|
+
music?: EdlMusic | null;
|
|
262
|
+
/** feature-ai-media-generation Phase 2 — layered audio (music/VO/SFX). */
|
|
263
|
+
audioTracks?: EdlAudioTrack[];
|
|
264
|
+
/** feature-ai-media-generation Phase 2 — the caption/subtitle track. */
|
|
265
|
+
captions?: EdlCaptions | null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── shared helpers ───────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
const ASSET_REL_RE = /^assets\/[0-9a-f]{8,}[A-Za-z0-9._-]*$/;
|
|
271
|
+
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
272
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
273
|
+
|
|
274
|
+
/** True when the analysis has nothing usable in it (no shots, or none usable). */
|
|
275
|
+
export function isEmptyAnalysis(a?: FootageAnalysis | null): boolean {
|
|
276
|
+
if (!a || !a.shots || a.shots.length === 0) return true;
|
|
277
|
+
return !a.shots.some((s) => s.usable !== false);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** A fresh, empty analysis for `asset`. */
|
|
281
|
+
export function emptyFootageAnalysis(asset?: string): FootageAnalysis {
|
|
282
|
+
return { version: FOOTAGE_ANALYSIS_VERSION, ...(asset ? { asset } : {}), shots: [] };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The provenance tag every AI-generated clip's sidecar carries. The
|
|
286
|
+
* footage-analyst preserves it when it enriches the stub with real shots. It is
|
|
287
|
+
* an ADVISORY hint, not a trust boundary — the sidecar is versioned + peer-
|
|
288
|
+
* editable (DDR-054), so a peer can add/strip it; nothing security-relevant keys
|
|
289
|
+
* off it (it only labels a beat in the UI). */
|
|
290
|
+
export const AI_GENERATED_TAG = 'ai-generated';
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* A provenance `FootageAnalysis` stub for an AI-GENERATED clip (DDR-164 Phase 3,
|
|
294
|
+
* Task 3.2). Written by the `/_api/generate-jobs` route the moment a video job
|
|
295
|
+
* lands, so the generated clip is immediately KNOWN to the footage pipeline —
|
|
296
|
+
* `getAnalysis` returns non-null and the reel director can see the clip is
|
|
297
|
+
* synthetic (the `ai-generated` tag) and read what it was generated FOR (the
|
|
298
|
+
* prompt, as the summary) without re-watching.
|
|
299
|
+
*
|
|
300
|
+
* It carries NO shots on purpose: the `footage-analyst` still probes the
|
|
301
|
+
* keyframes and fills the real shot list (so `isEmptyAnalysis` is true and the
|
|
302
|
+
* reel's shot-aware cache re-analyzes it), but the provenance tag + summary
|
|
303
|
+
* survive. Duration/dimensions are unknown until that probe, so they are omitted.
|
|
304
|
+
*/
|
|
305
|
+
export function generatedClipAnalysis(
|
|
306
|
+
asset: string,
|
|
307
|
+
provenance: { provider?: string; model?: string; prompt?: string } = {}
|
|
308
|
+
): FootageAnalysis {
|
|
309
|
+
const tags = [AI_GENERATED_TAG];
|
|
310
|
+
if (provenance.provider) tags.push(provenance.provider);
|
|
311
|
+
const label = [provenance.provider, provenance.model].filter(Boolean).join(' · ');
|
|
312
|
+
// This is the LOCAL generation request's prompt (not a peer file), but its text
|
|
313
|
+
// becomes the sidecar `summary`, which IS versioned + peer-syncable (DDR-054)
|
|
314
|
+
// and read by the footage agents downstream. Map C0/C1 control chars to spaces
|
|
315
|
+
// (matching generation's sanitizeReuseText) so it can't fabricate multi-line
|
|
316
|
+
// instructions, then collapse whitespace; bounded to the summary cap below. The
|
|
317
|
+
// agents' own "treat sidecar text as data" posture is the mitigation for
|
|
318
|
+
// arbitrary PEER-authored summaries — this only cleans what we write.
|
|
319
|
+
const prompt = [...(provenance.prompt ?? '')]
|
|
320
|
+
.map((ch) => {
|
|
321
|
+
const c = ch.charCodeAt(0);
|
|
322
|
+
return (c >= 0x00 && c <= 0x1f) || (c >= 0x7f && c <= 0x9f) ? ' ' : ch;
|
|
323
|
+
})
|
|
324
|
+
.join('')
|
|
325
|
+
.replace(/\s+/g, ' ')
|
|
326
|
+
.trim();
|
|
327
|
+
let summary = `AI-generated clip${label ? ` (${label})` : ''}${prompt ? ` — ${prompt}` : ''}`;
|
|
328
|
+
if (summary.length > 3900) summary = `${summary.slice(0, 3900)}…`;
|
|
329
|
+
return {
|
|
330
|
+
version: FOOTAGE_ANALYSIS_VERSION,
|
|
331
|
+
asset,
|
|
332
|
+
keyframes: 0,
|
|
333
|
+
shots: [],
|
|
334
|
+
summary,
|
|
335
|
+
tags,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── structural validation (dependency-free — no Ajv) ─────────────────────────
|
|
340
|
+
// Mirrors photo/schema.ts: the `/_api/footage` route validates untrusted JSON
|
|
341
|
+
// with these before persisting. Unknown keys / wrong types / out-of-range
|
|
342
|
+
// numbers / non-relative asset paths all fail — no crafted field round-trips.
|
|
343
|
+
|
|
344
|
+
export interface ValidationResult {
|
|
345
|
+
ok: boolean;
|
|
346
|
+
errors: string[];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function num(
|
|
350
|
+
errors: string[],
|
|
351
|
+
obj: Record<string, unknown>,
|
|
352
|
+
key: string,
|
|
353
|
+
lo: number,
|
|
354
|
+
hi: number,
|
|
355
|
+
where: string
|
|
356
|
+
) {
|
|
357
|
+
if (!(key in obj) || obj[key] == null) return;
|
|
358
|
+
const v = obj[key];
|
|
359
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
360
|
+
errors.push(`${where}.${key}: must be a finite number`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (v < lo || v > hi) errors.push(`${where}.${key}: ${v} out of range [${lo}, ${hi}]`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function intGe(
|
|
367
|
+
errors: string[],
|
|
368
|
+
obj: Record<string, unknown>,
|
|
369
|
+
key: string,
|
|
370
|
+
lo: number,
|
|
371
|
+
where: string
|
|
372
|
+
) {
|
|
373
|
+
if (!(key in obj) || obj[key] == null) return;
|
|
374
|
+
const v = obj[key];
|
|
375
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < lo)
|
|
376
|
+
errors.push(`${where}.${key}: must be an integer ≥ ${lo}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function bool(errors: string[], obj: Record<string, unknown>, key: string, where: string) {
|
|
380
|
+
if (key in obj && obj[key] != null && typeof obj[key] !== 'boolean')
|
|
381
|
+
errors.push(`${where}.${key}: must be a boolean`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function str(
|
|
385
|
+
errors: string[],
|
|
386
|
+
obj: Record<string, unknown>,
|
|
387
|
+
key: string,
|
|
388
|
+
maxLen: number,
|
|
389
|
+
where: string
|
|
390
|
+
) {
|
|
391
|
+
if (!(key in obj) || obj[key] == null) return;
|
|
392
|
+
const v = obj[key];
|
|
393
|
+
if (typeof v !== 'string') errors.push(`${where}.${key}: must be a string`);
|
|
394
|
+
else if (v.length > maxLen) errors.push(`${where}.${key}: exceeds ${maxLen} chars`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function assetRel(errors: string[], obj: Record<string, unknown>, key: string, where: string) {
|
|
398
|
+
if (!(key in obj) || obj[key] == null) return;
|
|
399
|
+
const v = obj[key];
|
|
400
|
+
if (typeof v !== 'string' || !ASSET_REL_RE.test(v))
|
|
401
|
+
errors.push(`${where}.${key}: must be a relative assets/<sha8>.<ext> path`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function assertKeys(
|
|
405
|
+
errors: string[],
|
|
406
|
+
obj: Record<string, unknown>,
|
|
407
|
+
allowed: string[],
|
|
408
|
+
where: string
|
|
409
|
+
) {
|
|
410
|
+
for (const k of Object.keys(obj))
|
|
411
|
+
if (!allowed.includes(k)) errors.push(`${where}: unknown key "${k}"`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function strArray(
|
|
415
|
+
errors: string[],
|
|
416
|
+
obj: Record<string, unknown>,
|
|
417
|
+
key: string,
|
|
418
|
+
maxItems: number,
|
|
419
|
+
where: string
|
|
420
|
+
) {
|
|
421
|
+
if (!(key in obj) || obj[key] == null) return;
|
|
422
|
+
const v = obj[key];
|
|
423
|
+
if (!Array.isArray(v)) {
|
|
424
|
+
errors.push(`${where}.${key}: must be an array`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (v.length > maxItems) errors.push(`${where}.${key}: more than ${maxItems} items`);
|
|
428
|
+
for (const s of v)
|
|
429
|
+
if (typeof s !== 'string') errors.push(`${where}.${key}: items must be strings`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Structurally validate an untrusted value as a `FootageAnalysis`. */
|
|
433
|
+
export function validateFootageAnalysis(input: unknown): ValidationResult {
|
|
434
|
+
const errors: string[] = [];
|
|
435
|
+
if (!isPlainObject(input)) return { ok: false, errors: ['root: must be an object'] };
|
|
436
|
+
|
|
437
|
+
assertKeys(
|
|
438
|
+
errors,
|
|
439
|
+
input,
|
|
440
|
+
['version', 'asset', 'durationSec', 'width', 'height', 'keyframes', 'shots', 'summary', 'tags'],
|
|
441
|
+
'root'
|
|
442
|
+
);
|
|
443
|
+
if ('version' in input && input.version != null && typeof input.version !== 'number')
|
|
444
|
+
errors.push('version: must be a number');
|
|
445
|
+
assetRel(errors, input, 'asset', 'root');
|
|
446
|
+
num(errors, input, 'durationSec', 0, 24 * 3600, 'root');
|
|
447
|
+
num(errors, input, 'width', 0, 100000, 'root');
|
|
448
|
+
num(errors, input, 'height', 0, 100000, 'root');
|
|
449
|
+
intGe(errors, input, 'keyframes', 0, 'root');
|
|
450
|
+
str(errors, input, 'summary', 4000, 'root');
|
|
451
|
+
strArray(errors, input, 'tags', 64, 'root');
|
|
452
|
+
|
|
453
|
+
const dur = typeof input.durationSec === 'number' ? input.durationSec : Infinity;
|
|
454
|
+
if ('shots' in input && input.shots != null) {
|
|
455
|
+
if (!Array.isArray(input.shots)) errors.push('shots: must be an array');
|
|
456
|
+
else if (input.shots.length > 512) errors.push('shots: more than 512 items');
|
|
457
|
+
else
|
|
458
|
+
input.shots.forEach((raw, i) => {
|
|
459
|
+
const where = `shots[${i}]`;
|
|
460
|
+
if (!isPlainObject(raw)) {
|
|
461
|
+
errors.push(`${where}: must be an object`);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
assertKeys(
|
|
465
|
+
errors,
|
|
466
|
+
raw,
|
|
467
|
+
[
|
|
468
|
+
'start',
|
|
469
|
+
'end',
|
|
470
|
+
'kind',
|
|
471
|
+
'motion',
|
|
472
|
+
'subject',
|
|
473
|
+
'lighting',
|
|
474
|
+
'mood',
|
|
475
|
+
'quality',
|
|
476
|
+
'usable',
|
|
477
|
+
'note',
|
|
478
|
+
],
|
|
479
|
+
where
|
|
480
|
+
);
|
|
481
|
+
num(errors, raw, 'start', 0, 24 * 3600, where);
|
|
482
|
+
num(errors, raw, 'end', 0, 24 * 3600, where);
|
|
483
|
+
if (typeof raw.start === 'number' && typeof raw.end === 'number' && !(raw.end > raw.start))
|
|
484
|
+
errors.push(`${where}: end must be > start`);
|
|
485
|
+
if (typeof raw.end === 'number' && raw.end > dur + 0.5)
|
|
486
|
+
errors.push(`${where}: end ${raw.end}s exceeds clip duration ${dur}s`);
|
|
487
|
+
if ('kind' in raw && raw.kind != null && !SHOT_KINDS.includes(raw.kind as ShotKind))
|
|
488
|
+
errors.push(`${where}.kind: must be one of ${SHOT_KINDS.join(', ')}`);
|
|
489
|
+
if (
|
|
490
|
+
'motion' in raw &&
|
|
491
|
+
raw.motion != null &&
|
|
492
|
+
!MOTION_KINDS.includes(raw.motion as MotionKind)
|
|
493
|
+
)
|
|
494
|
+
errors.push(`${where}.motion: must be one of ${MOTION_KINDS.join(', ')}`);
|
|
495
|
+
str(errors, raw, 'subject', 500, where);
|
|
496
|
+
str(errors, raw, 'lighting', 500, where);
|
|
497
|
+
str(errors, raw, 'mood', 500, where);
|
|
498
|
+
str(errors, raw, 'note', 1000, where);
|
|
499
|
+
num(errors, raw, 'quality', 0, 1, where);
|
|
500
|
+
bool(errors, raw, 'usable', where);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return { ok: errors.length === 0, errors };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function validateOverlay(errors: string[], raw: unknown, where: string) {
|
|
508
|
+
if (raw == null) return;
|
|
509
|
+
if (!isPlainObject(raw)) {
|
|
510
|
+
errors.push(`${where}: must be an object`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
assertKeys(errors, raw, ['kind', 'text'], where);
|
|
514
|
+
const kinds = ['title', 'lower-third', 'caption', 'logo'];
|
|
515
|
+
if (!kinds.includes(raw.kind as string))
|
|
516
|
+
errors.push(`${where}.kind: must be one of ${kinds.join(', ')}`);
|
|
517
|
+
str(errors, raw, 'text', 500, where);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function validateTransition(errors: string[], raw: unknown, where: string) {
|
|
521
|
+
if (raw == null) return;
|
|
522
|
+
if (!isPlainObject(raw)) {
|
|
523
|
+
errors.push(`${where}: must be an object`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
assertKeys(errors, raw, ['presentation', 'frames'], where);
|
|
527
|
+
if (!TRANSITION_PRESENTATIONS.includes(raw.presentation as TransitionPresentation))
|
|
528
|
+
errors.push(`${where}.presentation: must be one of ${TRANSITION_PRESENTATIONS.join(', ')}`);
|
|
529
|
+
intGe(errors, raw, 'frames', 0, where);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Structurally validate an untrusted value as an `Edl`. */
|
|
533
|
+
export function validateEdl(input: unknown): ValidationResult {
|
|
534
|
+
const errors: string[] = [];
|
|
535
|
+
if (!isPlainObject(input)) return { ok: false, errors: ['root: must be an object'] };
|
|
536
|
+
|
|
537
|
+
assertKeys(
|
|
538
|
+
errors,
|
|
539
|
+
input,
|
|
540
|
+
['version', 'title', 'fps', 'width', 'height', 'beats', 'music', 'audioTracks', 'captions'],
|
|
541
|
+
'root'
|
|
542
|
+
);
|
|
543
|
+
if ('version' in input && input.version != null && typeof input.version !== 'number')
|
|
544
|
+
errors.push('version: must be a number');
|
|
545
|
+
str(errors, input, 'title', 300, 'root');
|
|
546
|
+
num(errors, input, 'fps', 1, 120, 'root');
|
|
547
|
+
num(errors, input, 'width', 1, 100000, 'root');
|
|
548
|
+
num(errors, input, 'height', 1, 100000, 'root');
|
|
549
|
+
|
|
550
|
+
if ('beats' in input && input.beats != null) {
|
|
551
|
+
if (!Array.isArray(input.beats)) errors.push('beats: must be an array');
|
|
552
|
+
else if (input.beats.length > 512) errors.push('beats: more than 512 items');
|
|
553
|
+
else
|
|
554
|
+
input.beats.forEach((raw, i) => {
|
|
555
|
+
const where = `beats[${i}]`;
|
|
556
|
+
if (!isPlainObject(raw)) {
|
|
557
|
+
errors.push(`${where}: must be an object`);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
assertKeys(
|
|
561
|
+
errors,
|
|
562
|
+
raw,
|
|
563
|
+
['clip', 'startSec', 'durationFrames', 'why', 'transition', 'overlay', 'name'],
|
|
564
|
+
where
|
|
565
|
+
);
|
|
566
|
+
assetRel(errors, raw, 'clip', where);
|
|
567
|
+
if (!('clip' in raw) || raw.clip == null) errors.push(`${where}.clip: required`);
|
|
568
|
+
num(errors, raw, 'startSec', 0, 24 * 3600, where);
|
|
569
|
+
intGe(errors, raw, 'durationFrames', 1, where);
|
|
570
|
+
str(errors, raw, 'why', 1000, where);
|
|
571
|
+
str(errors, raw, 'name', 120, where);
|
|
572
|
+
if ('transition' in raw) validateTransition(errors, raw.transition, `${where}.transition`);
|
|
573
|
+
if ('overlay' in raw) validateOverlay(errors, raw.overlay, `${where}.overlay`);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if ('music' in input && input.music != null) {
|
|
578
|
+
const m = input.music;
|
|
579
|
+
if (!isPlainObject(m)) errors.push('music: must be an object');
|
|
580
|
+
else {
|
|
581
|
+
assertKeys(errors, m, ['asset', 'fadeOutFrames'], 'music');
|
|
582
|
+
assetRel(errors, m, 'asset', 'music');
|
|
583
|
+
if (!('asset' in m) || m.asset == null) errors.push('music.asset: required');
|
|
584
|
+
intGe(errors, m, 'fadeOutFrames', 0, 'music');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// feature-ai-media-generation Phase 2 — layered audio tracks (music/VO/SFX).
|
|
589
|
+
if ('audioTracks' in input && input.audioTracks != null) {
|
|
590
|
+
if (!Array.isArray(input.audioTracks)) errors.push('audioTracks: must be an array');
|
|
591
|
+
else if (input.audioTracks.length > 64) errors.push('audioTracks: more than 64 items');
|
|
592
|
+
else
|
|
593
|
+
input.audioTracks.forEach((raw, i) => {
|
|
594
|
+
const where = `audioTracks[${i}]`;
|
|
595
|
+
if (!isPlainObject(raw)) {
|
|
596
|
+
errors.push(`${where}: must be an object`);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
assertKeys(
|
|
600
|
+
errors,
|
|
601
|
+
raw,
|
|
602
|
+
[
|
|
603
|
+
'asset',
|
|
604
|
+
'kind',
|
|
605
|
+
'startFrame',
|
|
606
|
+
'durationFrames',
|
|
607
|
+
'gainDb',
|
|
608
|
+
'fadeInFrames',
|
|
609
|
+
'fadeOutFrames',
|
|
610
|
+
'name',
|
|
611
|
+
],
|
|
612
|
+
where
|
|
613
|
+
);
|
|
614
|
+
assetRel(errors, raw, 'asset', where);
|
|
615
|
+
if (!('asset' in raw) || raw.asset == null) errors.push(`${where}.asset: required`);
|
|
616
|
+
if (
|
|
617
|
+
'kind' in raw &&
|
|
618
|
+
raw.kind != null &&
|
|
619
|
+
!['music', 'voiceover', 'sfx'].includes(raw.kind as string)
|
|
620
|
+
)
|
|
621
|
+
errors.push(`${where}.kind: must be music|voiceover|sfx`);
|
|
622
|
+
intGe(errors, raw, 'startFrame', 0, where);
|
|
623
|
+
intGe(errors, raw, 'durationFrames', 1, where);
|
|
624
|
+
num(errors, raw, 'gainDb', -60, 24, where);
|
|
625
|
+
intGe(errors, raw, 'fadeInFrames', 0, where);
|
|
626
|
+
intGe(errors, raw, 'fadeOutFrames', 0, where);
|
|
627
|
+
str(errors, raw, 'name', 120, where);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// feature-ai-media-generation Phase 2 — the caption/subtitle track.
|
|
632
|
+
if ('captions' in input && input.captions != null) {
|
|
633
|
+
const c = input.captions;
|
|
634
|
+
if (!isPlainObject(c)) errors.push('captions: must be an object');
|
|
635
|
+
else {
|
|
636
|
+
assertKeys(errors, c, ['cues', 'style'], 'captions');
|
|
637
|
+
if (
|
|
638
|
+
'style' in c &&
|
|
639
|
+
c.style != null &&
|
|
640
|
+
!['lower-third', 'centered', 'top'].includes(c.style as string)
|
|
641
|
+
)
|
|
642
|
+
errors.push('captions.style: must be lower-third|centered|top');
|
|
643
|
+
if (!('cues' in c) || !Array.isArray(c.cues)) errors.push('captions.cues: must be an array');
|
|
644
|
+
else if (c.cues.length > 5000) errors.push('captions.cues: more than 5000 items');
|
|
645
|
+
else
|
|
646
|
+
c.cues.forEach((raw, i) => {
|
|
647
|
+
const where = `captions.cues[${i}]`;
|
|
648
|
+
if (!isPlainObject(raw)) {
|
|
649
|
+
errors.push(`${where}: must be an object`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
assertKeys(errors, raw, ['startSec', 'endSec', 'text'], where);
|
|
653
|
+
num(errors, raw, 'startSec', 0, 24 * 3600, where);
|
|
654
|
+
num(errors, raw, 'endSec', 0, 24 * 3600, where);
|
|
655
|
+
str(errors, raw, 'text', 2000, where);
|
|
656
|
+
if (!('text' in raw) || raw.text == null) errors.push(`${where}.text: required`);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return { ok: errors.length === 0, errors };
|
|
662
|
+
}
|