creator-editing-studio 1.2.8 → 1.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "creator-editing-studio",
3
- "version": "1.2.8",
3
+ "version": "1.3.1",
4
4
  "description": "Scaffolds a Creator Editing OS studio — components, scripts and rules for editing video from a plain-English brief.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -48,53 +48,91 @@ Everything else in `src/design/` is the system and should be left alone. If a vi
48
48
  a colour or a size that is not in tokens.ts, the answer is to add it to tokens.ts, never to
49
49
  type it into a component.
50
50
 
51
- ## The order of work — the plan is approved before anything is cut
51
+ ## The order of work
52
52
 
53
- **Never render an edit the owner has not agreed to in writing first.** A render is the last
54
- step, not the first. Rebuilding a rejected edit costs more than every minute the plan takes.
53
+ Two things are fixed: **you read their references before you design anything**, and **they see
54
+ the edit as a plan before you render it.** Everything else bends to the job in front of you.
55
55
 
56
- Every edit runs in this order. Do not skip a step, and do not collapse two into one.
56
+ ### 0. References first. Always. Before the brief, before the footage.
57
57
 
58
- **1. Watch and probe.** `npm run prep -- <name>`. Get the real duration, fps and resolution
59
- of every clip. Never assume them.
58
+ This is the first thing you do in any editing session, and it is not optional, because it is the
59
+ only reason their edits look like theirs instead of like everyone's.
60
60
 
61
- **2. Transcribe with word-level timings.** The transcript is the truth — not the script, not
62
- what the owner remembers saying. Every beat you place later is anchored to a word index, and
63
- every word index comes from this file.
61
+ Read both, in this order:
64
62
 
65
- **3. Read the owner's style first, in this order:** `STYLE.md` §9 Rejected, then §8 Approved,
66
- then `references/references.md` and the contact sheets in `work/refs/`. Rejected comes first
67
- because removing options is faster than adding them. A reference beats a default wherever
68
- they disagree.
63
+ 1. **The references for this project** — anything they put in the video's own folder or named in
64
+ its `BRIEF.md`: a clip they want it to feel like, a screenshot, a link, a sentence like
65
+ "similar to that last one but calmer".
66
+ 2. **The references saved for them generally** — `references/references.md` and the contact
67
+ sheets in `work/refs/`, plus `STYLE.md` §9 Rejected and §8 Approved. §9 first: ruling options
68
+ out is faster than adding them.
69
69
 
70
- **4. Write the plan, and hand it over.** Two things. Neither is a markdown file for them to
71
- open — a plan only gets corrected if it is in front of them.
70
+ Then say back, in one or two lines, which of their references you are using and for what. Not a
71
+ summary of all of them — the ones that will show up in this edit.
72
+
73
+ A reference beats a house default wherever the two disagree. A default is a guess that suits
74
+ everyone; a reference is a fact about them.
75
+
76
+ **If they have no references yet, do not stall and do not nag.** Say so in one line, use the
77
+ house defaults, and ask for one reference at the end when they are reacting to something real —
78
+ that is when people can actually name what they like.
79
+
80
+ ### 1. Prep and transcribe
81
+
82
+ `npm run prep -- <name>`. Real duration, fps and resolution for every clip — never assumed.
83
+ Then transcribe with word-level timings. **The transcript is the truth, not the script and not
84
+ what they remember saying.**
85
+
86
+ ### 2. The first cut, before the plan
87
+
88
+ Cut the silence, the filler and the retakes first. This is mechanical — no design decisions in
89
+ it — so it does not need their approval, and doing it first is what makes the plan possible:
90
+ every beat is anchored to a word, and words only have frame numbers once the cut exists.
91
+
92
+ Re-map the word timings onto the cut timeline before going further.
93
+
94
+ ### 3. The plan — and this is where you stop
95
+
96
+ Two things, neither of them a file for them to open. A plan only gets corrected if it is in
97
+ front of them.
72
98
 
73
99
  **The plan goes in the reply, as a table**, one row per beat:
74
100
 
75
101
  | # | In | Out | Trigger word (frame) | What is on screen | Audio under it |
76
102
  |---|---|---|---|---|---|
77
103
 
