@1agh/maude 0.42.0 → 0.43.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/annotations-layer.tsx +1055 -278
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +173 -22
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- 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 +160 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/canvas-edit.ts +459 -10
- package/apps/studio/canvas-lib.tsx +523 -5
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +451 -34
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/github.js +4 -0
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- 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 +437 -0
- package/apps/studio/client/styles/3-shell-maude.css +42 -0
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/context-menu.tsx +31 -6
- package/apps/studio/dist/client.bundle.js +5561 -19
- 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/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +195 -0
- package/apps/studio/footage/schema.ts +484 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +196 -20
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +173 -14
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/paths.ts +10 -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/runtime-bundle.ts +29 -6
- package/apps/studio/server.ts +5 -1
- 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/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-commands.test.ts +2 -1
- 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 +13 -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 +46 -0
- package/apps/studio/test/footage-store.test.ts +100 -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/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -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/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/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 +43 -20
- 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 +38 -1
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +53 -0
- package/cli/commands/design.mjs +34 -3
- package/cli/lib/gitignore-block.mjs +1 -0
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -0,0 +1,484 @@
|
|
|
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
|
+
/**
|
|
201
|
+
* The edit decision list — the director's output, one per cut. `beats` is the
|
|
202
|
+
* ordered timeline; the composition's total length = sum(durationFrames) −
|
|
203
|
+
* sum(transition overlaps).
|
|
204
|
+
*/
|
|
205
|
+
export interface Edl {
|
|
206
|
+
version?: number;
|
|
207
|
+
title?: string;
|
|
208
|
+
/** Output composition fps (default 30). */
|
|
209
|
+
fps?: number;
|
|
210
|
+
/** Output composition dimensions. */
|
|
211
|
+
width?: number;
|
|
212
|
+
height?: number;
|
|
213
|
+
beats?: EdlBeat[];
|
|
214
|
+
music?: EdlMusic | null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── shared helpers ───────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
const ASSET_REL_RE = /^assets\/[0-9a-f]{8,}[A-Za-z0-9._-]*$/;
|
|
220
|
+
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
221
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
222
|
+
|
|
223
|
+
/** True when the analysis has nothing usable in it (no shots, or none usable). */
|
|
224
|
+
export function isEmptyAnalysis(a?: FootageAnalysis | null): boolean {
|
|
225
|
+
if (!a || !a.shots || a.shots.length === 0) return true;
|
|
226
|
+
return !a.shots.some((s) => s.usable !== false);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** A fresh, empty analysis for `asset`. */
|
|
230
|
+
export function emptyFootageAnalysis(asset?: string): FootageAnalysis {
|
|
231
|
+
return { version: FOOTAGE_ANALYSIS_VERSION, ...(asset ? { asset } : {}), shots: [] };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── structural validation (dependency-free — no Ajv) ─────────────────────────
|
|
235
|
+
// Mirrors photo/schema.ts: the `/_api/footage` route validates untrusted JSON
|
|
236
|
+
// with these before persisting. Unknown keys / wrong types / out-of-range
|
|
237
|
+
// numbers / non-relative asset paths all fail — no crafted field round-trips.
|
|
238
|
+
|
|
239
|
+
export interface ValidationResult {
|
|
240
|
+
ok: boolean;
|
|
241
|
+
errors: string[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function num(
|
|
245
|
+
errors: string[],
|
|
246
|
+
obj: Record<string, unknown>,
|
|
247
|
+
key: string,
|
|
248
|
+
lo: number,
|
|
249
|
+
hi: number,
|
|
250
|
+
where: string
|
|
251
|
+
) {
|
|
252
|
+
if (!(key in obj) || obj[key] == null) return;
|
|
253
|
+
const v = obj[key];
|
|
254
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
255
|
+
errors.push(`${where}.${key}: must be a finite number`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (v < lo || v > hi) errors.push(`${where}.${key}: ${v} out of range [${lo}, ${hi}]`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function intGe(
|
|
262
|
+
errors: string[],
|
|
263
|
+
obj: Record<string, unknown>,
|
|
264
|
+
key: string,
|
|
265
|
+
lo: number,
|
|
266
|
+
where: string
|
|
267
|
+
) {
|
|
268
|
+
if (!(key in obj) || obj[key] == null) return;
|
|
269
|
+
const v = obj[key];
|
|
270
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < lo)
|
|
271
|
+
errors.push(`${where}.${key}: must be an integer ≥ ${lo}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function bool(errors: string[], obj: Record<string, unknown>, key: string, where: string) {
|
|
275
|
+
if (key in obj && obj[key] != null && typeof obj[key] !== 'boolean')
|
|
276
|
+
errors.push(`${where}.${key}: must be a boolean`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function str(
|
|
280
|
+
errors: string[],
|
|
281
|
+
obj: Record<string, unknown>,
|
|
282
|
+
key: string,
|
|
283
|
+
maxLen: number,
|
|
284
|
+
where: string
|
|
285
|
+
) {
|
|
286
|
+
if (!(key in obj) || obj[key] == null) return;
|
|
287
|
+
const v = obj[key];
|
|
288
|
+
if (typeof v !== 'string') errors.push(`${where}.${key}: must be a string`);
|
|
289
|
+
else if (v.length > maxLen) errors.push(`${where}.${key}: exceeds ${maxLen} chars`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function assetRel(errors: string[], obj: Record<string, unknown>, key: string, where: string) {
|
|
293
|
+
if (!(key in obj) || obj[key] == null) return;
|
|
294
|
+
const v = obj[key];
|
|
295
|
+
if (typeof v !== 'string' || !ASSET_REL_RE.test(v))
|
|
296
|
+
errors.push(`${where}.${key}: must be a relative assets/<sha8>.<ext> path`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function assertKeys(
|
|
300
|
+
errors: string[],
|
|
301
|
+
obj: Record<string, unknown>,
|
|
302
|
+
allowed: string[],
|
|
303
|
+
where: string
|
|
304
|
+
) {
|
|
305
|
+
for (const k of Object.keys(obj))
|
|
306
|
+
if (!allowed.includes(k)) errors.push(`${where}: unknown key "${k}"`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function strArray(
|
|
310
|
+
errors: string[],
|
|
311
|
+
obj: Record<string, unknown>,
|
|
312
|
+
key: string,
|
|
313
|
+
maxItems: number,
|
|
314
|
+
where: string
|
|
315
|
+
) {
|
|
316
|
+
if (!(key in obj) || obj[key] == null) return;
|
|
317
|
+
const v = obj[key];
|
|
318
|
+
if (!Array.isArray(v)) {
|
|
319
|
+
errors.push(`${where}.${key}: must be an array`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (v.length > maxItems) errors.push(`${where}.${key}: more than ${maxItems} items`);
|
|
323
|
+
for (const s of v)
|
|
324
|
+
if (typeof s !== 'string') errors.push(`${where}.${key}: items must be strings`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Structurally validate an untrusted value as a `FootageAnalysis`. */
|
|
328
|
+
export function validateFootageAnalysis(input: unknown): ValidationResult {
|
|
329
|
+
const errors: string[] = [];
|
|
330
|
+
if (!isPlainObject(input)) return { ok: false, errors: ['root: must be an object'] };
|
|
331
|
+
|
|
332
|
+
assertKeys(
|
|
333
|
+
errors,
|
|
334
|
+
input,
|
|
335
|
+
['version', 'asset', 'durationSec', 'width', 'height', 'keyframes', 'shots', 'summary', 'tags'],
|
|
336
|
+
'root'
|
|
337
|
+
);
|
|
338
|
+
if ('version' in input && input.version != null && typeof input.version !== 'number')
|
|
339
|
+
errors.push('version: must be a number');
|
|
340
|
+
assetRel(errors, input, 'asset', 'root');
|
|
341
|
+
num(errors, input, 'durationSec', 0, 24 * 3600, 'root');
|
|
342
|
+
num(errors, input, 'width', 0, 100000, 'root');
|
|
343
|
+
num(errors, input, 'height', 0, 100000, 'root');
|
|
344
|
+
intGe(errors, input, 'keyframes', 0, 'root');
|
|
345
|
+
str(errors, input, 'summary', 4000, 'root');
|
|
346
|
+
strArray(errors, input, 'tags', 64, 'root');
|
|
347
|
+
|
|
348
|
+
const dur = typeof input.durationSec === 'number' ? input.durationSec : Infinity;
|
|
349
|
+
if ('shots' in input && input.shots != null) {
|
|
350
|
+
if (!Array.isArray(input.shots)) errors.push('shots: must be an array');
|
|
351
|
+
else if (input.shots.length > 512) errors.push('shots: more than 512 items');
|
|
352
|
+
else
|
|
353
|
+
input.shots.forEach((raw, i) => {
|
|
354
|
+
const where = `shots[${i}]`;
|
|
355
|
+
if (!isPlainObject(raw)) {
|
|
356
|
+
errors.push(`${where}: must be an object`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
assertKeys(
|
|
360
|
+
errors,
|
|
361
|
+
raw,
|
|
362
|
+
[
|
|
363
|
+
'start',
|
|
364
|
+
'end',
|
|
365
|
+
'kind',
|
|
366
|
+
'motion',
|
|
367
|
+
'subject',
|
|
368
|
+
'lighting',
|
|
369
|
+
'mood',
|
|
370
|
+
'quality',
|
|
371
|
+
'usable',
|
|
372
|
+
'note',
|
|
373
|
+
],
|
|
374
|
+
where
|
|
375
|
+
);
|
|
376
|
+
num(errors, raw, 'start', 0, 24 * 3600, where);
|
|
377
|
+
num(errors, raw, 'end', 0, 24 * 3600, where);
|
|
378
|
+
if (typeof raw.start === 'number' && typeof raw.end === 'number' && !(raw.end > raw.start))
|
|
379
|
+
errors.push(`${where}: end must be > start`);
|
|
380
|
+
if (typeof raw.end === 'number' && raw.end > dur + 0.5)
|
|
381
|
+
errors.push(`${where}: end ${raw.end}s exceeds clip duration ${dur}s`);
|
|
382
|
+
if ('kind' in raw && raw.kind != null && !SHOT_KINDS.includes(raw.kind as ShotKind))
|
|
383
|
+
errors.push(`${where}.kind: must be one of ${SHOT_KINDS.join(', ')}`);
|
|
384
|
+
if (
|
|
385
|
+
'motion' in raw &&
|
|
386
|
+
raw.motion != null &&
|
|
387
|
+
!MOTION_KINDS.includes(raw.motion as MotionKind)
|
|
388
|
+
)
|
|
389
|
+
errors.push(`${where}.motion: must be one of ${MOTION_KINDS.join(', ')}`);
|
|
390
|
+
str(errors, raw, 'subject', 500, where);
|
|
391
|
+
str(errors, raw, 'lighting', 500, where);
|
|
392
|
+
str(errors, raw, 'mood', 500, where);
|
|
393
|
+
str(errors, raw, 'note', 1000, where);
|
|
394
|
+
num(errors, raw, 'quality', 0, 1, where);
|
|
395
|
+
bool(errors, raw, 'usable', where);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return { ok: errors.length === 0, errors };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function validateOverlay(errors: string[], raw: unknown, where: string) {
|
|
403
|
+
if (raw == null) return;
|
|
404
|
+
if (!isPlainObject(raw)) {
|
|
405
|
+
errors.push(`${where}: must be an object`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
assertKeys(errors, raw, ['kind', 'text'], where);
|
|
409
|
+
const kinds = ['title', 'lower-third', 'caption', 'logo'];
|
|
410
|
+
if (!kinds.includes(raw.kind as string))
|
|
411
|
+
errors.push(`${where}.kind: must be one of ${kinds.join(', ')}`);
|
|
412
|
+
str(errors, raw, 'text', 500, where);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function validateTransition(errors: string[], raw: unknown, where: string) {
|
|
416
|
+
if (raw == null) return;
|
|
417
|
+
if (!isPlainObject(raw)) {
|
|
418
|
+
errors.push(`${where}: must be an object`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
assertKeys(errors, raw, ['presentation', 'frames'], where);
|
|
422
|
+
if (!TRANSITION_PRESENTATIONS.includes(raw.presentation as TransitionPresentation))
|
|
423
|
+
errors.push(`${where}.presentation: must be one of ${TRANSITION_PRESENTATIONS.join(', ')}`);
|
|
424
|
+
intGe(errors, raw, 'frames', 0, where);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Structurally validate an untrusted value as an `Edl`. */
|
|
428
|
+
export function validateEdl(input: unknown): ValidationResult {
|
|
429
|
+
const errors: string[] = [];
|
|
430
|
+
if (!isPlainObject(input)) return { ok: false, errors: ['root: must be an object'] };
|
|
431
|
+
|
|
432
|
+
assertKeys(
|
|
433
|
+
errors,
|
|
434
|
+
input,
|
|
435
|
+
['version', 'title', 'fps', 'width', 'height', 'beats', 'music'],
|
|
436
|
+
'root'
|
|
437
|
+
);
|
|
438
|
+
if ('version' in input && input.version != null && typeof input.version !== 'number')
|
|
439
|
+
errors.push('version: must be a number');
|
|
440
|
+
str(errors, input, 'title', 300, 'root');
|
|
441
|
+
num(errors, input, 'fps', 1, 120, 'root');
|
|
442
|
+
num(errors, input, 'width', 1, 100000, 'root');
|
|
443
|
+
num(errors, input, 'height', 1, 100000, 'root');
|
|
444
|
+
|
|
445
|
+
if ('beats' in input && input.beats != null) {
|
|
446
|
+
if (!Array.isArray(input.beats)) errors.push('beats: must be an array');
|
|
447
|
+
else if (input.beats.length > 512) errors.push('beats: more than 512 items');
|
|
448
|
+
else
|
|
449
|
+
input.beats.forEach((raw, i) => {
|
|
450
|
+
const where = `beats[${i}]`;
|
|
451
|
+
if (!isPlainObject(raw)) {
|
|
452
|
+
errors.push(`${where}: must be an object`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
assertKeys(
|
|
456
|
+
errors,
|
|
457
|
+
raw,
|
|
458
|
+
['clip', 'startSec', 'durationFrames', 'why', 'transition', 'overlay', 'name'],
|
|
459
|
+
where
|
|
460
|
+
);
|
|
461
|
+
assetRel(errors, raw, 'clip', where);
|
|
462
|
+
if (!('clip' in raw) || raw.clip == null) errors.push(`${where}.clip: required`);
|
|
463
|
+
num(errors, raw, 'startSec', 0, 24 * 3600, where);
|
|
464
|
+
intGe(errors, raw, 'durationFrames', 1, where);
|
|
465
|
+
str(errors, raw, 'why', 1000, where);
|
|
466
|
+
str(errors, raw, 'name', 120, where);
|
|
467
|
+
if ('transition' in raw) validateTransition(errors, raw.transition, `${where}.transition`);
|
|
468
|
+
if ('overlay' in raw) validateOverlay(errors, raw.overlay, `${where}.overlay`);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if ('music' in input && input.music != null) {
|
|
473
|
+
const m = input.music;
|
|
474
|
+
if (!isPlainObject(m)) errors.push('music: must be an object');
|
|
475
|
+
else {
|
|
476
|
+
assertKeys(errors, m, ['asset', 'fadeOutFrames'], 'music');
|
|
477
|
+
assetRel(errors, m, 'asset', 'music');
|
|
478
|
+
if (!('asset' in m) || m.asset == null) errors.push('music.asset: required');
|
|
479
|
+
intGe(errors, m, 'fadeOutFrames', 0, 'music');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return { ok: errors.length === 0, errors };
|
|
484
|
+
}
|