ossclip 0.1.22 → 0.1.24

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ossclip",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Local-first CLI video producer: cuts silence and fillers, word-timed captions, face-aware framing, and LLM-planned code-rendered graphics — transcription and rendering never leave your machine",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,9 +36,9 @@
36
36
  "commander": "^12.1.0",
37
37
  "tsx": "^4.19.0",
38
38
  "zod": "^3.25.76",
39
- "@ossclip/renderer": "0.1.22",
40
- "@ossclip/scenes": "0.1.22",
41
- "@ossclip/core": "0.1.22"
39
+ "@ossclip/core": "0.1.24",
40
+ "@ossclip/scenes": "0.1.24",
41
+ "@ossclip/renderer": "0.1.24"
42
42
  },
43
43
  "homepage": "https://github.com/AhsanAyaz/ossclip#readme",
44
44
  "bugs": {
package/src/edit.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn, type ChildProcess } from "node:child_process";
2
- import { createReadStream, existsSync, statSync } from "node:fs";
2
+ import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
4
4
  import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
5
5
  import { homedir } from "node:os";
package/src/produce.ts CHANGED
@@ -1257,6 +1257,7 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
1257
1257
  graphicsLine = cached.graphics;
1258
1258
  beatIssues = cached.issues ?? [];
1259
1259
  }
