@a-company/atelier 0.28.2 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { AtelierDocument } from '@a-company/atelier-types';
2
+ import { AtelierDocument, StudioRecipe, VideoProjectManifest, CutEntry, VideoCutList, VideoTranscript, Layer, Delta, TranscriptWord } from '@a-company/atelier-types';
3
3
  import { ResolvedFrame } from '@a-company/atelier-core';
4
4
 
5
5
  /**
@@ -85,6 +85,182 @@ declare function exportSvgCommand(program: Command): void;
85
85
  /** Register the `export-lottie` subcommand on the Commander program. */
86
86
  declare function exportLottieCommand(program: Command): void;
87
87
 
88
+ /**
89
+ * Shared single-frame render-to-PNG path.
90
+ *
91
+ * Extracted from `atelier export-image` so the carousel batch driver and the
92
+ * single-frame export command render through one code path. Rasterizes via
93
+ * `@napi-rs/canvas` — a Canvas2D implementation shipped as prebuilt platform
94
+ * binaries, so PNG export works on a plain install with no node-gyp build and
95
+ * no system libraries (cairo/pango/etc.). Pre-loads any ImageVisual layers via
96
+ * its `loadImage` (file path / data-URL / buffer all work server-side, the same
97
+ * mechanism the MP4 render-pipeline uses) and renders one resolved frame scaled
98
+ * to the requested output dimensions.
99
+ */
100
+
101
+ /** @napi-rs/canvas surface — the subset this module touches. */
102
+ interface NodeCanvas {
103
+ getContext(id: "2d"): unknown;
104
+ toBuffer(format: "image/png"): Buffer;
105
+ }
106
+ /** A loaded image — width/height are the natural pixel dims. */
107
+ interface LoadedImage {
108
+ width: number;
109
+ height: number;
110
+ }
111
+ /** Canvas module surface (createCanvas + loadImage). */
112
+ interface CanvasModule {
113
+ createCanvas: (w: number, h: number) => NodeCanvas;
114
+ loadImage: (src: string) => Promise<LoadedImage>;
115
+ }
116
+ /** Bounds + centered frame for fitting an image into a canvas. */
117
+ interface ImageFitResult {
118
+ bounds: {
119
+ width: number;
120
+ height: number;
121
+ };
122
+ frame: {
123
+ x: number;
124
+ y: number;
125
+ };
126
+ }
127
+ /**
128
+ * Fit a natural-sized image into a canvas while preserving aspect ratio,
129
+ * centered. Adapted from studio's image-drop helper (kept local so the CLI's
130
+ * server-side render path doesn't import the browser studio package).
131
+ *
132
+ * Landscape (wider than canvas) → fit to width; portrait (taller or equal) →
133
+ * fit to height; both dims capped at the canvas extents. Degenerate natural
134
+ * sizes fall back to canvas extents so callers always get valid bounds.
135
+ */
136
+ declare function fitImageToCanvas(canvas: {
137
+ width: number;
138
+ height: number;
139
+ }, natural: {
140
+ width: number;
141
+ height: number;
142
+ }): ImageFitResult;
143
+ /**
144
+ * Raised when the `@napi-rs/canvas` rasterizer cannot be loaded. In practice
145
+ * this never fires: `@napi-rs/canvas` is a hard dependency that ships prebuilt
146
+ * platform binaries (no node-gyp, no system libraries). It can only happen if
147
+ * the install is corrupt or running on an unsupported platform with no prebuilt
148
+ * binary — in which case a reinstall is the fix. Kept as a typed seam so call
149
+ * sites can still distinguish a missing-rasterizer failure from a bad document.
150
+ */
151
+ declare class CanvasUnavailableError extends Error {
152
+ constructor();
153
+ }
154
+ /**
155
+ * Resolve the canvas module. `@napi-rs/canvas` is a hard, statically-imported
156
+ * dependency, so this is a thin accessor that surfaces a typed
157
+ * {@link CanvasUnavailableError} on the (essentially impossible) chance the
158
+ * platform binary failed to load.
159
+ */
160
+ declare function loadCanvasModule(): Promise<CanvasModule>;
161
+ /**
162
+ * Compute final output dimensions from the document canvas and optional
163
+ * width/height overrides. When only one of width/height is provided, the
164
+ * other is derived to preserve the document's aspect ratio. When both are
165
+ * provided, both are used verbatim (allows non-uniform scaling).
166
+ */
167
+ declare function resolveExportDimensions(docWidth: number, docHeight: number, width?: number, height?: number): {
168
+ width: number;
169
+ height: number;
170
+ };
171
+ interface RenderToPngOptions {
172
+ /** State to resolve (defaults to the first state). */
173
+ state?: string;
174
+ /** Frame within the state (defaults to 0). */
175
+ frame?: number;
176
+ /** Output width override (px). */
177
+ width?: number;
178
+ /** Output height override (px). */
179
+ height?: number;
180
+ /**
181
+ * Optional hook to adjust each image layer's bounds once natural dimensions
182
+ * are known from the loaded image. Receives the layer src + natural dims and
183
+ * the doc canvas; returns new bounds (and frame). Used by the carousel driver
184
+ * to aspect-fit fit-to-canvas images that were composed with placeholder
185
+ * bounds (natural dims are unknown until the rasterizer decodes the file).
186
+ */
187
+ refitImageBounds?: (args: {
188
+ canvas: {
189
+ width: number;
190
+ height: number;
191
+ };
192
+ natural: {
193
+ width: number;
194
+ height: number;
195
+ };
196
+ }) => {
197
+ bounds: {
198
+ width: number;
199
+ height: number;
200
+ };
201
+ frame: {
202
+ x: number;
203
+ y: number;
204
+ };
205
+ };
206
+ }
207
+ /**
208
+ * Render a single resolved frame of a document to a PNG buffer.
209
+ *
210
+ * Validates state/frame, pre-loads image layers, scales rendering to fit the
211
+ * requested output dimensions, and returns the encoded PNG bytes. Throws
212
+ * {@link CanvasUnavailableError} when the rasterizer is missing and a plain
213
+ * Error for bad state/frame selection.
214
+ */
215
+ declare function renderDocumentToPng(doc: AtelierDocument, opts?: RenderToPngOptions): Promise<Buffer>;
216
+
217
+ /** Register the `export-image` subcommand on the Commander program. */
218
+ declare function exportImageCommand(program: Command): void;
219
+
220
+ /**
221
+ * Expand an --inputs pattern into a sorted list of absolute image file paths.
222
+ *
223
+ * Deliberately minimal (no new glob dependency): supports a directory path
224
+ * (lists all image files within), a single file path, or a single-segment
225
+ * `*`-glob in the final path component (e.g. `shots/*.png`, `shots/img-*`).
226
+ * Multi-segment / recursive globs are out of scope — point --inputs at a
227
+ * directory instead. The returned list is filtered to image extensions and
228
+ * sorted lexicographically so output ordering is stable.
229
+ */
230
+ declare function expandInputs(pattern: string): string[];
231
+ /**
232
+ * Build a single-frame carousel document for one image: a canvas-sized doc with
233
+ * a background and one fit-to-canvas ImageVisual layer, then the recipe's
234
+ * overlay_rules applied with currentIndex/totalCount threaded so the page-number
235
+ * overlay renders "i/N".
236
+ *
237
+ * Pure + synchronous (no node-canvas): image bounds use the canvas-extent
238
+ * fallback because natural dimensions aren't known until node-canvas decodes
239
+ * the file at render time. {@link renderDocumentToPng} re-fits the bounds via
240
+ * `refitImageBounds` once the image is loaded.
241
+ */
242
+ declare function composeCarouselFrameDoc(args: {
243
+ imagePath: string;
244
+ index: number;
245
+ total: number;
246
+ width: number;
247
+ height: number;
248
+ recipe: StudioRecipe;
249
+ background?: string;
250
+ }): AtelierDocument;
251
+ /** Zero-padded sortable filename prefix: width = max(2, digits in N). */
252
+ declare function carouselFileName(index: number, total: number, imagePath: string): string;
253
+ /**
254
+ * Register `atelier carousel <recipe> --inputs <glob> --out-dir <dir>` — batch
255
+ * compose a folder of images into recipe-overlaid PNGs.
256
+ *
257
+ * For each image i of N: build a fit-to-canvas doc, apply the recipe's
258
+ * overlay_rules with currentIndex=i / totalCount=N (handle + "i/N" page-number),
259
+ * render via the shared export-image path, and write a zero-padded sortable
260
+ * PNG into --out-dir.
261
+ */
262
+ declare function carouselCommand(program: Command): void;
263
+
88
264
  /** Asset summary entry */
