@odori/cli 0.0.2
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 +22 -0
- package/bin/odori.mjs +39 -0
- package/dist/chunk-7XJL2BYO.js +3552 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +10 -0
- package/dist/index.d.ts +622 -0
- package/dist/index.js +156 -0
- package/dist/registry-snapshot-NIH2JMQ6.js +3559 -0
- package/package.json +50 -0
- package/src/audio-mix.ts +133 -0
- package/src/binaries.ts +241 -0
- package/src/brand-file.ts +94 -0
- package/src/chunk-cache.ts +85 -0
- package/src/chunks.ts +78 -0
- package/src/cli.ts +319 -0
- package/src/commands/add.ts +151 -0
- package/src/commands/dev.ts +160 -0
- package/src/commands/doctor.ts +162 -0
- package/src/commands/exportVideo.ts +198 -0
- package/src/commands/init.ts +56 -0
- package/src/commands/inspect.ts +72 -0
- package/src/commands/list.ts +22 -0
- package/src/commands/new.ts +126 -0
- package/src/commands/shared.ts +96 -0
- package/src/commands/still.ts +40 -0
- package/src/commands/test.ts +265 -0
- package/src/commands/update.ts +183 -0
- package/src/config.ts +84 -0
- package/src/contracts.ts +159 -0
- package/src/cues.ts +141 -0
- package/src/determinism.ts +82 -0
- package/src/diff.ts +71 -0
- package/src/discovery.ts +216 -0
- package/src/formats.ts +119 -0
- package/src/index.ts +58 -0
- package/src/integrity.ts +101 -0
- package/src/jobs.ts +151 -0
- package/src/log.ts +17 -0
- package/src/open.ts +32 -0
- package/src/paths.ts +12 -0
- package/src/prepare-cache.ts +58 -0
- package/src/project.ts +196 -0
- package/src/registry-snapshot.json +3431 -0
- package/src/registry-source.ts +269 -0
- package/src/render.ts +627 -0
- package/src/server.ts +307 -0
- package/studio/index.html +41 -0
- package/studio/src/Studio.tsx +192 -0
- package/studio/src/components/AudioClip.tsx +64 -0
- package/studio/src/components/CanvasStage.tsx +79 -0
- package/studio/src/components/CommandPalette.tsx +129 -0
- package/studio/src/components/Diagnostics.tsx +93 -0
- package/studio/src/components/ExportPanel.tsx +234 -0
- package/studio/src/components/InputControls.tsx +110 -0
- package/studio/src/components/Thumbnail.tsx +71 -0
- package/studio/src/components/Transport.tsx +237 -0
- package/studio/src/components/Waveform.tsx +114 -0
- package/studio/src/components/Wordmark.tsx +449 -0
- package/studio/src/components/ui.tsx +138 -0
- package/studio/src/lib/mix-loudness.ts +52 -0
- package/studio/src/main.tsx +34 -0
- package/studio/src/shortcuts.ts +27 -0
- package/studio/src/studio.css +1232 -0
- package/studio/src/theme.ts +61 -0
- package/studio/src/views/AssetsView.tsx +111 -0
- package/studio/src/views/BrandsView.tsx +139 -0
- package/studio/src/views/ComponentsView.tsx +285 -0
- package/studio/src/views/HomeView.tsx +122 -0
- package/studio/src/views/VideosView.tsx +343 -0
- package/studio/src/virtual.d.ts +25 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Flags = Record<string, string | boolean>;
|
|
2
|
+
declare const parseArgs: (argv: string[]) => {
|
|
3
|
+
command: string;
|
|
4
|
+
positionals: string[];
|
|
5
|
+
flags: Flags;
|
|
6
|
+
};
|
|
7
|
+
declare const checkFlags: (command: string, flags: Flags) => void;
|
|
8
|
+
declare const run: (argv: string[]) => Promise<number>;
|
|
9
|
+
|
|
10
|
+
export { checkFlags, parseArgs, run };
|
package/dist/cli.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { ViteDevServer } from 'vite';
|
|
2
|
+
import * as odori from 'odori';
|
|
3
|
+
import { VideoEntry, RenderManifest, AudioCue, ManifestAudioCue, ExportJob } from 'odori';
|
|
4
|
+
import { Browser, Page } from 'playwright-core';
|
|
5
|
+
export { parseArgs, run } from './cli.js';
|
|
6
|
+
|
|
7
|
+
type OdoriConfig = {
|
|
8
|
+
/** Source root that contains video entries and video components. */
|
|
9
|
+
videosDir: string;
|
|
10
|
+
/** Generated output directory. Never edited or committed. */
|
|
11
|
+
outDir: string;
|
|
12
|
+
/** Where exports are written. */
|
|
13
|
+
exportDir: string;
|
|
14
|
+
/** Registry components are copied here. */
|
|
15
|
+
componentsDir: string;
|
|
16
|
+
/** Audio library, discovered so Studio can list and audition it. Served from public/. */
|
|
17
|
+
audioDir: string;
|
|
18
|
+
/** Studio dev server port. */
|
|
19
|
+
port: number;
|
|
20
|
+
/** Where Studio's docs link points. Defaults to the hosted docs site. */
|
|
21
|
+
docsUrl: string;
|
|
22
|
+
/** Open Studio in the default browser when `odori dev` starts. */
|
|
23
|
+
open?: boolean;
|
|
24
|
+
/** Chrome or Chromium executable used by the render worker. */
|
|
25
|
+
chromePath?: string;
|
|
26
|
+
/**
|
|
27
|
+
* FFmpeg executable used to encode. Set it to pin a build; leave it and
|
|
28
|
+
* Odori uses its own managed copy, which is what keeps two machines
|
|
29
|
+
* producing the same file.
|
|
30
|
+
*/
|
|
31
|
+
ffmpegPath?: string;
|
|
32
|
+
/** Static asset references available to every video. */
|
|
33
|
+
assets?: Array<{
|
|
34
|
+
reference: string;
|
|
35
|
+
url: string;
|
|
36
|
+
}>;
|
|
37
|
+
/** Parallel render workers. Defaults to the machine's spare cores, capped at four. */
|
|
38
|
+
concurrency?: number;
|
|
39
|
+
/** x264 preset for exports. Defaults to medium. */
|
|
40
|
+
preset?: string;
|
|
41
|
+
/** Default container and codec: mp4, webm, prores, gif, or png. */
|
|
42
|
+
format?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Where `odori add` fetches components from. Point it at a fork, a mirror,
|
|
45
|
+
* or a pinned version; `ODORI_REGISTRY` overrides it for one command.
|
|
46
|
+
*/
|
|
47
|
+
registryUrl?: string;
|
|
48
|
+
/** Reuse the previous frame when the rendered picture is unchanged. Defaults to true. */
|
|
49
|
+
skipUnchangedFrames?: boolean;
|
|
50
|
+
/** Reuse encoded chunks whose frames still look identical. Defaults to true. */
|
|
51
|
+
cacheChunks?: boolean;
|
|
52
|
+
};
|
|
53
|
+
declare const resolveChromePath: (configured?: string) => string | undefined;
|
|
54
|
+
type ResolvedConfig = OdoriConfig & {
|
|
55
|
+
root: string;
|
|
56
|
+
configPath?: string;
|
|
57
|
+
};
|
|
58
|
+
declare const loadConfig: (root: string) => Promise<ResolvedConfig>;
|
|
59
|
+
declare const defineConfig: (config: Partial<OdoriConfig>) => Partial<OdoriConfig>;
|
|
60
|
+
|
|
61
|
+
type DiscoveredVideo = {
|
|
62
|
+
/** Directory-derived id used before the module is loaded. */
|
|
63
|
+
slug: string;
|
|
64
|
+
file: string;
|
|
65
|
+
relativeFile: string;
|
|
66
|
+
importPath: string;
|
|
67
|
+
identifier: string;
|
|
68
|
+
};
|
|
69
|
+
type DiscoveredPreview = {
|
|
70
|
+
name: string;
|
|
71
|
+
file: string;
|
|
72
|
+
relativeFile: string;
|
|
73
|
+
importPath: string;
|
|
74
|
+
identifier: string;
|
|
75
|
+
};
|
|
76
|
+
type DiscoveredBrandModule = {
|
|
77
|
+
name: string;
|
|
78
|
+
file: string;
|
|
79
|
+
relativeFile: string;
|
|
80
|
+
identifier: string;
|
|
81
|
+
};
|
|
82
|
+
type DiscoveredAudio = {
|
|
83
|
+
/** Path under the audio directory, without extension. */
|
|
84
|
+
name: string;
|
|
85
|
+
/** The URL the dev server and the render worker both serve. */
|
|
86
|
+
url: string;
|
|
87
|
+
relativeFile: string;
|
|
88
|
+
bytes: number;
|
|
89
|
+
};
|
|
90
|
+
type ProjectGraph = {
|
|
91
|
+
videos: DiscoveredVideo[];
|
|
92
|
+
previews: DiscoveredPreview[];
|
|
93
|
+
brands: DiscoveredBrandModule[];
|
|
94
|
+
audio: DiscoveredAudio[];
|
|
95
|
+
sourceHash: string;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Discovery is filesystem based and filename driven, exactly as documented:
|
|
99
|
+
* `videos/**\/video.tsx` are exportable videos and `videos/**\/*.preview.tsx`
|
|
100
|
+
* are development-only component fixtures. Directory names carry no meaning.
|
|
101
|
+
*/
|
|
102
|
+
declare const discoverProject: (config: ResolvedConfig) => Promise<ProjectGraph>;
|
|
103
|
+
/**
|
|
104
|
+
* Static imports keep the module graph compatible with Next.js, tests,
|
|
105
|
+
* browsers, and Node render workers.
|
|
106
|
+
*/
|
|
107
|
+
declare const generateImports: (graph: ProjectGraph, outDir: string) => string;
|
|
108
|
+
declare const writeGenerated: (config: ResolvedConfig, graph: ProjectGraph) => Promise<string>;
|
|
109
|
+
|
|
110
|
+
type StudioServer = {
|
|
111
|
+
vite: ViteDevServer;
|
|
112
|
+
url: string;
|
|
113
|
+
graph: ProjectGraph;
|
|
114
|
+
close(): Promise<void>;
|
|
115
|
+
};
|
|
116
|
+
declare const startStudioServer: (initialConfig: ResolvedConfig, options?: {
|
|
117
|
+
port?: number;
|
|
118
|
+
strictPort?: boolean;
|
|
119
|
+
middleware?: (server: ViteDevServer) => void;
|
|
120
|
+
}) => Promise<StudioServer>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Video ids are paths under `videos/`; files on disk are flat.
|
|
124
|
+
*
|
|
125
|
+
* Two encodings, deliberately different. An output file is something a person
|
|
126
|
+
* hands to someone else, so `social/announcement` becomes the readable
|
|
127
|
+
* `social-announcement`. A cache file is addressed, not read, so it encodes
|
|
128
|
+
* the separator instead of replacing it: `social+announcement` cannot collide
|
|
129
|
+
* with a video genuinely named `social-announcement`.
|
|
130
|
+
*/
|
|
131
|
+
declare const outputName: (id: string) => string;
|
|
132
|
+
declare const fileKey: (id: string) => string;
|
|
133
|
+
|
|
134
|
+
type LoadedVideo = {
|
|
135
|
+
entry: VideoEntry;
|
|
136
|
+
file: string;
|
|
137
|
+
relativeFile: string;
|
|
138
|
+
durationInFrames: number;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Load discovered entries in Node so the CLI can inspect metadata, validate
|
|
142
|
+
* inputs, and freeze a manifest without opening a browser.
|
|
143
|
+
*/
|
|
144
|
+
declare const loadVideos: (graph: ProjectGraph) => Promise<LoadedVideo[]>;
|
|
145
|
+
|
|
146
|
+
declare const findVideo: (videos: LoadedVideo[], id: string) => LoadedVideo;
|
|
147
|
+
/**
|
|
148
|
+
* Run prepare.ts beside a video entry, if it exists.
|
|
149
|
+
*
|
|
150
|
+
* Results are cached on disk by source hash, validated input, and prepare
|
|
151
|
+
* version, so repeated stills and exports of an approved cut do not refetch.
|
|
152
|
+
*/
|
|
153
|
+
declare const runPrepare: (video: LoadedVideo, config: ResolvedConfig, graph: ProjectGraph, input: Record<string, unknown>, options?: {
|
|
154
|
+
refresh?: boolean;
|
|
155
|
+
}) => Promise<unknown>;
|
|
156
|
+
type FreezeOptions = {
|
|
157
|
+
scenes?: RenderManifest["scenes"];
|
|
158
|
+
audio?: AudioCue[];
|
|
159
|
+
refreshPrepare?: boolean;
|
|
160
|
+
};
|
|
161
|
+
declare const freezeManifest: (video: LoadedVideo, graph: ProjectGraph, config: ResolvedConfig, rawInput: Record<string, unknown>, options?: FreezeOptions) => Promise<{
|
|
162
|
+
manifest: RenderManifest;
|
|
163
|
+
input: Record<string, unknown>;
|
|
164
|
+
prepared: unknown;
|
|
165
|
+
}>;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* What an export can produce.
|
|
169
|
+
*
|
|
170
|
+
* One codec is enough until it is not: an overlay needs an alpha channel that
|
|
171
|
+
* H.264 cannot carry, an editor wants ProRes, a README wants a GIF, and a
|
|
172
|
+
* thumbnail sheet wants frames. Each of those is a different container and a
|
|
173
|
+
* different pixel format, so the choice belongs in one table rather than in a
|
|
174
|
+
* flag that quietly means five things.
|
|
175
|
+
*
|
|
176
|
+
* Chunked capture is how a render stays fast, and only some codecs can be
|
|
177
|
+
* concatenated without re-encoding. `chunked: false` says a format has to be
|
|
178
|
+
* encoded in one pass, which is slower and correct.
|
|
179
|
+
*/
|
|
180
|
+
type VideoFormat = {
|
|
181
|
+
name: string;
|
|
182
|
+
extension: string;
|
|
183
|
+
/** Whether the codec supports an alpha channel. */
|
|
184
|
+
alpha: boolean;
|
|
185
|
+
/** Whether chunks can be joined with a stream copy. */
|
|
186
|
+
chunked: boolean;
|
|
187
|
+
/** Whether the container carries an audio track at all. */
|
|
188
|
+
audio: boolean;
|
|
189
|
+
/** FFmpeg arguments for the video stream, given the requested preset. */
|
|
190
|
+
args: (preset: string) => string[];
|
|
191
|
+
description: string;
|
|
192
|
+
};
|
|
193
|
+
declare const FORMATS: Record<string, VideoFormat>;
|
|
194
|
+
declare const formatNames: () => string[];
|
|
195
|
+
/**
|
|
196
|
+
* The format for an export.
|
|
197
|
+
*
|
|
198
|
+
* An explicit `--format` wins. Otherwise the output's extension decides, so
|
|
199
|
+
* `--output cut.webm` does what it looks like it does rather than writing
|
|
200
|
+
* H.264 into a file named `.webm`.
|
|
201
|
+
*/
|
|
202
|
+
declare const resolveFormat: (requested: string | undefined, output: string | undefined) => VideoFormat;
|
|
203
|
+
/**
|
|
204
|
+
* Whether a composition's transparency can survive into the file.
|
|
205
|
+
*
|
|
206
|
+
* A video with a transparent background exported to MP4 is not an error, it is
|
|
207
|
+
* a black rectangle — the alpha is composited away by the pixel format. Worth
|
|
208
|
+
* saying out loud at the point the choice is made.
|
|
209
|
+
*/
|
|
210
|
+
declare const alphaWarning: (format: VideoFormat, transparent: boolean) => string | null;
|
|
211
|
+
|
|
212
|
+
type RenderTarget = {
|
|
213
|
+
videoId: string;
|
|
214
|
+
width: number;
|
|
215
|
+
height: number;
|
|
216
|
+
fps: number;
|
|
217
|
+
durationInFrames: number;
|
|
218
|
+
input?: Record<string, unknown>;
|
|
219
|
+
prepared?: unknown;
|
|
220
|
+
audio?: ManifestAudioCue[];
|
|
221
|
+
targetLufs?: number;
|
|
222
|
+
scenes?: Array<{
|
|
223
|
+
id: string;
|
|
224
|
+
start: number;
|
|
225
|
+
durationInFrames: number;
|
|
226
|
+
}>;
|
|
227
|
+
};
|
|
228
|
+
type RenderPage = {
|
|
229
|
+
browser: Browser;
|
|
230
|
+
page: Page;
|
|
231
|
+
errors: string[];
|
|
232
|
+
};
|
|
233
|
+
/** Open one video in render mode and wait for its first frame to mount. */
|
|
234
|
+
declare const openRenderPage: (origin: string, target: RenderTarget, config: ResolvedConfig) => Promise<RenderPage>;
|
|
235
|
+
/** Seek through the readiness handshake instead of guessing with timeouts. */
|
|
236
|
+
declare const seekTo: (page: Page, frame: number) => Promise<void>;
|
|
237
|
+
declare const readTimeline: (page: Page) => Promise<odori.CompiledTimeline>;
|
|
238
|
+
declare const readAudio: (page: Page) => Promise<odori.AudioTrack>;
|
|
239
|
+
declare const probeSignatures: (page: Page, frames: number[]) => Promise<string[]>;
|
|
240
|
+
/** Kept for callers that only want to know an encoder exists. */
|
|
241
|
+
declare const ensureFfmpeg: (config: ResolvedConfig) => Promise<void>;
|
|
242
|
+
declare const renderStill: (origin: string, target: RenderTarget, frame: number, output: string, config: ResolvedConfig) => Promise<string>;
|
|
243
|
+
declare const defaultConcurrency: () => number;
|
|
244
|
+
type RenderProgress = (progress: number, stage: "rendering" | "encoding") => void;
|
|
245
|
+
type RenderTimings = {
|
|
246
|
+
captureMs: number;
|
|
247
|
+
encodeMs: number;
|
|
248
|
+
frames: number;
|
|
249
|
+
reusedFrames: number;
|
|
250
|
+
cachedChunks: number;
|
|
251
|
+
chunks: number;
|
|
252
|
+
concurrency: number;
|
|
253
|
+
};
|
|
254
|
+
type RenderOptions = {
|
|
255
|
+
concurrency?: number;
|
|
256
|
+
preset?: string;
|
|
257
|
+
/** Container and codec. Defaults to H.264 in MP4. */
|
|
258
|
+
format?: VideoFormat;
|
|
259
|
+
skipUnchangedFrames?: boolean;
|
|
260
|
+
/** Reuse encoded chunks whose frames still look identical. */
|
|
261
|
+
cache?: boolean;
|
|
262
|
+
signal?: AbortSignal;
|
|
263
|
+
/** Where frames and chunk files are written. Defaults to the generated directory. */
|
|
264
|
+
workDir?: string;
|
|
265
|
+
onTimings?: (timings: RenderTimings) => void;
|
|
266
|
+
};
|
|
267
|
+
declare const renderMovie: (origin: string, target: RenderTarget, output: string, config: ResolvedConfig, onProgress?: RenderProgress, options?: RenderOptions) => Promise<string>;
|
|
268
|
+
|
|
269
|
+
type Context = {
|
|
270
|
+
config: ResolvedConfig;
|
|
271
|
+
graph: ProjectGraph;
|
|
272
|
+
videos: LoadedVideo[];
|
|
273
|
+
};
|
|
274
|
+
declare const createContext: (root?: string) => Promise<Context>;
|
|
275
|
+
declare const targetFor: (video: LoadedVideo, input?: Record<string, unknown>, prepared?: unknown, audio?: ManifestAudioCue[], scenes?: Array<{
|
|
276
|
+
id: string;
|
|
277
|
+
start: number;
|
|
278
|
+
durationInFrames: number;
|
|
279
|
+
}>) => RenderTarget;
|
|
280
|
+
/**
|
|
281
|
+
* `metadata.duration` is authoritative. When it is absent, the compiled
|
|
282
|
+
* timeline reported by the runtime is used instead.
|
|
283
|
+
*/
|
|
284
|
+
type CompileResult = {
|
|
285
|
+
durationInFrames: number;
|
|
286
|
+
scenes: Array<{
|
|
287
|
+
id: string;
|
|
288
|
+
start: number;
|
|
289
|
+
durationInFrames: number;
|
|
290
|
+
}>;
|
|
291
|
+
audio: AudioCue[];
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Compile the timeline and the audio track in the browser, where the runtime
|
|
295
|
+
* already knows how to lay both out. `metadata.duration` stays authoritative
|
|
296
|
+
* when it is declared.
|
|
297
|
+
*/
|
|
298
|
+
declare const compileInBrowser: (origin: string, target: RenderTarget, config: ResolvedConfig) => Promise<CompileResult>;
|
|
299
|
+
declare const withServer: <Value>(config: ResolvedConfig, handler: (server: StudioServer) => Promise<Value>) => Promise<Value>;
|
|
300
|
+
|
|
301
|
+
type JobRecord = {
|
|
302
|
+
job: ExportJob;
|
|
303
|
+
manifest: RenderManifest;
|
|
304
|
+
output: string;
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* An export job records the frozen manifest, progress, attempts, and result,
|
|
308
|
+
* so a retry reuses the approved inputs instead of resolving them again.
|
|
309
|
+
*/
|
|
310
|
+
declare const createJob: (config: ResolvedConfig, manifest: RenderManifest, output: string) => Promise<JobRecord>;
|
|
311
|
+
declare const readJob: (config: ResolvedConfig, id: string) => Promise<JobRecord>;
|
|
312
|
+
declare const updateJob: (config: ResolvedConfig, job: ExportJob) => Promise<ExportJob>;
|
|
313
|
+
declare const appendJobLog: (config: ResolvedConfig, id: string, message: string) => Promise<ExportJob>;
|
|
314
|
+
/**
|
|
315
|
+
* A killed process leaves its job claiming to be rendering forever. Anything
|
|
316
|
+
* that reads job state reconciles first, so `odori jobs` never reports a lie.
|
|
317
|
+
*/
|
|
318
|
+
declare const reconcileJobs: (config: ResolvedConfig) => Promise<number>;
|
|
319
|
+
declare const listJobs: (config: ResolvedConfig, options?: {
|
|
320
|
+
reconcile?: boolean;
|
|
321
|
+
}) => Promise<ExportJob[]>;
|
|
322
|
+
/**
|
|
323
|
+
* A single-lane queue.
|
|
324
|
+
*
|
|
325
|
+
* Rendering saturates the machine's cores already, so jobs run one at a time
|
|
326
|
+
* in submission order. The queue is a promise chain rather than a daemon, which
|
|
327
|
+
* keeps `odori export` and the Studio export button on exactly the same path.
|
|
328
|
+
*/
|
|
329
|
+
declare class JobQueue {
|
|
330
|
+
private chain;
|
|
331
|
+
enqueue<Value>(task: () => Promise<Value>): Promise<Value>;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
type MixInput = {
|
|
335
|
+
file: string;
|
|
336
|
+
cue: ManifestAudioCue;
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* Resolve a cue source to a local file. Sources are project-relative or
|
|
340
|
+
* public-relative paths, which is what the dev server and the render worker
|
|
341
|
+
* both serve.
|
|
342
|
+
*/
|
|
343
|
+
declare const resolveCueFile: (config: ResolvedConfig, src: string) => string | null;
|
|
344
|
+
declare const buildAudioFilter: (inputs: MixInput[], options: {
|
|
345
|
+
fps: number;
|
|
346
|
+
durationInFrames: number;
|
|
347
|
+
targetLufs: number;
|
|
348
|
+
}) => {
|
|
349
|
+
filter: string;
|
|
350
|
+
label: string;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
type DeterminismFinding = {
|
|
354
|
+
file: string;
|
|
355
|
+
line: number;
|
|
356
|
+
source: string;
|
|
357
|
+
message: string;
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* Every module under the source root, checked for the calls above.
|
|
361
|
+
*
|
|
362
|
+
* Preview fixtures are skipped: they are development-only, never in a render,
|
|
363
|
+
* and a fixture that wants a throwaway value is not lying to anyone.
|
|
364
|
+
*/
|
|
365
|
+
declare const checkDeterminism: (config: ResolvedConfig) => Promise<DeterminismFinding[]>;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The two programs a render actually runs.
|
|
369
|
+
*
|
|
370
|
+
* Both are pinned. A video is supposed to be reproducible from its source, and
|
|
371
|
+
* that promise is only as good as the software that draws and encodes it: a
|
|
372
|
+
* different Chrome lays out text a fraction differently, and a different
|
|
373
|
+
* FFmpeg build can be compiled with different defaults. A machine that resolves
|
|
374
|
+
* "whatever is installed" produces frames that are almost the same, which is
|
|
375
|
+
* the worst kind of difference — invisible in review, present in the file.
|
|
376
|
+
*
|
|
377
|
+
* So Odori manages its own copies, keyed by version, in a cache outside the
|
|
378
|
+
* project. Local and cloud converge on the same bytes because they download
|
|
379
|
+
* the same bytes.
|
|
380
|
+
*/
|
|
381
|
+
/** Chrome build the runtime is exercised against. Changing it changes frames. */
|
|
382
|
+
declare const CHROME_BUILD = "131.0.6778.204";
|
|
383
|
+
type BinaryOrigin = "configured" | "environment" | "managed" | "package" | "system";
|
|
384
|
+
type ResolvedBinary = {
|
|
385
|
+
path: string;
|
|
386
|
+
origin: BinaryOrigin;
|
|
387
|
+
/** Pinned version for managed copies; whatever the host has otherwise. */
|
|
388
|
+
version: string;
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Where managed binaries live. Outside the project on purpose: one download
|
|
392
|
+
* serves every project on the machine, and a `node_modules` wipe does not cost
|
|
393
|
+
* a 150 MB round trip. `ODORI_CACHE` moves it, which is what a CI cache key or
|
|
394
|
+
* a Docker layer wants.
|
|
395
|
+
*/
|
|
396
|
+
declare const cacheRoot: () => string;
|
|
397
|
+
/**
|
|
398
|
+
* Chrome, in the order a reader would guess: what the project configured, what
|
|
399
|
+
* the environment overrode, the managed copy, then whatever the machine has.
|
|
400
|
+
*
|
|
401
|
+
* The system fallback is last and deliberate. It keeps a machine that already
|
|
402
|
+
* has Chrome working with no download, and `odori doctor` says which one was
|
|
403
|
+
* used so an unexplained difference between two renders has somewhere to start.
|
|
404
|
+
*/
|
|
405
|
+
declare const resolveBrowser: (config: ResolvedConfig) => Promise<ResolvedBinary | null>;
|
|
406
|
+
declare const resolveFfmpeg: (config: ResolvedConfig) => Promise<ResolvedBinary | null>;
|
|
407
|
+
/**
|
|
408
|
+
* Download the pinned Chrome if it is not already cached.
|
|
409
|
+
*
|
|
410
|
+
* `chrome-headless-shell` rather than full Chrome: it is the build made for
|
|
411
|
+
* exactly this — rendering frames with no window, no profile, and no updater —
|
|
412
|
+
* and it is a third of the download.
|
|
413
|
+
*/
|
|
414
|
+
declare const installBrowser: (options?: {
|
|
415
|
+
onProgress?: (fraction: number) => void;
|
|
416
|
+
}) => Promise<string>;
|
|
417
|
+
/**
|
|
418
|
+
* Download the pinned FFmpeg if it is not already cached.
|
|
419
|
+
*
|
|
420
|
+
* The static build is fetched through its own package rather than from a URL
|
|
421
|
+
* we maintain, so the platform matrix and the checksums stay somebody else's
|
|
422
|
+
* job. It runs here instead of at install time because package managers now
|
|
423
|
+
* block install scripts by default, and a binary that only arrives when a
|
|
424
|
+
* postinstall is allowed is a binary that is missing on half of all machines.
|
|
425
|
+
*/
|
|
426
|
+
declare const installFfmpeg: () => Promise<string>;
|
|
427
|
+
/**
|
|
428
|
+
* What a render will use, as a line of provenance.
|
|
429
|
+
*
|
|
430
|
+
* This goes in the manifest. Two renders of the same source that differ are
|
|
431
|
+
* either a bug or a different toolchain, and without this there is no way to
|
|
432
|
+
* tell which from the artefacts alone.
|
|
433
|
+
*/
|
|
434
|
+
declare const renderToolchain: (config: ResolvedConfig) => Promise<{
|
|
435
|
+
chrome: string;
|
|
436
|
+
chromeOrigin: BinaryOrigin | "missing";
|
|
437
|
+
ffmpeg: string;
|
|
438
|
+
ffmpegOrigin: BinaryOrigin | "missing";
|
|
439
|
+
}>;
|
|
440
|
+
|
|
441
|
+
/** Local candidates for a project-relative or public-relative URL. */
|
|
442
|
+
declare const localCandidates: (config: ResolvedConfig, url: string) => string[];
|
|
443
|
+
/**
|
|
444
|
+
* Content addressing for everything a render depends on.
|
|
445
|
+
*
|
|
446
|
+
* Local files hash their bytes. Remote files are fetched once and cached by
|
|
447
|
+
* URL, so a manifest stays comparable across runs without refetching on every
|
|
448
|
+
* export. A source that cannot be read is recorded as unresolved rather than
|
|
449
|
+
* silently pretending to be verified.
|
|
450
|
+
*/
|
|
451
|
+
declare const createIntegrityResolver: (config: ResolvedConfig) => Promise<{
|
|
452
|
+
resolve: (url: string) => Promise<string>;
|
|
453
|
+
flush: () => Promise<void>;
|
|
454
|
+
}>;
|
|
455
|
+
|
|
456
|
+
type PrepareCacheKey = {
|
|
457
|
+
videoId: string;
|
|
458
|
+
sourceHash: string;
|
|
459
|
+
input: unknown;
|
|
460
|
+
version: string;
|
|
461
|
+
dependencies?: unknown;
|
|
462
|
+
};
|
|
463
|
+
/**
|
|
464
|
+
* Prepared data is cached by video source hash, validated input, prepare
|
|
465
|
+
* version, and declared dependencies. Changing scene styling does not refetch
|
|
466
|
+
* source data, and changing a data dependency invalidates deterministically.
|
|
467
|
+
*/
|
|
468
|
+
declare const prepareCacheKey: (key: PrepareCacheKey) => string;
|
|
469
|
+
declare const readPrepareCache: (config: ResolvedConfig, key: PrepareCacheKey) => Promise<{
|
|
470
|
+
hit: boolean;
|
|
471
|
+
value: unknown;
|
|
472
|
+
}>;
|
|
473
|
+
declare const writePrepareCache: (config: ResolvedConfig, key: PrepareCacheKey, value: unknown) => Promise<void>;
|
|
474
|
+
/** Remove cached preparations for a video, or for the whole project. */
|
|
475
|
+
declare const clearPrepareCache: (config: ResolvedConfig, videoId?: string) => Promise<number>;
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Studio previews without encoding. These endpoints exist so an explicit
|
|
479
|
+
* export action in the browser reaches the same queue and render worker the
|
|
480
|
+
* CLI uses.
|
|
481
|
+
*/
|
|
482
|
+
declare const devCommand: (options?: {
|
|
483
|
+
port?: number;
|
|
484
|
+
root?: string;
|
|
485
|
+
open?: boolean;
|
|
486
|
+
}) => Promise<StudioServer>;
|
|
487
|
+
|
|
488
|
+
declare const exportQueue: JobQueue;
|
|
489
|
+
declare const cancelJob: (id: string) => boolean;
|
|
490
|
+
/**
|
|
491
|
+
* Run one recorded job. Retries reuse the frozen manifest, so a failed export
|
|
492
|
+
* never silently re-resolves data or reruns prepare.
|
|
493
|
+
*/
|
|
494
|
+
declare const runJob: (config: ResolvedConfig, origin: string, record: JobRecord, video: LoadedVideo, options?: {
|
|
495
|
+
concurrency?: number;
|
|
496
|
+
preset?: string;
|
|
497
|
+
format?: VideoFormat;
|
|
498
|
+
skipUnchangedFrames?: boolean;
|
|
499
|
+
signal?: AbortSignal;
|
|
500
|
+
onProgress?: (job: ExportJob) => void;
|
|
501
|
+
}) => Promise<ExportJob>;
|
|
502
|
+
declare const exportCommand: (id: string, options?: {
|
|
503
|
+
output?: string;
|
|
504
|
+
input?: Record<string, unknown>;
|
|
505
|
+
concurrency?: number;
|
|
506
|
+
preset?: string;
|
|
507
|
+
format?: string;
|
|
508
|
+
skipUnchangedFrames?: boolean;
|
|
509
|
+
retry?: string;
|
|
510
|
+
}) => Promise<ExportJob>;
|
|
511
|
+
declare const jobsCommand: () => Promise<void>;
|
|
512
|
+
|
|
513
|
+
type FrameChunk = {
|
|
514
|
+
index: number;
|
|
515
|
+
start: number;
|
|
516
|
+
end: number;
|
|
517
|
+
sceneId?: string;
|
|
518
|
+
};
|
|
519
|
+
type ChunkPlan = {
|
|
520
|
+
chunks: FrameChunk[];
|
|
521
|
+
lanes: FrameChunk[][];
|
|
522
|
+
};
|
|
523
|
+
type PlanOptions = {
|
|
524
|
+
durationInFrames: number;
|
|
525
|
+
scenes?: Array<{
|
|
526
|
+
id: string;
|
|
527
|
+
start: number;
|
|
528
|
+
durationInFrames: number;
|
|
529
|
+
}>;
|
|
530
|
+
concurrency: number;
|
|
531
|
+
/** Scenes longer than this are split so one long scene cannot own a worker. */
|
|
532
|
+
maxChunkFrames?: number;
|
|
533
|
+
};
|
|
534
|
+
/**
|
|
535
|
+
* Split a timeline into contiguous chunks, then deal them out to workers.
|
|
536
|
+
*
|
|
537
|
+
* Chunks follow scene boundaries because a scene is the unit the runtime
|
|
538
|
+
* already names, which is what lets a chunk be encoded on its own, cached, or
|
|
539
|
+
* rendered on another machine. Long scenes are split so a single expensive
|
|
540
|
+
* scene cannot pin one worker while the others idle.
|
|
541
|
+
*/
|
|
542
|
+
declare const planChunks: ({ durationInFrames, scenes, concurrency, maxChunkFrames, }: PlanOptions) => ChunkPlan;
|
|
543
|
+
declare const chunkFrames: (chunk: FrameChunk) => number[];
|
|
544
|
+
|
|
545
|
+
declare const stillCommand: (id: string, options?: {
|
|
546
|
+
frame?: number;
|
|
547
|
+
output?: string;
|
|
548
|
+
input?: Record<string, unknown>;
|
|
549
|
+
}) => Promise<string>;
|
|
550
|
+
|
|
551
|
+
declare const testCommand: (id?: string, options?: {
|
|
552
|
+
json?: boolean;
|
|
553
|
+
}) => Promise<void>;
|
|
554
|
+
|
|
555
|
+
declare const listCommand: () => Promise<void>;
|
|
556
|
+
|
|
557
|
+
declare const inspectCommand: (id: string, options?: {
|
|
558
|
+
json?: boolean;
|
|
559
|
+
input?: Record<string, unknown>;
|
|
560
|
+
}) => Promise<void>;
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Installation copies source into the project. Provenance is recorded so the
|
|
564
|
+
* CLI can report upstream changes, but a locally modified component is never
|
|
565
|
+
* replaced without an explicit decision.
|
|
566
|
+
*/
|
|
567
|
+
declare const addCommand: (names: string[], options?: {
|
|
568
|
+
force?: boolean;
|
|
569
|
+
dryRun?: boolean;
|
|
570
|
+
}) => Promise<void>;
|
|
571
|
+
|
|
572
|
+
type ComponentState = "pristine" | "modified" | "outdated" | "diverged" | "missing";
|
|
573
|
+
type ComponentStatus = {
|
|
574
|
+
name: string;
|
|
575
|
+
state: ComponentState;
|
|
576
|
+
files: Array<{
|
|
577
|
+
file: string;
|
|
578
|
+
localPath: string;
|
|
579
|
+
/** Upstream source itself. The registry is documents now, not a directory. */
|
|
580
|
+
content: string;
|
|
581
|
+
local: string | null;
|
|
582
|
+
installed: string | null;
|
|
583
|
+
upstream: string;
|
|
584
|
+
}>;
|
|
585
|
+
};
|
|
586
|
+
/**
|
|
587
|
+
* Compare installed component source with the version that was installed and
|
|
588
|
+
* with the version the registry ships today.
|
|
589
|
+
*
|
|
590
|
+
* - pristine: identical everywhere
|
|
591
|
+
* - modified: the project edited it, upstream has not moved
|
|
592
|
+
* - outdated: upstream moved, the project did not edit it
|
|
593
|
+
* - diverged: both moved, so an update would overwrite local work
|
|
594
|
+
*/
|
|
595
|
+
declare const componentStatus: (config: ResolvedConfig, only?: string[]) => Promise<ComponentStatus[]>;
|
|
596
|
+
declare const diffCommand: (names: string[], options?: {
|
|
597
|
+
full?: boolean;
|
|
598
|
+
}) => Promise<void>;
|
|
599
|
+
declare const updateCommand: (names: string[], options?: {
|
|
600
|
+
force?: boolean;
|
|
601
|
+
}) => Promise<void>;
|
|
602
|
+
|
|
603
|
+
type DiffLine = {
|
|
604
|
+
type: "context" | "add" | "remove";
|
|
605
|
+
text: string;
|
|
606
|
+
};
|
|
607
|
+
declare const diffLines: (before: string, after: string) => DiffLine[];
|
|
608
|
+
declare const countChanges: (lines: DiffLine[]) => {
|
|
609
|
+
added: number;
|
|
610
|
+
removed: number;
|
|
611
|
+
};
|
|
612
|
+
/** Collapse unchanged runs so a small edit does not print the whole file. */
|
|
613
|
+
declare const formatDiff: (lines: DiffLine[], context?: number) => string[];
|
|
614
|
+
|
|
615
|
+
declare const newCommand: (name: string, options?: {
|
|
616
|
+
blank?: boolean;
|
|
617
|
+
}) => Promise<void>;
|
|
618
|
+
|
|
619
|
+
/** Add the videos source root and configuration to an existing project. */
|
|
620
|
+
declare const initCommand: (root?: string) => Promise<void>;
|
|
621
|
+
|
|
622
|
+
export { CHROME_BUILD, type ChunkPlan, type CompileResult, type ComponentStatus, type Context, type DeterminismFinding, type DiffLine, FORMATS, type FrameChunk, JobQueue, type JobRecord, type LoadedVideo, type MixInput, type OdoriConfig, type ProjectGraph, type RenderOptions, type RenderTarget, type RenderTimings, type ResolvedBinary, type ResolvedConfig, type StudioServer, type VideoFormat, addCommand, alphaWarning, appendJobLog, buildAudioFilter, cacheRoot, cancelJob, checkDeterminism, chunkFrames, clearPrepareCache, compileInBrowser, componentStatus, countChanges, createContext, createIntegrityResolver, createJob, defaultConcurrency, defineConfig, devCommand, diffCommand, diffLines, discoverProject, ensureFfmpeg, exportCommand, exportQueue, fileKey, findVideo, formatDiff, formatNames, freezeManifest, generateImports, initCommand, inspectCommand, installBrowser, installFfmpeg, jobsCommand, listCommand, listJobs, loadConfig, loadVideos, localCandidates, newCommand, openRenderPage, outputName, planChunks, prepareCacheKey, probeSignatures, readAudio, readJob, readPrepareCache, readTimeline, reconcileJobs, renderMovie, renderStill, renderToolchain, resolveBrowser, resolveChromePath, resolveCueFile, resolveFfmpeg, resolveFormat, runJob, runPrepare, seekTo, startStudioServer, stillCommand, targetFor, testCommand, updateCommand, updateJob, withServer, writeGenerated, writePrepareCache };
|