Skip to content

Commit 2fdef32

Browse files
committed
fully declare new schema in zod
1 parent 68044ae commit 2fdef32

3 files changed

Lines changed: 118 additions & 37 deletions

File tree

lib/parsers/template.zod.ts

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { z } from 'https://deno.land/x/zod@v3.9.0/mod.ts'
22
import * as t from '../template_input.zod.ts'
3-
import { assert } from '../../type-equality.ts'
3+
import { assert } from '../type-equality.ts'
44

55

6-
const Id = z.string().regex(/[a-zA-Z0-9-_]/)
6+
const ClipId = z.string().regex(/[a-zA-Z0-9-_]/)
77

88
const Pixels = z.string().regex(/\d+px/)
99

@@ -16,29 +16,49 @@ const Color = z.string()
1616
const Timestamp = z.string() // I think we will delay parsing this till after we probe files because we need access to full file durations to resolve variables
1717

1818
const Size = z.object({
19-
width: z.union([Pixels, Percentage]).optional(),
20-
height: z.union([Pixels, Percentage]).optional(),
21-
relative_to: Id.optional(),
22-
})
19+
width: z.union([Pixels, Percentage]).default('100%'),
20+
height: z.union([Pixels, Percentage]).default('100%'),
21+
relative_to: ClipId.optional(),
22+
}).strict()
2323

2424
const AlignX = z.union([z.literal('left'), z.literal('right'), z.literal('center')])
2525
const AlignY = z.union([z.literal('top'), z.literal('bottom'), z.literal('center')])
2626
const Layout = Size.extend({
27-
x: z.union([AlignX, z.object({ offset: z.union([Pixels, Percentage]).optional(), align: AlignX.optional() })]).optional(),
28-
y: z.union([AlignY, z.object({ offset: z.union([Pixels, Percentage]).optional(), align: AlignY.optional() })]).optional(),
29-
})
27+
x: z.union([AlignX, z.object({ offset: z.union([Pixels, Percentage]).optional(), align: AlignX.optional() })]).default('left').transform(val => typeof val === 'object' ? val : { offset: '0px', align: val }),
28+
y: z.union([AlignY, z.object({ offset: z.union([Pixels, Percentage]).optional(), align: AlignY.optional() })]).default('top').transform(val => typeof val === 'object' ? val : { offset: '0px', align: val }),
29+
}).strict()
3030

3131
const ClipBase = z.object({
32-
id: Id.optional(),
32+
id: ClipId.optional(),
3333
layout: Layout.optional(),
3434
crop: Layout.optional(),
35+
zoompan: z.object({
36+
keyframe: Timestamp,
37+
zoom: Percentage.optional(),
38+
x: z.union([Pixels, Percentage]).optional(),
39+
y: z.union([Pixels, Percentage]).optional(),
40+
}).strict().array().optional(),
3541
rotate: Degrees.optional(),
42+
speed: Percentage.default('100%'),
43+
framerate: z.object({
44+
fps: z.number().min(0),
45+
smooth: z.boolean().default(false),
46+
}).strict().optional(),
47+
transition: z.object({
48+
fade_in: Timestamp.optional(),
49+
fade_out: Timestamp.optional(),
50+
}).strict().optional(),
51+
trim: z.object({
52+
start: Timestamp.optional(),
53+
stop: Timestamp.optional(),
54+
variable_length: z.union([z.literal('start'), z.literal('stop')]),
55+
}).strict().optional(),
3656
}).strict()
3757

3858
const MediaClip = ClipBase.extend({
3959
file: z.string(),
40-
volume: z.number().min(0).optional(),
41-
})
60+
volume: Percentage.default('100%'),
61+
}).strict()
4262

4363
const TextClip = ClipBase.extend({
4464
text: z.string(),
@@ -51,15 +71,28 @@ const TextClip = ClipBase.extend({
5171
background_color: Color.optional(),
5272
outline_color: Color.optional(),
5373
outline_size: z.number().optional(),
54-
}).strict().optional()
55-
})
74+
}).strict().optional(),
75+
76+
duration: Timestamp.optional(),
77+
}).strict()
78+
79+
const TimelineClip: z.ZodSchema<t.TimelineClip> = z.lazy(() => z.object({
80+
id: ClipId.optional(),
81+
offset: Timestamp.default('0'),
82+
z_index: z.number().default(0),
83+
type: z.union([z.literal('parallel'), z.literal('sequence')]).default('parallel'),
84+
next: TimelineClip.array().optional(),
85+
}))
5686

5787
const Template = z.object({
5888
size: Size.optional(),
5989
clips: MediaClip.array().min(1),
6090
captions: TextClip.array().min(1).optional(),
91+
timeline: TimelineClip.array().min(1).optional(),
6192
preview: Timestamp.optional(),
6293
})
6394

6495
// this is a typescript exacty type assertion. It does nothing at runtime
6596
assert({} as z.input<typeof Template>, {} as t.Template)
97+
98+
export { Template }

