ossclip 0.1.21 → 0.1.23

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.
@@ -0,0 +1,463 @@
1
+ import { isInteractive } from "../interactive/tty";
2
+
3
+ /**
4
+ * ANSI Color & Style Helpers (zero-dependency, ultra-fast string templates).
5
+ */
6
+ export const ansi = {
7
+ reset: "\x1b[0m",
8
+ bold: "\x1b[1m",
9
+ dim: "\x1b[2m",
10
+ italic: "\x1b[3m",
11
+ underline: "\x1b[4m",
12
+ blink: "\x1b[5m",
13
+ inverse: "\x1b[7m",
14
+
15
+ // Foreground
16
+ black: "\x1b[30m",
17
+ red: "\x1b[31m",
18
+ green: "\x1b[32m",
19
+ yellow: "\x1b[33m",
20
+ blue: "\x1b[34m",
21
+ magenta: "\x1b[35m",
22
+ cyan: "\x1b[36m",
23
+ white: "\x1b[37m",
24
+
25
+ // Bright foreground
26
+ brightBlack: "\x1b[90m",
27
+ brightRed: "\x1b[91m",
28
+ brightGreen: "\x1b[92m",
29
+ brightYellow: "\x1b[93m",
30
+ brightBlue: "\x1b[94m",
31
+ brightMagenta: "\x1b[95m",
32
+ brightCyan: "\x1b[96m",
33
+ brightWhite: "\x1b[97m",
34
+
35
+ // 256-color & RGB
36
+ rgb: (r: number, g: number, b: number) => `\x1b[38;2;${r};${g};${b}m`,
37
+ bgRgb: (r: number, g: number, b: number) => `\x1b[48;2;${r};${g};${b}m`,
38
+
39
+ // Cursor control
40
+ cursorHide: "\x1b[?25l",
41
+ cursorShow: "\x1b[?25h",
42
+ clearLine: "\x1b[2K\r",
43
+ up: (lines = 1) => `\x1b[${lines}A`,
44
+ };
45
+
46
+ /**
47
+ * Format SMPTE timecode (HH:MM:SS:FF) from seconds and fps.
48
+ */
49
+ export function formatTimecode(seconds: number, fps = 30): string {
50
+ const totalFrames = Math.max(0, Math.floor(seconds * fps));
51
+ const ff = totalFrames % fps;
52
+ const totalSecs = Math.floor(totalFrames / fps);
53
+ const ss = totalSecs % 60;
54
+ const totalMins = Math.floor(totalSecs / 60);
55
+ const mm = totalMins % 60;
56
+ const hh = Math.floor(totalMins / 60);
57
+ const pad = (n: number, w = 2) => String(n).padStart(w, "0");
58
+ return `${pad(hh)}:${pad(mm)}:${pad(ss)}:${pad(ff)}`;
59
+ }
60
+
61
+ /**
62
+ * Strip ANSI escape codes to calculate visual string width.
63
+ */
64
+ export function stripAnsi(str: string): string {
65
+ // eslint-disable-next-line no-control-regex
66
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
67
+ }
68
+
69
+ /**
70
+ * Unicode Audio Waves, Synth Oscilloscope & Braille spinners.
71
+ */
72
+ const AUDIO_WAVE_CHARS = [" ", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
73
+ const SPINNERS = {
74
+ dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
75
+ filmReel: ["◐", "◓", "◑", "◒"],
76
+ sprockets: ["[░▒▓█▓▒░]", "[▒▓█▓▒░░]", "[▓█▓▒░░▒]", "[█▓▒░░▒▓]"],
77
+ equalizer: [" ▂▃▅▆▇█", "▂▃▅▆▇█ ", "▃▅▆▇█ ▂", "▅▆▇█ ▂▃", "▆▇█ ▂▃▅", "▇█ ▂▃▅▆", "█ ▂▃▅▆▇"],
78
+ neural: ["◈◇◇◇", "◇◈◇◇", "◇◇◈◇", "◇◇◇◈", "◇◇◈◇", "◇◈◇◇"],
79
+ };
80
+
81
+ /**
82
+ * Render an organic multi-harmonic analog synthesizer oscilloscope wave.
83
+ */
84
+ export function renderSynthWave(frame: number, width = 16): string {
85
+ const BRAILLE_WAVE = ["⣀", "⠤", "⠒", "⠊", "⠉", "⠑", "⠒", "⠤", "⣀", "⡀", "⠄", "⠂", "⠁", "⠈", "⠐", "⠠", "⢀"];
86
+ let wave = "";
87
+ for (let i = 0; i < width; i++) {
88
+ const phase = frame * 0.25 + i * 0.45;
89
+ const harmonic = Math.sin(phase) * 0.65 + Math.sin(phase * 2.1 + frame * 0.1) * 0.35;
90
+ const normalized = Math.max(0, Math.min(1, (harmonic + 1) / 2));
91
+ const idx = Math.floor(normalized * (BRAILLE_WAVE.length - 1));
92
+ const char = BRAILLE_WAVE[idx];
93
+
94
+ // Phosphor glow gradient: Neon Amber to Electric Cyan
95
+ const r = Math.round(255 - normalized * 180);
96
+ const g = Math.round(160 + normalized * 70);
97
+ const b = Math.round(40 + normalized * 210);
98
+ wave += `${ansi.rgb(r, g, b)}${char}`;
99
+ }
100
+ return wave + ansi.reset;
101
+ }
102
+
103
+ /**
104
+ * Generate a dynamic gradient progress bar with customizable character resolution.
105
+ */
106
+ export function renderProgressBar(
107
+ progress: number,
108
+ width = 32,
109
+ options: {
110
+ filledChar?: string;
111
+ emptyChar?: string;
112
+ gradient?: "cyan-magenta" | "emerald" | "amber" | "cyberpunk";
113
+ } = {},
114
+ ): string {
115
+ const clamped = Math.max(0, Math.min(1, progress));
116
+ const filledCount = Math.round(clamped * width);
117
+ const emptyCount = width - filledCount;
118
+ const fillChar = options.filledChar ?? "█";
119
+ const emptyChar = options.emptyChar ?? "░";
120
+
121
+ let coloredFill = "";
122
+ for (let i = 0; i < filledCount; i++) {
123
+ const frac = i / Math.max(1, width - 1);
124
+ if (options.gradient === "emerald") {
125
+ // Emerald to neon cyan
126
+ const r = Math.round(16 + frac * 20);
127
+ const g = Math.round(185 + frac * 50);
128
+ const b = Math.round(129 + frac * 120);
129
+ coloredFill += `${ansi.rgb(r, g, b)}${fillChar}`;
130
+ } else if (options.gradient === "amber") {
131
+ // Amber to gold
132
+ const r = Math.round(245 + frac * 10);
133
+ const g = Math.round(158 + frac * 70);
134
+ const b = Math.round(11 + frac * 30);
135
+ coloredFill += `${ansi.rgb(r, g, b)}${fillChar}`;
136
+ } else if (options.gradient === "cyberpunk") {
137
+ // Neon yellow to magenta to cyan
138
+ const r = Math.round(255 - frac * 150);
139
+ const g = Math.round(70 + frac * 170);
140
+ const b = Math.round(180 + frac * 75);
141
+ coloredFill += `${ansi.rgb(r, g, b)}${fillChar}`;
142
+ } else {
143
+ // Default: Cyan to Electric Violet
144
+ const r = Math.round(56 + frac * 180);
145
+ const g = Math.round(189 - frac * 60);
146
+ const b = Math.round(248 + frac * 7);
147
+ coloredFill += `${ansi.rgb(r, g, b)}${fillChar}`;
148
+ }
149
+ }
150
+
151
+ const empty = `${ansi.dim}${ansi.brightBlack}${emptyChar.repeat(emptyCount)}${ansi.reset}`;
152
+ return `${coloredFill}${empty}${ansi.reset}`;
153
+ }
154
+
155
+ /**
156
+ * Animated Terminal Stage / Activity Monitor
157
+ */
158
+ export class StageAnimator {
159
+ private timer: NodeJS.Timeout | null = null;
160
+ private frameIndex = 0;
161
+ private linesRendered = 0;
162
+ private startedAt = Date.now();
163
+ private isTty = isInteractive();
164
+
165
+ constructor(
166
+ private title: string,
167
+ private subtitle: string,
168
+ private type: "audio" | "whisper" | "ai" | "render" | "master" = "render",
169
+ ) {}
170
+
171
+ public start(intervalMs = 60): this {
172
+ if (!this.isTty) return this;
173
+ this.startedAt = Date.now();
174
+ process.stdout.write(ansi.cursorHide);
175
+ this.timer = setInterval(() => this.tick(), intervalMs);
176
+ this.tick();
177
+ return this;
178
+ }
179
+
180
+ public update(subtitle: string): void {
181
+ this.subtitle = subtitle;
182
+ if (this.isTty) this.render();
183
+ }
184
+
185
+ private tick(): void {
186
+ this.frameIndex++;
187
+ this.render();
188
+ }
189
+
190
+ private render(): void {
191
+ if (!this.isTty) return;
192
+ const elapsedSec = ((Date.now() - this.startedAt) / 1000).toFixed(1);
193
+ const cols = Math.min(100, process.stdout.columns || 80);
194
+
195
+ let visual = "";
196
+ const maxVisualWidth = Math.max(40, (process.stdout.columns || 80) - 4);
197
+ const clamp = (s: string) => {
198
+ const plain = stripAnsi(s);
199
+ if (plain.length <= maxVisualWidth) return s;
200
+ return s.slice(0, maxVisualWidth - 3) + "...";
201
+ };
202
+
203
+ if (this.type === "ai") {
204
+ const spinner = SPINNERS.neural[this.frameIndex % SPINNERS.neural.length];
205
+ const dots = SPINNERS.dots[this.frameIndex % SPINNERS.dots.length];
206
+ visual =
207
+ `${ansi.bold}${ansi.brightMagenta}⚡ GEMINI 3.7 FLASH // NEURAL SCENE PLANNER${ansi.reset} ` +
208
+ `${ansi.cyan}[${spinner}]${ansi.reset} ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
209
+ ` ${ansi.brightCyan}${dots}${ansi.reset} ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset}`;
210
+ } else if (this.type === "whisper") {
211
+ const dots = SPINNERS.dots[this.frameIndex % SPINNERS.dots.length];
212
+ const eq = SPINNERS.equalizer[this.frameIndex % SPINNERS.equalizer.length];
213
+ visual =
214
+ `${ansi.bold}${ansi.brightCyan}🎙️ WHISPER ASR ENGINE // NEURAL PHONEME STREAM${ansi.reset} ` +
215
+ `${ansi.green}${eq}${ansi.reset} ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
216
+ ` ${ansi.cyan}${dots}${ansi.reset} ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset}`;
217
+ } else if (this.type === "audio") {
218
+ const synth = renderSynthWave(this.frameIndex, 14);
219
+ visual =
220
+ `${ansi.bold}${ansi.brightYellow}🔊 ANALOG SYNTH AUDIO OSCILLOSCOPE${ansi.reset} ` +
221
+ `[${synth}] ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
222
+ ` ${ansi.brightYellow}▸${ansi.reset} ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset} ` +
223
+ `${ansi.dim}48kHz 24-bit PCM │ EBU R128 (-14.0 LUFS)${ansi.reset}`;
224
+ } else if (this.type === "master") {
225
+ const dots = SPINNERS.dots[this.frameIndex % SPINNERS.dots.length];
226
+ visual =
227
+ `${ansi.bold}${ansi.brightGreen}🎛️ MASTERING AUDIO // EBU R128 DUAL-PASS LOUDNORM${ansi.reset} ` +
228
+ `${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
229
+ ` ${ansi.brightGreen}${dots}${ansi.reset} ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset}`;
230
+ } else {
231
+ const film = SPINNERS.filmReel[this.frameIndex % SPINNERS.filmReel.length];
232
+ visual =
233
+ `${ansi.bold}${ansi.brightCyan}🎞️ ${this.title}${ansi.reset} ` +
234
+ `${ansi.brightYellow}${film}${ansi.reset} ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
235
+ ` ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset}`;
236
+ }
237
+
238
+ this.clearRenderedLines();
239
+ const lines = visual.split("\n");
240
+ for (const l of lines) {
241
+ process.stdout.write(`${l}\n`);
242
+ }
243
+ this.linesRendered = lines.length;
244
+ }
245
+
246
+ private clearRenderedLines(): void {
247
+ if (this.linesRendered > 0) {
248
+ process.stdout.write(ansi.up(this.linesRendered) + "\r");
249
+ for (let i = 0; i < this.linesRendered; i++) {
250
+ process.stdout.write(ansi.clearLine);
251
+ if (i < this.linesRendered - 1) process.stdout.write("\n");
252
+ }
253
+ if (this.linesRendered > 1) process.stdout.write(ansi.up(this.linesRendered - 1));
254
+ process.stdout.write("\r");
255
+ }
256
+ }
257
+
258
+ public stop(successMessage?: string): void {
259
+ if (this.timer) {
260
+ clearInterval(this.timer);
261
+ this.timer = null;
262
+ }
263
+ if (!this.isTty) return;
264
+ this.clearRenderedLines();
265
+ process.stdout.write(ansi.cursorShow);
266
+ if (successMessage) {
267
+ process.stdout.write(`${successMessage}\n`);
268
+ }
269
+ this.linesRendered = 0;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * High-Performance Live Render HUD for Remotion Video Rendering.
275
+ * Emits dynamic timeline tracks, SMPTE timecode, FPS ticker, scene badges & progress.
276
+ */
277
+ export class RenderTimelineHUD {
278
+ private isTty = isInteractive();
279
+ private startedAt = Date.now();
280
+ private lastUpdateMs = 0;
281
+ private currentProgress = 0;
282
+ private totalDurationSec: number;
283
+ private sceneNames: string[];
284
+ private fps: number;
285
+ private timer: NodeJS.Timeout | null = null;
286
+ private frameCount = 0;
287
+ private linesRendered = 0;
288
+ private aspect: string;
289
+
290
+ constructor(opts: {
291
+ totalDurationSec: number;
292
+ sceneNames?: string[];
293
+ fps?: number;
294
+ aspect?: string;
295
+ }) {
296
+ this.totalDurationSec = Math.max(1, opts.totalDurationSec);
297
+ this.sceneNames = opts.sceneNames ?? [];
298
+ this.fps = opts.fps ?? 30;
299
+ this.aspect = opts.aspect ?? "9:16";
300
+ }
301
+
302
+ public start(): this {
303
+ if (!this.isTty) return this;
304
+ this.startedAt = Date.now();
305
+ process.stdout.write(ansi.cursorHide);
306
+ this.timer = setInterval(() => this.tick(), 60);
307
+ this.render();
308
+ return this;
309
+ }
310
+
311
+ public setProgress(progress: number): void {
312
+ this.currentProgress = Math.max(0, Math.min(1, progress));
313
+ this.lastUpdateMs = Date.now();
314
+ }
315
+
316
+ private tick(): void {
317
+ this.frameCount++;
318
+ this.render();
319
+ }
320
+
321
+ private render(): void {
322
+ if (!this.isTty) return;
323
+ const now = Date.now();
324
+ const elapsedSec = Math.max(0.1, (now - this.startedAt) / 1000);
325
+ const progress = this.currentProgress;
326
+ const totalFrames = Math.round(this.totalDurationSec * this.fps);
327
+ const renderedFrames = Math.round(progress * totalFrames);
328
+ const renderFps = (renderedFrames / elapsedSec).toFixed(1);
329
+ const realtimeMultiplier = (renderedFrames / (elapsedSec * this.fps)).toFixed(2);
330
+ const etaSec = progress > 0.02 ? Math.max(0, (elapsedSec / progress) * (1 - progress)).toFixed(1) : "...";
331
+
332
+ const currentTimecode = formatTimecode(progress * this.totalDurationSec, this.fps);
333
+ const totalTimecode = formatTimecode(this.totalDurationSec, this.fps);
334
+
335
+ // Compute exact bounded terminal box dimensions (guaranteed never to wrap)
336
+ const termCols = Math.min(74, Math.max(50, (process.stdout.columns || 80) - 2));
337
+ const innerWidth = termCols - 4; // between ║ ... ║
338
+ const barWidth = Math.max(12, Math.min(22, innerWidth - 28));
339
+
340
+ // Dynamic track representation
341
+ const reel = SPINNERS.filmReel[this.frameCount % SPINNERS.filmReel.length];
342
+ const synthWaveA1 = renderSynthWave(this.frameCount, 12);
343
+
344
+ // Identify active scene
345
+ let activeSceneLabel = "Main Composition";
346
+ if (this.sceneNames.length > 0) {
347
+ const sceneIndex = Math.min(
348
+ this.sceneNames.length - 1,
349
+ Math.floor(progress * this.sceneNames.length),
350
+ );
351
+ activeSceneLabel = `Scene ${sceneIndex + 1}/${this.sceneNames.length}: ${this.sceneNames[sceneIndex]}`;
352
+ }
353
+
354
+ const pctString = (progress * 100).toFixed(1).padStart(5, " ");
355
+ const bar = renderProgressBar(progress, barWidth, { gradient: "cyberpunk" });
356
+
357
+ const padLine = (content: string) => {
358
+ const vis = stripAnsi(content).length;
359
+ const pad = Math.max(0, innerWidth - vis);
360
+ return `${ansi.bold}║${ansi.reset} ${content}${" ".repeat(pad)} ${ansi.bold}║${ansi.reset}`;
361
+ };
362
+
363
+ const headerContent = `${ansi.yellow}${reel}${ansi.reset} ${ansi.bold}${ansi.brightCyan}${currentTimecode}${ansi.reset} / ${ansi.dim}${totalTimecode}${ansi.reset} │ ${ansi.brightMagenta}${this.aspect}${ansi.reset} │ ${ansi.green}${realtimeMultiplier}x Speed${ansi.reset}`;
364
+ const trackV1Content = `${ansi.brightWhite}V1:${ansi.reset} [${bar}] ${ansi.bold}${ansi.brightYellow}${pctString}%${ansi.reset} │ Frame: ${renderedFrames}/${totalFrames}`;
365
+ const trackA1Content = `${ansi.brightWhite}A1:${ansi.reset} [${synthWaveA1}] │ ${renderFps}fps │ ETA:${ansi.brightYellow}${etaSec}s${ansi.reset}`;
366
+ const sceneContent = `${ansi.dim}Scene:${ansi.reset} ${ansi.brightMagenta}${activeSceneLabel.slice(0, innerWidth - 8)}${ansi.reset}`;
367
+
368
+ const topBorder = `${ansi.bold}${ansi.brightCyan}╔══ 🎞️ REMOTION RENDER ENGINE ${"═".repeat(Math.max(2, innerWidth - 27))}╗${ansi.reset}`;
369
+ const bottomBorder = `${ansi.bold}${ansi.brightCyan}╚${"═".repeat(innerWidth + 2)}╝${ansi.reset}`;
370
+
371
+ const lines = [
372
+ topBorder,
373
+ padLine(headerContent),
374
+ padLine(trackV1Content),
375
+ padLine(trackA1Content),
376
+ padLine(sceneContent),
377
+ bottomBorder,
378
+ ];
379
+
380
+ this.clearRenderedLines();
381
+ for (const l of lines) {
382
+ process.stdout.write(`${l}\n`);
383
+ }
384
+ this.linesRendered = lines.length;
385
+ }
386
+
387
+ private clearRenderedLines(): void {
388
+ if (this.linesRendered > 0) {
389
+ process.stdout.write(ansi.up(this.linesRendered) + "\r");
390
+ for (let i = 0; i < this.linesRendered; i++) {
391
+ process.stdout.write(ansi.clearLine);
392
+ if (i < this.linesRendered - 1) process.stdout.write("\n");
393
+ }
394
+ if (this.linesRendered > 1) process.stdout.write(ansi.up(this.linesRendered - 1));
395
+ process.stdout.write("\r");
396
+ }
397
+ }
398
+
399
+ public stop(): void {
400
+ if (this.timer) {
401
+ clearInterval(this.timer);
402
+ this.timer = null;
403
+ }
404
+ if (!this.isTty) return;
405
+ this.clearRenderedLines();
406
+ process.stdout.write(ansi.cursorShow);
407
+ const totalSec = ((Date.now() - this.startedAt) / 1000).toFixed(1);
408
+ const multiplier = (this.totalDurationSec / Math.max(0.1, (Date.now() - this.startedAt) / 1000)).toFixed(2);
409
+ process.stdout.write(
410
+ ` ${ansi.green}✓${ansi.reset} ${ansi.bold}${ansi.brightWhite}Render complete in ${totalSec}s${ansi.reset} ` +
411
+ `(${ansi.brightGreen}${multiplier}x realtime speed${ansi.reset} · ${Math.round(this.totalDurationSec * this.fps)} frames @ ${this.fps}fps)\n`,
412
+ );
413
+ this.linesRendered = 0;
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Render the ultimate celebration & project stats banner.
419
+ */
420
+ export function printProductionCompleteBanner(summary: {
421
+ outPath: string;
422
+ coverPath?: string;
423
+ sourceDurationSec: number;
424
+ outputDurationSec: number;
425
+ sceneCount: number;
426
+ llmProvider?: string;
427
+ renderTimeSec?: number;
428
+ }): void {
429
+ const savedSec = Math.max(0, summary.sourceDurationSec - summary.outputDurationSec);
430
+ const cutPct = summary.sourceDurationSec > 0
431
+ ? Math.round((savedSec / summary.sourceDurationSec) * 100)
432
+ : 0;
433
+
434
+ const providerLabel = summary.llmProvider
435
+ ? summary.llmProvider === "gemini"
436
+ ? "Gemini 3.7 Flash API"
437
+ : summary.llmProvider === "antigravity"
438
+ ? "Google Antigravity (Gemini 3.7 Flash)"
439
+ : summary.llmProvider
440
+ : "Local Fast-Cut Engine";
441
+
442
+ const box = [
443
+ `${ansi.bold}${ansi.brightCyan}╔════════════════════════════════════════════════════════════════════════════════╗${ansi.reset}`,
444
+ `${ansi.bold}║${ansi.reset} ${ansi.bold}${ansi.brightYellow}🎬 MASTER REEL PRODUCED SUCCESSFULLY${ansi.reset} ${ansi.bold}║${ansi.reset}`,
445
+ `${ansi.bold}╠════════════════════════════════════════════════════════════════════════════════╣${ansi.reset}`,
446
+ `${ansi.bold}║${ansi.reset} ${ansi.brightCyan}▸ Video:${ansi.reset} ${ansi.bold}${ansi.brightWhite}${summary.outPath}${ansi.reset}`,
447
+ ...(summary.coverPath
448
+ ? [`${ansi.bold}║${ansi.reset} ${ansi.brightMagenta}▸ Cover:${ansi.reset} ${ansi.white}${summary.coverPath}${ansi.reset}`]
449
+ : []),
450
+ `${ansi.bold}║${ansi.reset} ${ansi.green}▸ Engine:${ansi.reset} ${ansi.bold}${ansi.brightCyan}${providerLabel}${ansi.reset} + ${ansi.brightWhite}Remotion${ansi.reset} + ${ansi.brightWhite}Whisper.cpp${ansi.reset}`,
451
+ `${ansi.bold}║${ansi.reset} ${ansi.yellow}▸ Cut:${ansi.reset} ${ansi.bold}${summary.outputDurationSec.toFixed(1)}s${ansi.reset} (trimmed from ${summary.sourceDurationSec.toFixed(1)}s · ${ansi.green}-${cutPct}% dead air/flubs${ansi.reset})`,
452
+ `${ansi.bold}║${ansi.reset} ${ansi.blue}▸ Scenes:${ansi.reset} ${ansi.bold}${summary.sceneCount}${ansi.reset} dynamic AI graphic overlay${summary.sceneCount === 1 ? "" : "s"} placed`,
453
+ ...(summary.renderTimeSec !== undefined
454
+ ? [`${ansi.bold}║${ansi.reset} ${ansi.brightGreen}▸ Render:${ansi.reset} ${summary.renderTimeSec.toFixed(1)}s (${(summary.outputDurationSec / summary.renderTimeSec).toFixed(2)}x realtime export)`]
455
+ : []),
456
+ `${ansi.bold}╚════════════════════════════════════════════════════════════════════════════════╝${ansi.reset}`,
457
+ ];
458
+
459
+ console.log("");
460
+ for (const l of box) {
461
+ console.log(l);
462
+ }
463
+ }