creator-editing-studio 1.3.5 → 1.3.6

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.
@@ -1,141 +1,141 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Does this machine actually have everything the studio claims to do?
4
- *
5
- * npm run doctor
6
- *
7
- * This exists because a working setup went quiet. WhisperX had been installed and used
8
- * (there are words.json files from a fortnight earlier), then the Python environment
9
- * underneath it was emptied, and nothing noticed until a job needed a transcript. The
10
- * code was fine the whole time. The environment was not, and there was no way to tell
11
- * except by running something and watching it fail.
12
- *
13
- * So this checks CAPABILITIES rather than files: it runs the binaries, imports the
14
- * modules, and reports what you can and cannot do right now. A capability that is
15
- * missing is reported with the command that fixes it, because "not installed" with no
16
- * next step is what turns a five-minute problem into a phone call.
17
- *
18
- * Exit code is 0 when everything the studio needs for a render is present, 1 when
19
- * something core is broken. Optional pieces report but never fail the run.
20
- */
21
- import {execFileSync} from 'node:child_process';
22
- import {existsSync, readdirSync} from 'node:fs';
23
- import {join, dirname, resolve} from 'node:path';
24
- import {fileURLToPath} from 'node:url';
25
-
26
- // fileURLToPath, not URL.pathname: a path with a space in it comes back percent-encoded
27
- // from pathname, so every existsSync below silently answers false and the report claims
28
- // a working studio is broken.
29
- const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
30
- const VENV = process.platform === 'win32'
31
- ? join(ROOT, '.venv', 'Scripts', 'python.exe')
32
- : join(ROOT, '.venv', 'bin', 'python');
33
-
34
- const results = [];
35
- const record = (area, name, ok, detail, fix, core = true) =>
36
- results.push({area, name, ok, detail, fix, core});
37
-
38
- const tryRun = (bin, args, pick = (s) => s.trim().split('\n')[0]) => {
39
- try {
40
- return {ok: true, out: pick(execFileSync(bin, args, {encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']}))};
41
- } catch (e) {
42
- return {ok: false, out: (e.stderr || e.message || '').toString().trim().split('\n')[0]};
43
- }
44
- };
45
-
46
- /* ------------------------------------------------------------------ the engine */
47
- const node = process.versions.node;
48
- const nodeMajor = Number(node.split('.')[0]);
49
- record('engine', 'Node', nodeMajor >= 20, `v${node}`, 'Install the LTS build from nodejs.org');
50
-
51
- const hasModules = existsSync(join(ROOT, 'node_modules', 'remotion'));
52
- const pkgCount = existsSync(join(ROOT, 'node_modules')) ? readdirSync(join(ROOT, 'node_modules')).length : 0;
53
- record('engine', 'Video engine', hasModules, hasModules ? `${pkgCount} packages` : 'node_modules/remotion missing', 'npm install');
54
-
55
- /* ------------------------------------------------------------- media handling */
56
- let FFMPEG = 'ffmpeg', FFPROBE = 'ffprobe';
57
- try {
58
- const media = await import('./lib/media.mjs');
59
- FFMPEG = media.FFMPEG;
60
- FFPROBE = media.FFPROBE;
61
- } catch { /* fall back to PATH and let the run below report it */ }
62
-
63
- for (const [name, bin] of [['ffmpeg', FFMPEG], ['ffprobe', FFPROBE]]) {
64
- const r = tryRun(bin, ['-version']);
65
- record('media', name, r.ok, r.ok ? r.out.split(' ').slice(0, 3).join(' ') : 'not runnable',
66
- 'Set $FFMPEG / $FFPROBE, or install ffmpeg so it is on PATH');
67
- }
68
-
69
- /* ------------------------------------------------------ the transcriber (optional) */
70
- const venvOk = existsSync(VENV);
71
- record('transcriber', 'Python 3.11/3.12 venv', venvOk, venvOk ? VENV.replace(ROOT, '.') : '.venv missing',
72
- 'py -3.12 -m venv .venv (3.13+ will not build WhisperX)', false);
73
-
74
- if (venvOk) {
75
- const ver = tryRun(VENV, ['-c', 'import sys;print(sys.version.split()[0])']);
76
- const minor = Number((ver.out || '0.0').split('.')[1]);
77
- record('transcriber', 'Python version', ver.ok && (minor === 11 || minor === 12),
78
- ver.out, 'Rebuild the venv with python 3.11 or 3.12', false);
79
-
80
- // Every module the Python half imports, not just the headline one.
81
- for (const mod of ['whisperx', 'torch', 'numpy', 'PIL', 'certifi']) {
82
- const r = tryRun(VENV, ['-c', `import ${mod}`], () => 'ok');
83
- record('transcriber', mod, r.ok, r.ok ? 'ok' : 'not importable',
84
- '.venv/Scripts/pip install -r requirements.txt', false);
85
- }
86
- }
87
-
88
- /* ------------------------------------------------- the python half, script by script */
89
- if (venvOk) {
90
- // Importing whisperx is not the same as the scripts running. Compile each one, which
91
- // catches a missing dependency or a syntax error without doing any of the slow work.
92
- const py = readdirSync(join(ROOT, 'scripts')).filter((f) => f.endsWith('.py'));
93
- const bad = [];
94
- for (const f of py) {
95
- const r = tryRun(VENV, ['-m', 'py_compile', join(ROOT, 'scripts', f)], () => 'ok');
96
- if (!r.ok) bad.push(f);
97
- }
98
- record('transcriber', 'Python scripts', bad.length === 0,
99
- bad.length ? `${bad.join(', ')} will not compile` : `${py.length} compile clean`,
100
- '.venv/Scripts/pip install -r requirements.txt', false);
101
- }
102
-
103
- /* ----------------------------------------------------------------------- brand */
104
- const tokens = existsSync(join(ROOT, 'src', 'design', 'tokens.ts'));
105
- record('brand', 'Design tokens', tokens, tokens ? 'src/design/tokens.ts' : 'missing',
106
- 'Restore src/design/tokens.ts, which is where the whole look lives');
107
-
108
- // A missing font file does not fail a render: it silently swaps in a fallback, and the
109
- // video comes out looking almost right, which is worse than an error.
110
- const fontDir = join(ROOT, 'assets', 'fonts');
111
- const fonts = existsSync(fontDir) ? readdirSync(fontDir).filter((f) => /\.(ttf|otf|woff2?)$/i.test(f)) : [];
112
- record('brand', 'Fonts', fonts.length > 0,
113
- fonts.length ? `${fonts.length} file(s): ${fonts.slice(0, 2).join(', ')}` : 'assets/fonts is empty',
114
- 'Put the brand font files back in assets/fonts, or a render falls back to a system face');
115
-
116
- /* --------------------------------------------------------------------- report */
117
- const W = 22;
118
- let lastArea = '';
119
- for (const r of results) {
120
- if (r.area !== lastArea) { console.log(`\n${r.area.toUpperCase()}`); lastArea = r.area; }
121
- const mark = r.ok ? ' ok ' : (r.core ? ' FAIL ' : ' -- ');
122
- console.log(`${mark} ${r.name.padEnd(W)} ${r.detail}`);
123
- if (!r.ok) console.log(` ${''.padEnd(W)} fix: ${r.fix}`);
124
- }
125
-
126
- const brokenCore = results.filter((r) => !r.ok && r.core);
127
- const brokenOpt = results.filter((r) => !r.ok && !r.core);
128
-
129
- console.log('\n' + '-'.repeat(64));
130
- if (!brokenCore.length && !brokenOpt.length) {
131
- console.log('Everything is present. You can cut, render and caption.');
132
- } else if (!brokenCore.length) {
133
- console.log(`Rendering works. ${brokenOpt.length} optional piece(s) missing:`);
134
- console.log(` ${[...new Set(brokenOpt.map((r) => r.fix))].join('\n ')}`);
135
- console.log('Without these: no word-level captions, no auto cut detection, no green-screen cutouts.');
136
- } else {
137
- console.log(`${brokenCore.length} thing(s) needed for a render are missing. Fix these first:`);
138
- console.log(` ${[...new Set(brokenCore.map((r) => r.fix))].join('\n ')}`);
139
- }
140
-
141
- process.exit(brokenCore.length ? 1 : 0);
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Does this machine actually have everything the studio claims to do?
4
+ *
5
+ * npm run doctor
6
+ *
7
+ * This exists because a working setup went quiet. WhisperX had been installed and used
8
+ * (there are words.json files from a fortnight earlier), then the Python environment
9
+ * underneath it was emptied, and nothing noticed until a job needed a transcript. The
10
+ * code was fine the whole time. The environment was not, and there was no way to tell
11
+ * except by running something and watching it fail.
12
+ *
13
+ * So this checks CAPABILITIES rather than files: it runs the binaries, imports the
14
+ * modules, and reports what you can and cannot do right now. A capability that is
15
+ * missing is reported with the command that fixes it, because "not installed" with no
16
+ * next step is what turns a five-minute problem into a phone call.
17
+ *
18
+ * Exit code is 0 when everything the studio needs for a render is present, 1 when
19
+ * something core is broken. Optional pieces report but never fail the run.
20
+ */
21
+ import {execFileSync} from 'node:child_process';
22
+ import {existsSync, readdirSync} from 'node:fs';
23
+ import {join, dirname, resolve} from 'node:path';
24
+ import {fileURLToPath} from 'node:url';
25
+
26
+ // fileURLToPath, not URL.pathname: a path with a space in it comes back percent-encoded
27
+ // from pathname, so every existsSync below silently answers false and the report claims
28
+ // a working studio is broken.
29
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
30
+ const VENV = process.platform === 'win32'
31
+ ? join(ROOT, '.venv', 'Scripts', 'python.exe')
32
+ : join(ROOT, '.venv', 'bin', 'python');
33
+
34
+ const results = [];
35
+ const record = (area, name, ok, detail, fix, core = true) =>
36
+ results.push({area, name, ok, detail, fix, core});
37
+
38
+ const tryRun = (bin, args, pick = (s) => s.trim().split('\n')[0]) => {
39
+ try {
40
+ return {ok: true, out: pick(execFileSync(bin, args, {encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']}))};
41
+ } catch (e) {
42
+ return {ok: false, out: (e.stderr || e.message || '').toString().trim().split('\n')[0]};
43
+ }
44
+ };
45
+
46
+ /* ------------------------------------------------------------------ the engine */
47
+ const node = process.versions.node;
48
+ const nodeMajor = Number(node.split('.')[0]);
49
+ record('engine', 'Node', nodeMajor >= 20, `v${node}`, 'Install the LTS build from nodejs.org');
50
+
51
+ const hasModules = existsSync(join(ROOT, 'node_modules', 'remotion'));
52
+ const pkgCount = existsSync(join(ROOT, 'node_modules')) ? readdirSync(join(ROOT, 'node_modules')).length : 0;
53
+ record('engine', 'Video engine', hasModules, hasModules ? `${pkgCount} packages` : 'node_modules/remotion missing', 'npm install');
54
+
55
+ /* ------------------------------------------------------------- media handling */
56
+ let FFMPEG = 'ffmpeg', FFPROBE = 'ffprobe';
57
+ try {
58
+ const media = await import('./lib/media.mjs');
59
+ FFMPEG = media.FFMPEG;
60
+ FFPROBE = media.FFPROBE;
61
+ } catch { /* fall back to PATH and let the run below report it */ }
62
+
63
+ for (const [name, bin] of [['ffmpeg', FFMPEG], ['ffprobe', FFPROBE]]) {
64
+ const r = tryRun(bin, ['-version']);
65
+ record('media', name, r.ok, r.ok ? r.out.split(' ').slice(0, 3).join(' ') : 'not runnable',
66
+ 'Set $FFMPEG / $FFPROBE, or install ffmpeg so it is on PATH');
67
+ }
68
+
69
+ /* ------------------------------------------------------ the transcriber (optional) */
70
+ const venvOk = existsSync(VENV);
71
+ record('transcriber', 'Python 3.11/3.12 venv', venvOk, venvOk ? VENV.replace(ROOT, '.') : '.venv missing',
72
+ 'py -3.12 -m venv .venv (3.13+ will not build WhisperX)', false);
73
+
74
+ if (venvOk) {
75
+ const ver = tryRun(VENV, ['-c', 'import sys;print(sys.version.split()[0])']);
76
+ const minor = Number((ver.out || '0.0').split('.')[1]);
77
+ record('transcriber', 'Python version', ver.ok && (minor === 11 || minor === 12),
78
+ ver.out, 'Rebuild the venv with python 3.11 or 3.12', false);
79
+
80
+ // Every module the Python half imports, not just the headline one.
81
+ for (const mod of ['whisperx', 'torch', 'numpy', 'PIL', 'certifi']) {
82
+ const r = tryRun(VENV, ['-c', `import ${mod}`], () => 'ok');
83
+ record('transcriber', mod, r.ok, r.ok ? 'ok' : 'not importable',
84
+ '.venv/Scripts/pip install -r requirements.txt', false);
85
+ }
86
+ }
87
+
88
+ /* ------------------------------------------------- the python half, script by script */
89
+ if (venvOk) {
90
+ // Importing whisperx is not the same as the scripts running. Compile each one, which
91
+ // catches a missing dependency or a syntax error without doing any of the slow work.
92
+ const py = readdirSync(join(ROOT, 'scripts')).filter((f) => f.endsWith('.py'));
93
+ const bad = [];
94
+ for (const f of py) {
95
+ const r = tryRun(VENV, ['-m', 'py_compile', join(ROOT, 'scripts', f)], () => 'ok');
96
+ if (!r.ok) bad.push(f);
97
+ }
98
+ record('transcriber', 'Python scripts', bad.length === 0,
99
+ bad.length ? `${bad.join(', ')} will not compile` : `${py.length} compile clean`,
100
+ '.venv/Scripts/pip install -r requirements.txt', false);
101
+ }
102
+
103
+ /* ----------------------------------------------------------------------- brand */
104
+ const tokens = existsSync(join(ROOT, 'src', 'design', 'tokens.ts'));
105
+ record('brand', 'Design tokens', tokens, tokens ? 'src/design/tokens.ts' : 'missing',
106
+ 'Restore src/design/tokens.ts, which is where the whole look lives');
107
+
108
+ // A missing font file does not fail a render: it silently swaps in a fallback, and the
109
+ // video comes out looking almost right, which is worse than an error.
110
+ const fontDir = join(ROOT, 'assets', 'fonts');
111
+ const fonts = existsSync(fontDir) ? readdirSync(fontDir).filter((f) => /\.(ttf|otf|woff2?)$/i.test(f)) : [];
112
+ record('brand', 'Fonts', fonts.length > 0,
113
+ fonts.length ? `${fonts.length} file(s): ${fonts.slice(0, 2).join(', ')}` : 'assets/fonts is empty',
114
+ 'Put the brand font files back in assets/fonts, or a render falls back to a system face');
115
+
116
+ /* --------------------------------------------------------------------- report */
117
+ const W = 22;
118
+ let lastArea = '';
119
+ for (const r of results) {
120
+ if (r.area !== lastArea) { console.log(`\n${r.area.toUpperCase()}`); lastArea = r.area; }
121
+ const mark = r.ok ? ' ok ' : (r.core ? ' FAIL ' : ' -- ');
122
+ console.log(`${mark} ${r.name.padEnd(W)} ${r.detail}`);
123
+ if (!r.ok) console.log(` ${''.padEnd(W)} fix: ${r.fix}`);
124
+ }
125
+
126
+ const brokenCore = results.filter((r) => !r.ok && r.core);
127
+ const brokenOpt = results.filter((r) => !r.ok && !r.core);
128
+
129
+ console.log('\n' + '-'.repeat(64));
130
+ if (!brokenCore.length && !brokenOpt.length) {
131
+ console.log('Everything is present. You can cut, render and caption.');
132
+ } else if (!brokenCore.length) {
133
+ console.log(`Rendering works. ${brokenOpt.length} optional piece(s) missing:`);
134
+ console.log(` ${[...new Set(brokenOpt.map((r) => r.fix))].join('\n ')}`);
135
+ console.log('Without these: no word-level captions, no auto cut detection, no green-screen cutouts.');
136
+ } else {
137
+ console.log(`${brokenCore.length} thing(s) needed for a render are missing. Fix these first:`);
138
+ console.log(` ${[...new Set(brokenCore.map((r) => r.fix))].join('\n ')}`);
139
+ }
140
+
141
+ process.exit(brokenCore.length ? 1 : 0);
@@ -1,45 +1,45 @@
1
- """Find jump cuts in a talking-head clip (frames where the picture jumps instead of moving).
2
-
3
- Usage (from the project root):
4
- python scripts/find_cuts.py <video> <out.json>
5
-
6
- Writes a JSON array of frame numbers: each is the first frame AFTER a cut. Used to
7
- - punch in/out on each cut (the Stage `punchIns` prop), so cuts read as intentional camera changes
8
- - tell motion-check these changes are in the footage (--allow-file)
9
- """
10
-
11
- import json
12
- import subprocess
13
- import sys
14
-
15
- import numpy as np
16
- import sys as _sys, pathlib as _pl
17
- _sys.path.insert(0, str(_pl.Path(__file__).resolve().parent / 'lib'))
18
- from media import FFMPEG, FFPROBE # resolved per platform - never hardcode a tool path
19
-
20
-
21
-
22
- def main() -> None:
23
- video, out_path = sys.argv[1], sys.argv[2]
24
- raw = subprocess.run(
25
- [FFMPEG, "-v", "error", "-i", video, "-vf", "scale=320:180", "-f", "rawvideo", "-pix_fmt", "gray", "-"],
26
- capture_output=True,
27
- check=True,
28
- ).stdout
29
- frames = np.frombuffer(raw, np.uint8).reshape(-1, 180, 320).astype(np.float32)
30
- change = np.abs(np.diff(frames, axis=0)).mean(axis=(1, 2))
31
- typical = float(np.median(change))
32
-
33
- cuts = []
34
- for i in range(2, len(change) - 2):
35
- neighbours = float(np.median(np.concatenate([change[i - 2 : i], change[i + 1 : i + 3]])))
36
- # A cut: one transition far larger than both the clip's typical motion and its neighbours.
37
- if change[i] > 6 and change[i] > 4 * max(neighbours, typical):
38
- cuts.append(i + 1)
39
-
40
- json.dump(cuts, open(out_path, "w"), indent=0)
41
- print(f"{len(cuts)} cuts -> {out_path}: {cuts}")
42
-
43
-
44
- if __name__ == "__main__":
45
- main()
1
+ """Find jump cuts in a talking-head clip (frames where the picture jumps instead of moving).
2
+
3
+ Usage (from the project root):
4
+ python scripts/find_cuts.py <video> <out.json>
5
+
6
+ Writes a JSON array of frame numbers: each is the first frame AFTER a cut. Used to
7
+ - punch in/out on each cut (the Stage `punchIns` prop), so cuts read as intentional camera changes
8
+ - tell motion-check these changes are in the footage (--allow-file)
9
+ """
10
+
11
+ import json
12
+ import subprocess
13
+ import sys
14
+
15
+ import numpy as np
16
+ import sys as _sys, pathlib as _pl
17
+ _sys.path.insert(0, str(_pl.Path(__file__).resolve().parent / 'lib'))
18
+ from media import FFMPEG, FFPROBE # resolved per platform - never hardcode a tool path
19
+
20
+
21
+
22
+ def main() -> None:
23
+ video, out_path = sys.argv[1], sys.argv[2]
24
+ raw = subprocess.run(
25
+ [FFMPEG, "-v", "error", "-i", video, "-vf", "scale=320:180", "-f", "rawvideo", "-pix_fmt", "gray", "-"],
26
+ capture_output=True,
27
+ check=True,
28
+ ).stdout
29
+ frames = np.frombuffer(raw, np.uint8).reshape(-1, 180, 320).astype(np.float32)
30
+ change = np.abs(np.diff(frames, axis=0)).mean(axis=(1, 2))
31
+ typical = float(np.median(change))
32
+
33
+ cuts = []
34
+ for i in range(2, len(change) - 2):
35
+ neighbours = float(np.median(np.concatenate([change[i - 2 : i], change[i + 1 : i + 3]])))
36
+ # A cut: one transition far larger than both the clip's typical motion and its neighbours.
37
+ if change[i] > 6 and change[i] > 4 * max(neighbours, typical):
38
+ cuts.append(i + 1)
39
+
40
+ json.dump(cuts, open(out_path, "w"), indent=0)
41
+ print(f"{len(cuts)} cuts -> {out_path}: {cuts}")
42
+
43
+
44
+ if __name__ == "__main__":
45
+ main()