creator-editing-studio 1.3.5 → 1.3.7

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/template/STYLE.md CHANGED
@@ -1,179 +1,179 @@
1
- <!-- This is YOUR style guide, and it starts deliberately half-empty.
2
-
3
- Parts 1 to 6 are craft standards: things that hold for any brand, most of them
4
- measured rather than argued. Leave them alone until you have a reason not to, and
5
- your edits will come out clean rather than random.
6
-
7
- Parts 7 and 8 are yours and begin blank. Every time you approve or reject something,
8
- write it down there. That is the half that makes edits look like YOURS instead of
9
- merely competent, and it is the half nobody can hand you. -->
10
-
11
- # House style
12
-
13
- What every edit must look, move and sound like. `CLAUDE.md` says *how* to build; this
14
- file says *what it must come out like*.
15
-
16
- When this file and a reference disagree, this file wins unless you say otherwise.
17
-
18
- ---
19
-
20
- ## 1. Your identity
21
-
22
- Everything visual comes from `src/design/tokens.ts`. That file is the single source of
23
- your typeface, your colours, your spacing and your type scale — change it there and every
24
- video changes with it. Never hard-code a colour or a font family into a composition.
25
-
26
- - **Type:** one display typeface. Heavy weights for headlines, numbers and hero words;
27
- medium for labels and pointers.
28
- - **Colour:** one accent, plus ink and a light ground. Resist a second accent — two
29
- accents read as a template, one reads as a brand.
30
- - On footage: accent text, with a soft glow so it survives a busy frame.
31
- - On a light ground: ink text, with the accent used only to mark the word that matters.
32
-
33
- ## 2. Type scale
34
-
35
- Sizes are a starting ladder, not a law. What matters is that you keep to *a* ladder.
36
-
37
- | Role | 16:9 (1920x1080) | 9:16 (1080x1920) | Notes |
38
- |---|---|---|---|
39
- | Hero word | 200–320px | 130–220px | One on screen at a time, never two |
40
- | Headline | ~116px | ~100px | Tight tracking, around -0.03em |
41
- | Big number | ~170px | ~150px | Tabular digits while a counter runs |
42
- | Label / eyebrow | ~40px | ~32px | Uppercase, open tracking, ~0.14em |
43
- | Pointer / list item | ~42px | ~36px | |
44
- | Captions | 46px | 42px | Crisp white, bold, soft dark shadow |
45
-
46
- **If a word needs to be bigger than the ladder to feel important, the layout is wrong,
47
- not the size.** Oversized type is the most common way an edit starts looking amateur.
48
-
49
- ## 3. Layout
50
-
51
- - **The speaker stays full screen by default.** Rounded cards and frames around a talking
52
- head make the edit look assembled rather than shot. A card is fine as a *transition*,
53
- not as a container.
54
- - Put text in the clear space the shot already gives you: sky, a wall, out-of-focus
55
- background. Check where the head lands on every cut before committing a position.
56
- - Captions live in one consistent zone per layout, over a dark area. They hide while the
57
- same words are on screen as a graphic, and never linger into the next scene.
58
- - Graphics are built from type, numbers, real screenshots and real footage. Decorative
59
- shapes that mean nothing age badly and read as filler.
60
-
61
- ## 4. Motion
62
-
63
- - The motion laws in `CLAUDE.md`, always. Eased, motion-blurred, nothing pops into place.
64
- - **Zoom through jump cuts.** A small push on the cut hides the join; a hard snap exposes it.
65
- - **Hard cuts only as a deliberate burst** — a few beats of solid colour synced to the
66
- words, then back to smooth. Everything else morphs from one state to the next.
67
- - Cover anything off-camera — reading, looking away, a fumble — with a graphic or a cut.
68
- - Counters start at zero and only go up. No grey placeholder digits.
69
- - Vary your shot lengths. Cutting everything to the same duration is the clearest sign
70
- an edit was made by a machine rather than by someone making decisions.
71
-
72
- ## 5. Sound (half the reel)
73
-
74
- - **The voice is never processed.** No reverb, no heavy compression, no "enhancement".
75
- - Sound effects are short — whooshes 0.4–0.8s, hits 0.3–0.6s — faded, and placed in the
76
- gaps between words rather than under them.
77
- - Every new visual gets its own fitting sound. Reusing a sound that belonged to a
78
- different look is the audio version of a template.
79
- - **Subtle beats loud.** Big cinematic impacts on text almost always read as too much.
80
- - Hand over voice and SFX stems plus a timecoded cue sheet with every delivery.
81
-
82
- ## 6. Colour, film looks and presets
83
-
84
- **Most of a look comes from the shoot, not the grade.** Golden hour or one good window,
85
- light behind or beside the subject, background well behind the subject, white balance
86
- fixed rather than auto. Automated grading is a starting point and only when the footage's
87
- light already resembles the reference — on mismatched light it will look wrong, and you
88
- should trust your eye over any automatic match.
89
-
90
- | Transition | When |
91
- |---|---|
92
- | whipPan | Between unrelated shots. Never twice in a row. |
93
- | push | Moving forward through a list or a sequence. |
94
- | maskWipe | A calm reveal. Pairs with a soft sound. |
95
- | colourFlip | A burst on a phrase. Two or three, then back to smooth. |
96
- | matchCut | Only when both shots share a shape or a direction. |
97
- | dip | Changing subject without drama. |
98
-
99
- | Caption look | When |
100
- |---|---|
101
- | clean | Default. Works over almost anything. |
102
- | block | Busy or bright footage, where white alone gets lost. |
103
- | highlight | Keeps the eye on the word being spoken. |
104
- | pop | Fast edits. Not over heavy motion. |
105
-
106
- | Film look | When |
107
- |---|---|
108
- | none | Graphics, screen recordings, anything where text is the subject. |
109
- | clean | Default for a talking head. Enough grain to stop flat areas banding. |
110
- | film | B-roll and travel. The one to reach for first. |
111
- | super8 | Memory and nostalgia moments only. Never a whole video. |
112
- | cinema | A held, wide, quiet shot. |
113
-
114
- **Three things here were measured, not guessed. Do not quietly undo them:**
115
-
116
- - Grain composites **normally**, not with `overlay`. On flat cream or flat ink there are
117
- no midtones, and overlay grain measures 0.000 stddev — the layer does nothing at all.
118
- - Grain is paid for in black level — blacks lift measurably at medium strength. Raise it
119
- per shot when a nostalgia moment is worth milkier blacks; never raise the default.
120
- - Halation blooms **highlights only**. The plate is crushed to black before it is blurred
121
- and screened back. Without that crush, a bright frame blooms end to end and the blacks
122
- go brown.
123
-
124
- Speed ramps are eased, never a hard speed switch. Below 0.4x needs 50fps or higher source
125
- footage — you cannot rescue slow motion that was not shot for it.
126
-
127
- ---
128
-
129
- ## 7. Where these defaults end and you begin
130
-
131
- The sections above are **defaults**: a starting point tuned so that a video made on day one,
132
- before you have told the system anything, comes out clean rather than embarrassing. Captions
133
- sit at a readable size with a measured shadow, cuts are eased, grain is restrained, the voice
134
- is untouched.
135
-
136
- Defaults are also the reason an edit can come out *anonymous*. They are what everybody gets.
137
- Three things move an edit from the default toward yours, in increasing order of power:
138
-
139
- **1. Your tokens.** `src/design/tokens.ts` carries your typeface, your colours, your scale.
140
- This makes an edit *yours to look at* immediately.
141
-
142
- **2. Your references.** Saved moments from edits you admire, each with a timecode and a note
143
- on what you like about that exact moment. These are read before the plan for every video, and
144
- they beat the defaults wherever the two disagree — a default is a guess that suits everyone,
145
- a reference is a fact about you.
146
-
147
- Roughly: **none** gives you clean and forgettable. **Three** is enough to spot a pattern.
148
- **Seven or eight** is enough to tell a preference from a coincidence, and it is where edits
149
- start looking authored.
150
-
151
- **3. This file, as it fills up.** Sections 8 and 9 are the part no default can supply and no
152
- competitor can copy, because they are a record of decisions only you have made. Every entry
153
- narrows the next edit toward what you actually want.
154
-
155
- Nothing here learns on its own. It gets better because the brief gets richer — so the ten
156
- minutes spent writing down why you rejected something is the highest-return ten minutes
157
- available to you.
158
-
159
- ---
160
-
161
- ## 8. Approved (your effects library)
162
-
163
- <!-- Empty on purpose. When something works, write it down here: what it is, and where
164
- you approved it. Next time you can ask for it by name instead of describing it
165
- again, and every future video inherits the decision. -->
166
-
167
- | Beat | What it is | Where you approved it |
168
- |---|---|---|
169
- | | | |
170
-
171
- ## 9. Rejected (don't do these again)
172
-
173
- <!-- This table is worth more than the one above. A list of things you have ruled out is
174
- the fastest way to stop an edit drifting back toward generic, because it removes
175
- options rather than adding them. Add a line every time you say "not that". -->
176
-
177
- | What | Why | Where |
178
- |---|---|---|
179
- | | | |
1
+ <!-- This is YOUR style guide, and it starts deliberately half-empty.
2
+
3
+ Parts 1 to 6 are craft standards: things that hold for any brand, most of them
4
+ measured rather than argued. Leave them alone until you have a reason not to, and
5
+ your edits will come out clean rather than random.
6
+
7
+ Parts 7 and 8 are yours and begin blank. Every time you approve or reject something,
8
+ write it down there. That is the half that makes edits look like YOURS instead of
9
+ merely competent, and it is the half nobody can hand you. -->
10
+
11
+ # House style
12
+
13
+ What every edit must look, move and sound like. `CLAUDE.md` says *how* to build; this
14
+ file says *what it must come out like*.
15
+
16
+ When this file and a reference disagree, this file wins unless you say otherwise.
17
+
18
+ ---
19
+
20
+ ## 1. Your identity
21
+
22
+ Everything visual comes from `src/design/tokens.ts`. That file is the single source of
23
+ your typeface, your colours, your spacing and your type scale — change it there and every
24
+ video changes with it. Never hard-code a colour or a font family into a composition.
25
+
26
+ - **Type:** one display typeface. Heavy weights for headlines, numbers and hero words;
27
+ medium for labels and pointers.
28
+ - **Colour:** one accent, plus ink and a light ground. Resist a second accent — two
29
+ accents read as a template, one reads as a brand.
30
+ - On footage: accent text, with a soft glow so it survives a busy frame.
31
+ - On a light ground: ink text, with the accent used only to mark the word that matters.
32
+
33
+ ## 2. Type scale
34
+
35
+ Sizes are a starting ladder, not a law. What matters is that you keep to *a* ladder.
36
+
37
+ | Role | 16:9 (1920x1080) | 9:16 (1080x1920) | Notes |
38
+ |---|---|---|---|
39
+ | Hero word | 200–320px | 130–220px | One on screen at a time, never two |
40
+ | Headline | ~116px | ~100px | Tight tracking, around -0.03em |
41
+ | Big number | ~170px | ~150px | Tabular digits while a counter runs |
42
+ | Label / eyebrow | ~40px | ~32px | Uppercase, open tracking, ~0.14em |
43
+ | Pointer / list item | ~42px | ~36px | |
44
+ | Captions | 46px | 42px | Crisp white, bold, soft dark shadow |
45
+
46
+ **If a word needs to be bigger than the ladder to feel important, the layout is wrong,
47
+ not the size.** Oversized type is the most common way an edit starts looking amateur.
48
+
49
+ ## 3. Layout
50
+
51
+ - **The speaker stays full screen by default.** Rounded cards and frames around a talking
52
+ head make the edit look assembled rather than shot. A card is fine as a *transition*,
53
+ not as a container.
54
+ - Put text in the clear space the shot already gives you: sky, a wall, out-of-focus
55
+ background. Check where the head lands on every cut before committing a position.
56
+ - Captions live in one consistent zone per layout, over a dark area. They hide while the
57
+ same words are on screen as a graphic, and never linger into the next scene.
58
+ - Graphics are built from type, numbers, real screenshots and real footage. Decorative
59
+ shapes that mean nothing age badly and read as filler.
60
+
61
+ ## 4. Motion
62
+
63
+ - The motion laws in `CLAUDE.md`, always. Eased, motion-blurred, nothing pops into place.
64
+ - **Zoom through jump cuts.** A small push on the cut hides the join; a hard snap exposes it.
65
+ - **Hard cuts only as a deliberate burst** — a few beats of solid colour synced to the
66
+ words, then back to smooth. Everything else morphs from one state to the next.
67
+ - Cover anything off-camera — reading, looking away, a fumble — with a graphic or a cut.
68
+ - Counters start at zero and only go up. No grey placeholder digits.
69
+ - Vary your shot lengths. Cutting everything to the same duration is the clearest sign
70
+ an edit was made by a machine rather than by someone making decisions.
71
+
72
+ ## 5. Sound (half the reel)
73
+
74
+ - **The voice is never processed.** No reverb, no heavy compression, no "enhancement".
75
+ - Sound effects are short — whooshes 0.4–0.8s, hits 0.3–0.6s — faded, and placed in the
76
+ gaps between words rather than under them.
77
+ - Every new visual gets its own fitting sound. Reusing a sound that belonged to a
78
+ different look is the audio version of a template.
79
+ - **Subtle beats loud.** Big cinematic impacts on text almost always read as too much.
80
+ - Hand over voice and SFX stems plus a timecoded cue sheet with every delivery.
81
+
82
+ ## 6. Colour, film looks and presets
83
+
84
+ **Most of a look comes from the shoot, not the grade.** Golden hour or one good window,
85
+ light behind or beside the subject, background well behind the subject, white balance
86
+ fixed rather than auto. Automated grading is a starting point and only when the footage's
87
+ light already resembles the reference — on mismatched light it will look wrong, and you
88
+ should trust your eye over any automatic match.
89
+
90
+ | Transition | When |
91
+ |---|---|
92
+ | whipPan | Between unrelated shots. Never twice in a row. |
93
+ | push | Moving forward through a list or a sequence. |
94
+ | maskWipe | A calm reveal. Pairs with a soft sound. |
95
+ | colourFlip | A burst on a phrase. Two or three, then back to smooth. |
96
+ | matchCut | Only when both shots share a shape or a direction. |
97
+ | dip | Changing subject without drama. |
98
+
99
+ | Caption look | When |
100
+ |---|---|
101
+ | clean | Default. Works over almost anything. |
102
+ | block | Busy or bright footage, where white alone gets lost. |
103
+ | highlight | Keeps the eye on the word being spoken. |
104
+ | pop | Fast edits. Not over heavy motion. |
105
+
106
+ | Film look | When |
107
+ |---|---|
108
+ | none | Graphics, screen recordings, anything where text is the subject. |
109
+ | clean | Default for a talking head. Enough grain to stop flat areas banding. |
110
+ | film | B-roll and travel. The one to reach for first. |
111
+ | super8 | Memory and nostalgia moments only. Never a whole video. |
112
+ | cinema | A held, wide, quiet shot. |
113
+
114
+ **Three things here were measured, not guessed. Do not quietly undo them:**
115
+
116
+ - Grain composites **normally**, not with `overlay`. On flat cream or flat ink there are
117
+ no midtones, and overlay grain measures 0.000 stddev — the layer does nothing at all.
118
+ - Grain is paid for in black level — blacks lift measurably at medium strength. Raise it
119
+ per shot when a nostalgia moment is worth milkier blacks; never raise the default.
120
+ - Halation blooms **highlights only**. The plate is crushed to black before it is blurred
121
+ and screened back. Without that crush, a bright frame blooms end to end and the blacks
122
+ go brown.
123
+
124
+ Speed ramps are eased, never a hard speed switch. Below 0.4x needs 50fps or higher source
125
+ footage — you cannot rescue slow motion that was not shot for it.
126
+
127
+ ---
128
+
129
+ ## 7. Where these defaults end and you begin
130
+
131
+ The sections above are **defaults**: a starting point tuned so that a video made on day one,
132
+ before you have told the system anything, comes out clean rather than embarrassing. Captions
133
+ sit at a readable size with a measured shadow, cuts are eased, grain is restrained, the voice
134
+ is untouched.
135
+
136
+ Defaults are also the reason an edit can come out *anonymous*. They are what everybody gets.
137
+ Three things move an edit from the default toward yours, in increasing order of power:
138
+
139
+ **1. Your tokens.** `src/design/tokens.ts` carries your typeface, your colours, your scale.
140
+ This makes an edit *yours to look at* immediately.
141
+
142
+ **2. Your references.** Saved moments from edits you admire, each with a timecode and a note
143
+ on what you like about that exact moment. These are read before the plan for every video, and
144
+ they beat the defaults wherever the two disagree — a default is a guess that suits everyone,
145
+ a reference is a fact about you.
146
+
147
+ Roughly: **none** gives you clean and forgettable. **Three** is enough to spot a pattern.
148
+ **Seven or eight** is enough to tell a preference from a coincidence, and it is where edits
149
+ start looking authored.
150
+
151
+ **3. This file, as it fills up.** Sections 8 and 9 are the part no default can supply and no
152
+ competitor can copy, because they are a record of decisions only you have made. Every entry
153
+ narrows the next edit toward what you actually want.
154
+
155
+ Nothing here learns on its own. It gets better because the brief gets richer — so the ten
156
+ minutes spent writing down why you rejected something is the highest-return ten minutes
157
+ available to you.
158
+
159
+ ---
160
+
161
+ ## 8. Approved (your effects library)
162
+
163
+ <!-- Empty on purpose. When something works, write it down here: what it is, and where
164
+ you approved it. Next time you can ask for it by name instead of describing it
165
+ again, and every future video inherits the decision. -->
166
+
167
+ | Beat | What it is | Where you approved it |
168
+ |---|---|---|
169
+ | | | |
170
+
171
+ ## 9. Rejected (don't do these again)
172
+
173
+ <!-- This table is worth more than the one above. A list of things you have ruled out is
174
+ the fastest way to stop an edit drifting back toward generic, because it removes
175
+ options rather than adding them. Add a line every time you say "not that". -->
176
+
177
+ | What | Why | Where |
178
+ |---|---|---|
179
+ | | | |
@@ -17,6 +17,7 @@
17
17
  "stills": "node scripts/stills.mjs",
