ossclip 0.1.21 → 0.1.22

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,421 @@
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
+ * Unicode Audio Waves & Braille spinners.
63
+ */
64
+ const AUDIO_WAVE_CHARS = [" ", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
65
+ const SPINNERS = {
66
+ dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
67
+ filmReel: ["◐", "◓", "◑", "◒"],
68
+ sprockets: ["[░▒▓█▓▒░]", "[▒▓█▓▒░░]", "[▓█▓▒░░▒]", "[█▓▒░░▒▓]"],
69
+ equalizer: [" ▂▃▅▆▇█", "▂▃▅▆▇█ ", "▃▅▆▇█ ▂", "▅▆▇█ ▂▃", "▆▇█ ▂▃▅", "▇█ ▂▃▅▆", "█ ▂▃▅▆▇"],
70
+ neural: ["◈◇◇◇", "◇◈◇◇", "◇◇◈◇", "◇◇◇◈", "◇◇◈◇", "◇◈◇◇"],
71
+ };
72
+
73
+ /**
74
+ * Generate a dynamic gradient progress bar with customizable character resolution.
75
+ */
76
+ export function renderProgressBar(
77
+ progress: number,
78
+ width = 32,
79
+ options: {
80
+ filledChar?: string;
81
+ emptyChar?: string;
82
+ gradient?: "cyan-magenta" | "emerald" | "amber" | "cyberpunk";
83
+ } = {},
84
+ ): string {
85
+ const clamped = Math.max(0, Math.min(1, progress));
86
+ const filledCount = Math.round(clamped * width);
87
+ const emptyCount = width - filledCount;
88
+ const fillChar = options.filledChar ?? "█";
89
+ const emptyChar = options.emptyChar ?? "░";
90
+
91
+ let coloredFill = "";
92
+ for (let i = 0; i < filledCount; i++) {
93
+ const frac = i / Math.max(1, width - 1);
94
+ if (options.gradient === "emerald") {
95
+ // Emerald to neon cyan
96
+ const r = Math.round(16 + frac * 20);
97
+ const g = Math.round(185 + frac * 50);
98
+ const b = Math.round(129 + frac * 120);
99
+ coloredFill += `${ansi.rgb(r, g, b)}${fillChar}`;
100
+ } else if (options.gradient === "amber") {
101
+ // Amber to gold
102
+ const r = Math.round(245 + frac * 10);
103
+ const g = Math.round(158 + frac * 70);
104
+ const b = Math.round(11 + frac * 30);
105
+ coloredFill += `${ansi.rgb(r, g, b)}${fillChar}`;
106
+ } else if (options.gradient === "cyberpunk") {
107
+ // Neon yellow to magenta to cyan
108
+ const r = Math.round(255 - frac * 150);
109
+ const g = Math.round(70 + frac * 170);
110
+ const b = Math.round(180 + frac * 75);
111
+ coloredFill += `${ansi.rgb(r, g, b)}${fillChar}`;
112
+ } else {
113
+ // Default: Cyan to Electric Violet
114
+ const r = Math.round(56 + frac * 180);
115
+ const g = Math.round(189 - frac * 60);
116
+ const b = Math.round(248 + frac * 7);
117
+ coloredFill += `${ansi.rgb(r, g, b)}${fillChar}`;
118
+ }
119
+ }
120
+
121
+ const empty = `${ansi.dim}${ansi.brightBlack}${emptyChar.repeat(emptyCount)}${ansi.reset}`;
122
+ return `${coloredFill}${empty}${ansi.reset}`;
123
+ }
124
+
125
+ /**
126
+ * Animated Terminal Stage / Activity Monitor
127
+ */
128
+ export class StageAnimator {
129
+ private timer: NodeJS.Timeout | null = null;
130
+ private frameIndex = 0;
131
+ private linesRendered = 0;
132
+ private startedAt = Date.now();
133
+ private isTty = isInteractive();
134
+
135
+ constructor(
136
+ private title: string,
137
+ private subtitle: string,
138
+ private type: "audio" | "whisper" | "ai" | "render" | "master" = "render",
139
+ ) {}
140
+
141
+ public start(intervalMs = 60): this {
142
+ if (!this.isTty) return this;
143
+ this.startedAt = Date.now();
144
+ process.stdout.write(ansi.cursorHide);
145
+ this.timer = setInterval(() => this.tick(), intervalMs);
146
+ this.tick();
147
+ return this;
148
+ }
149
+
150
+ public update(subtitle: string): void {
151
+ this.subtitle = subtitle;
152
+ if (this.isTty) this.render();
153
+ }
154
+
155
+ private tick(): void {
156
+ this.frameIndex++;
157
+ this.render();
158
+ }
159
+
160
+ private render(): void {
161
+ if (!this.isTty) return;
162
+ const elapsedSec = ((Date.now() - this.startedAt) / 1000).toFixed(1);
163
+ const cols = Math.min(100, process.stdout.columns || 80);
164
+
165
+ let visual = "";
166
+ if (this.type === "ai") {
167
+ const spinner = SPINNERS.neural[this.frameIndex % SPINNERS.neural.length];
168
+ const dots = SPINNERS.dots[this.frameIndex % SPINNERS.dots.length];
169
+ visual =
170
+ `${ansi.bold}${ansi.brightMagenta}⚡ GEMINI 3.7 FLASH // NEURAL SCENE PLANNER${ansi.reset} ` +
171
+ `${ansi.cyan}[${spinner}]${ansi.reset} ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
172
+ ` ${ansi.brightCyan}${dots}${ansi.reset} ${ansi.brightWhite}${this.subtitle}${ansi.reset}`;
173
+ } else if (this.type === "whisper") {
174
+ const dots = SPINNERS.dots[this.frameIndex % SPINNERS.dots.length];
175
+ const eq = SPINNERS.equalizer[this.frameIndex % SPINNERS.equalizer.length];
176
+ visual =
177
+ `${ansi.bold}${ansi.brightCyan}🎙️ WHISPER ASR ENGINE // NEURAL PHONEME STREAM${ansi.reset} ` +
178
+ `${ansi.green}${eq}${ansi.reset} ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
179
+ ` ${ansi.cyan}${dots}${ansi.reset} ${ansi.brightWhite}${this.subtitle}${ansi.reset}`;
180
+ } else if (this.type === "audio") {
181
+ const eq = SPINNERS.equalizer[this.frameIndex % SPINNERS.equalizer.length];
182
+ const wave = Array.from({ length: 14 }, (_, i) => {
183
+ const h = Math.abs(Math.sin((this.frameIndex + i * 2) * 0.35));
184
+ const idx = Math.floor(h * (AUDIO_WAVE_CHARS.length - 1));
185
+ return AUDIO_WAVE_CHARS[idx];
186
+ }).join("");
187
+ visual =
188
+ `${ansi.bold}${ansi.brightYellow}🔊 AUDIO ANALYSIS & LEVELING${ansi.reset} ` +
189
+ `${ansi.yellow}[${wave}]${ansi.reset} ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
190
+ ` ${ansi.brightYellow}▸${ansi.reset} ${ansi.brightWhite}${this.subtitle}${ansi.reset} ` +
191
+ `${ansi.dim}VU L/R: -14.0 LUFS broadcast target${ansi.reset}`;
192
+ } else if (this.type === "master") {
193
+ const dots = SPINNERS.dots[this.frameIndex % SPINNERS.dots.length];
194
+ visual =
195
+ `${ansi.bold}${ansi.brightGreen}🎛️ MASTERING AUDIO // EBU R128 DUAL-PASS LOUDNORM${ansi.reset} ` +
196
+ `${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
197
+ ` ${ansi.brightGreen}${dots}${ansi.reset} ${ansi.brightWhite}${this.subtitle}${ansi.reset}`;
198
+ } else {
199
+ const film = SPINNERS.filmReel[this.frameIndex % SPINNERS.filmReel.length];
200
+ visual =
201
+ `${ansi.bold}${ansi.brightCyan}🎞️ ${this.title}${ansi.reset} ` +
202
+ `${ansi.brightYellow}${film}${ansi.reset} ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
203
+ ` ${ansi.brightWhite}${this.subtitle}${ansi.reset}`;
204
+ }
205
+
206
+ this.clearRenderedLines();
207
+ const lines = visual.split("\n");
208
+ for (const l of lines) {
209
+ process.stdout.write(`${l}\n`);
210
+ }
211
+ this.linesRendered = lines.length;
212
+ }
213
+
214
+ private clearRenderedLines(): void {
215
+ if (this.linesRendered > 0) {
216
+ process.stdout.write(ansi.up(this.linesRendered));
217
+ for (let i = 0; i < this.linesRendered; i++) {
218
+ process.stdout.write(ansi.clearLine);
219
+ if (i < this.linesRendered - 1) process.stdout.write("\n");
220
+ }
221
+ if (this.linesRendered > 1) process.stdout.write(ansi.up(this.linesRendered - 1));
222
+ process.stdout.write("\r");
223
+ }
224
+ }
225
+
226
+ public stop(successMessage?: string): void {
227
+ if (this.timer) {
228
+ clearInterval(this.timer);
229
+ this.timer = null;
230
+ }
231
+ if (!this.isTty) return;
232
+ this.clearRenderedLines();
233
+ process.stdout.write(ansi.cursorShow);
234
+ if (successMessage) {
235
+ process.stdout.write(`${successMessage}\n`);
236
+ }
237
+ this.linesRendered = 0;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * High-Performance Live Render HUD for Remotion Video Rendering.
243
+ * Emits dynamic timeline tracks, SMPTE timecode, FPS ticker, scene badges & progress.
244
+ */
245
+ export class RenderTimelineHUD {
246
+ private isTty = isInteractive();
247
+ private startedAt = Date.now();
248
+ private lastUpdateMs = 0;
249
+ private currentProgress = 0;
250
+ private totalDurationSec: number;
251
+ private sceneNames: string[];
252
+ private fps: number;
253
+ private timer: NodeJS.Timeout | null = null;
254
+ private frameCount = 0;
255
+ private linesRendered = 0;
256
+ private aspect: string;
257
+
258
+ constructor(opts: {
259
+ totalDurationSec: number;
260
+ sceneNames?: string[];
261
+ fps?: number;
262
+ aspect?: string;
263
+ }) {
264
+ this.totalDurationSec = Math.max(1, opts.totalDurationSec);
265
+ this.sceneNames = opts.sceneNames ?? [];
266
+ this.fps = opts.fps ?? 30;
267
+ this.aspect = opts.aspect ?? "9:16";
268
+ }
269
+
270
+ public start(): this {
271
+ if (!this.isTty) return this;
272
+ this.startedAt = Date.now();
273
+ process.stdout.write(ansi.cursorHide);
274
+ this.timer = setInterval(() => this.tick(), 60);
275
+ this.render();
276
+ return this;
277
+ }
278
+
279
+ public setProgress(progress: number): void {
280
+ this.currentProgress = Math.max(0, Math.min(1, progress));
281
+ this.lastUpdateMs = Date.now();
282
+ if (this.isTty) this.render();
283
+ }
284
+
285
+ private tick(): void {
286
+ this.frameCount++;
287
+ this.render();
288
+ }
289
+
290
+ private render(): void {
291
+ if (!this.isTty) return;
292
+ const now = Date.now();
293
+ const elapsedSec = Math.max(0.1, (now - this.startedAt) / 1000);
294
+ const progress = this.currentProgress;
295
+ const totalFrames = Math.round(this.totalDurationSec * this.fps);
296
+ const renderedFrames = Math.round(progress * totalFrames);
297
+ const renderFps = (renderedFrames / elapsedSec).toFixed(1);
298
+ const realtimeMultiplier = (renderedFrames / (elapsedSec * this.fps)).toFixed(2);
299
+ const etaSec = progress > 0.02 ? Math.max(0, (elapsedSec / progress) * (1 - progress)).toFixed(1) : "calculating...";
300
+
301
+ const currentTimecode = formatTimecode(progress * this.totalDurationSec, this.fps);
302
+ const totalTimecode = formatTimecode(this.totalDurationSec, this.fps);
303
+
304
+ const termWidth = Math.max(60, Math.min(100, process.stdout.columns || 80));
305
+ const barWidth = Math.max(20, Math.min(36, termWidth - 46));
306
+
307
+ // Dynamic track representation
308
+ const reel = SPINNERS.filmReel[this.frameCount % SPINNERS.filmReel.length];
309
+ const wave1 = SPINNERS.equalizer[this.frameCount % SPINNERS.equalizer.length];
310
+ const wave2 = SPINNERS.equalizer[(this.frameCount + 3) % SPINNERS.equalizer.length];
311
+
312
+ // Identify active scene
313
+ let activeSceneLabel = "Main Composition";
314
+ if (this.sceneNames.length > 0) {
315
+ const sceneIndex = Math.min(
316
+ this.sceneNames.length - 1,
317
+ Math.floor(progress * this.sceneNames.length),
318
+ );
319
+ activeSceneLabel = `Scene ${sceneIndex + 1}/${this.sceneNames.length}: ${this.sceneNames[sceneIndex]}`;
320
+ }
321
+
322
+ const pctString = (progress * 100).toFixed(1).padStart(5, " ");
323
+ const bar = renderProgressBar(progress, barWidth, { gradient: "cyberpunk" });
324
+
325
+ const lines = [
326
+ `${ansi.bold}${ansi.brightCyan}╔══ 🎞️ REMOTION MULTI-THREADED RENDER ENGINE ═══════════════════════════╗${ansi.reset}`,
327
+ `${ansi.bold}║${ansi.reset} ${ansi.yellow}${reel}${ansi.reset} ${ansi.brightWhite}TIMECODE:${ansi.reset} ${ansi.bold}${ansi.brightCyan}${currentTimecode}${ansi.reset} / ${ansi.dim}${totalTimecode}${ansi.reset} ` +
328
+ `│ ${ansi.brightMagenta}${this.aspect}${ansi.reset} @ ${this.fps}fps │ ${ansi.green}${realtimeMultiplier}x Speed${ansi.reset}`,
329
+ `${ansi.bold}║${ansi.reset} ${ansi.brightWhite}TRACK V1:${ansi.reset} [${bar}] ${ansi.bold}${ansi.brightYellow}${pctString}%${ansi.reset}`,
330
+ `${ansi.bold}║${ansi.reset} ${ansi.brightWhite}TRACK A1:${ansi.reset} ${ansi.cyan}${wave1}${wave2}${wave1}${wave2}${wave1}${ansi.reset} ` +
331
+ `${ansi.dim}│ Frame:${ansi.reset} ${ansi.brightWhite}${renderedFrames}/${totalFrames}${ansi.reset} ` +
332
+ `${ansi.dim}│ Speed:${ansi.reset} ${ansi.brightGreen}${renderFps} fps${ansi.reset} ` +
333
+ `${ansi.dim}│ ETA:${ansi.reset} ${ansi.brightYellow}${etaSec}s${ansi.reset}`,
334
+ `${ansi.bold}║${ansi.reset} ${ansi.dim}▶ Active:${ansi.reset} ${ansi.brightMagenta}${activeSceneLabel}${ansi.reset}`,
335
+ `${ansi.bold}${ansi.brightCyan}╚════════════════════════════════════════════════════════════════════════╝${ansi.reset}`,
336
+ ];
337
+
338
+ this.clearRenderedLines();
339
+ for (const l of lines) {
340
+ process.stdout.write(`${l}\n`);
341
+ }
342
+ this.linesRendered = lines.length;
343
+ }
344
+
345
+ private clearRenderedLines(): void {
346
+ if (this.linesRendered > 0) {
347
+ process.stdout.write(ansi.up(this.linesRendered));
348
+ for (let i = 0; i < this.linesRendered; i++) {
349
+ process.stdout.write(ansi.clearLine);
350
+ if (i < this.linesRendered - 1) process.stdout.write("\n");
351
+ }
352
+ if (this.linesRendered > 1) process.stdout.write(ansi.up(this.linesRendered - 1));
353
+ process.stdout.write("\r");
354
+ }
355
+ }
356
+
357
+ public stop(): void {
358
+ if (this.timer) {
359
+ clearInterval(this.timer);
360
+ this.timer = null;
361
+ }
362
+ if (!this.isTty) return;
363
+ this.clearRenderedLines();
364
+ process.stdout.write(ansi.cursorShow);
365
+ const totalSec = ((Date.now() - this.startedAt) / 1000).toFixed(1);
366
+ const multiplier = (this.totalDurationSec / Math.max(0.1, (Date.now() - this.startedAt) / 1000)).toFixed(2);
367
+ process.stdout.write(
368
+ ` ${ansi.green}✓${ansi.reset} ${ansi.bold}${ansi.brightWhite}Render complete in ${totalSec}s${ansi.reset} ` +
369
+ `(${ansi.brightGreen}${multiplier}x realtime speed${ansi.reset} · ${Math.round(this.totalDurationSec * this.fps)} frames @ ${this.fps}fps)\n`,
370
+ );
371
+ this.linesRendered = 0;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Render the ultimate celebration & project stats banner.
377
+ */
378
+ export function printProductionCompleteBanner(summary: {
379
+ outPath: string;
380
+ coverPath?: string;
381
+ sourceDurationSec: number;
382
+ outputDurationSec: number;
383
+ sceneCount: number;
384
+ llmProvider?: string;
385
+ renderTimeSec?: number;
386
+ }): void {
387
+ const savedSec = Math.max(0, summary.sourceDurationSec - summary.outputDurationSec);
388
+ const cutPct = summary.sourceDurationSec > 0
389
+ ? Math.round((savedSec / summary.sourceDurationSec) * 100)
390
+ : 0;
391
+
392
+ const providerLabel = summary.llmProvider
393
+ ? summary.llmProvider === "gemini"
394
+ ? "Gemini 3.7 Flash API"
395
+ : summary.llmProvider === "antigravity"
396
+ ? "Google Antigravity (Gemini 3.7 Flash)"
397
+ : summary.llmProvider
398
+ : "Local Fast-Cut Engine";
399
+
400
+ const box = [
401
+ `${ansi.bold}${ansi.brightCyan}╔════════════════════════════════════════════════════════════════════════════════╗${ansi.reset}`,
402
+ `${ansi.bold}║${ansi.reset} ${ansi.bold}${ansi.brightYellow}🎬 MASTER REEL PRODUCED SUCCESSFULLY${ansi.reset} ${ansi.bold}║${ansi.reset}`,
403
+ `${ansi.bold}╠════════════════════════════════════════════════════════════════════════════════╣${ansi.reset}`,
404
+ `${ansi.bold}║${ansi.reset} ${ansi.brightCyan}▸ Video:${ansi.reset} ${ansi.bold}${ansi.brightWhite}${summary.outPath}${ansi.reset}`,
405
+ ...(summary.coverPath
406
+ ? [`${ansi.bold}║${ansi.reset} ${ansi.brightMagenta}▸ Cover:${ansi.reset} ${ansi.white}${summary.coverPath}${ansi.reset}`]
407
+ : []),
408
+ `${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}`,
409
+ `${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})`,
410
+ `${ansi.bold}║${ansi.reset} ${ansi.blue}▸ Scenes:${ansi.reset} ${ansi.bold}${summary.sceneCount}${ansi.reset} dynamic AI graphic overlay${summary.sceneCount === 1 ? "" : "s"} placed`,
411
+ ...(summary.renderTimeSec !== undefined
412
+ ? [`${ansi.bold}║${ansi.reset} ${ansi.brightGreen}▸ Render:${ansi.reset} ${summary.renderTimeSec.toFixed(1)}s (${(summary.outputDurationSec / summary.renderTimeSec).toFixed(2)}x realtime export)`]
413
+ : []),
414
+ `${ansi.bold}╚════════════════════════════════════════════════════════════════════════════════╝${ansi.reset}`,
415
+ ];
416
+
417
+ console.log("");
418
+ for (const l of box) {
419
+ console.log(l);
420
+ }
421
+ }