78
- Followed by the total runtime, the caption script, and a short list of what you chose not to do
79
- and why. In the reply, not attached.
104
+ Then the runtime, the caption script, and a short note on what you chose not to do and why.
105
+
106
+ **The motion sheet is a PNG of real rendered frames.** Build the graphics, render one still per
107
+ beat with `npm run stills`, and lay them out as a numbered contact sheet — each tile labelled
108
+ with its timecode, the words that trigger it, and the approved beat it comes from. Prose about
109
+ motion cannot be corrected, because they cannot see it. A picture can: they point at a tile
110
+ number and say what to change.
111
+
112
+ Every graphic names the beat it comes from, out of `STYLE.md` §8 or their references. **A
113
+ graphic that cannot name its beat is not approved and does not go on the sheet.** That rule is
114
+ what stops motion graphics drifting into generic.
115
+
116
+ Then stop and ask: *here is the plan, change anything that is off, say go ahead and I will cut
117
+ it.* Wait for a yes. "Go ahead", "yes", "approved", "ship it" is a yes. Silence is not.
80
118
 
81
- **The motion sheet is a PNG of real rendered frames** — one tile per beat, numbered, each
82
- labelled with its timecode, the words that trigger it, and the approved beat it comes from. Build
83
- the graphics, render one still per beat with `npm run stills`, and lay them out as a contact
84
- sheet composition. A graphic that cannot name its beat is not approved and does not go on the
85
- sheet. Describing motion in prose does not work: they cannot see it, so they cannot correct it.
86
- They point at a tile number and say what to change.
119
+ ### Keep it proportionate
87
120
 
88
- Then stop and ask. Say plainly: *here is the plan, change anything that is off, say go ahead
89
- and I will cut it.* **Wait for a yes.** "Go ahead", "approved", "ship it" is a yes. Silence is
90
- not, and neither is a reply about something else.
121
+ The plan is there to save them a rebuild, not to put a form in front of them.
91
122
 
92
- **5. Only then cut, render, and verify.** Verification is not optional and is not a claim —
93
- see below.
123
+ - **A short or simple job gets a short plan.** Three beats, three rows, three tiles. Do not pad
124
+ a thirty-second clip into a fifteen-row table.
125
+ - **A fix to an edit they have already approved is not a new plan.** They said "captions lower,
126
+ hold that shot longer" — do it, show it, done. Re-plan only when the structure changes.
127
+ - **If they say "just do it", do it.** Put the table in one short message and cut straight
128
+ after, so there is still a record of what was intended — but do not block on an answer.
129
+ - **Never ask a question you can answer yourself.** Probe the file, read the brief, pick the
130
+ sensible default and say which one you picked. Questions are for the things that would
131
+ genuinely change the edit.
132
+ - **One round of questions, not a drip.** If you need to ask, ask everything at once.
94
133
 
95
- If the owner says "just edit it" and skips the plan, write the plan anyway, in one short
96
- message, and cut immediately after. It costs thirty seconds and it is the only record of
97
- what was intended.
134
+ The failure this is guarding against is a whole edit rendered the wrong way. Anything that does
135
+ not risk that should move fast.
98
136
 
99
137
  ## Verify the render before you call it done
100
138
 
@@ -129,10 +167,25 @@ and not in the stills, it did not render — find out why before shipping.
129
167
  `OffthreadVideo` placed in one starts from its own frame 0 and plays its own audio over the
130
168
  live line. Pass an explicit `startFrom`, and mute it unless its sound is wanted.
131
169
 
132
- **Repetition the owner can hear but you cannot.** If the owner says a line repeats, transcribe
133
- the rendered output over that exact window before arguing. If the audio is clean, the repeat
134
- is visual — a held caption, a reused graphic, or a duplicated framing — and that is still a
135
- real fault worth fixing, not a misunderstanding to explain away.
170
+ **Something they can hear and you cannot.** When they report a fault you cannot find, your check
171
+ is the thing most likely to be wrong, not them. Three rules, learned the hard way:
172
+
173
+ - **Transcribe the WHOLE file, not the window they named.** A seven-second sample came back clean
174
+ and was used to tell the owner he was imagining a repeat that was really there.
175
+ - **Check every render in `out/`, not the one you just made.** They are probably playing an older
176
+ file, because nothing on their machine says which is current. Ask which file, or test them all.
177
+ - **If they say it more than once, stop checking and start believing.** A repeated complaint is
178
+ evidence your method is wrong. Change the method.
179
+
180
+ If the audio really is clean, the repeat is visual — a held caption, a reused graphic, a duplicated
181
+ framing — and that is still a real fault, not a misunderstanding to explain away.
182
+
183
+ **Four generations of lossy audio.** The voice must be cut as PCM and encoded exactly once, at
184
+ delivery. Cutting an AAC file, trimming the result, and then letting the renderer re-encode it
185
+ puts the voice through four generations of loss, and raising the bitrate on the later passes
186
+ preserves the damage rather than repairing it. It is clearly audible and it gets worse with
187
+ every extra render. Use `node scripts/voice.mjs` to pull the voice losslessly from the camera
188
+ file, render the picture muted, and mux the two together at the end.
136
189
 