18
18
  "refs": "node scripts/refs.mjs",
19
19
  "refstyle": "node scripts/refstyle.mjs",
20
+ "refmotion": "node scripts/refmotion.mjs",
20
21
  "beats": "node scripts/beats.mjs",
21
22
  "sync": "node scripts/sync-audio.mjs",
22
23
  "ramp": "node scripts/ramp.mjs",
@@ -25,16 +26,20 @@
25
26
  "sound": "node scripts/sounddesign.mjs"
26
27
  },
27
28
  "dependencies": {
28
- "ffmpeg-static": "5.3.0",
29
- "ffprobe-static": "3.1.0",
29
+ "@modelcontextprotocol/ext-apps": "1.7.5",
30
+ "@modelcontextprotocol/sdk": "1.32.1",
30
31
  "@remotion/cli": "4.0.525",
31
32
  "@remotion/fonts": "4.0.525",
32
33
  "@remotion/google-fonts": "4.0.525",
33
34
  "@remotion/media-utils": "4.0.525",
34
35
  "@remotion/motion-blur": "4.0.525",
36
+ "esbuild": "0.28.1",
37
+ "ffmpeg-static": "5.3.0",
38
+ "ffprobe-static": "3.1.0",
35
39
  "react": "19.3.0",
36
40
  "react-dom": "19.3.0",
37
- "remotion": "4.0.525"
41
+ "remotion": "4.0.525",
42
+ "zod": "4.5.4"
38
43
  },
