voxflow 1.18.0 → 1.18.1

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.
@@ -29,7 +29,7 @@ const path = require('path');
29
29
  const { execFile } = require('child_process');
30
30
 
31
31
  const { parseFlag } = require('../core/args');
32
- const { runCommand, checkFfmpeg } = require('../core/ffmpeg');
32
+ const { runCommand, checkFfmpeg, resolveFfmpegStaticBin } = require('../core/ffmpeg');
33
33
 
34
34
  // ── ffmpeg binary capability probe ────────────────────────────────────────────
35
35
 
@@ -62,10 +62,14 @@ function probeSubtitlesCapableFfmpeg() {
62
62
 
63
63
  // 1. System ffmpeg via PATH (or whatever core/ffmpeg.js already resolved)
64
64
  tryBinary('ffmpeg', 'system', () => {
65
- // 2. Bundled ffmpeg-static
66
- let staticPath = null;
67
- try { staticPath = require('ffmpeg-static'); } catch { /* not installed */ }
68
- if (!staticPath || !fs.existsSync(staticPath)) {
65
+ // 2. Bundled ffmpeg-static.
66
+ // NOTE: cannot use `require('ffmpeg-static')` directly here — when this
67
+ // module is ncc-bundled into dist/index.js, ffmpeg-static's own
68
+ // `path.join(__dirname, ...)` collapses to the bundle's directory and
69
+ // returns a non-existent path. resolveFfmpegStaticBin() handles the
70
+ // ncc-safe recovery via require.resolve('ffmpeg-static/package.json').
71
+ const staticPath = resolveFfmpegStaticBin();
72
+ if (!staticPath) {
69
73
  return reject(new Error(
70
74
  'No ffmpeg with libass / `subtitles` filter found.\n' +
71
75
  ' System ffmpeg lacks libass (e.g. Homebrew default formula).\n' +
@@ -14,6 +14,43 @@ const fs = require('fs');
14
14
 
15
15
  let _resolvedFfmpegPath = null;
16
16
 
17
+ /**
18
+ * Resolve the bundled `ffmpeg-static` binary path.
19
+ *
20
+ * Naively, `require('ffmpeg-static')` returns the path of its sibling `ffmpeg`
21
+ * binary. But ffmpeg-static's index.js uses `path.join(__dirname, ...)` to
22
+ * compute that path — and when we ncc-bundle this CLI into a single
23
+ * `dist/index.js`, `__dirname` inside the inlined ffmpeg-static module
24
+ * collapses to the *bundle*'s directory (`<install>/voxflow/dist/`), not its
25
+ * real `<install>/voxflow/node_modules/ffmpeg-static/` location. The returned
26
+ * string then points at a file that doesn't exist.
27
+ *
28
+ * Recovery: `require.resolve('ffmpeg-static/package.json')` honors Node's
29
+ * runtime module resolution (not the `__dirname` baked in at bundle time), so
30
+ * it finds the real package directory. The binary lives next to that
31
+ * package.json. This works in both source-mode (npm test) and ncc-bundled
32
+ * mode (the published CLI).
33
+ *
34
+ * Returns: a path string (may or may not exist; caller should fs.existsSync).
35
+ * Or null when ffmpeg-static is not installed at all.
36
+ */
37
+ function resolveFfmpegStaticBin() {
38
+ // 1. Naive path — works in source mode and any non-ncc context.
39
+ let direct = null;
40
+ try { direct = require('ffmpeg-static'); } catch { /* not installed */ }
41
+ if (direct && fs.existsSync(direct)) return direct;
42
+
43
+ // 2. ncc-safe recovery via package.json resolution.
44
+ try {
45
+ const pkgJson = require.resolve('ffmpeg-static/package.json');
46
+ const exe = process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
47
+ const recovered = path.join(path.dirname(pkgJson), exe);
48
+ if (fs.existsSync(recovered)) return recovered;
49
+ } catch { /* not installed */ }
50
+
51
+ return null;
52
+ }
53
+
17
54
  /**
18
55
  * Resolve the ffmpeg binary path. Priority:
19
56
  * 1. System `ffmpeg` on PATH
@@ -30,13 +67,11 @@ function resolveFfmpegBin() {
30
67
  } catch { /* not on PATH */ }
31
68
 
32
69
  // Fall back to ffmpeg-static
33
- try {
34
- const staticPath = require('ffmpeg-static');
35
- if (staticPath && fs.existsSync(staticPath)) {
36
- _resolvedFfmpegPath = staticPath;
37
- return _resolvedFfmpegPath;
38
- }
39
- } catch { /* not installed */ }
70
+ const staticPath = resolveFfmpegStaticBin();
71
+ if (staticPath) {
72
+ _resolvedFfmpegPath = staticPath;
73
+ return _resolvedFfmpegPath;
74
+ }
40
75
 
41
76
  // Nothing found — return 'ffmpeg' and let it fail with a helpful error
42
77
  _resolvedFfmpegPath = 'ffmpeg';
@@ -536,4 +571,6 @@ module.exports = {
536
571
  concatVideos,
537
572
  normalizeVideo,
538
573
  detectCjkFont,
574
+ // ffmpeg-static path resolution (used by card-subtitle's libass fallback)
575
+ resolveFfmpegStaticBin,
539
576
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voxflow",
3
- "version": "1.18.0",
3
+ "version": "1.18.1",
4
4
  "description": "AI audio content creation CLI — stories, podcasts, narration, dubbing, transcription, translation, and video translation with TTS",
5
5
  "bin": {
6
6
  "voxflow": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voxflow",
3
- "version": "1.18.0",
3
+ "version": "1.18.1",
4
4
  "description": "AI voice CLI bundled as 6 skills (hub, podcast, transcribe, video, slice, card). Synthesize speech in 200+ voices across 40+ languages, generate multi-speaker AI podcasts, transcribe audio/video with word-level timestamps, dub videos from SRT subtitles, run end-to-end video translation, turn long articles into vertical card video reels via Remotion, and turn text into polished shareable card images or narrated card videos. Backed by a hosted TTS/ASR/LLM/render service with per-user quota (free tier 10K/mo).",
5
5
  "author": {
6
6
  "name": "VoxFlow",