@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.
@@ -0,0 +1,77 @@
1
+ import { z } from "zod";
2
+ import { type Result } from "./errors.ts";
3
+ /** Delivery-platform preset: frame, codec, loudness and UI-safe margins (fractions of the frame). */
4
+ export declare const PlatformPresetSchema: z.ZodObject<{
5
+ id: z.ZodString;
6
+ aliases: z.ZodArray<z.ZodString>;
7
+ aspect: z.ZodString;
8
+ width: z.ZodNumber;
9
+ height: z.ZodNumber;
10
+ maxFps: z.ZodNumber;
11
+ maxDurationSec: z.ZodOptional<z.ZodNumber>;
12
+ codec: z.ZodObject<{
13
+ video: z.ZodString;
14
+ audio: z.ZodString;
15
+ }, z.core.$strict>;
16
+ audio: z.ZodObject<{
17
+ lufsTarget: z.ZodNumber;
18
+ truePeakDb: z.ZodNumber;
19
+ }, z.core.$strict>;
20
+ safeZones: z.ZodObject<{
21
+ top: z.ZodNumber;
22
+ right: z.ZodNumber;
23
+ bottom: z.ZodNumber;
24
+ left: z.ZodNumber;
25
+ }, z.core.$strict>;
26
+ safe: z.ZodOptional<z.ZodObject<{
27
+ top: z.ZodNumber;
28
+ bottom: z.ZodNumber;
29
+ side: z.ZodNumber;
30
+ }, z.core.$strict>>;
31
+ notes: z.ZodOptional<z.ZodString>;
32
+ source: z.ZodString;
33
+ verified: z.ZodBoolean;
34
+ }, z.core.$strict>;
35
+ export type PlatformPreset = z.infer<typeof PlatformPresetSchema>;
36
+ /**
37
+ * PLAN 7f.4: `PlatformTemplate` clashed with the template contract (`TemplateManifest` et al).
38
+ * Canonical name is now `PlatformPreset` (platform delivery preset, not an authoring template).
39
+ * The `PlatformTemplate*` names below are deprecated aliases kept for the CLI + existing
40
+ * imports; remove them only with a major version + migration note. No data change.
41
+ */
42
+ export declare const PlatformTemplateSchema: z.ZodObject<{
43
+ id: z.ZodString;
44
+ aliases: z.ZodArray<z.ZodString>;
45
+ aspect: z.ZodString;
46
+ width: z.ZodNumber;
47
+ height: z.ZodNumber;
48
+ maxFps: z.ZodNumber;
49
+ maxDurationSec: z.ZodOptional<z.ZodNumber>;
50
+ codec: z.ZodObject<{
51
+ video: z.ZodString;
52
+ audio: z.ZodString;
53
+ }, z.core.$strict>;
54
+ audio: z.ZodObject<{
55
+ lufsTarget: z.ZodNumber;
56
+ truePeakDb: z.ZodNumber;
57
+ }, z.core.$strict>;
58
+ safeZones: z.ZodObject<{
59
+ top: z.ZodNumber;
60
+ right: z.ZodNumber;
61
+ bottom: z.ZodNumber;
62
+ left: z.ZodNumber;
63
+ }, z.core.$strict>;
64
+ safe: z.ZodOptional<z.ZodObject<{
65
+ top: z.ZodNumber;
66
+ bottom: z.ZodNumber;
67
+ side: z.ZodNumber;
68
+ }, z.core.$strict>>;
69
+ notes: z.ZodOptional<z.ZodString>;
70
+ source: z.ZodString;
71
+ verified: z.ZodBoolean;
72
+ }, z.core.$strict>;
73
+ export type PlatformTemplate = PlatformPreset;
74
+ /** Registry-free: validates one preset document; callers decide where it came from. */
75
+ export declare const parsePlatformPreset: (input: unknown) => Result<PlatformPreset>;
76
+ /** @deprecated Use `parsePlatformPreset`; alias kept so old imports stay valid. */
77
+ export declare const parsePlatformTemplate: typeof parsePlatformPreset;
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ import { toJsonPointer } from "./errors.js";
3
+ const fraction = z.number().min(0).max(1);
4
+ /** Delivery-platform preset: frame, codec, loudness and UI-safe margins (fractions of the frame). */
5
+ export const PlatformPresetSchema = z.strictObject({
6
+ id: z.string().regex(/^[a-z][a-z0-9-]*$/),
7
+ aliases: z.array(z.string().regex(/^[a-z][a-z0-9-]*$/)),
8
+ aspect: z.string().regex(/^\d+:\d+$/),
9
+ width: z.number().int().positive(),
10
+ height: z.number().int().positive(),
11
+ maxFps: z.number().int().positive(),
12
+ maxDurationSec: z.number().positive().optional(),
13
+ codec: z.strictObject({ video: z.string().min(1), audio: z.string().min(1) }),
14
+ audio: z.strictObject({ lufsTarget: z.number().max(0), truePeakDb: z.number().max(0) }),
15
+ safeZones: z.strictObject({
16
+ top: fraction,
17
+ right: fraction,
18
+ bottom: fraction,
19
+ left: fraction,
20
+ }),
21
+ /**
22
+ * Conservative margins (fractions) that base text must stay outside: `side` applies to left and right.
23
+ * Common values, not per-platform measurements; `safeZones` holds the platform-specific UI bands.
24
+ */
25
+ safe: z.strictObject({ top: fraction, bottom: fraction, side: fraction }).optional(),
26
+ notes: z.string().optional(),
27
+ source: z.string().min(1),
28
+ verified: z.boolean(),
29
+ });
30
+ /**
31
+ * PLAN 7f.4: `PlatformTemplate` clashed with the template contract (`TemplateManifest` et al).
32
+ * Canonical name is now `PlatformPreset` (platform delivery preset, not an authoring template).
33
+ * The `PlatformTemplate*` names below are deprecated aliases kept for the CLI + existing
34
+ * imports; remove them only with a major version + migration note. No data change.
35
+ */
36
+ export const PlatformTemplateSchema = PlatformPresetSchema;
37
+ /** Registry-free: validates one preset document; callers decide where it came from. */
38
+ export const parsePlatformPreset = (input) => {
39
+ const parsed = PlatformPresetSchema.safeParse(input);
40
+ if (parsed.success)
41
+ return { ok: true, value: parsed.data };
42
+ return {
43
+ ok: false,
44
+ errors: parsed.error.issues.map((issue) => ({
45
+ path: toJsonPointer(issue.path),
46
+ message: issue.message,
47
+ })),
48
+ };
49
+ };
50
+ /** @deprecated Use `parsePlatformPreset`; alias kept so old imports stay valid. */
51
+ export const parsePlatformTemplate = parsePlatformPreset;
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ export declare const RESERVED_MESSAGE = "reserved key is not allowed";
3
+ /** Record key schema; zod silently drops an own `__proto__` key, so raw input is also scanned. */
4
+ export declare const reservedKey: z.ZodString;
5
+ type Path = (string | number)[];
6
+ export interface ForbiddenKey {
7
+ readonly path: Path;
8
+ readonly message: string;
9
+ readonly suggestion?: string;
10
+ }
11
+ /** Forbidden keys (prototype-polluting or execution-shaped) anywhere inside a value. */
12
+ export declare const forbiddenKeys: (value: unknown, path: Path) => ForbiddenKey[];
13
+ export {};
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ export const RESERVED_MESSAGE = "reserved key is not allowed";
3
+ const RESERVED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
4
+ /** Execution-shaped keys: storyboards are data, never commands. */
5
+ const EXECUTION_KEYS = new Set([
6
+ "command",
7
+ "argv",
8
+ "shell",
9
+ "filter",
10
+ "executable",
11
+ "env",
12
+ ]);
13
+ /** Record key schema; zod silently drops an own `__proto__` key, so raw input is also scanned. */
14
+ export const reservedKey = z.string().refine((key) => !RESERVED_KEYS.has(key), RESERVED_MESSAGE);
15
+ const forbiddenAt = (path, key) => {
16
+ if (RESERVED_KEYS.has(key))
17
+ return [{ path, message: RESERVED_MESSAGE }];
18
+ if (EXECUTION_KEYS.has(key)) {
19
+ return [
20
+ {
21
+ path,
22
+ message: `forbidden key "${key}": storyboards are data only, never commands`,
23
+ suggestion: `remove "${key}"`,
24
+ },
25
+ ];
26
+ }
27
+ return [];
28
+ };
29
+ /** Forbidden keys (prototype-polluting or execution-shaped) anywhere inside a value. */
30
+ export const forbiddenKeys = (value, path) => {
31
+ if (Array.isArray(value)) {
32
+ return value.flatMap((item, index) => forbiddenKeys(item, [...path, index]));
33
+ }
34
+ if (typeof value !== "object" || value === null)
35
+ return [];
36
+ return Object.entries(value).flatMap(([key, child]) => [
37
+ ...forbiddenAt([...path, key], key),
38
+ ...forbiddenKeys(child, [...path, key]),
39
+ ]);
40
+ };
@@ -0,0 +1,147 @@
1
+ import { z } from "zod";
2
+ import { STORYBOARD_VERSION } from "./version.ts";
3
+ export { STORYBOARD_VERSION };
4
+ export declare const SHOTS: readonly ["wide", "medium", "close", "overhead"];
5
+ export declare const MOVES: readonly ["static", "push-in", "pull-out", "pan-l", "pan-r", "tilt", "orbit", "reveal"];
6
+ export declare const EASES: readonly ["linear", "in", "out", "in-out", "spring-soft", "spring-snappy"];
7
+ export declare const TRANSITIONS: readonly ["cut", "crossfade", "wipe", "push", "zoom-through"];
8
+ export declare const LAYERS: readonly ["base", "overlay"];
9
+ /** Render-gate policy twin of the sidecar approval (PLAN 7d.3): absent means "auto". */
10
+ export declare const APPROVALS: readonly ["auto", "confirm"];
11
+ export type Approval = (typeof APPROVALS)[number];
12
+ export declare const SLUG: RegExp;
13
+ export declare const SLUG_MESSAGE = "must be a lowercase slug (a-z, 0-9, hyphens)";
14
+ export declare const SceneEntryBaseSchema: z.ZodObject<{
15
+ id: z.ZodString;
16
+ type: z.ZodString;
17
+ duration: z.ZodOptional<z.ZodNumber>;
18
+ narration: z.ZodOptional<z.ZodString>;
19
+ words: z.ZodOptional<z.ZodArray<z.ZodObject<{
20
+ w: z.ZodString;
21
+ start: z.ZodNumber;
22
+ end: z.ZodNumber;
23
+ confidence: z.ZodOptional<z.ZodNumber>;
24
+ }, z.core.$strict>>>;
25
+ props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
26
+ shot: z.ZodOptional<z.ZodEnum<{
27
+ close: "close";
28
+ medium: "medium";
29
+ overhead: "overhead";
30
+ wide: "wide";
31
+ }>>;
32
+ move: z.ZodOptional<z.ZodEnum<{
33
+ orbit: "orbit";
34
+ "pan-l": "pan-l";
35
+ "pan-r": "pan-r";
36
+ "pull-out": "pull-out";
37
+ "push-in": "push-in";
38
+ reveal: "reveal";
39
+ static: "static";
40
+ tilt: "tilt";
41
+ }>>;
42
+ ease: z.ZodOptional<z.ZodEnum<{
43
+ in: "in";
44
+ "in-out": "in-out";
45
+ linear: "linear";
46
+ out: "out";
47
+ "spring-snappy": "spring-snappy";
48
+ "spring-soft": "spring-soft";
49
+ }>>;
50
+ transition: z.ZodOptional<z.ZodEnum<{
51
+ crossfade: "crossfade";
52
+ cut: "cut";
53
+ push: "push";
54
+ wipe: "wipe";
55
+ "zoom-through": "zoom-through";
56
+ }>>;
57
+ focus: z.ZodOptional<z.ZodString>;
58
+ layer: z.ZodOptional<z.ZodEnum<{
59
+ base: "base";
60
+ overlay: "overlay";
61
+ }>>;
62
+ over: z.ZodOptional<z.ZodString>;
63
+ offset: z.ZodOptional<z.ZodNumber>;
64
+ }, z.core.$strict>;
65
+ export declare const StoryboardMetaSchema: z.ZodObject<{
66
+ title: z.ZodString;
67
+ fps: z.ZodDefault<z.ZodNumber>;
68
+ width: z.ZodDefault<z.ZodNumber>;
69
+ height: z.ZodDefault<z.ZodNumber>;
70
+ theme: z.ZodDefault<z.ZodString>;
71
+ lang: z.ZodOptional<z.ZodString>;
72
+ platform: z.ZodOptional<z.ZodString>;
73
+ approval: z.ZodOptional<z.ZodEnum<{
74
+ auto: "auto";
75
+ confirm: "confirm";
76
+ }>>;
77
+ }, z.core.$strict>;
78
+ export declare const StoryboardSchema: z.ZodObject<{
79
+ version: z.ZodLiteral<1>;
80
+ meta: z.ZodObject<{
81
+ title: z.ZodString;
82
+ fps: z.ZodDefault<z.ZodNumber>;
83
+ width: z.ZodDefault<z.ZodNumber>;
84
+ height: z.ZodDefault<z.ZodNumber>;
85
+ theme: z.ZodDefault<z.ZodString>;
86
+ lang: z.ZodOptional<z.ZodString>;
87
+ platform: z.ZodOptional<z.ZodString>;
88
+ approval: z.ZodOptional<z.ZodEnum<{
89
+ auto: "auto";
90
+ confirm: "confirm";
91
+ }>>;
92
+ }, z.core.$strict>;
93
+ scenes: z.ZodArray<z.ZodObject<{
94
+ id: z.ZodString;
95
+ type: z.ZodString;
96
+ duration: z.ZodOptional<z.ZodNumber>;
97
+ narration: z.ZodOptional<z.ZodString>;
98
+ words: z.ZodOptional<z.ZodArray<z.ZodObject<{
99
+ w: z.ZodString;
100
+ start: z.ZodNumber;
101
+ end: z.ZodNumber;
102
+ confidence: z.ZodOptional<z.ZodNumber>;
103
+ }, z.core.$strict>>>;
104
+ props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
105
+ shot: z.ZodOptional<z.ZodEnum<{
106
+ close: "close";
107
+ medium: "medium";
108
+ overhead: "overhead";
109
+ wide: "wide";
110
+ }>>;
111
+ move: z.ZodOptional<z.ZodEnum<{
112
+ orbit: "orbit";
113
+ "pan-l": "pan-l";
114
+ "pan-r": "pan-r";
115
+ "pull-out": "pull-out";
116
+ "push-in": "push-in";
117
+ reveal: "reveal";
118
+ static: "static";
119
+ tilt: "tilt";
120
+ }>>;
121
+ ease: z.ZodOptional<z.ZodEnum<{
122
+ in: "in";
123
+ "in-out": "in-out";
124
+ linear: "linear";
125
+ out: "out";
126
+ "spring-snappy": "spring-snappy";
127
+ "spring-soft": "spring-soft";
128
+ }>>;
129
+ transition: z.ZodOptional<z.ZodEnum<{
130
+ crossfade: "crossfade";
131
+ cut: "cut";
132
+ push: "push";
133
+ wipe: "wipe";
134
+ "zoom-through": "zoom-through";
135
+ }>>;
136
+ focus: z.ZodOptional<z.ZodString>;
137
+ layer: z.ZodOptional<z.ZodEnum<{
138
+ base: "base";
139
+ overlay: "overlay";
140
+ }>>;
141
+ over: z.ZodOptional<z.ZodString>;
142
+ offset: z.ZodOptional<z.ZodNumber>;
143
+ }, z.core.$strict>>;
144
+ }, z.core.$strict>;
145
+ export type SceneEntry = z.output<typeof SceneEntryBaseSchema>;
146
+ export type StoryboardMeta = z.output<typeof StoryboardMetaSchema>;
147
+ export type Storyboard = z.output<typeof StoryboardSchema>;
@@ -0,0 +1,136 @@
1
+ import { z } from "zod";
2
+ import { WordTimingsSchema } from "./audio.js";
3
+ import { forbiddenKeys, reservedKey } from "./reserved-keys.js";
4
+ import { STORYBOARD_VERSION } from "./version.js";
5
+ export { STORYBOARD_VERSION };
6
+ export const SHOTS = ["wide", "medium", "close", "overhead"];
7
+ export const MOVES = [
8
+ "static",
9
+ "push-in",
10
+ "pull-out",
11
+ "pan-l",
12
+ "pan-r",
13
+ "tilt",
14
+ "orbit",
15
+ "reveal",
16
+ ];
17
+ export const EASES = ["linear", "in", "out", "in-out", "spring-soft", "spring-snappy"];
18
+ export const TRANSITIONS = ["cut", "crossfade", "wipe", "push", "zoom-through"];
19
+ export const LAYERS = ["base", "overlay"];
20
+ /** Render-gate policy twin of the sidecar approval (PLAN 7d.3): absent means "auto". */
21
+ export const APPROVALS = ["auto", "confirm"];
22
+ export const SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/;
23
+ export const SLUG_MESSAGE = "must be a lowercase slug (a-z, 0-9, hyphens)";
24
+ export const SceneEntryBaseSchema = z.strictObject({
25
+ id: z.string().regex(SLUG, SLUG_MESSAGE),
26
+ type: z.string().min(1),
27
+ duration: z.number().positive().optional(),
28
+ narration: z.string().optional(),
29
+ /** Voice word timings for the narration (aligner output, same order as spoken). Additive, v1. */
30
+ words: WordTimingsSchema.optional(),
31
+ props: z.record(reservedKey, z.unknown()),
32
+ shot: z.enum(SHOTS).optional(),
33
+ move: z.enum(MOVES).optional(),
34
+ ease: z.enum(EASES).optional(),
35
+ transition: z.enum(TRANSITIONS).optional(),
36
+ focus: z.string().min(1).optional(),
37
+ /** "overlay" stacks above the base timeline and does not advance it (default "base"). */
38
+ layer: z.enum(LAYERS).optional(),
39
+ /** Overlay only: id of the base scene it is placed over. */
40
+ over: z.string().regex(SLUG, SLUG_MESSAGE).optional(),
41
+ /** Overlay only: seconds after the start of the `over` scene (default 0). */
42
+ offset: z.number().min(0).optional(),
43
+ });
44
+ export const StoryboardMetaSchema = z.strictObject({
45
+ title: z.string().min(1),
46
+ fps: z.number().int().positive().default(30),
47
+ width: z.number().int().positive().default(1920),
48
+ height: z.number().int().positive().default(1080),
49
+ theme: z.string().min(1).default("default"),
50
+ lang: z.string().min(2).optional(),
51
+ /** Delivery-platform id (packages/schema/data/platforms); lets lint apply that platform's safe margins. */
52
+ platform: z.string().regex(SLUG, SLUG_MESSAGE).optional(),
53
+ /**
54
+ * Render-gate policy (PLAN 7d.3 schema twin of the `.mk approval.json` sidecar):
55
+ * "auto" renders without a gate, "confirm" requires `mk approve` + `mk render --gated`.
56
+ * Optional, absent means "auto" (current behaviour); additive, no version bump.
57
+ */
58
+ approval: z.enum(APPROVALS).optional(),
59
+ });
60
+ const overlayProblems = (scene) => {
61
+ if (scene.layer !== "overlay") {
62
+ return ["over", "offset"]
63
+ .filter((field) => Reflect.get(scene, field) !== undefined)
64
+ .map((field) => ({
65
+ field,
66
+ message: 'only overlays take over/offset; set layer: "overlay"',
67
+ suggestion: 'remove it or set layer: "overlay"',
68
+ }));
69
+ }
70
+ const problems = [];
71
+ if (scene.over === undefined) {
72
+ problems.push({
73
+ field: "over",
74
+ message: "overlay needs over: <base scene id>",
75
+ suggestion: "set over to the id of the base scene it covers",
76
+ });
77
+ }
78
+ if (scene.duration === undefined) {
79
+ problems.push({
80
+ field: "duration",
81
+ message: "overlay needs its own duration",
82
+ suggestion: "set duration in seconds (must fit inside the base scene)",
83
+ });
84
+ }
85
+ if (scene.transition !== undefined) {
86
+ problems.push({
87
+ field: "transition",
88
+ message: "overlays do not take a transition",
89
+ suggestion: "remove transition; overlays fade in and out themselves",
90
+ });
91
+ }
92
+ return problems;
93
+ };
94
+ export const StoryboardSchema = z
95
+ .strictObject({
96
+ version: z.literal(STORYBOARD_VERSION),
97
+ meta: StoryboardMetaSchema,
98
+ scenes: z.array(SceneEntryBaseSchema).min(1),
99
+ })
100
+ .superRefine((storyboard, ctx) => {
101
+ const seen = new Set();
102
+ storyboard.scenes.forEach((scene, index) => {
103
+ for (const { path, message, suggestion } of forbiddenKeys(scene.props, [
104
+ "scenes",
105
+ index,
106
+ "props",
107
+ ])) {
108
+ ctx.addIssue({ code: "custom", path, message, params: { suggestion } });
109
+ }
110
+ if (seen.has(scene.id)) {
111
+ ctx.addIssue({
112
+ code: "custom",
113
+ path: ["scenes", index, "id"],
114
+ message: `duplicate scene id "${scene.id}"`,
115
+ params: { suggestion: "give every scene a unique id" },
116
+ });
117
+ }
118
+ seen.add(scene.id);
119
+ for (const { field, message, suggestion } of overlayProblems(scene)) {
120
+ ctx.addIssue({
121
+ code: "custom",
122
+ path: ["scenes", index, field],
123
+ message,
124
+ params: { suggestion },
125
+ });
126
+ }
127
+ });
128
+ if (storyboard.scenes.every((scene) => scene.layer === "overlay")) {
129
+ ctx.addIssue({
130
+ code: "custom",
131
+ path: ["scenes"],
132
+ message: "needs at least one base scene",
133
+ params: { suggestion: "overlays sit over base scenes; add a scene without layer" },
134
+ });
135
+ }
136
+ });