39
44
  "devDependencies": {
40
45
  "@types/react": "19.3.0",
@@ -0,0 +1,34 @@
1
+ # Live preview
2
+
3
+ The edit plays inside the chat, from its code. Nothing is rendered until you press **Render final video**.
4
+
5
+ - **The video** on the left; **every beat** on the right with its timestamp.
6
+ - **Instant controls under each beat** change the video on the spot — no chat, no waiting:
7
+ Size − / +, Move ↑ ↓, Timing Earlier / Later, Sound − / + / Mute, Icon, **Follow head** (the text rides
8
+ with the speaker's head — motion tracking), Reset.
9
+ - **Save** writes all your adjustments in one go; **Render final video** saves them and renders, and tells
10
+ you how long the render will take first.
11
+ - **"Something else? Describe it for Claude"** under a beat is for new ideas. It goes into the chat, Claude
12
+ makes the change, and the updated preview opens below. 📎 attaches a reference image (the app puts it in
13
+ your chat box — press Send).
14
+ - **Undo last change** puts the edit back exactly as it was before the last change.
15
+
16
+ Claude opens the preview by itself once you approve a plan. You never need to ask for it.
17
+
18
+ ## How it is switched on
19
+
20
+ Setup registers it once (Claude Code: `claude mcp add live-preview --scope user -- node "<studio>/preview/server.mjs"`;
21
+ Codex: `codex mcp add live-preview -- node "<studio>/preview/server.mjs"`). Apps load these when a chat starts,
22
+ so it works from the next chat after setup.
23
+
24
+ ## For Claude
25
+
26
+ - Open it with `preview_edit({video: "<folder in src/videos>"})` right after the plan is approved, and again
27
+ after every change you make. `preview_undo` restores the previous version.
28
+ - Each video needs `src/videos/<video>/preview.tsx` exporting `Comp` (the edit + its voice as
29
+ `<Audio src={staticFile('__stems/voice.mp3')} />`, taking `tweaks` and passing them to the edit), `META`,
30
+ `BEATS` (one per plan row: name, time, beat, says, from, to, icons?, follow?) and `TWEAKS` (saved tweaks,
31
+ the tweaks.json path, icons, limits, trackFile?). The edit applies tweaks with `TweaksProvider` / `Tweaked`
32
+ from `src/lib/tweaks.tsx` — full-screen cards that hide cut joins never go inside a moving `Tweaked`.
33
+ - The player cannot call this server or the computer (the app blocks it): everything it shows is packed into
34
+ the page when it opens, and it talks only by sending chat messages.
@@ -0,0 +1,177 @@
1
+ /**
2
+ * What the live preview needs from a video, worked out once and used by both halves:
3
+ * server.mjs — the local preview server (our machine, reads files directly)
4
+ * publish.mjs — sends the same things to the online preview, so it works in any chat with no restart
5
+ *
6
+ * For a video it finds: the preview module (Comp, META, BEATS), every file the edit reads with
7
+ * staticFile() as a small preview copy, and the edit compiled to one script.
8
+ */
9
+ import {spawnSync} from 'node:child_process';
10
+ import {createHash} from 'node:crypto';
11
+ import {createRequire} from 'node:module';
12
+ import {existsSync, mkdirSync, readdirSync, readFileSync, statSync} from 'node:fs';
13
+ import {dirname, extname, join, resolve} from 'node:path';
14
+ import {fileURLToPath, pathToFileURL} from 'node:url';
15
+
16
+ export const HERE = dirname(fileURLToPath(import.meta.url));
17
+ export const STUDIO = resolve(HERE, '..');
18
+ export const PUBLIC = join(STUDIO, 'assets');
19
+ export const SRC = join(STUDIO, 'src');
20
+ const CACHE = join(HERE, '.cache');
21
+ mkdirSync(CACHE, {recursive: true});
22
+
23
+ const require = createRequire(join(STUDIO, 'package.json'));
24
+ const esbuild = require('esbuild');
25
+ const {FFMPEG} = await import(pathToFileURL(join(STUDIO, 'scripts', 'lib', 'media.mjs')).href);
26
+
27
+ /* ----------------------------------------------------------------------------- which video */
28
+
29
+ /**
30
+ * Everything the preview needs to know about one video, read from its own folder:
31
+ * src/videos/<name>/preview.tsx, the *Edit.tsx beside it, the asset folders its code mentions, its stems.
32
+ */
33
+ export function configFor(name) {
34
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name ?? '')) throw new Error(`"${name}" is not a video name (lowercase letters, digits, dashes)`);
35
+ const code = join(SRC, 'videos', name);
36
+ if (!existsSync(join(code, 'preview.tsx'))) {
37
+ throw new Error(
38
+ `No src/videos/${name}/preview.tsx. Write it first: export Comp (the edit + its voice as <Audio src={staticFile('__stems/voice.mp3')} />), META and BEATS — copy src/videos/noufal/preview.tsx.`,
39
+ );
40
+ }
41
+ const source = readdirSync(code)
42
+ .filter((f) => /\.(tsx?|json)$/.test(f))
43
+ .map((f) => readFileSync(join(code, f), 'utf8'))
44
+ .join('\n');
45
+ const folders = new Set(['fonts']);
46
+ // Only the fonts this edit names — the studio has many, and each one is ~1 MB in the page.
47
+ const fonts = [...new Set([...source.matchAll(/fonts\/([\w.-]+\.(?:ttf|otf|woff2?))/g)].map((m) => m[1]))];
48
+ // The whole folder path up to the file name — cutouts live a level deeper (cutouts/<shot>/f123.png).
49
+ for (const m of source.matchAll(new RegExp(`projects/${name}/((?:[\\w-]+/)+)`, 'g'))) folders.add(`projects/${name}/${m[1].replace(/\/$/, '')}`);
50
+ for (const f of folders) if (!existsSync(join(PUBLIC, f))) folders.delete(f);
51
+
52
+ const stems = {};
53
+ if (existsSync(join(STUDIO, 'work', name, 'voice.wav'))) stems.voice = `work/${name}/voice.wav`;
54
+ const out = join(STUDIO, 'out', name);
55
+ if (existsSync(out))
56
+ for (const f of readdirSync(out)) {
57
+ const m = f.match(/^stem-(\w+)\.wav$/);
58
+ if (m) stems[m[1]] = `out/${name}/${f}`;
59
+ }
60
+
61
+ // The edit is the file named *Edit.tsx; its composition id is that name (as registered in Root.tsx).
62
+ const editFile = readdirSync(code).find((f) => /Edit\.tsx$/.test(f));
63
+ return {
64
+ project: name,
65
+ preview: `src/videos/${name}/preview.tsx`,
66
+ edit: `src/videos/${name}/${editFile ?? 'preview.tsx'}`,
67
+ composition: editFile?.replace(/\.tsx$/, '') ?? null,
68
+ assets: [...folders],
69
+ fonts,
70
+ stems,
71
+ };
72
+ }
73
+
74
+ /* ----------------------------------------------------------------------------- the footage */
75
+
76
+ /** Every file the edit asks for by name (staticFile), plus the stems. Keys are the names the edit uses. */
77
+ function assetList(c) {
78
+ const files = {};
79
+ const addDir = (rel) => {
80
+ const dir = join(PUBLIC, rel);
81
+ if (!existsSync(dir)) return;
82
+ for (const f of readdirSync(dir)) {
83
+ if (rel === 'fonts' && c.fonts && !c.fonts.includes(f)) continue;
84
+ if (statSync(join(dir, f)).isFile()) files[`${rel}/${f}`] = join(dir, f);
85
+ }
86
+ };
87
+ for (const d of c.assets) addDir(d);
88
+ for (const [k, f] of Object.entries(c.stems ?? {})) if (existsSync(join(STUDIO, f))) files[`__stems/${k}.mp3`] = join(STUDIO, f);
89
+ return Object.fromEntries(Object.entries(files).filter(([k]) => /\.(mp4|mov|png|jpe?g|webp|svg|ttf|woff2?|mp3)$/i.test(k)));
90
+ }
91
+
92
+ /**
93
+ * Preview copies: small enough to travel inside the player page, good enough to judge the edit in a chat-
94
+ * sized player. Anything fits a 540px box on its longest side. The final render always uses the originals.
95
+ */
96
+ const FIT = "scale='if(gt(iw,ih),min(540,iw),-2)':'if(gt(iw,ih),-2,min(540,ih))'";
97
+ const PROXY = {
98
+ video: ['-vf', FIT, '-an', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '31', '-g', '15', '-pix_fmt', 'yuv420p', '-movflags', '+faststart'],
99
+ image: ['-vf', FIT, '-c:v', 'libwebp', '-quality', '72'],
100
+ audio: ['-vn', '-ac', '2', '-ar', '44100', '-c:a', 'libmp3lame', '-b:a', '128k'],
101
+ };
102
+ const MIME = {'.mp4': 'video/mp4', '.webp': 'image/webp', '.mp3': 'audio/mpeg', '.svg': 'image/svg+xml', '.ttf': 'font/ttf', '.woff': 'font/woff', '.woff2': 'font/woff2', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png'};
103
+
104
+ function proxyOf(name, original) {
105
+ const st = statSync(original);
106
+ // The settings are part of the key, so changing them makes new copies instead of reusing old ones.
107
+ const key = createHash('sha1').update(`${original}:${st.size}:${st.mtimeMs}:${JSON.stringify(PROXY)}`).digest('hex').slice(0, 16);
108
+ const ext = extname(name).toLowerCase();
109
+ const kind = /\.(mp4|mov)$/.test(ext) ? 'video' : /\.(png|jpe?g)$/.test(ext) ? 'image' : ext === '.mp3' ? 'audio' : null;
110
+ if (!kind) return {file: original, mime: MIME[ext] ?? 'application/octet-stream'};
111
+ const outExt = kind === 'video' ? '.mp4' : kind === 'image' ? '.webp' : '.mp3';
112
+ const out = join(CACHE, `${key}${outExt}`);
113
+ if (!existsSync(out)) {
114
+ const r = spawnSync(FFMPEG, ['-y', '-v', 'error', '-i', original, ...PROXY[kind], out], {encoding: 'utf8'});
115
+ if (r.status !== 0) throw new Error(`could not make a preview copy of ${name}: ${r.stderr}`);
116
+ }
117
+ return {file: out, mime: MIME[outExt]};
118
+ }
119
+
120
+ /** [{name, file, mime, size, hash}] — hash is of the preview copy, so unchanged files are never re-sent. */
121
+ export function buildManifest(c) {
122
+ return Object.entries(assetList(c)).map(([name, original]) => {
123
+ const p = proxyOf(name, original);
124
+ const hash = createHash('sha256').update(readFileSync(p.file)).digest('hex').slice(0, 32);
125
+ return {name, file: p.file, mime: p.mime, size: statSync(p.file).size, hash};
126
+ });
127
+ }
128
+
129
+ /* ----------------------------------------------------------------------------- the edit, compiled */
130
+
131
+ /**
132
+ * The edit runs inside the player, so it must use the player's own React and Remotion — two copies
133
+ * would break every hook. Those imports become reads of window.__shared.
134
+ */
135
+ const SHARED = ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'remotion'];
136
+ const sharedPlugin = {
137
+ name: 'shared',
138
+ setup(b) {
139
+ b.onResolve({filter: new RegExp(`^(${SHARED.map((s) => s.replace('/', '\\/')).join('|')})$`)}, (a) => ({path: a.path, namespace: 'shared'}));
140
+ b.onLoad({filter: /.*/, namespace: 'shared'}, (a) => {
141
+ const keys = Object.keys(require(a.path)).filter((k) => /^[A-Za-z_$][\w$]*$/.test(k) && k !== 'default');
142
+ return {
143
+ contents: `const m = window.__shared[${JSON.stringify(a.path)}];\nexport default m;\n${keys.map((k) => `export const ${k} = m.${k};`).join('\n')}`,
144
+ loader: 'js',
145
+ };
146
+ });
147
+ },
148
+ };
149
+
150
+ const common = {bundle: true, jsx: 'automatic', logLevel: 'silent', loader: {'.png': 'dataurl', '.svg': 'dataurl'}};
151
+
152
+ /** The edit as one script that sets window.__lpComp. Throws with a readable message if it doesn't compile. */
153
+ export async function compileEdit(c) {
154
+ try {
155
+ const r = await esbuild.build({...common, entryPoints: [join(STUDIO, c.preview)], format: 'iife', globalName: '__lpComp', write: false, plugins: [sharedPlugin], define: {'process.env.NODE_ENV': '"development"'}});
156
+ return r.outputFiles[0].text;
157
+ } catch (e) {
158
+ throw new Error((e.errors ?? []).map((x) => `${x.location?.file ?? ''}:${x.location?.line ?? ''} ${x.text}`).join('\n') || String(e));
159
+ }
160
+ }
161
+
162
+ /**
163
+ * The player page. Customers get it pre-built (player.html, made when the studio ships), so they need none of
164
+ * the player's build tools; in the source studio it is built from ui/app.tsx.
165
+ */
166
+ export async function playerHtml() {
167
+ const built = join(HERE, 'player.html');
168
+ if (existsSync(built) && !existsSync(join(HERE, 'ui', 'app.tsx'))) return readFileSync(built, 'utf8');
169
+ return buildUi();
170
+ }
171
+
172
+ export async function buildUi() {
173
+ const r = await esbuild.build({...common, entryPoints: [join(HERE, 'ui', 'app.tsx')], format: 'iife', write: false, minify: true, define: {'process.env.NODE_ENV': '"production"'}});
174
+ const js = r.outputFiles[0].text;
175
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
176
+ <body><div id="root"></div><script>${js.replace(/<\/script>/g, '<\\/script>')}</script></body></html>`;
177
+ }