@anvpx/motion-schema 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/data/platforms/facebook.json +30 -0
- package/data/platforms/linkedin.json +31 -0
- package/data/platforms/podcast.json +30 -0
- package/data/platforms/reels.json +31 -0
- package/data/platforms/shorts.json +31 -0
- package/data/platforms/tiktok.json +31 -0
- package/data/platforms/x.json +31 -0
- package/data/platforms/youtube.json +30 -0
- package/dist/audio.d.ts +83 -0
- package/dist/audio.js +87 -0
- package/dist/contract.d.ts +2 -0
- package/dist/contract.js +11 -0
- package/dist/contracts.d.ts +58 -0
- package/dist/contracts.js +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +3 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +11 -0
- package/dist/json-schema.d.ts +2 -0
- package/dist/json-schema.js +4 -0
- package/dist/ledger.d.ts +19 -0
- package/dist/ledger.js +42 -0
- package/dist/platform.d.ts +77 -0
- package/dist/platform.js +51 -0
- package/dist/reserved-keys.d.ts +13 -0
- package/dist/reserved-keys.js +40 -0
- package/dist/storyboard.d.ts +147 -0
- package/dist/storyboard.js +136 -0
- package/dist/template.d.ts +478 -0
- package/dist/template.js +254 -0
- package/dist/theme.d.ts +28 -0
- package/dist/theme.js +1 -0
- package/dist/timing.d.ts +44 -0
- package/dist/timing.js +44 -0
- package/dist/validate.d.ts +5 -0
- package/dist/validate.js +58 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.js +19 -0
- package/package.json +33 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 anvpx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"aliases": [],
|
|
3
|
+
"aspect": "16:9",
|
|
4
|
+
"audio": {
|
|
5
|
+
"lufsTarget": -14,
|
|
6
|
+
"truePeakDb": -1
|
|
7
|
+
},
|
|
8
|
+
"codec": {
|
|
9
|
+
"audio": "aac",
|
|
10
|
+
"video": "h264"
|
|
11
|
+
},
|
|
12
|
+
"height": 1080,
|
|
13
|
+
"id": "facebook",
|
|
14
|
+
"maxFps": 30,
|
|
15
|
+
"notes": "Feed video; 9:16 and 1:1 also accepted. Safe zone is the generic 5% title-safe margin. safe{} is the generic 5% title-safe margin.",
|
|
16
|
+
"safe": {
|
|
17
|
+
"bottom": 0.05,
|
|
18
|
+
"side": 0.05,
|
|
19
|
+
"top": 0.05
|
|
20
|
+
},
|
|
21
|
+
"safeZones": {
|
|
22
|
+
"bottom": 0.05,
|
|
23
|
+
"left": 0.05,
|
|
24
|
+
"right": 0.05,
|
|
25
|
+
"top": 0.05
|
|
26
|
+
},
|
|
27
|
+
"source": "Meta video specs (general guidance); loudness is the common social norm, not a platform-published target",
|
|
28
|
+
"verified": false,
|
|
29
|
+
"width": 1920
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"aliases": [],
|
|
3
|
+
"aspect": "16:9",
|
|
4
|
+
"audio": {
|
|
5
|
+
"lufsTarget": -14,
|
|
6
|
+
"truePeakDb": -1
|
|
7
|
+
},
|
|
8
|
+
"codec": {
|
|
9
|
+
"audio": "aac",
|
|
10
|
+
"video": "h264"
|
|
11
|
+
},
|
|
12
|
+
"height": 1080,
|
|
13
|
+
"id": "linkedin",
|
|
14
|
+
"maxDurationSec": 600,
|
|
15
|
+
"maxFps": 30,
|
|
16
|
+
"notes": "Feed video up to 10 min. Safe zone is the generic 5% title-safe margin. safe{} is the generic 5% title-safe margin.",
|
|
17
|
+
"safe": {
|
|
18
|
+
"bottom": 0.05,
|
|
19
|
+
"side": 0.05,
|
|
20
|
+
"top": 0.05
|
|
21
|
+
},
|
|
22
|
+
"safeZones": {
|
|
23
|
+
"bottom": 0.05,
|
|
24
|
+
"left": 0.05,
|
|
25
|
+
"right": 0.05,
|
|
26
|
+
"top": 0.05
|
|
27
|
+
},
|
|
28
|
+
"source": "LinkedIn video ad/post specs (general guidance); loudness is the common social norm",
|
|
29
|
+
"verified": false,
|
|
30
|
+
"width": 1920
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"aliases": [],
|
|
3
|
+
"aspect": "1:1",
|
|
4
|
+
"audio": {
|
|
5
|
+
"lufsTarget": -16,
|
|
6
|
+
"truePeakDb": -1
|
|
7
|
+
},
|
|
8
|
+
"codec": {
|
|
9
|
+
"audio": "aac",
|
|
10
|
+
"video": "h264"
|
|
11
|
+
},
|
|
12
|
+
"height": 1080,
|
|
13
|
+
"id": "podcast",
|
|
14
|
+
"maxFps": 30,
|
|
15
|
+
"notes": "Audio-first; the video frame is an optional audiogram/cover. -16 LUFS is the common podcast target. safe{} is the generic 5% title-safe margin.",
|
|
16
|
+
"safe": {
|
|
17
|
+
"bottom": 0.05,
|
|
18
|
+
"side": 0.05,
|
|
19
|
+
"top": 0.05
|
|
20
|
+
},
|
|
21
|
+
"safeZones": {
|
|
22
|
+
"bottom": 0.05,
|
|
23
|
+
"left": 0.05,
|
|
24
|
+
"right": 0.05,
|
|
25
|
+
"top": 0.05
|
|
26
|
+
},
|
|
27
|
+
"source": "Apple/Spotify podcast loudness convention (-16 LUFS stereo); 1:1 frame is a pragmatic default",
|
|
28
|
+
"verified": false,
|
|
29
|
+
"width": 1080
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"aliases": ["instagram-reels"],
|
|
3
|
+
"aspect": "9:16",
|
|
4
|
+
"audio": {
|
|
5
|
+
"lufsTarget": -14,
|
|
6
|
+
"truePeakDb": -1
|
|
7
|
+
},
|
|
8
|
+
"codec": {
|
|
9
|
+
"audio": "aac",
|
|
10
|
+
"video": "h264"
|
|
11
|
+
},
|
|
12
|
+
"height": 1920,
|
|
13
|
+
"id": "reels",
|
|
14
|
+
"maxDurationSec": 180,
|
|
15
|
+
"maxFps": 30,
|
|
16
|
+
"notes": "Bottom and top bands are covered by caption and profile UI; fractions are approximate. safe{} is the conservative common margin (top 15%, bottom 20%, sides 5%) shared by vertical platforms; not a per-platform measurement.",
|
|
17
|
+
"safe": {
|
|
18
|
+
"bottom": 0.2,
|
|
19
|
+
"side": 0.05,
|
|
20
|
+
"top": 0.15
|
|
21
|
+
},
|
|
22
|
+
"safeZones": {
|
|
23
|
+
"bottom": 0.2,
|
|
24
|
+
"left": 0.06,
|
|
25
|
+
"right": 0.06,
|
|
26
|
+
"top": 0.14
|
|
27
|
+
},
|
|
28
|
+
"source": "Meta Reels specs (1080x1920, 9:16); UI margins and duration cap are approximations",
|
|
29
|
+
"verified": false,
|
|
30
|
+
"width": 1080
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"aliases": ["youtube-shorts"],
|
|
3
|
+
"aspect": "9:16",
|
|
4
|
+
"audio": {
|
|
5
|
+
"lufsTarget": -14,
|
|
6
|
+
"truePeakDb": -1
|
|
7
|
+
},
|
|
8
|
+
"codec": {
|
|
9
|
+
"audio": "aac",
|
|
10
|
+
"video": "h264"
|
|
11
|
+
},
|
|
12
|
+
"height": 1920,
|
|
13
|
+
"id": "shorts",
|
|
14
|
+
"maxDurationSec": 180,
|
|
15
|
+
"maxFps": 60,
|
|
16
|
+
"notes": "Right rail holds action buttons; bottom holds title/channel. Fractions are approximate. safe{} is the conservative common margin (top 15%, bottom 20%, sides 5%) shared by vertical platforms; not a per-platform measurement.",
|
|
17
|
+
"safe": {
|
|
18
|
+
"bottom": 0.2,
|
|
19
|
+
"side": 0.05,
|
|
20
|
+
"top": 0.15
|
|
21
|
+
},
|
|
22
|
+
"safeZones": {
|
|
23
|
+
"bottom": 0.2,
|
|
24
|
+
"left": 0.05,
|
|
25
|
+
"right": 0.12,
|
|
26
|
+
"top": 0.1
|
|
27
|
+
},
|
|
28
|
+
"source": "YouTube Shorts specs (9:16, up to 3 min); UI margins approximate",
|
|
29
|
+
"verified": false,
|
|
30
|
+
"width": 1080
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"aliases": [],
|
|
3
|
+
"aspect": "9:16",
|
|
4
|
+
"audio": {
|
|
5
|
+
"lufsTarget": -14,
|
|
6
|
+
"truePeakDb": -1
|
|
7
|
+
},
|
|
8
|
+
"codec": {
|
|
9
|
+
"audio": "aac",
|
|
10
|
+
"video": "h264"
|
|
11
|
+
},
|
|
12
|
+
"height": 1920,
|
|
13
|
+
"id": "tiktok",
|
|
14
|
+
"maxDurationSec": 600,
|
|
15
|
+
"maxFps": 60,
|
|
16
|
+
"notes": "Right rail holds action buttons; bottom holds caption and music bar. Fractions are approximate. safe{} is the conservative common margin (top 15%, bottom 20%, sides 5%) shared by vertical platforms; not a per-platform measurement.",
|
|
17
|
+
"safe": {
|
|
18
|
+
"bottom": 0.2,
|
|
19
|
+
"side": 0.05,
|
|
20
|
+
"top": 0.15
|
|
21
|
+
},
|
|
22
|
+
"safeZones": {
|
|
23
|
+
"bottom": 0.22,
|
|
24
|
+
"left": 0.05,
|
|
25
|
+
"right": 0.12,
|
|
26
|
+
"top": 0.12
|
|
27
|
+
},
|
|
28
|
+
"source": "TikTok video specs (1080x1920, 9:16); duration cap and UI margins are approximations",
|
|
29
|
+
"verified": false,
|
|
30
|
+
"width": 1080
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"aliases": ["twitter"],
|
|
3
|
+
"aspect": "16:9",
|
|
4
|
+
"audio": {
|
|
5
|
+
"lufsTarget": -14,
|
|
6
|
+
"truePeakDb": -1
|
|
7
|
+
},
|
|
8
|
+
"codec": {
|
|
9
|
+
"audio": "aac",
|
|
10
|
+
"video": "h264"
|
|
11
|
+
},
|
|
12
|
+
"height": 1080,
|
|
13
|
+
"id": "x",
|
|
14
|
+
"maxDurationSec": 140,
|
|
15
|
+
"maxFps": 60,
|
|
16
|
+
"notes": "140 s applies to standard accounts; longer for subscribers. Safe zone is the generic 5% title-safe margin. safe{} is the generic 5% title-safe margin.",
|
|
17
|
+
"safe": {
|
|
18
|
+
"bottom": 0.05,
|
|
19
|
+
"side": 0.05,
|
|
20
|
+
"top": 0.05
|
|
21
|
+
},
|
|
22
|
+
"safeZones": {
|
|
23
|
+
"bottom": 0.05,
|
|
24
|
+
"left": 0.05,
|
|
25
|
+
"right": 0.05,
|
|
26
|
+
"top": 0.05
|
|
27
|
+
},
|
|
28
|
+
"source": "X media specs (general guidance); loudness is the common social norm",
|
|
29
|
+
"verified": false,
|
|
30
|
+
"width": 1920
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"aliases": [],
|
|
3
|
+
"aspect": "16:9",
|
|
4
|
+
"audio": {
|
|
5
|
+
"lufsTarget": -14,
|
|
6
|
+
"truePeakDb": -1
|
|
7
|
+
},
|
|
8
|
+
"codec": {
|
|
9
|
+
"audio": "aac",
|
|
10
|
+
"video": "h264"
|
|
11
|
+
},
|
|
12
|
+
"height": 1080,
|
|
13
|
+
"id": "youtube",
|
|
14
|
+
"maxFps": 60,
|
|
15
|
+
"notes": "YouTube normalises playback to about -14 LUFS. Safe zone is the generic 5% title-safe margin. safe{} is the generic 5% title-safe margin.",
|
|
16
|
+
"safe": {
|
|
17
|
+
"bottom": 0.05,
|
|
18
|
+
"side": 0.05,
|
|
19
|
+
"top": 0.05
|
|
20
|
+
},
|
|
21
|
+
"safeZones": {
|
|
22
|
+
"bottom": 0.05,
|
|
23
|
+
"left": 0.05,
|
|
24
|
+
"right": 0.05,
|
|
25
|
+
"top": 0.05
|
|
26
|
+
},
|
|
27
|
+
"source": "YouTube recommended upload encoding (H.264/AAC, 1080p); -14 LUFS playback normalisation",
|
|
28
|
+
"verified": true,
|
|
29
|
+
"width": 1920
|
|
30
|
+
}
|
package/dist/audio.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Voice word timings (additive, v1 — no version bump, no migration step).
|
|
4
|
+
*
|
|
5
|
+
* Binds `projects/spike-aligner/results/stage2-en-short.json`: words are
|
|
6
|
+
* `{w, start, end}` on the clip clock (seconds); the aligner emits no
|
|
7
|
+
* confidence, so it stays optional. Field name `w` matches the spike output
|
|
8
|
+
* verbatim to avoid a lossy rename at the producer boundary.
|
|
9
|
+
*/
|
|
10
|
+
export declare const WordTimingSchema: z.ZodObject<{
|
|
11
|
+
w: z.ZodString;
|
|
12
|
+
start: z.ZodNumber;
|
|
13
|
+
end: z.ZodNumber;
|
|
14
|
+
confidence: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
}, z.core.$strict>;
|
|
16
|
+
/** Word lists are sorted by start and non-overlapping (touching edges allowed). */
|
|
17
|
+
export declare const WordTimingsSchema: z.ZodArray<z.ZodObject<{
|
|
18
|
+
w: z.ZodString;
|
|
19
|
+
start: z.ZodNumber;
|
|
20
|
+
end: z.ZodNumber;
|
|
21
|
+
confidence: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
}, z.core.$strict>>;
|
|
23
|
+
/** A silence gap on the clip clock (seconds); maps `silence_intervals` pairs. */
|
|
24
|
+
export declare const SilenceIntervalSchema: z.ZodObject<{
|
|
25
|
+
start: z.ZodNumber;
|
|
26
|
+
end: z.ZodNumber;
|
|
27
|
+
}, z.core.$strict>;
|
|
28
|
+
/**
|
|
29
|
+
* Aligner output for one audio clip (additive, v1 — no version bump).
|
|
30
|
+
* `model`/`modelRevision`/`modelLicense` map the spike's
|
|
31
|
+
* `model`/`model_revision`/`model_license` to repo camelCase. `peaks` are
|
|
32
|
+
* normalized waveform peaks (0..1) for caption/scroll rendering; all fields
|
|
33
|
+
* except `words` are optional so partial analyses still parse.
|
|
34
|
+
*/
|
|
35
|
+
export declare const AudioAnalysisSchema: z.ZodObject<{
|
|
36
|
+
words: z.ZodArray<z.ZodObject<{
|
|
37
|
+
w: z.ZodString;
|
|
38
|
+
start: z.ZodNumber;
|
|
39
|
+
end: z.ZodNumber;
|
|
40
|
+
confidence: z.ZodOptional<z.ZodNumber>;
|
|
41
|
+
}, z.core.$strict>>;
|
|
42
|
+
silences: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
43
|
+
start: z.ZodNumber;
|
|
44
|
+
end: z.ZodNumber;
|
|
45
|
+
}, z.core.$strict>>>;
|
|
46
|
+
peaks: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
47
|
+
model: z.ZodOptional<z.ZodString>;
|
|
48
|
+
modelRevision: z.ZodOptional<z.ZodString>;
|
|
49
|
+
modelLicense: z.ZodOptional<z.ZodString>;
|
|
50
|
+
duration: z.ZodOptional<z.ZodNumber>;
|
|
51
|
+
}, z.core.$strict>;
|
|
52
|
+
export type WordTiming = z.output<typeof WordTimingSchema>;
|
|
53
|
+
export type SilenceInterval = z.output<typeof SilenceIntervalSchema>;
|
|
54
|
+
export type AudioAnalysis = z.output<typeof AudioAnalysisSchema>;
|
|
55
|
+
/**
|
|
56
|
+
* Opt-in user CC0 music bed: an explicit local `.wav` plus an optional
|
|
57
|
+
* ducked level (audible bed, narration stays ahead). Single source of truth for the
|
|
58
|
+
* bed guard: `@anvpx/motion-audio` (CLI validation) and `@anvpx/motion-build`
|
|
59
|
+
* (mix graph) both parse through here, so neither layer carries its own
|
|
60
|
+
* copy of the rule (constitution 2.1: both packages import schema only).
|
|
61
|
+
*/
|
|
62
|
+
export declare const DEFAULT_MUSIC_VOLUME = 0.5;
|
|
63
|
+
export declare const MusicBedSchema: z.ZodObject<{
|
|
64
|
+
wav: z.ZodString;
|
|
65
|
+
volume: z.ZodOptional<z.ZodNumber>;
|
|
66
|
+
}, z.core.$strict>;
|
|
67
|
+
export type MusicBedInput = {
|
|
68
|
+
readonly wav: string;
|
|
69
|
+
readonly volume?: number;
|
|
70
|
+
};
|
|
71
|
+
export type ValidMusicBed = {
|
|
72
|
+
readonly wav: string;
|
|
73
|
+
readonly volume: number;
|
|
74
|
+
};
|
|
75
|
+
export type MusicBedResult = {
|
|
76
|
+
ok: true;
|
|
77
|
+
bed: ValidMusicBed;
|
|
78
|
+
} | {
|
|
79
|
+
ok: false;
|
|
80
|
+
hint: string;
|
|
81
|
+
};
|
|
82
|
+
/** Parse an explicit user bed; applies the default level, never invents a path. */
|
|
83
|
+
export declare const parseMusicBed: (value: unknown) => MusicBedResult;
|
package/dist/audio.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Voice word timings (additive, v1 — no version bump, no migration step).
|
|
4
|
+
*
|
|
5
|
+
* Binds `projects/spike-aligner/results/stage2-en-short.json`: words are
|
|
6
|
+
* `{w, start, end}` on the clip clock (seconds); the aligner emits no
|
|
7
|
+
* confidence, so it stays optional. Field name `w` matches the spike output
|
|
8
|
+
* verbatim to avoid a lossy rename at the producer boundary.
|
|
9
|
+
*/
|
|
10
|
+
export const WordTimingSchema = z
|
|
11
|
+
.strictObject({
|
|
12
|
+
w: z.string().min(1),
|
|
13
|
+
start: z.number().min(0),
|
|
14
|
+
end: z.number().positive(),
|
|
15
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
16
|
+
})
|
|
17
|
+
.refine((word) => word.end > word.start, "end must be after start");
|
|
18
|
+
/** Word lists are sorted by start and non-overlapping (touching edges allowed). */
|
|
19
|
+
export const WordTimingsSchema = z.array(WordTimingSchema).superRefine((words, ctx) => {
|
|
20
|
+
words.forEach((word, index) => {
|
|
21
|
+
const prev = words[index - 1];
|
|
22
|
+
if (prev && word.start < prev.end) {
|
|
23
|
+
ctx.addIssue({
|
|
24
|
+
code: "custom",
|
|
25
|
+
path: [index, "start"],
|
|
26
|
+
message: `word "${word.w}" overlaps "${prev.w}"`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
/** A silence gap on the clip clock (seconds); maps `silence_intervals` pairs. */
|
|
32
|
+
export const SilenceIntervalSchema = z
|
|
33
|
+
.strictObject({
|
|
34
|
+
start: z.number().min(0),
|
|
35
|
+
end: z.number().positive(),
|
|
36
|
+
})
|
|
37
|
+
.refine((gap) => gap.end > gap.start, "end must be after start");
|
|
38
|
+
/**
|
|
39
|
+
* Aligner output for one audio clip (additive, v1 — no version bump).
|
|
40
|
+
* `model`/`modelRevision`/`modelLicense` map the spike's
|
|
41
|
+
* `model`/`model_revision`/`model_license` to repo camelCase. `peaks` are
|
|
42
|
+
* normalized waveform peaks (0..1) for caption/scroll rendering; all fields
|
|
43
|
+
* except `words` are optional so partial analyses still parse.
|
|
44
|
+
*/
|
|
45
|
+
export const AudioAnalysisSchema = z.strictObject({
|
|
46
|
+
words: WordTimingsSchema.min(1),
|
|
47
|
+
silences: z.array(SilenceIntervalSchema).optional(),
|
|
48
|
+
peaks: z.array(z.number().min(0).max(1)).optional(),
|
|
49
|
+
model: z.string().min(1).optional(),
|
|
50
|
+
modelRevision: z.string().min(1).optional(),
|
|
51
|
+
modelLicense: z.string().min(1).optional(),
|
|
52
|
+
duration: z.number().positive().optional(),
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Opt-in user CC0 music bed: an explicit local `.wav` plus an optional
|
|
56
|
+
* ducked level (audible bed, narration stays ahead). Single source of truth for the
|
|
57
|
+
* bed guard: `@anvpx/motion-audio` (CLI validation) and `@anvpx/motion-build`
|
|
58
|
+
* (mix graph) both parse through here, so neither layer carries its own
|
|
59
|
+
* copy of the rule (constitution 2.1: both packages import schema only).
|
|
60
|
+
*/
|
|
61
|
+
export const DEFAULT_MUSIC_VOLUME = 0.5;
|
|
62
|
+
export const MusicBedSchema = z
|
|
63
|
+
.strictObject({
|
|
64
|
+
wav: z.string().min(1),
|
|
65
|
+
volume: z
|
|
66
|
+
.number()
|
|
67
|
+
.refine((n) => Number.isFinite(n) && n > 0 && n <= 1, "must be within (0, 1]")
|
|
68
|
+
.optional(),
|
|
69
|
+
})
|
|
70
|
+
.refine((bed) => bed.wav.endsWith(".wav") && !bed.wav.includes(".."), {
|
|
71
|
+
message: "must be a local .wav without traversal",
|
|
72
|
+
path: ["wav"],
|
|
73
|
+
});
|
|
74
|
+
/** Parse an explicit user bed; applies the default level, never invents a path. */
|
|
75
|
+
export const parseMusicBed = (value) => {
|
|
76
|
+
const parsed = MusicBedSchema.safeParse(value);
|
|
77
|
+
if (!parsed.success) {
|
|
78
|
+
const problems = parsed.error.issues
|
|
79
|
+
.map((issue) => `${issue.path.join("/")}: ${issue.message}`)
|
|
80
|
+
.join("; ");
|
|
81
|
+
return { ok: false, hint: `invalid music bed: ${problems}` };
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
bed: { wav: parsed.data.wav, volume: parsed.data.volume ?? DEFAULT_MUSIC_VOLUME },
|
|
86
|
+
};
|
|
87
|
+
};
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const sortDeep = (value) => {
|
|
2
|
+
if (Array.isArray(value))
|
|
3
|
+
return value.map(sortDeep);
|
|
4
|
+
if (typeof value !== "object" || value === null)
|
|
5
|
+
return value;
|
|
6
|
+
return Object.fromEntries(Object.keys(value)
|
|
7
|
+
.sort()
|
|
8
|
+
.map((key) => [key, sortDeep(Reflect.get(value, key))]));
|
|
9
|
+
};
|
|
10
|
+
/** Deterministic JSON: keys sorted by UTF-16 code unit at every depth, 2-space indent, final newline. */
|
|
11
|
+
export const canonicalJson = (value) => `${JSON.stringify(sortDeep(value), null, 2)}\n`;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ZodType } from "zod";
|
|
2
|
+
import type { SceneEntry } from "./storyboard.ts";
|
|
3
|
+
import type { ResolvedTheme } from "./theme.ts";
|
|
4
|
+
export interface EmitContext {
|
|
5
|
+
readonly theme: ResolvedTheme;
|
|
6
|
+
readonly fps: number;
|
|
7
|
+
readonly width: number;
|
|
8
|
+
readonly height: number;
|
|
9
|
+
/**
|
|
10
|
+
* Platform UI bands as fractions of the frame (top, right, bottom, left); scene padding is raised to
|
|
11
|
+
* at least these. Absent: the plain 5 % margin.
|
|
12
|
+
*/
|
|
13
|
+
readonly insets?: {
|
|
14
|
+
readonly top: number;
|
|
15
|
+
readonly right: number;
|
|
16
|
+
readonly bottom: number;
|
|
17
|
+
readonly left: number;
|
|
18
|
+
};
|
|
19
|
+
/** Scene duration in seconds. */
|
|
20
|
+
readonly duration: number;
|
|
21
|
+
/** Absolute placement of this scene on the final timeline (seconds). */
|
|
22
|
+
readonly timeline?: {
|
|
23
|
+
readonly start: number;
|
|
24
|
+
readonly total: number;
|
|
25
|
+
};
|
|
26
|
+
assetResolver(ref: string): string;
|
|
27
|
+
}
|
|
28
|
+
/** Catalog documentation of a scene; fields and types are derived from `configSchema`. */
|
|
29
|
+
export interface SceneMeta {
|
|
30
|
+
/** One line: what the scene is for. */
|
|
31
|
+
readonly purpose: string;
|
|
32
|
+
/** Smallest props object that passes `configSchema`. */
|
|
33
|
+
readonly example: Readonly<Record<string, unknown>>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A browser library a scene may import by bare specifier. The build copies the library's ESM entry
|
|
37
|
+
* into the emitted project under `vendor/` and writes an import map, so `import ... from "three"`
|
|
38
|
+
* resolves offline with no CDN and no bundler (ADR-0006).
|
|
39
|
+
*/
|
|
40
|
+
export type VendorLib = "three" | "postprocessing";
|
|
41
|
+
export interface ScenePlugin {
|
|
42
|
+
readonly type: string;
|
|
43
|
+
/** "overlay" scenes are transparent layers and only valid with `layer: "overlay"`; omitted = base. */
|
|
44
|
+
readonly layer?: "overlay";
|
|
45
|
+
readonly configSchema: ZodType;
|
|
46
|
+
/** Optional catalog documentation (`mk catalog`). */
|
|
47
|
+
readonly meta?: SceneMeta;
|
|
48
|
+
/** Vendored browser libraries this scene imports; omitted = none, and no `vendor/` is emitted. */
|
|
49
|
+
readonly vendor?: readonly VendorLib[];
|
|
50
|
+
/** Returns output path -> file content. */
|
|
51
|
+
emit(entry: SceneEntry, ctx: EmitContext): Map<string, string>;
|
|
52
|
+
}
|
|
53
|
+
/** Runtime contract of the `createScene` factory that emitted scenes export. */
|
|
54
|
+
export interface SceneRuntime {
|
|
55
|
+
seek(t: number): void | Promise<void>;
|
|
56
|
+
dispose(): void;
|
|
57
|
+
readonly ready: Promise<void>;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Terse, fixable error: JSON pointer path + what is wrong + how to fix it. */
|
|
2
|
+
export interface ValidationError {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly message: string;
|
|
5
|
+
readonly suggestion?: string;
|
|
6
|
+
}
|
|
7
|
+
export type Result<T> = {
|
|
8
|
+
readonly ok: true;
|
|
9
|
+
readonly value: T;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly errors: readonly ValidationError[];
|
|
13
|
+
};
|
|
14
|
+
/** RFC 6901 pointer; the root document is the empty string. */
|
|
15
|
+
export declare const toJsonPointer: (path: readonly PropertyKey[]) => string;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
const escapePointerSegment = (segment) => String(segment).replaceAll("~", "~0").replaceAll("/", "~1");
|
|
2
|
+
/** RFC 6901 pointer; the root document is the empty string. */
|
|
3
|
+
export const toJsonPointer = (path) => path.map((segment) => `/${escapePointerSegment(segment)}`).join("");
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { type AudioAnalysis, AudioAnalysisSchema, DEFAULT_MUSIC_VOLUME, type MusicBedInput, type MusicBedResult, MusicBedSchema, parseMusicBed, type SilenceInterval, SilenceIntervalSchema, type ValidMusicBed, type WordTiming, WordTimingSchema, WordTimingsSchema, } from "./audio.ts";
|
|
2
|
+
export { canonicalJson } from "./contract.ts";
|
|
3
|
+
export type { EmitContext, SceneMeta, ScenePlugin, SceneRuntime, VendorLib, } from "./contracts.ts";
|
|
4
|
+
export { type Result, toJsonPointer, type ValidationError } from "./errors.ts";
|
|
5
|
+
export { storyboardJsonSchema } from "./json-schema.ts";
|
|
6
|
+
export { type AssetLedger, AssetLedgerSchema, type AssetRow, AssetRowSchema, renderAssetsMarkdown, } from "./ledger.ts";
|
|
7
|
+
export { type PlatformPreset, PlatformPresetSchema, type PlatformTemplate, PlatformTemplateSchema, parsePlatformPreset, parsePlatformTemplate, } from "./platform.ts";
|
|
8
|
+
export { forbiddenKeys } from "./reserved-keys.ts";
|
|
9
|
+
export { APPROVALS, type Approval, EASES, LAYERS, MOVES, type SceneEntry, SceneEntryBaseSchema, SHOTS, STORYBOARD_VERSION, type Storyboard, type StoryboardMeta, StoryboardSchema, TRANSITIONS, } from "./storyboard.ts";
|
|
10
|
+
export { type BlueprintDiscover, BlueprintDiscoverSchema, type BlueprintDoc, BlueprintDocSchema, type BlueprintRepeat, BlueprintRepeatSchema, type BlueprintScene, BlueprintSceneSchema, LENGTH_CLASSES, type ManifestDiscover, ManifestDiscoverSchema, type PacingDoc, PacingDocSchema, parseBlueprintDoc, parsePacingDoc, parseStylePreset, parseTemplateManifest, SLOT_TRANSFORMS, type SlotDef, SlotSchema, type StylePreset, StylePresetSchema, TEMPLATE_VERSION, type TemplateManifest, TemplateManifestSchema, templateJsonSchemas, } from "./template.ts";
|
|
11
|
+
export type { ResolvedTheme, TextPair } from "./theme.ts";
|
|
12
|
+
export { TIMING_SOURCES, type TimingManifest, TimingManifestSchema } from "./timing.ts";
|
|
13
|
+
export { validateStoryboard } from "./validate.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { AudioAnalysisSchema, DEFAULT_MUSIC_VOLUME, MusicBedSchema, parseMusicBed, SilenceIntervalSchema, WordTimingSchema, WordTimingsSchema, } from "./audio.js";
|
|
2
|
+
export { canonicalJson } from "./contract.js";
|
|
3
|
+
export { toJsonPointer } from "./errors.js";
|
|
4
|
+
export { storyboardJsonSchema } from "./json-schema.js";
|
|
5
|
+
export { AssetLedgerSchema, AssetRowSchema, renderAssetsMarkdown, } from "./ledger.js";
|
|
6
|
+
export { PlatformPresetSchema, PlatformTemplateSchema, parsePlatformPreset, parsePlatformTemplate, } from "./platform.js";
|
|
7
|
+
export { forbiddenKeys } from "./reserved-keys.js";
|
|
8
|
+
export { APPROVALS, EASES, LAYERS, MOVES, SceneEntryBaseSchema, SHOTS, STORYBOARD_VERSION, StoryboardSchema, TRANSITIONS, } from "./storyboard.js";
|
|
9
|
+
export { BlueprintDiscoverSchema, BlueprintDocSchema, BlueprintRepeatSchema, BlueprintSceneSchema, LENGTH_CLASSES, ManifestDiscoverSchema, PacingDocSchema, parseBlueprintDoc, parsePacingDoc, parseStylePreset, parseTemplateManifest, SLOT_TRANSFORMS, SlotSchema, StylePresetSchema, TEMPLATE_VERSION, TemplateManifestSchema, templateJsonSchemas, } from "./template.js";
|
|
10
|
+
export { TIMING_SOURCES, TimingManifestSchema } from "./timing.js";
|
|
11
|
+
export { validateStoryboard } from "./validate.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { StoryboardSchema } from "./storyboard.js";
|
|
3
|
+
/** Duplicate scene ids are a cross-field rule JSON Schema cannot express; `validateStoryboard` enforces it. */
|
|
4
|
+
export const storyboardJsonSchema = () => z.toJSONSchema(StoryboardSchema, { io: "input" });
|
package/dist/ledger.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const AssetRowSchema: z.ZodObject<{
|
|
3
|
+
path: z.ZodString;
|
|
4
|
+
sourceUrl: z.ZodURL;
|
|
5
|
+
licence: z.ZodString;
|
|
6
|
+
attribution: z.ZodOptional<z.ZodString>;
|
|
7
|
+
sha256: z.ZodString;
|
|
8
|
+
}, z.core.$strict>;
|
|
9
|
+
export declare const AssetLedgerSchema: z.ZodArray<z.ZodObject<{
|
|
10
|
+
path: z.ZodString;
|
|
11
|
+
sourceUrl: z.ZodURL;
|
|
12
|
+
licence: z.ZodString;
|
|
13
|
+
attribution: z.ZodOptional<z.ZodString>;
|
|
14
|
+
sha256: z.ZodString;
|
|
15
|
+
}, z.core.$strict>>;
|
|
16
|
+
export type AssetRow = z.output<typeof AssetRowSchema>;
|
|
17
|
+
export type AssetLedger = z.output<typeof AssetLedgerSchema>;
|
|
18
|
+
/** Pure and deterministic: rows sorted by path so output is diff-stable. */
|
|
19
|
+
export declare const renderAssetsMarkdown: (ledger: AssetLedger) => string;
|
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const AssetRowSchema = z.strictObject({
|
|
3
|
+
path: z.string().min(1),
|
|
4
|
+
sourceUrl: z.url(),
|
|
5
|
+
licence: z.string().min(1),
|
|
6
|
+
attribution: z.string().min(1).optional(),
|
|
7
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/, "must be 64 lowercase hex characters"),
|
|
8
|
+
});
|
|
9
|
+
export const AssetLedgerSchema = z.array(AssetRowSchema).superRefine((rows, ctx) => {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
rows.forEach((row, index) => {
|
|
12
|
+
if (seen.has(row.path)) {
|
|
13
|
+
ctx.addIssue({
|
|
14
|
+
code: "custom",
|
|
15
|
+
path: [index, "path"],
|
|
16
|
+
message: `duplicate ledger path "${row.path}"`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
seen.add(row.path);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
/** Locale-independent UTF-16 code-unit order, so output is byte-identical everywhere. */
|
|
23
|
+
const byPath = (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
|
|
24
|
+
const escapeCell = (text) => text.replaceAll("|", "\\|");
|
|
25
|
+
const renderRow = (row) => {
|
|
26
|
+
const cells = [row.path, row.sourceUrl, row.licence, row.attribution ?? "", row.sha256];
|
|
27
|
+
return `| ${cells.map(escapeCell).join(" | ")} |`;
|
|
28
|
+
};
|
|
29
|
+
/** Pure and deterministic: rows sorted by path so output is diff-stable. */
|
|
30
|
+
export const renderAssetsMarkdown = (ledger) => {
|
|
31
|
+
const rows = [...ledger].sort(byPath).map(renderRow);
|
|
32
|
+
return [
|
|
33
|
+
"# Assets",
|
|
34
|
+
"",
|
|
35
|
+
"Generated from `assets.json`. Do not edit.",
|
|
36
|
+
"",
|
|
37
|
+
"| Path | Source | Licence | Attribution | SHA-256 |",
|
|
38
|
+
"| --- | --- | --- | --- | --- |",
|
|
39
|
+
...rows,
|
|
40
|
+
"",
|
|
41
|
+
].join("\n");
|
|
42
|
+
};
|