lib/template_input.zod.ts

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,61 @@ export interface ClipBase {
6767
crop?: Layout
6868

6969
/** Zoom and pan a clip */
70-
// zoompan?: {
71-
// [timestamp: string]: {
72-
// zoom?: Percentage
73-
// x?: Percentage | Pixels
74-
// y?: Percentage | Pixels
75-
// }
76-
// }
70+
zoompan?: {
71+
/** The timstamp when the zoom and pan has settled */
72+
keyframe: Timestamp
73+
74+
/** A percentage that the clip should be zoomed in or zoomed out */
75+
zoom?: Percentage
76+
77+
/** A horizontal pan.
78+
* Percentages are relative to the cropped clip. E.g. 50% puts the left side in the center of the screen.
79+
* If there is a desire, we could add an `origin: 'center' | 'corner'` field to make the origin variable.
80+
*/
81+
x?: Percentage | Pixels
82+
83+
/** A vertical pan.
84+
* Percentages are relative to the cropped clip. E.g. 50% puts the top side in the center of the screen.
85+
*/
86+
y?: Percentage | Pixels
87+
}[]
7788

7889
/** Angle at which the clip should be rotated */
7990
rotate?: Degrees
91+
92+
/** Speed at which a clip should be played (200% is 2x speed) */
93+
speed?: Percentage
94+
95+
/** Set the framerate for the clip */
96+
framerate?: {
97+
98+
/** Set the frames per second for the input clip */
99+
fps: number
100+
101+
/** smooth: true will interpolate frames that are missing
102+
* _if_ the desired framerate is higher than the input framerate */
103+
smooth?: boolean
104+
}
105+
106+
/** Effect to transition a clip in or out of the page */
107+
transition?: { fade_in?: Timestamp; fade_out?: Timestamp }
108+
109+
/** Trim the duration of a clip */
110+
trim?: {
111+
112+
/** Trim time off the start of a clip (similar to -ss argument in ffmpeg) */
113+
start?: Timestamp
114+
115+
/** Trim time off the end of a clip (similar to -to argument in ffmpeg) */
116+
stop?: Timestamp
117+
118+
/**
119+
* Auto-trim the clip so that it is not longer than the other longest clip
120+
* If more than one variable_length clip is used in a sequence on the timeline, only the last clip will have variable length.
121+
* If all clips on the timeline have variable length, all clips will share the shortest clip's duration.
122+
*/
123+
variable_length: 'start' | 'stop'
124+
}
80125
}
81126

82127
/**
@@ -114,50 +159,53 @@ export interface TextClip extends ClipBase {
114159
/** Text outline size (default is zero) */
115160
outline_size?: number
116161
}
162+
163+
/**
164+
* Specify the length a caption should be shown in the render
165+
* If not specified, text clip length is essentially the same as `trim: { variable_length: 'end' }`
166+
*/
167+
duration?: Timestamp
117168
}
118169

119170
/**
120171
* A MediaClip is any clip which takes in an image, audio or video.
121172
* Note that an audio file will not allow visual fields like "layout" or "zoompan"
122173
*/
123174
export interface MediaClip extends ClipBase {
175+
124176
/** Audio volume of the clip, this number is relative to the other clip's volume values. Defaults to 1. */
125-
volume?: number
177+
volume?: Percentage
178+
126179
/** File path to the clip. If it is a relative path, it will be relative to the location of the template file */
127180
file: string
128181
}
129182

130183

131184
export interface TimelineClip {
132-
id: string
185+
/** Clip id that is being added to the timeline */
186+
id?: string
187+
133188
/**
134189
* offset the clip start position by a specified duration. (Maybe we support negative durations too?)
135190
* default is "0"
136191
*/
137192
offset?: Timestamp
138193

139-
/** trim works very similar to clips trim, except that we can specify 'fit' instead of a duration. */
140-
trim_to_fit: 'start' | 'end'
141-
142194
/**
143195
* specify the vertical height of a clip. Think foreground and background
144196
* default is 0
145197
*/
146198
z_index?: number
147199

148-
/** Specify list of clips that should appear after the specified clip */
149-
next?: TimelineItem[]
150-
}
200+
/** specify whether the next clips will be played one after another or all at the same time
201+
* @default 'sequence'
202+
*/
203+
type?: 'parallel' | 'sequence'
151204

152-
export interface TimelineSequence {
153-
/** Specify a list of timeline items that should occur sequentially one after another */
154-
sequence: TimelineItem
155-
/** specify the vertical height of a clip. Think foreground and background */
156-
z_index?: number
205+
/** Specify list of clips that should appear after the specified clip */
206+
next?: TimelineClip[]
157207
}
158208

159-
export type TimelineItem = TimelineClip | TimelineSequence
160-
161209

162210
export interface Template {
163211
/**
@@ -180,7 +228,7 @@ export interface Template {
180228
* The default timeline starts all the clips at the same time. E.g.
181229
* [{ id: "CLIP_0", offset: "0" }, { id: "CLIP_1", offset: "0" }, ...]
182230
*/
183-
// timeline?: TimelineItem[]
231+
timeline?: TimelineClip[]
184232

185233
/**
186234
* Preview the rendered output at a position. Used with the --preview flag.
File renamed without changes.

0 commit comments

Comments
 (0)