137
190
  ## Non-negotiables
138
191
 
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pull EXACT frame numbers out of a rendered file.
4
+ *
5
+ * node scripts/exact-frames.mjs <file.mp4> <frame,frame,...> <outDir>
6
+ *
7
+ * Why this exists: `-ss` seeking is not frame accurate. Checking v4's cut boundaries with it
8
+ * returned frames 1-3 later than asked for, which made clean cuts look like defects and nearly
9
+ * sent a good edit back for rework. Decoding from the start and selecting by frame index is
10
+ * slower and is the only thing that can be trusted for a boundary check.
11
+ *
12
+ * The select expression is passed as one argv entry through spawnSync, never a shell — its
13
+ * commas are exactly what breaks a shell-quoted filter graph.
14
+ */
15
+ import {spawnSync} from 'node:child_process';
16
+ import {mkdirSync, rmSync} from 'node:fs';
17
+ import {join} from 'node:path';
18
+ import {FFMPEG} from './lib/media.mjs';
19
+
20
+ const [, , file, framesArg, outDir = 'work/exact'] = process.argv;
21
+ if (!file || !framesArg) {
22
+ console.error('Usage: node scripts/exact-frames.mjs <file> <frame,frame,...> [outDir]');
23
+ process.exit(2);
24
+ }
25
+
26
+ const frames = framesArg.split(',').map(Number).sort((a, b) => a - b);
27
+ rmSync(outDir, {recursive: true, force: true});
28
+ mkdirSync(outDir, {recursive: true});
29
+
30
+ // eq(n\,N) — the backslash has to survive into the filter string, so it is doubled here.
31
+ const expr = frames.map((f) => `eq(n\\,${f})`).join('+');
32
+
33
+ const r = spawnSync(
34
+ FFMPEG,
35
+ [
36
+ '-y', '-v', 'error',
37
+ '-i', file,
38
+ '-vf', `select='${expr}',scale=440:-1`,
39
+ '-fps_mode', 'passthrough',
40
+ '-frames:v', String(frames.length),
41
+ join(outDir, 'x%03d.png'),
42
+ ],
43
+ {encoding: 'utf8'},
44
+ );
45
+ if (r.status !== 0) {
46
+ console.error(r.stderr?.slice(0, 900));
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log(frames.map((f, i) => `x${String(i + 1).padStart(3, '0')}.png = frame ${f}`).join('\n'));
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build the voice track ONCE, losslessly, straight from the camera file.
4
+ *
5
+ * node scripts/voice.mjs --source <clip.mp4> --takes <takes.json> [--drop 17.2333:17.6667] --out <voice.wav>
6
+ *
7
+ * WHY THIS EXISTS. The voice on demo-test went through FOUR lossy generations before it reached
8
+ * the viewer: the camera's own AAC 128k, then the cut, then a second trim, then the render — each
9
+ * one re-encoding what the last one had already damaged. Raising the bitrate on the later passes
10
+ * does not recover anything; it just preserves the damage at a bigger file size. It is audible,
11
+ * and it gets worse with every extra render.
12
+ *
13
+ * The rulebook already says it: render the picture muted and add the original voice back
14
+ * untouched. This is the tool that makes that easy. The voice is cut as PCM and stays PCM until
15
+ * the single encode at delivery, so the viewer hears one generation of loss instead of four.
16
+ *
17
+ * `atrim` takes colon-separated options, so the filter graph carries no commas inside a function
18
+ * call — and it is passed through spawnSync as one argv entry, never a shell.
19
+ */
20
+ import {spawnSync} from 'node:child_process';
21
+ import {readFileSync} from 'node:fs';
22
+ import {FFMPEG} from './lib/media.mjs';
23
+
24
+ const args = process.argv.slice(2);
25
+ const opt = (name) => {
26
+ const i = args.indexOf(`--${name}`);
27
+ return i >= 0 ? args[i + 1] : undefined;
28
+ };
29
+
30
+ const source = opt('source');
31
+ const takesFile = opt('takes');
32
+ const out = opt('out');
33
+ if (!source || !out) {
34
+ console.error('Usage: node scripts/voice.mjs --source <clip> [--takes <takes.json>] [--drop a:b] --out <voice.wav>');
35
+ process.exit(2);
36
+ }
37
+
38
+ /** The spans to keep, in source seconds. With no takes file, keep the whole clip. */
39
+ let keep = [{from: 0, to: null}];
40
+ if (takesFile) keep = JSON.parse(readFileSync(takesFile, 'utf8')).keep;
41
+
42
+ const parts = [];
43
+ keep.forEach((k, i) => {
44
+ const range = k.to == null ? `start=${k.from.toFixed(4)}` : `start=${k.from.toFixed(4)}:end=${k.to.toFixed(4)}`;
45
+ parts.push(`[0:a]atrim=${range},asetpts=PTS-STARTPTS[a${i}]`);
46
+ });
47
+ parts.push(`${keep.map((_, i) => `[a${i}]`).join('')}concat=n=${keep.length}:v=0:a=1[joined]`);
48
+
49
+ /**
50
+ * A second pass of removals, expressed in the CUT timeline rather than the source — that is where
51
+ * a tightened pause lives, because its position only exists after the first cut.
52
+ */
53
+ const drop = opt('drop');
54
+ let label = 'joined';
55
+ if (drop) {
56
+ const [a, b] = drop.split(':').map(Number);
57
+ parts.push(`[joined]asplit=2[d0][d1]`);
58
+ parts.push(`[d0]atrim=start=0:end=${a.toFixed(4)},asetpts=PTS-STARTPTS[p0]`);
59
+ parts.push(`[d1]atrim=start=${b.toFixed(4)},asetpts=PTS-STARTPTS[p1]`);
60
+ parts.push(`[p0][p1]concat=n=2:v=0:a=1[cut]`);
61
+ label = 'cut';
62
+ }
63
+
64
+ const r = spawnSync(
65
+ FFMPEG,
66
+ [
67
+ '-y', '-v', 'error',
68
+ '-i', source,
69
+ '-filter_complex', parts.join(';'),
70
+ '-map', `[${label}]`,
71
+ // PCM, and the source's own sample rate — no resampling, no encoding.
72
+ '-c:a', 'pcm_s16le',
73
+ out,
74
+ ],
75
+ {encoding: 'utf8'},
76
+ );
77
+ if (r.status !== 0) {
78
+ console.error(r.stderr?.slice(0, 900));
79
+ process.exit(1);
80
+ }
81
+
82
+ const probe = spawnSync(
83
+ FFMPEG.replace(/ffmpeg(\.exe)?$/i, (m) => (m.endsWith('.exe') ? 'ffprobe.exe' : 'ffprobe')),
84
+ ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', out],
85
+ {encoding: 'utf8'},
86
+ );
87
+ console.log(`voice written lossless -> ${out} (${Number(probe.stdout).toFixed(3)}s, pcm_s16le)`);
88
+ console.log('Render the picture muted, then mux this in with ONE encode at delivery.');
@@ -28,6 +28,8 @@ Length target: <!-- e.g. 25s, 90s -->
28
28
 
29
29
 
30
30
  ## Moments from references
31
+ <!-- READ FIRST, BEFORE ANY DESIGN DECISION. These, and the ones saved in references/references.md,
32
+ beat the house defaults wherever the two disagree. -->
31
33
  <!-- You DON'T need the name of the effect. Clip + time + what you like about it.
32
34
  Format: - clip-file.mp4 mm:ss-mm:ss — what you like (feel words are fine: snappy, floaty, punchy)
33
35
  - a-reel-you-like.mp4 00:02-00:04 — the colour screens flipping with each phrase, feels punchy -->