1260
+ } else {
1260
1261
  const aiAnim = isInteractive()
1261
1262
  ? new StageAnimator(
1262
1263
  "AI SCENE SYNTHESIZER",
@@ -59,7 +59,15 @@ export function formatTimecode(seconds: number, fps = 30): string {
59
59
  }
60
60
 
61
61
  /**
62
- * Unicode Audio Waves & Braille spinners.
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.
63
71
  */
64
72
  const AUDIO_WAVE_CHARS = [" ", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
65
73
  const SPINNERS = {
@@ -70,6 +78,28 @@ const SPINNERS = {
70
78
  neural: ["◈◇◇◇", "◇◈◇◇", "◇◇◈◇", "◇◇◇◈", "◇◇◈◇", "◇◈◇◇"],
71
79
  };
72
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
+
73
103
  /**
74
104
  * Generate a dynamic gradient progress bar with customizable character resolution.
75
105
  */
@@ -163,48 +193,54 @@ export class StageAnimator {
163
193
  const cols = Math.min(100, process.stdout.columns || 80);
164
194
 
165
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
+
166
203
  if (this.type === "ai") {
167
204
  const spinner = SPINNERS.neural[this.frameIndex % SPINNERS.neural.length];
168
205
  const dots = SPINNERS.dots[this.frameIndex % SPINNERS.dots.length];
169
206
  visual =
170
207
  `${ansi.bold}${ansi.brightMagenta}⚡ GEMINI 3.7 FLASH // NEURAL SCENE PLANNER${ansi.reset} ` +
171
208
  `${ansi.cyan}[${spinner}]${ansi.reset} ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
172
- ` ${ansi.brightCyan}${dots}${ansi.reset} ${ansi.brightWhite}${this.subtitle}${ansi.reset}`;
209
+ ` ${ansi.brightCyan}${dots}${ansi.reset} ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset}`;
173
210
  } else if (this.type === "whisper") {
174
211
  const dots = SPINNERS.dots[this.frameIndex % SPINNERS.dots.length];
175
212
  const eq = SPINNERS.equalizer[this.frameIndex % SPINNERS.equalizer.length];
176
213
  visual =
177
214
  `${ansi.bold}${ansi.brightCyan}🎙️ WHISPER ASR ENGINE // NEURAL PHONEME STREAM${ansi.reset} ` +
178
215
  `${ansi.green}${eq}${ansi.reset} ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
179
- ` ${ansi.cyan}${dots}${ansi.reset} ${ansi.brightWhite}${this.subtitle}${ansi.reset}`;
216
+ ` ${ansi.cyan}${dots}${ansi.reset} ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset}`;
180
217
  } 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("");
218
+ const synth = renderSynthWave(this.frameIndex, 12);
187
219
  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}`;
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}`;
192
223
  } else if (this.type === "master") {
193
224
  const dots = SPINNERS.dots[this.frameIndex % SPINNERS.dots.length];
194
225
  visual =
195
226
  `${ansi.bold}${ansi.brightGreen}🎛️ MASTERING AUDIO // EBU R128 DUAL-PASS LOUDNORM${ansi.reset} ` +
196
227
  `${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
197
- ` ${ansi.brightGreen}${dots}${ansi.reset} ${ansi.brightWhite}${this.subtitle}${ansi.reset}`;
228
+ ` ${ansi.brightGreen}${dots}${ansi.reset} ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset}`;
198
229
  } else {
199
230
  const film = SPINNERS.filmReel[this.frameIndex % SPINNERS.filmReel.length];
200
231
  visual =
201
232
  `${ansi.bold}${ansi.brightCyan}🎞️ ${this.title}${ansi.reset} ` +
202
233
  `${ansi.brightYellow}${film}${ansi.reset} ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
203
- ` ${ansi.brightWhite}${this.subtitle}${ansi.reset}`;
234
+ ` ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset}`;
204
235
  }
205
236
 
206
237
  this.clearRenderedLines();
207
- const lines = visual.split("\n");
238
+ const rawLines = visual.split("\n");
239
+ const lines = rawLines.map((l) => {
240
+ const vis = stripAnsi(l);
241
+ if (vis.length <= maxVisualWidth) return l;
242
+ return l.slice(0, maxVisualWidth - 3) + "..." + ansi.reset;
243
+ });
208
244
  for (const l of lines) {
209
245
  process.stdout.write(`${l}\n`);
210
246
  }
@@ -213,7 +249,7 @@ export class StageAnimator {
213
249
 
214
250
  private clearRenderedLines(): void {
215
251
  if (this.linesRendered > 0) {
216
- process.stdout.write(ansi.up(this.linesRendered));
252
+ process.stdout.write(ansi.up(this.linesRendered) + "\r");
217
253
  for (let i = 0; i < this.linesRendered; i++) {
218
254
  process.stdout.write(ansi.clearLine);
219
255
  if (i < this.linesRendered - 1) process.stdout.write("\n");
@@ -279,7 +315,6 @@ export class RenderTimelineHUD {
279
315
  public setProgress(progress: number): void {
280
316
  this.currentProgress = Math.max(0, Math.min(1, progress));
281
317
  this.lastUpdateMs = Date.now();
282
- if (this.isTty) this.render();
283
318
  }
284
319
 
285
320
  private tick(): void {
@@ -296,18 +331,19 @@ export class RenderTimelineHUD {
296
331
  const renderedFrames = Math.round(progress * totalFrames);
297
332
  const renderFps = (renderedFrames / elapsedSec).toFixed(1);
298
333
  const realtimeMultiplier = (renderedFrames / (elapsedSec * this.fps)).toFixed(2);
299
- const etaSec = progress > 0.02 ? Math.max(0, (elapsedSec / progress) * (1 - progress)).toFixed(1) : "calculating...";
334
+ const etaSec = progress > 0.02 ? Math.max(0, (elapsedSec / progress) * (1 - progress)).toFixed(1) : "...";
300
335
 
301
336
  const currentTimecode = formatTimecode(progress * this.totalDurationSec, this.fps);
302
337
  const totalTimecode = formatTimecode(this.totalDurationSec, this.fps);
303
338
 
304
- const termWidth = Math.max(60, Math.min(100, process.stdout.columns || 80));
305
- const barWidth = Math.max(20, Math.min(36, termWidth - 46));
339
+ // Compute exact bounded terminal box dimensions (guaranteed never to wrap)
340
+ const termCols = Math.min(74, Math.max(50, (process.stdout.columns || 80) - 2));
341
+ const innerWidth = termCols - 4; // between ║ ... ║
342
+ const barWidth = Math.max(12, Math.min(22, innerWidth - 28));
306
343
 
307
344
  // Dynamic track representation
308
345
  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];
346
+ const synthWaveA1 = renderSynthWave(this.frameCount, 12);
311
347
 
312
348
  // Identify active scene
313
349
  let activeSceneLabel = "Main Composition";
@@ -322,17 +358,27 @@ export class RenderTimelineHUD {
322
358
  const pctString = (progress * 100).toFixed(1).padStart(5, " ");
323
359
  const bar = renderProgressBar(progress, barWidth, { gradient: "cyberpunk" });
324
360
 
361
+ const padLine = (content: string) => {
362
+ const vis = stripAnsi(content).length;
363
+ const pad = Math.max(0, innerWidth - vis);
364
+ return `${ansi.bold}║${ansi.reset} ${content}${" ".repeat(pad)} ${ansi.bold}║${ansi.reset}`;
365
+ };
366
+
367
+ 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}`;
368
+ const trackV1Content = `${ansi.brightWhite}V1:${ansi.reset} [${bar}] ${ansi.bold}${ansi.brightYellow}${pctString}%${ansi.reset} │ Frame: ${renderedFrames}/${totalFrames}`;
369
+ const trackA1Content = `${ansi.brightWhite}A1:${ansi.reset} [${synthWaveA1}] │ ${renderFps}fps │ ETA:${ansi.brightYellow}${etaSec}s${ansi.reset}`;
370
+ const sceneContent = `${ansi.dim}Scene:${ansi.reset} ${ansi.brightMagenta}${activeSceneLabel.slice(0, innerWidth - 8)}${ansi.reset}`;
371
+
372
+ const topBorder = `${ansi.bold}${ansi.brightCyan}╔══ 🎞️ REMOTION RENDER ENGINE ${"═".repeat(Math.max(2, innerWidth - 27))}╗${ansi.reset}`;
373
+ const bottomBorder = `${ansi.bold}${ansi.brightCyan}╚${"═".repeat(innerWidth + 2)}╝${ansi.reset}`;
374
+
325
375
  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}`,
376
+ topBorder,
377
+ padLine(headerContent),
378
+ padLine(trackV1Content),
379
+ padLine(trackA1Content),
380
+ padLine(sceneContent),
381
+ bottomBorder,
336
382
  ];
337
383
 
338
384
  this.clearRenderedLines();
@@ -344,7 +390,7 @@ export class RenderTimelineHUD {
344
390
 
345
391
  private clearRenderedLines(): void {
346
392
  if (this.linesRendered > 0) {
347
- process.stdout.write(ansi.up(this.linesRendered));
393
+ process.stdout.write(ansi.up(this.linesRendered) + "\r");
348
394
  for (let i = 0; i < this.linesRendered; i++) {
349
395
  process.stdout.write(ansi.clearLine);
350
396
  if (i < this.linesRendered - 1) process.stdout.write("\n");