@a-company/atelier 0.29.0 → 0.37.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/chunk-5QQESXI6.js +4432 -0
- package/dist/chunk-5QQESXI6.js.map +1 -0
- package/dist/cli.cjs +2391 -530
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +301 -429
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2233 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +584 -2
- package/dist/index.d.ts +584 -2
- package/dist/index.js +111 -3
- package/dist/mcp.cjs +1215 -365
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +1209 -365
- package/dist/mcp.js.map +1 -1
- package/package.json +20 -9
- package/src/web/inline-app.ts +867 -0
- package/src/web/tsconfig.json +9 -0
- package/templates/welcome.atelier +67 -0
- package/university/content/notes/N-atel-001-first-render.md +114 -0
- package/university/content/notes/N-atel-001-install-and-launch.md +84 -0
- package/university/content/notes/N-atel-001-what-is-atelier.md +51 -0
- package/university/content/notes/N-atel-101-easings.md +97 -0
- package/university/content/notes/N-atel-101-layers.md +106 -0
- package/university/content/notes/N-atel-101-states-and-deltas.md +94 -0
- package/university/content/notes/N-atel-101-the-atelier-format.md +72 -0
- package/university/content/notes/N-atel-201-authoring-tools.md +141 -0
- package/university/content/notes/N-atel-201-mcp-overview.md +86 -0
- package/university/content/notes/N-atel-201-patterns.md +108 -0
- package/university/content/notes/N-atel-201-visual-and-effects.md +125 -0
- package/university/content/notes/N-atel-301-composition-and-overlays.md +141 -0
- package/university/content/notes/N-atel-301-effects.md +136 -0
- package/university/content/notes/N-atel-301-images-and-video.md +126 -0
- package/university/content/notes/N-atel-301-shapes-and-text.md +118 -0
- package/university/content/notes/N-atel-401-hierarchical-states.md +71 -0
- package/university/content/notes/N-atel-401-motion-deep-dive.md +106 -0
- package/university/content/notes/N-atel-401-presets-and-templates.md +98 -0
- package/university/content/notes/N-atel-401-transitions.md +94 -0
- package/university/content/notes/N-atel-501-detected-vs-user-edited.md +76 -0
- package/university/content/notes/N-atel-501-layer-tag-isolation.md +62 -0
- package/university/content/notes/N-atel-501-silence-trim.md +98 -0
- package/university/content/notes/N-atel-501-transcribe-and-captions.md +98 -0
- package/university/content/notes/N-atel-601-carousel.md +71 -0
- package/university/content/notes/N-atel-601-overlay-rules.md +96 -0
- package/university/content/notes/N-atel-601-recipe-tools-and-apply.md +84 -0
- package/university/content/notes/N-atel-601-studio-recipe.md +103 -0
- package/university/content/notes/N-atel-701-choosing-output.md +68 -0
- package/university/content/notes/N-atel-701-png-and-frames.md +84 -0
- package/university/content/notes/N-atel-701-vector.md +85 -0
- package/university/content/notes/N-atel-701-video.md +88 -0
- package/university/content/notes/N-atel-801-editing-surface.md +69 -0
- package/university/content/notes/N-atel-801-live-bridge.md +84 -0
- package/university/content/notes/N-atel-801-studio-app.md +72 -0
- package/university/content/notes/N-atel-801-symbiotic-loop.md +56 -0
- package/university/content/paths/LP-atel-001.yaml +21 -0
- package/university/content/paths/LP-atel-101.yaml +22 -0
- package/university/content/paths/LP-atel-201.yaml +23 -0
- package/university/content/paths/LP-atel-301.yaml +22 -0
- package/university/content/paths/LP-atel-401.yaml +22 -0
- package/university/content/paths/LP-atel-501.yaml +22 -0
- package/university/content/paths/LP-atel-601.yaml +22 -0
- package/university/content/paths/LP-atel-701.yaml +22 -0
- package/university/content/paths/LP-atel-801.yaml +22 -0
- package/university/content/quizzes/Q-atel-001-orientation.yaml +66 -0
- package/university/content/quizzes/Q-atel-101-document-model.yaml +66 -0
- package/university/content/quizzes/Q-atel-201-mcp-authoring.yaml +66 -0
- package/university/content/quizzes/Q-atel-301-visual-system.yaml +66 -0
- package/university/content/quizzes/Q-atel-401-state-machines.yaml +66 -0
- package/university/content/quizzes/Q-atel-501-video-pipeline.yaml +66 -0
- package/university/content/quizzes/Q-atel-601-recipes.yaml +66 -0
- package/university/content/quizzes/Q-atel-701-export.yaml +66 -0
- package/university/content/quizzes/Q-atel-801-studio-loop.yaml +66 -0
- package/university/index.yaml +720 -0
- package/university/pack.yaml +21 -0
- package/dist/chunk-JV7RGETS.js +0 -2292
- package/dist/chunk-JV7RGETS.js.map +0 -1
package/dist/index.d.ts
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
|
-
|
|
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 };
|