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
package/template/CLAUDE.md
CHANGED
|
@@ -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
|
|
51
|
+
## The order of work
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
56
|
+
### 0. References first. Always. Before the brief, before the footage.
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
**
|
|
133
|
-
the
|
|
134
|
-
|
|
135
|
-
|
|
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 -->
|