vargai 0.4.0-alpha14 → 0.4.0-alpha16

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/package.json CHANGED
@@ -64,7 +64,7 @@
64
64
  "replicate": "^1.4.0",
65
65
  "zod": "^4.2.1"
66
66
  },
67
- "version": "0.4.0-alpha14",
67
+ "version": "0.4.0-alpha16",
68
68
  "exports": {
69
69
  ".": "./src/index.ts",
70
70
  "./ai": "./src/ai-sdk/index.ts",
@@ -137,6 +137,15 @@ function isOverlayLayer(layer: Layer): boolean {
137
137
  return isVideoOverlayLayer(layer) || isImageOverlayLayer(layer);
138
138
  }
139
139
 
140
+ function isTextOverlayLayer(layer: Layer): boolean {
141
+ return (
142
+ layer.type === "title" ||
143
+ layer.type === "subtitle" ||
144
+ layer.type === "news-title" ||
145
+ layer.type === "slide-in-text"
146
+ );
147
+ }
148
+
140
149
  function buildBaseClipFilter(
141
150
  clip: ProcessedClip,
142
151
  clipIndex: number,
@@ -164,7 +173,10 @@ function buildBaseClipFilter(
164
173
  let baseLabel = "";
165
174
  let inputIdx = inputOffset;
166
175
 
167
- const baseLayers = clip.layers.filter((l) => l && !isOverlayLayer(l));
176
+ // Filter out overlay layers AND text overlay layers (text will be applied after image overlays)
177
+ const baseLayers = clip.layers.filter(
178
+ (l) => l && !isOverlayLayer(l) && !isTextOverlayLayer(l),
179
+ );
168
180
 
169
181
  for (let i = 0; i < baseLayers.length; i++) {
170
182
  const layer = baseLayers[i];
@@ -201,58 +213,6 @@ function buildBaseClipFilter(
201
213
  inputIdx++;
202
214
  }
203
215
  }
204
-
205
- if (layer.type === "title") {
206
- const titleFilter = getTitleFilter(
207
- layer as TitleLayer,
208
- baseLabel,
209
- width,
210
- height,
211
- clip.duration,
212
- );
213
- const newLabel = `title${clipIndex}_${i}`;
214
- filters.push(`${titleFilter}[${newLabel}]`);
215
- baseLabel = newLabel;
216
- }
217
-
218
- if (layer.type === "subtitle") {
219
- const subtitleFilter = getSubtitleFilter(
220
- layer as SubtitleLayer,
221
- baseLabel,
222
- width,
223
- height,
224
- clip.duration,
225
- );
226
- const newLabel = `sub${clipIndex}_${i}`;
227
- filters.push(`${subtitleFilter}[${newLabel}]`);
228
- baseLabel = newLabel;
229
- }
230
-
231
- if (layer.type === "news-title") {
232
- const newsFilter = getNewsTitleFilter(
233
- layer as NewsTitleLayer,
234
- baseLabel,
235
- width,
236
- height,
237
- clip.duration,
238
- );
239
- const newLabel = `news${clipIndex}_${i}`;
240
- filters.push(`${newsFilter}[${newLabel}]`);
241
- baseLabel = newLabel;
242
- }
243
-
244
- if (layer.type === "slide-in-text") {
245
- const slideFilter = getSlideInTextFilter(
246
- layer as SlideInTextLayer,
247
- baseLabel,
248
- width,
249
- height,
250
- clip.duration,
251
- );
252
- const newLabel = `slide${clipIndex}_${i}`;
253
- filters.push(`${slideFilter}[${newLabel}]`);
254
- baseLabel = newLabel;
255
- }
256
216
  }
257
217
 
258
218
  return {
@@ -358,6 +318,41 @@ function collectAudioLayers(
358
318
  return audioLayers;
359
319
  }
360
320
 
321
+ type TextLayer = TitleLayer | SubtitleLayer | NewsTitleLayer | SlideInTextLayer;
322
+
323
+ interface TimedTextLayer {
324
+ layer: TextLayer;
325
+ startTime: number;
326
+ duration: number;
327
+ }
328
+
329
+ function collectTextLayers(clips: ProcessedClip[]): TimedTextLayer[] {
330
+ const textLayers: TimedTextLayer[] = [];
331
+ let currentTime = 0;
332
+
333
+ for (let i = 0; i < clips.length; i++) {
334
+ const clip = clips[i];
335
+ if (!clip) continue;
336
+
337
+ for (const layer of clip.layers) {
338
+ if (layer && isTextOverlayLayer(layer)) {
339
+ textLayers.push({
340
+ layer: layer as TextLayer,
341
+ startTime: currentTime,
342
+ duration: clip.duration,
343
+ });
344
+ }
345
+ }
346
+
347
+ currentTime += clip.duration;
348
+ if (i < clips.length - 1) {
349
+ currentTime -= clip.transition.duration;
350
+ }
351
+ }
352
+
353
+ return textLayers;
354
+ }
355
+
361
356
  function buildTransitionFilter(
362
357
  fromLabel: string,
363
358
  toLabel: string,
@@ -744,6 +739,67 @@ export async function editly(config: EditlyConfig): Promise<void> {
744
739
  finalVideoLabel = currentBase;
745
740
  }
746
741
 
742
+ const textLayers = collectTextLayers(clips);
743
+ if (textLayers.length > 0) {
744
+ let currentBase = finalVideoLabel;
745
+
746
+ for (let i = 0; i < textLayers.length; i++) {
747
+ const timedLayer = textLayers[i];
748
+ if (!timedLayer) continue;
749
+
750
+ const { layer, startTime, duration } = timedLayer;
751
+ const outputLabel = `vwithtext${i}`;
752
+
753
+ const timedLayerWithEnable = {
754
+ ...layer,
755
+ start: layer.start ?? startTime,
756
+ stop: layer.stop ?? startTime + duration,
757
+ };
758
+
759
+ if (layer.type === "title") {
760
+ const titleFilter = getTitleFilter(
761
+ timedLayerWithEnable as TitleLayer,
762
+ currentBase,
763
+ width,
764
+ height,
765
+ totalDuration,
766
+ );
767
+ allFilters.push(`${titleFilter}[${outputLabel}]`);
768
+ } else if (layer.type === "subtitle") {
769
+ const subtitleFilter = getSubtitleFilter(
770
+ timedLayerWithEnable as SubtitleLayer,
771
+ currentBase,
772
+ width,
773
+ height,
774
+ totalDuration,
775
+ );
776
+ allFilters.push(`${subtitleFilter}[${outputLabel}]`);
777
+ } else if (layer.type === "news-title") {
778
+ const newsFilter = getNewsTitleFilter(
779
+ timedLayerWithEnable as NewsTitleLayer,
780
+ currentBase,
781
+ width,
782
+ height,
783
+ totalDuration,
784
+ );
785
+ allFilters.push(`${newsFilter}[${outputLabel}]`);
786
+ } else if (layer.type === "slide-in-text") {
787
+ const slideFilter = getSlideInTextFilter(
788
+ timedLayerWithEnable as SlideInTextLayer,
789
+ currentBase,
790
+ width,
791
+ height,
792
+ totalDuration,
793
+ );
794
+ allFilters.push(`${slideFilter}[${outputLabel}]`);
795
+ }
796
+
797
+ currentBase = outputLabel;
798
+ }
799
+
800
+ finalVideoLabel = currentBase;
801
+ }
802
+
747
803
  const clipAudioLayers = collectAudioLayers(clips);
748
804
  const videoInputCount = allInputs.length;
749
805
  const audioFilter = buildAudioFilter(
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * varg find command
3
4
  * Ink-based search view
@@ -1,7 +1,4 @@
1
- /**
2
- * varg help command
3
- * Ink-based help display
4
- */
1
+ /** @jsxImportSource react */
5
2
 
6
3
  import { defineCommand } from "citty";
7
4
  import { Box, Text } from "ink";
@@ -1,7 +1,7 @@
1
1
  export { findCmd, showFindHelp } from "./find.tsx";
2
2
  export { helpCmd, showHelp } from "./help.tsx";
3
3
  export { listCmd, showListHelp } from "./list.tsx";
4
- export { renderCmd } from "./render.ts";
4
+ export { fastCmd, previewCmd, renderCmd } from "./render.ts";
5
5
  export { runCmd, showRunHelp, showTargetHelp } from "./run.tsx";
6
6
  export { studioCmd } from "./studio.ts";
7
7
  export { showWhichHelp, whichCmd } from "./which.tsx";
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * varg list command
3
4
  * Ink-based discovery view
@@ -31,7 +31,8 @@ async function loadComponent(filePath: string): Promise<VargElement> {
31
31
  const resolvedPath = resolve(filePath);
32
32
  const source = await Bun.file(resolvedPath).text();
33
33
 
34
- const hasImports =
34
+ const hasAnyImport = source.includes(" from ");
35
+ const hasVargaiImport =
35
36
  source.includes("from 'vargai") ||
36
37
  source.includes('from "vargai') ||
37
38
  source.includes("from '@vargai") ||
@@ -40,22 +41,21 @@ async function loadComponent(filePath: string): Promise<VargElement> {
40
41
  const hasJsxPragma =
41
42
  source.includes("@jsxImportSource") || source.includes("@jsx ");
42
43
 
43
- if (hasImports && hasJsxPragma) {
44
+ // file has imports (relative or absolute) - import directly to preserve paths
45
+ if (hasAnyImport) {
44
46
  const mod = await import(resolvedPath);
45
47
  return mod.default;
46
48
  }
47
49
 
50
+ // no imports - inject auto-imports and jsx pragma
48
51
  const pkgDir = new URL("../../..", import.meta.url).pathname;
49
52
  const tmpDir = `${pkgDir}/.cache/varg-render`;
50
53
  if (!existsSync(tmpDir)) {
51
54
  mkdirSync(tmpDir, { recursive: true });
52
55
  }
53
56
 
54
- const prepended = hasImports
55
- ? `/** @jsxImportSource vargai */\n`
56
- : AUTO_IMPORTS;
57
57
  const tmpFile = `${tmpDir}/${Date.now()}.tsx`;
58
- await Bun.write(tmpFile, prepended + source);
58
+ await Bun.write(tmpFile, AUTO_IMPORTS + source);
59
59
 
60
60
  try {
61
61
  const mod = await import(tmpFile);
@@ -65,100 +65,119 @@ async function loadComponent(filePath: string): Promise<VargElement> {
65
65
  }
66
66
  }
67
67
 
68
+ const sharedArgs = {
69
+ file: {
70
+ type: "positional" as const,
71
+ description: "component file (.tsx)",
72
+ required: true,
73
+ },
74
+ output: {
75
+ type: "string" as const,
76
+ alias: "o",
77
+ description: "output path",
78
+ },
79
+ cache: {
80
+ type: "string" as const,
81
+ alias: "c",
82
+ description: "cache directory",
83
+ default: ".cache/ai",
84
+ },
85
+ quiet: {
86
+ type: "boolean" as const,
87
+ alias: "q",
88
+ description: "minimal output",
89
+ default: false,
90
+ },
91
+ "no-cache": {
92
+ type: "boolean" as const,
93
+ description: "disable cache (don't read or write)",
94
+ default: false,
95
+ },
96
+ verbose: {
97
+ type: "boolean" as const,
98
+ alias: "v",
99
+ description: "show ffmpeg commands",
100
+ default: false,
101
+ },
102
+ };
103
+
104
+ async function runRender(
105
+ args: Record<string, unknown>,
106
+ mode: RenderMode,
107
+ commandName: string,
108
+ ) {
109
+ const file = args.file as string;
110
+
111
+ if (!file) {
112
+ console.error(`usage: varg ${commandName} <component.tsx> [-o output.mp4]`);
113
+ process.exit(1);
114
+ }
115
+
116
+ const component = await loadComponent(file);
117
+
118
+ if (!component || component.type !== "render") {
119
+ console.error("error: default export must be a <Render> element");
120
+ process.exit(1);
121
+ }
122
+
123
+ const basename = file
124
+ .replace(/\.tsx?$/, "")
125
+ .split("/")
126
+ .pop();
127
+ const outputPath = (args.output as string) ?? `output/${basename}.mp4`;
128
+
129
+ if (!args.quiet) {
130
+ const modeLabel =
131
+ mode === "preview" ? " (fast)" : mode === "strict" ? "" : " (preview)";
132
+ console.log(`rendering ${file} → ${outputPath}${modeLabel}`);
133
+ }
134
+
135
+ const useCache = !args["no-cache"] && mode !== "preview";
136
+
137
+ const defaults = await detectDefaultModels();
138
+
139
+ const buffer = await render(component, {
140
+ output: outputPath,
141
+ cache: useCache ? (args.cache as string) : undefined,
142
+ mode,
143
+ defaults,
144
+ verbose: args.verbose as boolean,
145
+ });
146
+
147
+ if (!args.quiet) {
148
+ console.log(`done! ${buffer.byteLength} bytes → ${outputPath}`);
149
+ }
150
+ }
151
+
68
152
  export const renderCmd = defineCommand({
69
153
  meta: {
70
154
  name: "render",
71
- description: "render a react component to video",
155
+ description: "render to video (strict mode - fails on errors)",
72
156
  },
73
- args: {
74
- file: {
75
- type: "positional",
76
- description: "component file (.tsx)",
77
- required: true,
78
- },
79
- output: {
80
- type: "string",
81
- alias: "o",
82
- description: "output path",
83
- },
84
- cache: {
85
- type: "string",
86
- alias: "c",
87
- description: "cache directory",
88
- default: ".cache/ai",
89
- },
90
- quiet: {
91
- type: "boolean",
92
- alias: "q",
93
- description: "minimal output",
94
- default: false,
95
- },
96
- strict: {
97
- type: "boolean",
98
- description: "fail on provider errors (no fallback)",
99
- default: false,
100
- },
101
- preview: {
102
- type: "boolean",
103
- description: "skip all generation, use placeholders only",
104
- default: false,
105
- },
106
- "no-cache": {
107
- type: "boolean",
108
- description: "disable cache (don't read or write)",
109
- default: false,
110
- },
157
+ args: sharedArgs,
158
+ async run({ args }) {
159
+ await runRender(args, "strict", "render");
160
+ },
161
+ });
162
+
163
+ export const previewCmd = defineCommand({
164
+ meta: {
165
+ name: "preview",
166
+ description: "render with fallback placeholders on errors",
167
+ },
168
+ args: sharedArgs,
169
+ async run({ args }) {
170
+ await runRender(args, "default", "preview");
171
+ },
172
+ });
173
+
174
+ export const fastCmd = defineCommand({
175
+ meta: {
176
+ name: "fast",
177
+ description: "render with all placeholders (no generation)",
111
178
  },
179
+ args: sharedArgs,
112
180
  async run({ args }) {
113
- const file = args.file as string;
114
-
115
- if (!file) {
116
- console.error("usage: varg render <component.tsx> [-o output.mp4]");
117
- process.exit(1);
118
- }
119
-
120
- const component = await loadComponent(file);
121
-
122
- if (!component || component.type !== "render") {
123
- console.error("error: default export must be a <Render> element");
124
- process.exit(1);
125
- }
126
-
127
- const basename = file
128
- .replace(/\.tsx?$/, "")
129
- .split("/")
130
- .pop();
131
- const outputPath = args.output ?? `output/${basename}.mp4`;
132
-
133
- const mode: RenderMode = args.strict
134
- ? "strict"
135
- : args.preview
136
- ? "preview"
137
- : "default";
138
-
139
- if (!args.quiet) {
140
- const modeLabel =
141
- mode === "preview"
142
- ? " (preview)"
143
- : mode === "strict"
144
- ? " (strict)"
145
- : "";
146
- console.log(`rendering ${file} → ${outputPath}${modeLabel}`);
147
- }
148
-
149
- const useCache = !args["no-cache"] && mode !== "preview";
150
-
151
- const defaults = await detectDefaultModels();
152
-
153
- const buffer = await render(component, {
154
- output: outputPath,
155
- cache: useCache ? args.cache : undefined,
156
- mode,
157
- defaults,
158
- });
159
-
160
- if (!args.quiet) {
161
- console.log(`done! ${buffer.byteLength} bytes → ${outputPath}`);
162
- }
181
+ await runRender(args, "preview", "fast");
163
182
  },
164
183
  });
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * varg run command
3
4
  * Ink-based execution with live status updates
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * varg which command
3
4
  * Ink-based inspection view
package/src/cli/index.ts CHANGED
@@ -12,9 +12,11 @@ import { defineCommand, runMain } from "citty";
12
12
  import { registry } from "../core/registry";
13
13
  import { allDefinitions } from "../definitions";
14
14
  import {
15
+ fastCmd,
15
16
  findCmd,
16
17
  helpCmd,
17
18
  listCmd,
19
+ previewCmd,
18
20
  renderCmd,
19
21
  runCmd,
20
22
  showFindHelp,
@@ -104,6 +106,8 @@ const main = defineCommand({
104
106
  subCommands: {
105
107
  run: runCmd,
106
108
  render: renderCmd,
109
+ preview: previewCmd,
110
+ fast: fastCmd,
107
111
  studio: studioCmd,
108
112
  list: listCmd,
109
113
  ls: listCmd,
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * Badge - Type indicator badge
3
4
  * Shows [model] [action] [skill] with appropriate styling
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * DataTable - Table display component
3
4
  * Clean text-based table for listing items
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * Header - Section header component
3
4
  * Bold dimmed text for section titles
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * HelpBlock - Command help and examples
3
4
  * Displays usage patterns and command examples
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * KeyValue - Label-value pair display
3
4
  * Aligned key-value pairs with optional required marker
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * OptionRow - CLI option display with better spacing
3
4
  * Shows option name, description, type hints, defaults, and enums
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * Separator - Horizontal divider line
3
4
  * Minimal visual separator between sections
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * StatusBox - Execution status display
3
4
  * Shows running/done/error state with params and results
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * VargBox - Container component
3
4
  * Luxury minimal styled box with optional title and borders
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * VargProgress - Progress bar component
3
4
  * Elegant thin progress visualization
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * VargSpinner - Animated loading indicator
3
4
  * Elegant braille spinner with label
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource react */
1
2
  /**
2
3
  * VargText - Typography component
3
4
  * Provides semantic text variants for consistent styling
@@ -22,7 +22,7 @@ export async function renderMusic(
22
22
  duration: props.duration,
23
23
  });
24
24
 
25
- const modelId = model.modelId;
25
+ const modelId = model.modelId ?? "music";
26
26
  const taskId = ctx.progress ? addTask(ctx.progress, "music", modelId) : null;
27
27
 
28
28
  const generateFn = async () => {
@@ -103,7 +103,7 @@ export function completeTask(tracker: ProgressTracker, id: string): void {
103
103
  }
104
104
 
105
105
  function getEstimate(task: ProgressTask): number {
106
- const modelLower = task.model.toLowerCase();
106
+ const modelLower = task.model?.toLowerCase() ?? "";
107
107
  for (const [key, estimate] of Object.entries(MODEL_TIME_ESTIMATES)) {
108
108
  if (modelLower.includes(key.toLowerCase())) {
109
109
  return estimate;
@@ -70,42 +70,63 @@ export async function renderRoot(
70
70
  placeholderCount.total++;
71
71
  };
72
72
 
73
+ const cachedGenerateImage = options.cache
74
+ ? withCache(generateImage, { storage: fileCache({ dir: options.cache }) })
75
+ : generateImage;
76
+
77
+ const cachedGenerateVideo = options.cache
78
+ ? withCache(generateVideo, { storage: fileCache({ dir: options.cache }) })
79
+ : generateVideo;
80
+
73
81
  const wrapGenerateImage: typeof generateImage = async (opts) => {
74
82
  if (
75
83
  typeof opts.model === "string" ||
76
84
  opts.model.specificationVersion !== "v3"
77
85
  ) {
78
- return generateImage(opts);
86
+ return cachedGenerateImage(opts);
87
+ }
88
+
89
+ if (mode === "preview") {
90
+ trackPlaceholder("image");
91
+ }
92
+
93
+ try {
94
+ return await cachedGenerateImage(opts);
95
+ } catch (error) {
96
+ if (mode === "strict") throw error;
97
+ trackPlaceholder("image");
98
+ onFallback(error as Error, String(opts.prompt));
99
+ const wrappedModel = wrapImageModel({
100
+ model: opts.model,
101
+ middleware: imagePlaceholderFallbackMiddleware({
102
+ mode: "preview",
103
+ onFallback: () => {},
104
+ }),
105
+ });
106
+ return generateImage({ ...opts, model: wrappedModel });
79
107
  }
80
- const wrappedModel = wrapImageModel({
81
- model: opts.model,
82
- middleware: imagePlaceholderFallbackMiddleware({
83
- mode,
84
- onFallback: (error, prompt) => {
85
- trackPlaceholder("image");
86
- onFallback(error, prompt);
87
- },
88
- }),
89
- });
90
- const result = await generateImage({ ...opts, model: wrappedModel });
91
- if (mode === "preview") trackPlaceholder("image");
92
- return result;
93
108
  };
94
109
 
95
110
  const wrapGenerateVideo: typeof generateVideo = async (opts) => {
96
- const wrappedModel = wrapVideoModel({
97
- model: opts.model,
98
- middleware: placeholderFallbackMiddleware({
99
- mode,
100
- onFallback: (error, prompt) => {
101
- trackPlaceholder("video");
102
- onFallback(error, prompt);
103
- },
104
- }),
105
- });
106
- const result = await generateVideo({ ...opts, model: wrappedModel });
107
- if (mode === "preview") trackPlaceholder("video");
108
- return result;
111
+ if (mode === "preview") {
112
+ trackPlaceholder("video");
113
+ }
114
+
115
+ try {
116
+ return await cachedGenerateVideo(opts);
117
+ } catch (error) {
118
+ if (mode === "strict") throw error;
119
+ trackPlaceholder("video");
120
+ onFallback(error as Error, String(opts.prompt));
121
+ const wrappedModel = wrapVideoModel({
122
+ model: opts.model,
123
+ middleware: placeholderFallbackMiddleware({
124
+ mode: "preview",
125
+ onFallback: () => {},
126
+ }),
127
+ });
128
+ return generateVideo({ ...opts, model: wrappedModel });
129
+ }
109
130
  };
110
131
 
111
132
  const ctx: RenderContext = {
@@ -113,16 +134,8 @@ export async function renderRoot(
113
134
  height: props.height ?? 1080,
114
135
  fps: props.fps ?? 30,
115
136
  cache: options.cache ? fileCache({ dir: options.cache }) : undefined,
116
- generateImage: options.cache
117
- ? withCache(wrapGenerateImage, {
118
- storage: fileCache({ dir: options.cache }),
119
- })
120
- : wrapGenerateImage,
121
- generateVideo: options.cache
122
- ? withCache(wrapGenerateVideo, {
123
- storage: fileCache({ dir: options.cache }),
124
- })
125
- : wrapGenerateVideo,
137
+ generateImage: wrapGenerateImage,
138
+ generateVideo: wrapGenerateVideo,
126
139
  tempFiles: [],
127
140
  progress,
128
141
  pending: new Map(),
@@ -288,6 +301,7 @@ export async function renderRoot(
288
301
  fps: ctx.fps,
289
302
  clips,
290
303
  audioTracks: audioTracks.length > 0 ? audioTracks : undefined,
304
+ verbose: options.verbose,
291
305
  });
292
306
 
293
307
  completeTask(progress, editlyTaskId);
@@ -223,6 +223,7 @@ export interface RenderOptions {
223
223
  output?: string;
224
224
  cache?: string;
225
225
  quiet?: boolean;
226
+ verbose?: boolean;
226
227
  mode?: RenderMode;
227
228
  defaults?: DefaultModels;
228
229
  }