reelme 0.2.2 → 0.3.0
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/assets/audio/PROVENANCE.md +16 -0
- package/assets/audio/bright-sparks.mp3 +0 -0
- package/assets/audio/calm-keys.mp3 +0 -0
- package/assets/audio/circuit-pulse.mp3 +0 -0
- package/assets/audio/clean-horizon.mp3 +0 -0
- package/assets/audio/generate-tracks.mjs +213 -0
- package/assets/audio/manifest.json +83 -0
- package/assets/audio/midnight-protocol.mp3 +0 -0
- package/assets/audio/pixel-bounce.mp3 +0 -0
- package/assets/audio/steady-launch.mp3 +0 -0
- package/assets/audio/sunny-loop.mp3 +0 -0
- package/assets/audio/vector-grid.mp3 +0 -0
- package/package.json +2 -1
- package/src/cache.mjs +1 -0
- package/src/render.mjs +95 -4
- package/template/package.json +1 -0
- package/template/pnpm-lock.yaml +15 -0
- package/template/src/Root.tsx +55 -45
- package/template/src/audio.ts +24 -0
- package/template/src/benchmark.ts +18 -0
- package/template/src/brief.json +1 -0
- package/template/src/brief.ts +61 -3
- package/template/src/cinematic/Atmosphere.tsx +139 -0
- package/template/src/cinematic/Camera.tsx +54 -0
- package/template/src/cinematic/look.ts +89 -0
- package/template/src/cinematic/transitions.tsx +92 -0
- package/template/src/components/primitives/Bar.tsx +82 -0
- package/template/src/components/primitives/Caption.tsx +3 -2
- package/template/src/components/primitives/Kicker.tsx +46 -0
- package/template/src/components/primitives/Label.tsx +18 -24
- package/template/src/components/primitives/RevealText.tsx +101 -0
- package/template/src/components/primitives/Terminal.tsx +11 -0
- package/template/src/components/scenes/Benchmark.tsx +83 -0
- package/template/src/components/scenes/BrowserFrame.tsx +4 -3
- package/template/src/components/scenes/CTA.tsx +56 -10
- package/template/src/components/scenes/Clip.tsx +125 -0
- package/template/src/components/scenes/CodeReveal.tsx +4 -3
- package/template/src/components/scenes/DataFlow.tsx +4 -3
- package/template/src/components/scenes/FeatureList.tsx +13 -8
- package/template/src/components/scenes/FileTree.tsx +4 -3
- package/template/src/components/scenes/Hook.tsx +55 -0
- package/template/src/components/scenes/HotkeyScene.tsx +4 -3
- package/template/src/components/scenes/MobileScreen.tsx +120 -82
- package/template/src/components/scenes/OSWindowScene.tsx +4 -3
- package/template/src/components/scenes/Problem.tsx +28 -29
- package/template/src/components/scenes/SplitComparison.tsx +65 -92
- package/template/src/components/scenes/StatCallout.tsx +93 -4
- package/template/src/components/scenes/TerminalScene.tsx +5 -6
- package/template/src/duration.ts +12 -1
- package/template/src/platforms.ts +4 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ReelMe Bundled Audio Provenance
|
|
2
|
+
|
|
3
|
+
The bundled tracks in this directory are instrumental loops generated by
|
|
4
|
+
`generate-tracks.mjs` for ReelMe. They do not sample third-party recordings,
|
|
5
|
+
loops, MIDI files, or stems.
|
|
6
|
+
|
|
7
|
+
The generated tracks are dedicated to the public domain under CC0 1.0. The
|
|
8
|
+
manifest source URLs point to the generator definition used to create each
|
|
9
|
+
track. Regenerate the MP3 files with:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
node cli/assets/audio/generate-tracks.mjs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Each track is 32 seconds long, encoded as MP3, and kept below 2 MB for package
|
|
16
|
+
size.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const SAMPLE_RATE = 44100;
|
|
9
|
+
const DURATION = 32;
|
|
10
|
+
const TWO_PI = Math.PI * 2;
|
|
11
|
+
|
|
12
|
+
const TRACKS = [
|
|
13
|
+
{
|
|
14
|
+
id: "calm-keys",
|
|
15
|
+
file: "calm-keys.mp3",
|
|
16
|
+
bpm: 92,
|
|
17
|
+
key: 57,
|
|
18
|
+
scale: [0, 2, 4, 7, 9],
|
|
19
|
+
chords: [[0, 4, 7, 11], [9, 0, 4, 7], [5, 9, 0, 4], [7, 11, 2, 5]],
|
|
20
|
+
melody: [0, 2, 4, 7, 4, 2, 0, 9],
|
|
21
|
+
mood: "soft",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "steady-launch",
|
|
25
|
+
file: "steady-launch.mp3",
|
|
26
|
+
bpm: 104,
|
|
27
|
+
key: 50,
|
|
28
|
+
scale: [0, 2, 5, 7, 9],
|
|
29
|
+
chords: [[0, 5, 9], [7, 0, 5], [9, 2, 5], [5, 9, 0]],
|
|
30
|
+
melody: [0, 5, 7, 9, 7, 5, 2, 0],
|
|
31
|
+
mood: "drive",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "clean-horizon",
|
|
35
|
+
file: "clean-horizon.mp3",
|
|
36
|
+
bpm: 88,
|
|
37
|
+
key: 55,
|
|
38
|
+
scale: [0, 2, 4, 7, 11],
|
|
39
|
+
chords: [[0, 4, 7], [4, 7, 11], [9, 0, 4], [7, 11, 2]],
|
|
40
|
+
melody: [7, 4, 2, 0, 2, 4, 7, 11],
|
|
41
|
+
mood: "soft",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "bright-sparks",
|
|
45
|
+
file: "bright-sparks.mp3",
|
|
46
|
+
bpm: 118,
|
|
47
|
+
key: 60,
|
|
48
|
+
scale: [0, 2, 4, 7, 9],
|
|
49
|
+
chords: [[0, 4, 7], [5, 9, 0], [7, 11, 2], [4, 7, 11]],
|
|
50
|
+
melody: [0, 4, 7, 9, 7, 4, 2, 4],
|
|
51
|
+
mood: "bounce",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "pixel-bounce",
|
|
55
|
+
file: "pixel-bounce.mp3",
|
|
56
|
+
bpm: 126,
|
|
57
|
+
key: 58,
|
|
58
|
+
scale: [0, 3, 5, 7, 10],
|
|
59
|
+
chords: [[0, 3, 7], [10, 3, 5], [5, 8, 0], [7, 10, 2]],
|
|
60
|
+
melody: [0, 3, 7, 10, 7, 3, 5, 7],
|
|
61
|
+
mood: "chip",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "sunny-loop",
|
|
65
|
+
file: "sunny-loop.mp3",
|
|
66
|
+
bpm: 112,
|
|
67
|
+
key: 62,
|
|
68
|
+
scale: [0, 2, 4, 7, 9],
|
|
69
|
+
chords: [[0, 4, 7], [9, 0, 4], [5, 9, 0], [7, 11, 2]],
|
|
70
|
+
melody: [4, 7, 9, 12, 9, 7, 4, 2],
|
|
71
|
+
mood: "bounce",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "circuit-pulse",
|
|
75
|
+
file: "circuit-pulse.mp3",
|
|
76
|
+
bpm: 124,
|
|
77
|
+
key: 45,
|
|
78
|
+
scale: [0, 2, 3, 7, 10],
|
|
79
|
+
chords: [[0, 3, 7], [7, 10, 2], [3, 7, 10], [10, 2, 5]],
|
|
80
|
+
melody: [0, 7, 3, 10, 2, 7, 3, 0],
|
|
81
|
+
mood: "pulse",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "vector-grid",
|
|
85
|
+
file: "vector-grid.mp3",
|
|
86
|
+
bpm: 116,
|
|
87
|
+
key: 48,
|
|
88
|
+
scale: [0, 2, 5, 7, 10],
|
|
89
|
+
chords: [[0, 5, 10], [7, 0, 5], [10, 2, 7], [5, 10, 0]],
|
|
90
|
+
melody: [0, 2, 5, 7, 10, 7, 5, 2],
|
|
91
|
+
mood: "pulse",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "midnight-protocol",
|
|
95
|
+
file: "midnight-protocol.mp3",
|
|
96
|
+
bpm: 98,
|
|
97
|
+
key: 47,
|
|
98
|
+
scale: [0, 2, 3, 7, 9],
|
|
99
|
+
chords: [[0, 3, 7], [9, 0, 3], [7, 10, 2], [3, 7, 9]],
|
|
100
|
+
melody: [0, 3, 7, 9, 7, 3, 2, 0],
|
|
101
|
+
mood: "drive",
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
function midiToFreq(note) {
|
|
106
|
+
return 440 * 2 ** ((note - 69) / 12);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function triangle(phase) {
|
|
110
|
+
return (2 / Math.PI) * Math.asin(Math.sin(phase));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function env(position, attack, release) {
|
|
114
|
+
if (position < attack) return position / attack;
|
|
115
|
+
return Math.exp(-(position - attack) * release);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderSample(track, t, i) {
|
|
119
|
+
const beat = 60 / track.bpm;
|
|
120
|
+
const bar = beat * 4;
|
|
121
|
+
const chord = track.chords[Math.floor(t / (bar * 2)) % track.chords.length];
|
|
122
|
+
const chordPos = (t % (bar * 2)) / (bar * 2);
|
|
123
|
+
const noteStep = Math.floor(t / (beat / 2)) % track.melody.length;
|
|
124
|
+
const notePos = (t % (beat / 2)) / (beat / 2);
|
|
125
|
+
const root = track.key;
|
|
126
|
+
|
|
127
|
+
let sample = 0;
|
|
128
|
+
|
|
129
|
+
for (let c = 0; c < chord.length; c += 1) {
|
|
130
|
+
const freq = midiToFreq(root + chord[c]);
|
|
131
|
+
const wobble = 1 + Math.sin(TWO_PI * t * 0.08 + c) * 0.002;
|
|
132
|
+
const amp = track.mood === "soft" ? 0.055 : 0.04;
|
|
133
|
+
sample += Math.sin(TWO_PI * freq * wobble * t) * amp * env(chordPos, 0.08, 1.2);
|
|
134
|
+
sample += triangle(TWO_PI * freq * 2 * t) * amp * 0.18;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Sustained sub-bass that swells once per chord — a smooth harmonic floor
|
|
138
|
+
// rather than a per-beat thump. The old per-beat kick/hat read as a disturbing
|
|
139
|
+
// pulse under narration, so these beds are now ambient, not percussive.
|
|
140
|
+
const bassRoot = root - 24 + chord[0];
|
|
141
|
+
sample += Math.sin(TWO_PI * midiToFreq(bassRoot) * t) * 0.11 * env(chordPos, 0.06, 0.9);
|
|
142
|
+
|
|
143
|
+
const melodyNote = root + 12 + track.melody[noteStep];
|
|
144
|
+
const leadWave = track.mood === "chip"
|
|
145
|
+
? Math.sign(Math.sin(TWO_PI * midiToFreq(melodyNote) * t))
|
|
146
|
+
: triangle(TWO_PI * midiToFreq(melodyNote) * t);
|
|
147
|
+
sample += leadWave * 0.06 * env(notePos, 0.03, track.mood === "soft" ? 4 : 7);
|
|
148
|
+
|
|
149
|
+
return Math.max(-0.98, Math.min(0.98, sample));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function writeWav(path, track) {
|
|
153
|
+
const frames = SAMPLE_RATE * DURATION;
|
|
154
|
+
const dataSize = frames * 2 * 2;
|
|
155
|
+
const buffer = Buffer.alloc(44 + dataSize);
|
|
156
|
+
|
|
157
|
+
buffer.write("RIFF", 0);
|
|
158
|
+
buffer.writeUInt32LE(36 + dataSize, 4);
|
|
159
|
+
buffer.write("WAVE", 8);
|
|
160
|
+
buffer.write("fmt ", 12);
|
|
161
|
+
buffer.writeUInt32LE(16, 16);
|
|
162
|
+
buffer.writeUInt16LE(1, 20);
|
|
163
|
+
buffer.writeUInt16LE(2, 22);
|
|
164
|
+
buffer.writeUInt32LE(SAMPLE_RATE, 24);
|
|
165
|
+
buffer.writeUInt32LE(SAMPLE_RATE * 2 * 2, 28);
|
|
166
|
+
buffer.writeUInt16LE(4, 32);
|
|
167
|
+
buffer.writeUInt16LE(16, 34);
|
|
168
|
+
buffer.write("data", 36);
|
|
169
|
+
buffer.writeUInt32LE(dataSize, 40);
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < frames; i += 1) {
|
|
172
|
+
const t = i / SAMPLE_RATE;
|
|
173
|
+
const left = renderSample(track, t, i);
|
|
174
|
+
const right = renderSample(track, t + 0.006, i + 17);
|
|
175
|
+
buffer.writeInt16LE(Math.round(left * 32767), 44 + i * 4);
|
|
176
|
+
buffer.writeInt16LE(Math.round(right * 32767), 44 + i * 4 + 2);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
writeFileSync(path, buffer);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const outDir = dirname(fileURLToPath(import.meta.url));
|
|
183
|
+
|
|
184
|
+
for (const track of TRACKS) {
|
|
185
|
+
const wavPath = join(outDir, `${track.id}.wav`);
|
|
186
|
+
const mp3Path = join(outDir, track.file);
|
|
187
|
+
writeWav(wavPath, track);
|
|
188
|
+
const result = spawnSync(
|
|
189
|
+
"ffmpeg",
|
|
190
|
+
[
|
|
191
|
+
"-y",
|
|
192
|
+
"-hide_banner",
|
|
193
|
+
"-loglevel",
|
|
194
|
+
"error",
|
|
195
|
+
"-i",
|
|
196
|
+
wavPath,
|
|
197
|
+
"-af",
|
|
198
|
+
"loudnorm=I=-21:LRA=10:TP=-1.5",
|
|
199
|
+
"-codec:a",
|
|
200
|
+
"libmp3lame",
|
|
201
|
+
"-b:a",
|
|
202
|
+
"96k",
|
|
203
|
+
mp3Path,
|
|
204
|
+
],
|
|
205
|
+
{ stdio: "inherit" }
|
|
206
|
+
);
|
|
207
|
+
rmSync(wavPath, { force: true });
|
|
208
|
+
if (result.error || result.status !== 0) {
|
|
209
|
+
process.exitCode = result.status || 1;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
console.log(`wrote ${mp3Path}`);
|
|
213
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"file": "calm-keys.mp3",
|
|
4
|
+
"title": "Calm Keys",
|
|
5
|
+
"artist": "ReelMe procedural audio generator",
|
|
6
|
+
"source": "https://github.com/RubenGlez/reelme/blob/main/cli/assets/audio/generate-tracks.mjs#calm-keys",
|
|
7
|
+
"license": "CC0",
|
|
8
|
+
"tones": ["professional"],
|
|
9
|
+
"duration": 32
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"file": "steady-launch.mp3",
|
|
13
|
+
"title": "Steady Launch",
|
|
14
|
+
"artist": "ReelMe procedural audio generator",
|
|
15
|
+
"source": "https://github.com/RubenGlez/reelme/blob/main/cli/assets/audio/generate-tracks.mjs#steady-launch",
|
|
16
|
+
"license": "CC0",
|
|
17
|
+
"tones": ["professional", "technical"],
|
|
18
|
+
"duration": 32
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"file": "clean-horizon.mp3",
|
|
22
|
+
"title": "Clean Horizon",
|
|
23
|
+
"artist": "ReelMe procedural audio generator",
|
|
24
|
+
"source": "https://github.com/RubenGlez/reelme/blob/main/cli/assets/audio/generate-tracks.mjs#clean-horizon",
|
|
25
|
+
"license": "CC0",
|
|
26
|
+
"tones": ["professional"],
|
|
27
|
+
"duration": 32
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"file": "bright-sparks.mp3",
|
|
31
|
+
"title": "Bright Sparks",
|
|
32
|
+
"artist": "ReelMe procedural audio generator",
|
|
33
|
+
"source": "https://github.com/RubenGlez/reelme/blob/main/cli/assets/audio/generate-tracks.mjs#bright-sparks",
|
|
34
|
+
"license": "CC0",
|
|
35
|
+
"tones": ["playful"],
|
|
36
|
+
"duration": 32
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"file": "pixel-bounce.mp3",
|
|
40
|
+
"title": "Pixel Bounce",
|
|
41
|
+
"artist": "ReelMe procedural audio generator",
|
|
42
|
+
"source": "https://github.com/RubenGlez/reelme/blob/main/cli/assets/audio/generate-tracks.mjs#pixel-bounce",
|
|
43
|
+
"license": "CC0",
|
|
44
|
+
"tones": ["playful", "technical"],
|
|
45
|
+
"duration": 32
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"file": "sunny-loop.mp3",
|
|
49
|
+
"title": "Sunny Loop",
|
|
50
|
+
"artist": "ReelMe procedural audio generator",
|
|
51
|
+
"source": "https://github.com/RubenGlez/reelme/blob/main/cli/assets/audio/generate-tracks.mjs#sunny-loop",
|
|
52
|
+
"license": "CC0",
|
|
53
|
+
"tones": ["playful"],
|
|
54
|
+
"duration": 32
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"file": "circuit-pulse.mp3",
|
|
58
|
+
"title": "Circuit Pulse",
|
|
59
|
+
"artist": "ReelMe procedural audio generator",
|
|
60
|
+
"source": "https://github.com/RubenGlez/reelme/blob/main/cli/assets/audio/generate-tracks.mjs#circuit-pulse",
|
|
61
|
+
"license": "CC0",
|
|
62
|
+
"tones": ["technical"],
|
|
63
|
+
"duration": 32
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"file": "vector-grid.mp3",
|
|
67
|
+
"title": "Vector Grid",
|
|
68
|
+
"artist": "ReelMe procedural audio generator",
|
|
69
|
+
"source": "https://github.com/RubenGlez/reelme/blob/main/cli/assets/audio/generate-tracks.mjs#vector-grid",
|
|
70
|
+
"license": "CC0",
|
|
71
|
+
"tones": ["technical"],
|
|
72
|
+
"duration": 32
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"file": "midnight-protocol.mp3",
|
|
76
|
+
"title": "Midnight Protocol",
|
|
77
|
+
"artist": "ReelMe procedural audio generator",
|
|
78
|
+
"source": "https://github.com/RubenGlez/reelme/blob/main/cli/assets/audio/generate-tracks.mjs#midnight-protocol",
|
|
79
|
+
"license": "CC0",
|
|
80
|
+
"tones": ["technical", "professional"],
|
|
81
|
+
"duration": 32
|
|
82
|
+
}
|
|
83
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reelme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Turn your repo into launch videos for social platforms. Local Remotion rendering, zero cloud.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"src",
|
|
11
|
+
"assets",
|
|
11
12
|
"template",
|
|
12
13
|
"!template/out",
|
|
13
14
|
"!template/src/__tests__",
|
package/src/cache.mjs
CHANGED
|
@@ -20,6 +20,7 @@ export const CACHE_ROOT = join(homedir(), ".reelme", "cache");
|
|
|
20
20
|
|
|
21
21
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
22
|
export const TEMPLATE_DIR = join(PKG_ROOT, "template");
|
|
23
|
+
export const AUDIO_DIR = join(PKG_ROOT, "assets", "audio");
|
|
23
24
|
|
|
24
25
|
const CLI_VERSION = JSON.parse(
|
|
25
26
|
readFileSync(join(PKG_ROOT, "package.json"), "utf8")
|
package/src/render.mjs
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
// variant per social platform when the brief has a teaser cut. Outputs land
|
|
3
3
|
// in <repo>/reelme-out/; the heavy Remotion project stays in the cache.
|
|
4
4
|
|
|
5
|
-
import { cpSync, mkdirSync } from "node:fs";
|
|
6
|
-
import { basename, join } from "node:path";
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
6
|
+
import { basename, dirname, join } from "node:path";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { ensureScaffold, loadPlatforms, readBrief, fail } from "./cache.mjs";
|
|
8
|
+
import { AUDIO_DIR, ensureScaffold, loadPlatforms, readBrief, fail } from "./cache.mjs";
|
|
9
9
|
|
|
10
10
|
function renderComposition(cacheDir, compositionId, outFile, codec) {
|
|
11
11
|
const args = ["exec", "remotion", "render", compositionId, join("out", outFile)];
|
|
12
|
-
|
|
12
|
+
// Render gifs at 0.6 scale (e.g. 1920x1080 -> 1152x648): a README gif needs no
|
|
13
|
+
// more, and fewer pixels roughly halves the file size. Layout and timing are
|
|
14
|
+
// unchanged — --scale downscales the output, not the composition coordinates.
|
|
15
|
+
if (codec === "gif") args.push("--codec=gif", "--scale=0.6");
|
|
13
16
|
console.log(`reelme: rendering ${compositionId} → reelme-out/${outFile}`);
|
|
14
17
|
const result = spawnSync("pnpm", args, { cwd: cacheDir, stdio: "inherit" });
|
|
15
18
|
if (result.error || result.status !== 0) {
|
|
@@ -17,6 +20,90 @@ function renderComposition(cacheDir, compositionId, outFile, codec) {
|
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
function collectAssets(brief) {
|
|
24
|
+
const assets = [];
|
|
25
|
+
const cuts = [brief.cuts.main, brief.cuts.vertical, brief.cuts.teaser].filter(Boolean);
|
|
26
|
+
for (const cut of cuts) {
|
|
27
|
+
for (const scene of cut) {
|
|
28
|
+
if (scene.type === "clip" && scene.src) assets.push(scene.src);
|
|
29
|
+
if (scene.type === "mobile" && scene.screenshot) assets.push(scene.screenshot);
|
|
30
|
+
if (scene.type === "browser" && scene.image) assets.push(scene.image);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return [...new Set(assets)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function copyAssets(repoRoot, cacheDir, brief) {
|
|
37
|
+
const assets = collectAssets(brief);
|
|
38
|
+
if (assets.length === 0) return;
|
|
39
|
+
const publicDir = join(cacheDir, "public");
|
|
40
|
+
mkdirSync(publicDir, { recursive: true });
|
|
41
|
+
const missing = assets.filter((a) => !existsSync(join(repoRoot, a)));
|
|
42
|
+
if (missing.length > 0) {
|
|
43
|
+
fail(`missing asset file(s):\n${missing.map((a) => ` ${join(repoRoot, a)}`).join("\n")}`);
|
|
44
|
+
}
|
|
45
|
+
for (const asset of assets) {
|
|
46
|
+
const dest = join(publicDir, asset);
|
|
47
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
48
|
+
cpSync(join(repoRoot, asset), dest);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadAudioManifest() {
|
|
53
|
+
const manifestPath = join(AUDIO_DIR, "manifest.json");
|
|
54
|
+
if (!existsSync(manifestPath)) {
|
|
55
|
+
fail(`audio manifest is missing from the package: ${manifestPath}`);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
59
|
+
if (!Array.isArray(manifest)) throw new Error("manifest must be an array");
|
|
60
|
+
return manifest;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
fail(`audio manifest is invalid: ${e.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function copyAudioTrack(cacheDir, brief) {
|
|
67
|
+
const audio = brief.project?.audio;
|
|
68
|
+
if (!audio) return;
|
|
69
|
+
|
|
70
|
+
const track = audio.track;
|
|
71
|
+
if (typeof track !== "string" || track.length === 0 || basename(track) !== track) {
|
|
72
|
+
fail(`project.audio.track must be a filename from the bundled audio manifest.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const manifest = loadAudioManifest();
|
|
76
|
+
const validTracks = manifest.map((entry) => entry.file).filter(Boolean);
|
|
77
|
+
const entry = manifest.find((item) => item.file === track);
|
|
78
|
+
if (!entry) {
|
|
79
|
+
fail(
|
|
80
|
+
`unknown audio track "${track}". Valid tracks: ${validTracks.join(", ")}.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const source = join(AUDIO_DIR, track);
|
|
85
|
+
if (!existsSync(source)) {
|
|
86
|
+
fail(`bundled audio track "${track}" is missing from ${AUDIO_DIR}.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const audioDir = join(cacheDir, "public", "audio");
|
|
90
|
+
mkdirSync(audioDir, { recursive: true });
|
|
91
|
+
cpSync(source, join(audioDir, track));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function copyLogo(repoRoot, cacheDir, brief) {
|
|
95
|
+
const logo = brief.project?.logo;
|
|
96
|
+
if (!logo) return;
|
|
97
|
+
const source = join(repoRoot, logo);
|
|
98
|
+
if (!existsSync(source)) {
|
|
99
|
+
fail(`project.logo file not found: ${source}`);
|
|
100
|
+
}
|
|
101
|
+
const publicDir = join(cacheDir, "public");
|
|
102
|
+
const dest = join(publicDir, logo);
|
|
103
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
104
|
+
cpSync(source, dest);
|
|
105
|
+
}
|
|
106
|
+
|
|
20
107
|
export function render(repoRoot) {
|
|
21
108
|
const brief = readBrief(repoRoot);
|
|
22
109
|
const platforms = loadPlatforms();
|
|
@@ -24,6 +111,10 @@ export function render(repoRoot) {
|
|
|
24
111
|
const outDir = join(repoRoot, "reelme-out");
|
|
25
112
|
mkdirSync(outDir, { recursive: true });
|
|
26
113
|
|
|
114
|
+
copyAssets(repoRoot, cacheDir, brief);
|
|
115
|
+
copyAudioTrack(cacheDir, brief);
|
|
116
|
+
copyLogo(repoRoot, cacheDir, brief);
|
|
117
|
+
|
|
27
118
|
const verticalFallback = brief.project.platforms.filter(
|
|
28
119
|
(id) => platforms[id].cut === "vertical" && !brief.cuts.vertical?.length
|
|
29
120
|
);
|
package/template/package.json
CHANGED
package/template/pnpm-lock.yaml
CHANGED
|
@@ -11,6 +11,9 @@ importers:
|
|
|
11
11
|
'@remotion/cli':
|
|
12
12
|
specifier: 4.0.290
|
|
13
13
|
version: 4.0.290(postcss@8.5.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
14
|
+
'@remotion/gif':
|
|
15
|
+
specifier: 4.0.290
|
|
16
|
+
version: 4.0.290(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
14
17
|
'@remotion/google-fonts':
|
|
15
18
|
specifier: 4.0.290
|
|
16
19
|
version: 4.0.290(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
@@ -506,6 +509,12 @@ packages:
|
|
|
506
509
|
cpu: [x64]
|
|
507
510
|
os: [win32]
|
|
508
511
|
|
|
512
|
+
'@remotion/gif@4.0.290':
|
|
513
|
+
resolution: {integrity: sha512-oDQvgbyj0lRCh7BFgtBltCRnlLcwhpwlxVfb0U5YOpg75eV3cybbicvokHiCztbRRtoQ8tP7TUF2s54mDge3hA==}
|
|
514
|
+
peerDependencies:
|
|
515
|
+
react: '>=16.8.0'
|
|
516
|
+
react-dom: '>=16.8.0'
|
|
517
|
+
|
|
509
518
|
'@remotion/google-fonts@4.0.290':
|
|
510
519
|
resolution: {integrity: sha512-/bOAsxiCORmQLqjENuZytyYegb/Ih96dkHvaHU7t7zBoK77bVjP1HC6EwFIUfI7OBAyw6uCD9hc3ZAtLiDKa0g==}
|
|
511
520
|
|
|
@@ -2197,6 +2206,12 @@ snapshots:
|
|
|
2197
2206
|
'@remotion/compositor-win32-x64-msvc@4.0.290':
|
|
2198
2207
|
optional: true
|
|
2199
2208
|
|
|
2209
|
+
'@remotion/gif@4.0.290(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
|
2210
|
+
dependencies:
|
|
2211
|
+
react: 18.3.1
|
|
2212
|
+
react-dom: 18.3.1(react@18.3.1)
|
|
2213
|
+
remotion: 4.0.290(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
2214
|
+
|
|
2200
2215
|
'@remotion/google-fonts@4.0.290(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
|
2201
2216
|
dependencies:
|
|
2202
2217
|
remotion: 4.0.290(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|