@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.
Files changed (70) hide show
  1. package/LICENSE +22 -0
  2. package/bin/odori.mjs +39 -0
  3. package/dist/chunk-7XJL2BYO.js +3552 -0
  4. package/dist/cli.d.ts +10 -0
  5. package/dist/cli.js +10 -0
  6. package/dist/index.d.ts +622 -0
  7. package/dist/index.js +156 -0
  8. package/dist/registry-snapshot-NIH2JMQ6.js +3559 -0
  9. package/package.json +50 -0
  10. package/src/audio-mix.ts +133 -0
  11. package/src/binaries.ts +241 -0
  12. package/src/brand-file.ts +94 -0
  13. package/src/chunk-cache.ts +85 -0
  14. package/src/chunks.ts +78 -0
  15. package/src/cli.ts +319 -0
  16. package/src/commands/add.ts +151 -0
  17. package/src/commands/dev.ts +160 -0
  18. package/src/commands/doctor.ts +162 -0
  19. package/src/commands/exportVideo.ts +198 -0
  20. package/src/commands/init.ts +56 -0
  21. package/src/commands/inspect.ts +72 -0
  22. package/src/commands/list.ts +22 -0
  23. package/src/commands/new.ts +126 -0
  24. package/src/commands/shared.ts +96 -0
  25. package/src/commands/still.ts +40 -0
  26. package/src/commands/test.ts +265 -0
  27. package/src/commands/update.ts +183 -0
  28. package/src/config.ts +84 -0
  29. package/src/contracts.ts +159 -0
  30. package/src/cues.ts +141 -0
  31. package/src/determinism.ts +82 -0
  32. package/src/diff.ts +71 -0
  33. package/src/discovery.ts +216 -0
  34. package/src/formats.ts +119 -0
  35. package/src/index.ts +58 -0
  36. package/src/integrity.ts +101 -0
  37. package/src/jobs.ts +151 -0
  38. package/src/log.ts +17 -0
  39. package/src/open.ts +32 -0
  40. package/src/paths.ts +12 -0
  41. package/src/prepare-cache.ts +58 -0
  42. package/src/project.ts +196 -0
  43. package/src/registry-snapshot.json +3431 -0
  44. package/src/registry-source.ts +269 -0
  45. package/src/render.ts +627 -0
  46. package/src/server.ts +307 -0
  47. package/studio/index.html +41 -0
  48. package/studio/src/Studio.tsx +192 -0
  49. package/studio/src/components/AudioClip.tsx +64 -0
  50. package/studio/src/components/CanvasStage.tsx +79 -0
  51. package/studio/src/components/CommandPalette.tsx +129 -0
  52. package/studio/src/components/Diagnostics.tsx +93 -0
  53. package/studio/src/components/ExportPanel.tsx +234 -0
  54. package/studio/src/components/InputControls.tsx +110 -0
  55. package/studio/src/components/Thumbnail.tsx +71 -0
  56. package/studio/src/components/Transport.tsx +237 -0
  57. package/studio/src/components/Waveform.tsx +114 -0
  58. package/studio/src/components/Wordmark.tsx +449 -0
  59. package/studio/src/components/ui.tsx +138 -0
  60. package/studio/src/lib/mix-loudness.ts +52 -0
  61. package/studio/src/main.tsx +34 -0
  62. package/studio/src/shortcuts.ts +27 -0
  63. package/studio/src/studio.css +1232 -0
  64. package/studio/src/theme.ts +61 -0
  65. package/studio/src/views/AssetsView.tsx +111 -0
  66. package/studio/src/views/BrandsView.tsx +139 -0
  67. package/studio/src/views/ComponentsView.tsx +285 -0
  68. package/studio/src/views/HomeView.tsx +122 -0
  69. package/studio/src/views/VideosView.tsx +343 -0
  70. 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
@@ -0,0 +1,10 @@
1
+ import {
2
+ checkFlags,
3
+ parseArgs,
4
+ run
5
+ } from "./chunk-7XJL2BYO.js";
6
+ export {
7
+ checkFlags,
8
+ parseArgs,
9
+ run
10
+ };
@@ -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 };