89
265
  interface AssetInfo {
90
266
  assetId: string;
@@ -159,4 +335,410 @@ declare function buildFfmpegArgs(width: number, height: number, fps: number, for
159
335
  */
160
336
  declare function renderDocument(doc: AtelierDocument, opts: RenderOptions): Promise<RenderResult>;
161
337
 
162
- export { type AssetInfo, type DocumentInfo, type ProgressInfo, type RenderFormat, type RenderOptions, type RenderResult, type VariableInfo, assetsCommand, buildFfmpegArgs, checkFfmpeg, exportLottieCommand, exportSvgCommand, getAssets, getInfo, getVariables, infoCommand, renderCommand, renderDocument, resolveStill, stillCommand, validateCommand, validateFile, variablesCommand };
338
+ declare const VIDEO_PROJECT_VERSION = "1.0";
339
+ declare const VIDEO_CUTLIST_VERSION = "1.1";
340
+ declare const VIDEO_TRANSCRIPT_VERSION = "1.1";
341
+ /**
342
+ * Compute the effective in/out span for a cut, clamped to source bounds.
343
+ * `start = max(0, rawStart - paddingPre)`, `end = min(duration, rawEnd + paddingPost)`.
344
+ */
345
+ declare function effectiveSpan(cut: CutEntry, duration: number): {
346
+ start: number;
347
+ end: number;
348
+ };
349
+ /** Resolved absolute paths for all files in a VideoProject folder */
350
+ interface VideoProject {
351
+ /** Absolute path to the project folder */
352
+ dir: string;
353
+ /** Absolute path to source video file */
354
+ sourcePath: string;
355
+ /** Absolute path to project.atelier composition */
356
+ compositionPath: string;
357
+ /** Absolute path to transcript.json */
358
+ transcriptPath: string;
359
+ /** Absolute path to cuts.json */
360
+ cutsPath: string;
361
+ /** Absolute path to export/ directory */
362
+ exportDir: string;
363
+ /** Manifest metadata */
364
+ manifest: VideoProjectManifest;
365
+ }
366
+ /**
367
+ * Scaffold a new VideoProject folder from a source video file.
368
+ *
369
+ * Creates the folder at `destDir` (defaults to same directory as source,
370
+ * named after the video file without extension). Copies the source video
371
+ * into the folder as "source<ext>". Writes an empty draft project.atelier,
372
+ * an empty cuts.json, and creates the export/ directory.
373
+ *
374
+ * Does NOT run metadata extraction — the caller (`atelier edit`, T-005)
375
+ * probes duration/fps via ffprobe and fills in videoMeta before saving.
376
+ */
377
+ declare function createVideoProject(srcPath: string, destDir?: string): Promise<VideoProject>;
378
+ /**
379
+ * Load an existing VideoProject from a folder path.
380
+ * Does not validate that the composition or cut list are well-formed —
381
+ * callers that need that should use lintFile() after loading.
382
+ */
383
+ declare function loadVideoProject(dir: string): VideoProject;
384
+ /**
385
+ * Read and parse cuts.json from a VideoProject.
386
+ *
387
+ * Migrates legacy 1.0 cuts (flat { start, end }) to 1.1 parametric form
388
+ * (rawStart/rawEnd + zero padding) on the fly. Writers always emit 1.1.
389
+ */
390
+ declare function readCutList(project: VideoProject): VideoCutList;
391
+ /** Write cuts.json to a VideoProject (always at the current cut list version) */
392
+ declare function writeCutList(project: VideoProject, cuts: VideoCutList): void;
393
+ /**
394
+ * Read and parse transcript.json from a VideoProject.
395
+ *
396
+ * Migrates legacy 1.0 transcripts (TranscriptWord had flat `word: string`)
397
+ * to 1.1 (`detected` + `text` + flags) on the fly. Writers always emit 1.1.
398
+ */
399
+ declare function readTranscript(project: VideoProject): VideoTranscript | null;
400
+ /** Write transcript.json to a VideoProject (always at the current transcript version) */
401
+ declare function writeTranscript(project: VideoProject, transcript: VideoTranscript): void;
402
+ /** Read and parse project.atelier from a VideoProject */
403
+ declare function readComposition(project: VideoProject): AtelierDocument;
404
+ /** Write project.atelier to a VideoProject */
405
+ declare function writeComposition(project: VideoProject, doc: AtelierDocument): void;
406
+ /**
407
+ * Rewrite the silence-trim layers in a composition from a current cut list.
408
+ *
409
+ * Drops every layer tagged "silence-trim" then appends one VideoVisual layer
410
+ * per cut, in temporal order, with cumulative startFrame computed from prior
411
+ * clip durations. All other layers (user-authored, captions, overlays) are
412
+ * preserved untouched — this is the tag-namespace isolation invariant.
413
+ */
414
+ declare function rewriteCutLayers(doc: AtelierDocument, cuts: CutEntry[], sourceFilename: string, sourceDuration: number, assetId?: string): AtelierDocument;
415
+
416
+ interface TrimOptions {
417
+ /** silencedetect noise threshold, e.g. "-30dB" */
418
+ noise?: string;
419
+ /** Minimum silence duration to register, in seconds */
420
+ minSilence?: number;
421
+ /** Default leading padding for new cuts, in seconds */
422
+ padPre?: number;
423
+ /** Default trailing padding for new cuts, in seconds */
424
+ padPost?: number;
425
+ /** Re-detect match tolerance for preserving user padding, in seconds */
426
+ matchTolerance?: number;
427
+ /** Global tighten across all cuts, in milliseconds (positive number) */
428
+ tightenMs?: number;
429
+ /** Global loosen across all cuts, in milliseconds (positive number) */
430
+ loosenMs?: number;
431
+ /** Apply --pad-pre / --pad-post to one specific cut index only */
432
+ cutIndex?: number;
433
+ /** Discard existing padding; full fresh detect with default padding */
434
+ reset?: boolean;
435
+ /** Don't write files; return result */
436
+ dryRun?: boolean;
437
+ }
438
+ interface TrimResult {
439
+ projectDir: string;
440
+ duration: number;
441
+ cuts: CutEntry[];
442
+ layerCount: number;
443
+ }
444
+ /**
445
+ * Run the silence-trim pipeline on a VideoProject folder.
446
+ *
447
+ * Pipeline:
448
+ * 1. Probe ffmpeg has silencedetect filter
449
+ * 2. ffprobe source duration
450
+ * 3. Run silencedetect → silence intervals
451
+ * 4. Invert to speech intervals
452
+ * 5. Build fresh CutEntry[] with default (or recipe) padding
453
+ * 6. Merge with existing cuts (preserves user padding) unless --reset
454
+ * 7. Apply --tighten / --loosen / --cut overrides
455
+ * 8. Resolve overlaps at silence midpoints
456
+ * 9. Clamp boundaries to [0, duration]
457
+ * 10. Write cuts.json + rewrite silence-trim layers in project.atelier
458
+ */
459
+ declare function trimProject(projectDir: string, options?: TrimOptions): Promise<TrimResult>;
460
+ /** Register `atelier trim` on the Commander program */
461
+ declare function trimCommand(program: Command): void;
462
+
463
+ /** Whisper model selection (size/quality tradeoff) */
464
+ type WhisperModel = "tiny" | "tiny.en" | "base" | "base.en" | "small" | "small.en" | "medium" | "medium.en" | "large-v3";
465
+ interface WhisperOptions {
466
+ /** Model size (default: "base.en") */
467
+ model?: WhisperModel;
468
+ /** BCP-47 language hint (omit for autodetect) */
469
+ language?: string;
470
+ /** Explicit path to the model file — overrides model lookup by name */
471
+ modelPath?: string;
472
+ }
473
+ /** Backend that produced a transcript run */
474
+ type WhisperBackend = "whisper-cpp" | "openai-api" | "none";
475
+ /**
476
+ * Probe which Whisper backend is available on this machine.
477
+ * - "whisper-cpp" if `whisper-cli` is on PATH
478
+ * - "openai-api" if OPENAI_API_KEY is set and --use-api requested
479
+ * - "none" otherwise — caller throws with install guidance
480
+ */
481
+ declare function probeWhisper(): Promise<WhisperBackend>;
482
+ /**
483
+ * Run whisper.cpp on a source file and return the raw JSON stdout.
484
+ * Caller passes to parseWhisperCppJson() to get a VideoTranscript.
485
+ *
486
+ * Note: whisper-cli writes JSON to stdout when given --output-json and "-"
487
+ * as the output destination. Some builds always write to a file with the
488
+ * `.json` suffix appended to the input path; we handle both.
489
+ */
490
+ declare function runWhisperCpp(sourcePath: string, options?: WhisperOptions): Promise<string>;
491
+ /**
492
+ * Parse whisper.cpp --output-json output into a VideoTranscript.
493
+ *
494
+ * whisper.cpp emits a structure like:
495
+ * {
496
+ * "result": { "language": "en" },
497
+ * "transcription": [
498
+ * {
499
+ * "timestamps": { "from": "00:00:00,000", "to": "00:00:02,300" },
500
+ * "offsets": { "from": 0, "to": 2300 },
501
+ * "text": " Hello world",
502
+ * "tokens": [ ... ] // optional per-token detail
503
+ * }
504
+ * ]
505
+ * }
506
+ *
507
+ * Token-level word timestamps come via the "tokens" array on each segment
508
+ * when --output-json-full is used. With --output-json (lighter), only
509
+ * segment-level boundaries are present and we synthesize word timing by
510
+ * even split across the segment.
511
+ *
512
+ * Exported for unit testing.
513
+ */
514
+ declare function parseWhisperCppJson(jsonStr: string): VideoTranscript;
515
+
516
+ /** Default style for generated caption layers — overridable by Studio Recipe */
517
+ interface CaptionStyle {
518
+ fontFamily?: string;
519
+ fontSize?: number;
520
+ fontWeight?: number | "normal" | "bold";
521
+ textAlign?: "left" | "center" | "right";
522
+ color?: string;
523
+ /** Relative vertical position 0–1 (0 = top, 1 = bottom). Default 0.85 — lower-third. */
524
+ yRatio?: number;
525
+ /** Horizontal width as ratio of canvas width. Default 0.9. */
526
+ widthRatio?: number;
527
+ /** Fade duration in seconds for opacity in/out. Default 0.05 (3 frames at 60fps). */
528
+ fadeSeconds?: number;
529
+ }
530
+ interface BuildCaptionsOptions {
531
+ style?: CaptionStyle;
532
+ /** Phrase grouping — max words per caption phrase */
533
+ maxWords?: number;
534
+ /** Phrase grouping — pause gap in seconds that forces a phrase break */
535
+ pauseGap?: number;
536
+ }
537
+ /**
538
+ * Build caption-tagged TextVisual layers from a transcript + canvas dimensions.
539
+ *
540
+ * Each phrase becomes one Layer with:
541
+ * - tags: ["caption"]
542
+ * - TextVisual with merged style
543
+ * - opacity: 0 (default; deltas animate to 1 during the phrase)
544
+ * - positioned at canvas.width * 0.5, canvas.height * yRatio
545
+ *
546
+ * Caller is responsible for appending these to a state's deltas array
547
+ * (returned alongside the layers as Delta[] keyed to layer id).
548
+ */
549
+ declare function buildCaptionLayers(transcript: VideoTranscript, canvas: AtelierDocument["canvas"], options?: BuildCaptionsOptions): {
550
+ layers: Layer[];
551
+ deltas: Delta[];
552
+ };
553
+ /**
554
+ * Drop caption-tagged layers + their deltas from a composition,
555
+ * append fresh ones from the current transcript.
556
+ *
557
+ * Mirrors rewriteCutLayers' invariant: only caption-tagged layers touched;
558
+ * all other layers (silence-trim, overlay, user-authored) preserved.
559
+ *
560
+ * Deltas are written into the default state (or first state if no "default").
561
+ */
562
+ declare function rewriteCaptionLayers(doc: AtelierDocument, transcript: VideoTranscript, options?: BuildCaptionsOptions): AtelierDocument;
563
+
564
+ interface TranscribeOptions {
565
+ /** Whisper model selection */
566
+ model?: WhisperModel;
567
+ /** BCP-47 language hint (omit for autodetect) */
568
+ language?: string;
569
+ /** Discard existing user edits; full fresh transcript */
570
+ reset?: boolean;
571
+ /** Skip caption layer generation; transcript.json only */
572
+ noCaptions?: boolean;
573
+ /** Don't write files; return result */
574
+ dryRun?: boolean;
575
+ /** Caption style + grouping overrides, typically supplied by a Studio Recipe */
576
+ captionOptions?: BuildCaptionsOptions;
577
+ }
578
+ interface TranscribeResult {
579
+ projectDir: string;
580
+ backend: string;
581
+ transcript: VideoTranscript;
582
+ wordCount: number;
583
+ captionsGenerated: boolean;
584
+ }
585
+ /**
586
+ * Run the transcription pipeline on a VideoProject folder.
587
+ *
588
+ * Pipeline:
589
+ * 1. Probe Whisper backend (whisper-cpp / openai-api / none)
590
+ * 2. Run Whisper → raw transcript JSON
591
+ * 3. Parse into VideoTranscript shape
592
+ * 4. Merge with existing transcript (preserves user edits) unless --reset
593
+ * 5. Write transcript.json
594
+ * 6. Unless --no-captions: rewriteCaptionLayers in project.atelier
595
+ */
596
+ declare function transcribeProject(projectDir: string, options?: TranscribeOptions): Promise<TranscribeResult>;
597
+ /** Register `atelier transcribe` on the Commander program */
598
+ declare function transcribeCommand(program: Command): void;
599
+
600
+ /** Register the `atelier transcript` family of edit subcommands */
601
+ declare function transcriptCommand(program: Command): void;
602
+
603
+ /** Register `atelier captions regenerate <project>` */
604
+ declare function captionsCommand(program: Command): void;
605
+
606
+ /**
607
+ * Flatten a VideoTranscript's segments into a single ordered word array.
608
+ * Used by edit commands that take a global word index.
609
+ */
610
+ declare function flattenWords(transcript: VideoTranscript): TranscriptWord[];
611
+ /**
612
+ * Phrase block — one rendered caption layer's worth of words.
613
+ */
614
+ interface CaptionPhrase {
615
+ start: number;
616
+ end: number;
617
+ text: string;
618
+ words: TranscriptWord[];
619
+ }
620
+ /**
621
+ * Group transcript words into caption-sized phrases.
622
+ *
623
+ * Emits a new phrase when ANY of:
624
+ * - currentPhrase.length >= maxWords
625
+ * - currentWord.text ends in . ! ? , ; :
626
+ * - gap between currentWord and nextWord > pauseGap seconds
627
+ *
628
+ * Hidden words are skipped (do not appear in captions but still in transcript).
629
+ */
630
+ declare function groupIntoPhrases(transcript: VideoTranscript, options?: {
631
+ maxWords?: number;
632
+ pauseGap?: number;
633
+ }): CaptionPhrase[];
634
+ /**
635
+ * Merge a freshly-detected transcript with an existing one, preserving user
636
+ * edits (userEdited / userAdded / hidden + text override) on any word whose
637
+ * raw boundary still matches within `tolerance` seconds AND whose `detected`
638
+ * string is unchanged.
639
+ *
640
+ * userAdded words in the existing transcript have no matching fresh entry by
641
+ * definition — they're preserved as orphans, inserted into the appropriate
642
+ * segment by timing.
643
+ */
644
+ declare function mergeTranscriptWithExisting(fresh: VideoTranscript, existing: VideoTranscript, tolerance?: number): VideoTranscript;
645
+ /**
646
+ * Replace the text of word at the given global index. Sets userEdited=true.
647
+ * Returns a new VideoTranscript; does not mutate input.
648
+ */
649
+ declare function applyTextEdit(transcript: VideoTranscript, wordIndex: number, newText: string): VideoTranscript;
650
+ /**
651
+ * Apply a batch find/replace across all detected words.
652
+ * Sets userEdited=true on every word whose detected text matches `find`.
653
+ */
654
+ declare function applyBatchReplace(transcript: VideoTranscript, find: string, replace: string): VideoTranscript;
655
+ /** Hide a word (excluded from caption render, kept in transcript). Sets hidden=true. */
656
+ declare function applyHide(transcript: VideoTranscript, wordIndex: number): VideoTranscript;
657
+ /**
658
+ * Insert a user-added word after the given global index.
659
+ * Sets userAdded=true. Duration defaults to 0.15s if not specified.
660
+ * Word is placed in the segment containing the anchor word.
661
+ */
662
+ declare function applyAdd(transcript: VideoTranscript, afterIndex: number, text: string, duration?: number): VideoTranscript;
663
+ /**
664
+ * Merge adjacent words at indices i and i+1 into a single word.
665
+ * The merged word's detected/text becomes the concatenation; timing spans both.
666
+ * Sets userEdited=true.
667
+ */
668
+ declare function applyMerge(transcript: VideoTranscript, firstIndex: number): VideoTranscript;
669
+ /**
670
+ * Split one word at the given fractional point (0–1). Timing prorates.
671
+ * Sets userEdited=true on both halves. `firstText` and `secondText` default
672
+ * to slicing the original text at its character midpoint.
673
+ */
674
+ declare function applySplit(transcript: VideoTranscript, wordIndex: number, fraction: number, firstText?: string, secondText?: string): VideoTranscript;
675
+
676
+ /** Register `atelier recipe new/validate/show` family */
677
+ declare function recipeCommand(program: Command): void;
678
+
679
+ /**
680
+ * Register `atelier apply-recipe <project> <recipe>` — convenience verb that
681
+ * runs `atelier trim` and `atelier transcribe` against the same recipe in
682
+ * one shot. Useful for fresh-project bootstrap.
683
+ */
684
+ declare function applyRecipeCommand(program: Command): void;
685
+
686
+ declare const RECIPE_VERSION = "1.0";
687
+ interface LoadedRecipe {
688
+ recipe: StudioRecipe;
689
+ /** Absolute path the recipe was read from */
690
+ path: string;
691
+ /** Warnings from validateRecipe (e.g. reserved-field usage) */
692
+ warnings: string[];
693
+ }
694
+ /**
695
+ * Resolve a recipe reference to an absolute path.
696
+ *
697
+ * Resolution order (per studio-recipe.md §6.2):
698
+ * 1. <projectDir>/.atelier/recipes/<name>.recipe.{yaml,json}
699
+ * 2. ~/.atelier/recipes/<name>.recipe.{yaml,json}
700
+ * 3. The literal path passed (absolute or relative to cwd)
701
+ *
702
+ * Absolute paths and paths containing slashes skip resolution and load directly.
703
+ */
704
+ declare function resolveRecipePath(pathOrName: string, projectDir?: string): string;
705
+ /**
706
+ * Load and validate a recipe from a path or name.
707
+ *
708
+ * Sniffs YAML vs JSON by file extension; falls back to YAML parser when
709
+ * unclear (YAML is a superset of JSON for objects).
710
+ */
711
+ declare function loadRecipe(pathOrName: string, projectDir?: string): LoadedRecipe;
712
+ /**
713
+ * Generate a starter recipe YAML with every Phase 1 field present and
714
+ * inline comments documenting it. Authors learn the shape by editing.
715
+ */
716
+ declare function scaffoldRecipeYaml(name: string): string;
717
+ /**
718
+ * Merge a recipe's silence_policy into TrimOptions.
719
+ * CLI options take precedence (per studio-recipe.md §4.1).
720
+ */
721
+ declare function applyRecipeToTrimOptions(recipe: StudioRecipe | undefined, cliOptions: TrimOptions): TrimOptions;
722
+ /**
723
+ * Translate a recipe's caption_style + caption_grouping into runtime
724
+ * BuildCaptionsOptions for the caption builder.
725
+ *
726
+ * CLI doesn't currently expose per-invocation caption styling flags;
727
+ * the recipe is the canonical source for now.
728
+ */
729
+ declare function applyRecipeToCaptionOptions(recipe: StudioRecipe | undefined): BuildCaptionsOptions;
730
+ /**
731
+ * Merge a recipe into TranscribeOptions. Caption-related recipe fields
732
+ * are stashed on the transcribe options so the orchestrator can pass them
733
+ * through to rewriteCaptionLayers.
734
+ */
735
+ declare function applyRecipeToTranscribeOptions(recipe: StudioRecipe | undefined, cliOptions: TranscribeOptions): TranscribeOptions;
736
+ /**
737
+ * Render a recipe's effective values (recipe overlaid on code defaults).
738
+ * Used by `atelier recipe show --with-defaults`.
739
+ */
740
+ declare function renderRecipeWithDefaults(recipe: StudioRecipe): StudioRecipe;
741
+ /** Serialize a recipe back to YAML for `recipe show` output */
742
+ declare function recipeToYaml(recipe: StudioRecipe): string;
743
+
744
+ export { type AssetInfo, type BuildCaptionsOptions, CanvasUnavailableError, type CaptionStyle, type DocumentInfo, type LoadedRecipe, type ProgressInfo, RECIPE_VERSION, type RenderFormat, type RenderOptions, type RenderResult, type TranscribeOptions, type TranscribeResult, type TrimOptions, type TrimResult, VIDEO_CUTLIST_VERSION, VIDEO_PROJECT_VERSION, VIDEO_TRANSCRIPT_VERSION, type VariableInfo, type VideoProject, type WhisperBackend, type WhisperModel, type WhisperOptions, applyAdd, applyBatchReplace, applyHide, applyMerge, applyRecipeCommand, applyRecipeToCaptionOptions, applyRecipeToTranscribeOptions, applyRecipeToTrimOptions, applySplit, applyTextEdit, assetsCommand, buildCaptionLayers, buildFfmpegArgs, captionsCommand, carouselCommand, carouselFileName, checkFfmpeg, composeCarouselFrameDoc, createVideoProject, effectiveSpan, expandInputs, exportImageCommand, exportLottieCommand, exportSvgCommand, fitImageToCanvas, flattenWords, getAssets, getInfo, getVariables, groupIntoPhrases, infoCommand, loadCanvasModule, loadRecipe, loadVideoProject, mergeTranscriptWithExisting, parseWhisperCppJson, probeWhisper, readComposition, readCutList, readTranscript, recipeCommand, recipeToYaml, renderCommand, renderDocument, renderDocumentToPng, renderRecipeWithDefaults, resolveExportDimensions, resolveRecipePath, resolveStill, rewriteCaptionLayers, rewriteCutLayers, runWhisperCpp, scaffoldRecipeYaml, stillCommand, transcribeCommand, transcribeProject, transcriptCommand, trimCommand, trimProject, validateCommand, validateFile, variablesCommand, writeComposition, writeCutList, writeTranscript };