Skip to content

Commit f0eefb6

Browse files
author
Miriad
committed
fix(remotion): address PR #592 review feedback
- Use useVideoConfig() instead of hardcoded dimensions in HookScene - Reduce DotGrid to ~48 dots for Lambda performance - Add word chunking for narrations >60 words - Add missing extrapolateLeft: "clamp" for consistency - Remove unused imports (FPS in Scene, fps in GlowSweeps) - Extract shared VideoComposition component - Replace execSync("rm -rf") with fs.rmSync()
1 parent bcb829d commit f0eefb6

File tree

6 files changed

+206
-295
lines changed

6 files changed

+206
-295
lines changed

lib/services/ffmpeg-compress.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import { execFileSync, execSync } from "child_process";
11-
import { writeFileSync, readFileSync, unlinkSync, mkdtempSync } from "fs";
11+
import { writeFileSync, readFileSync, unlinkSync, mkdtempSync, rmSync } from "fs";
1212
import { join } from "path";
1313
import { tmpdir } from "os";
1414

@@ -133,7 +133,7 @@ function makeTempDir() {
133133
}
134134
try {
135135
// Remove the temp directory itself
136-
execSync(`rm -rf "${dir}"`, { stdio: "ignore" });
136+
rmSync(dir, { recursive: true, force: true });
137137
} catch {
138138
/* ignore */
139139
}

remotion/components/HookScene.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,18 @@ const DotGrid: React.FC<{
4040
durationInFrames: number;
4141
isVertical: boolean;
4242
}> = ({ frame, durationInFrames, isVertical }) => {
43-
const cols = isVertical ? 10 : 16;
44-
const rows = isVertical ? 18 : 10;
45-
const dotSpacingX = isVertical ? 108 : 120;
46-
const dotSpacingY = isVertical ? 107 : 108;
43+
const cols = isVertical ? 6 : 8;
44+
const rows = isVertical ? 8 : 6;
45+
const dotSpacingX = isVertical ? 180 : 240;
46+
const dotSpacingY = isVertical ? 240 : 180;
4747

4848
// Slow drift of the entire grid
4949
const driftX = interpolate(frame, [0, durationInFrames], [0, 30], {
50+
extrapolateLeft: "clamp",
5051
extrapolateRight: "clamp",
5152
});
5253
const driftY = interpolate(frame, [0, durationInFrames], [0, -20], {
54+
extrapolateLeft: "clamp",
5355
extrapolateRight: "clamp",
5456
});
5557

@@ -92,11 +94,13 @@ const Particles: React.FC<{
9294
frame: number;
9395
durationInFrames: number;
9496
isVertical: boolean;
95-
}> = ({ frame, durationInFrames, isVertical }) => {
97+
width: number;
98+
height: number;
99+
}> = ({ frame, durationInFrames, isVertical, width, height }) => {
96100
const count = 18;
97101
const particles: React.ReactNode[] = [];
98-
const baseWidth = isVertical ? 1080 : 1920;
99-
const baseHeight = isVertical ? 1920 : 1080;
102+
const baseWidth = width;
103+
const baseHeight = height;
100104

101105
for (let i = 0; i < count; i++) {
102106
const startX = seededRandom(i * 7 + 1) * baseWidth;
@@ -145,11 +149,12 @@ const Particles: React.FC<{
145149
/** Glowing sweep lines that cross the screen before text appears */
146150
const GlowSweeps: React.FC<{
147151
frame: number;
148-
fps: number;
149152
isVertical: boolean;
150-
}> = ({ frame, fps, isVertical }) => {
151-
const screenW = isVertical ? 1080 : 1920;
152-
const screenH = isVertical ? 1920 : 1080;
153+
width: number;
154+
height: number;
155+
}> = ({ frame, isVertical, width, height }) => {
156+
const screenW = width;
157+
const screenH = height;
153158

154159
// Line 1: horizontal sweep at ~frame 5-25
155160
const sweep1Progress = interpolate(frame, [5, 25], [0, 1], {
@@ -241,16 +246,18 @@ export const HookScene: React.FC<HookSceneProps> = ({
241246
isVertical = false,
242247
}) => {
243248
const frame = useCurrentFrame();
244-
const { fps } = useVideoConfig();
249+
const { fps, width, height } = useVideoConfig();
245250
const fonts = isVertical ? FONT_SIZES.portrait : FONT_SIZES.landscape;
246251

247252
// ── Dynamic gradient background ──────────────────────────────────────────
248253
// Rotate hue and shift gradient angle over the scene
249254
const gradientAngle = interpolate(frame, [0, durationInFrames], [135, 315], {
255+
extrapolateLeft: "clamp",
250256
extrapolateRight: "clamp",
251257
});
252258
// We'll use CSS hue-rotate for dynamic color shifting
253259
const hueShift = interpolate(frame, [0, durationInFrames], [0, 30], {
260+
extrapolateLeft: "clamp",
254261
extrapolateRight: "clamp",
255262
});
256263

@@ -342,11 +349,13 @@ export const HookScene: React.FC<HookSceneProps> = ({
342349

343350
// ── Decorative circles (enhanced) ────────────────────────────────────────
344351
const circle1Scale = interpolate(frame, [0, 60], [0.3, 1.3], {
352+
extrapolateLeft: "clamp",
345353
extrapolateRight: "clamp",
346354
});
347355
const circle1Pulse = Math.sin(frame * 0.05) * 0.1;
348356

349357
const circle2Scale = interpolate(frame, [10, 70], [0.2, 1.1], {
358+
extrapolateLeft: "clamp",
350359
extrapolateRight: "clamp",
351360
});
352361
const circle2Pulse = Math.sin(frame * 0.04 + 1) * 0.08;
@@ -366,6 +375,7 @@ export const HookScene: React.FC<HookSceneProps> = ({
366375
style={{
367376
background: `radial-gradient(ellipse at ${isVertical ? "50% 40%" : "30% 50%"}, ${COLORS.primary}33 0%, transparent 70%)`,
368377
opacity: interpolate(frame, [0, 40], [0, 0.8], {
378+
extrapolateLeft: "clamp",
369379
extrapolateRight: "clamp",
370380
}),
371381
}}
@@ -383,10 +393,12 @@ export const HookScene: React.FC<HookSceneProps> = ({
383393
frame={frame}
384394
durationInFrames={durationInFrames}
385395
isVertical={isVertical}
396+
width={width}
397+
height={height}
386398
/>
387399

388400
{/* ── Glow sweep lines ── */}
389-
<GlowSweeps frame={frame} fps={fps} isVertical={isVertical} />
401+
<GlowSweeps frame={frame} isVertical={isVertical} width={width} height={height} />
390402

391403
{/* ── Decorative circles ── */}
392404
<div

remotion/components/Scene.tsx

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
useVideoConfig,
99
} from "remotion";
1010
import type { SceneProps } from "../types";
11-
import { ANIMATION, COLORS, FONT_SIZES, FPS } from "../constants";
11+
import { ANIMATION, COLORS, FONT_SIZES } from "../constants";
12+
13+
/** When narration exceeds this many words, animate in chunks of 3 instead of per-word */
14+
const WORD_CHUNK_THRESHOLD = 60;
15+
const CHUNK_SIZE = 3;
1216

1317
export const Scene: React.FC<SceneProps> = ({
1418
narration,
@@ -96,12 +100,22 @@ export const Scene: React.FC<SceneProps> = ({
96100
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
97101
);
98102

99-
// --- Word-by-word reveal ---
103+
// --- Word-by-word reveal (with chunking for long narrations) ---
100104
const words = narration.split(/\s+/).filter(Boolean);
101105
const totalWords = words.length;
102-
// Spread word reveals across the available time (leave some buffer at end)
103-
const revealWindow = Math.max(durationInFrames * 0.65, ANIMATION.fadeIn + totalWords * 2);
104-
const framesPerWord = Math.max(2, (revealWindow - ANIMATION.fadeIn * 0.5) / totalWords);
106+
const useChunking = totalWords > WORD_CHUNK_THRESHOLD;
107+
108+
// Build display units: either individual words or chunks of CHUNK_SIZE
109+
const displayUnits: string[] = useChunking
110+
? Array.from({ length: Math.ceil(totalWords / CHUNK_SIZE) }, (_, i) =>
111+
words.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE).join(" ")
112+
)
113+
: words;
114+
const totalUnits = displayUnits.length;
115+
116+
// Spread reveals across the available time (leave some buffer at end)
117+
const revealWindow = Math.max(durationInFrames * 0.65, ANIMATION.fadeIn + totalUnits * 2);
118+
const framesPerUnit = Math.max(2, (revealWindow - ANIMATION.fadeIn * 0.5) / totalUnits);
105119

106120
// Alternating gradient directions for visual variety
107121
const gradientAngle = (sceneIndex % 4) * 90;
@@ -247,36 +261,36 @@ export const Scene: React.FC<SceneProps> = ({
247261
gap: "0 0.35em",
248262
}}
249263
>
250-
{words.map((word, i) => {
251-
const wordStartFrame = ANIMATION.fadeIn * 0.3 + i * framesPerWord;
252-
const wordOpacity = interpolate(
264+
{displayUnits.map((unit, i) => {
265+
const unitStartFrame = ANIMATION.fadeIn * 0.3 + i * framesPerUnit;
266+
const unitOpacity = interpolate(
253267
frame,
254268
[
255-
wordStartFrame,
256-
wordStartFrame + 6,
269+
unitStartFrame,
270+
unitStartFrame + 6,
257271
durationInFrames - ANIMATION.fadeOut,
258272
durationInFrames,
259273
],
260274
[0, 1, 1, 0],
261275
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
262276
);
263-
const wordScale = interpolate(
277+
const unitScale = interpolate(
264278
frame,
265-
[wordStartFrame, wordStartFrame + 6],
279+
[unitStartFrame, unitStartFrame + 6],
266280
[0.88, 1],
267281
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
268282
);
269283
return (
270284
<span
271-
key={`${i}-${word}`}
285+
key={`${i}-${unit}`}
272286
style={{
273-
opacity: wordOpacity,
274-
transform: `scale(${wordScale})`,
287+
opacity: unitOpacity,
288+
transform: `scale(${unitScale})`,
275289
display: "inline-block",
276290
willChange: "opacity, transform",
277291
}}
278292
>
279-
{word}
293+
{unit}
280294
</span>
281295
);
282296
})}
Lines changed: 3 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,7 @@
11
import React from "react";
2-
import { AbsoluteFill, Audio, Sequence } from "remotion";
32
import type { VideoInputProps } from "../types";
4-
import {
5-
DEFAULT_CTA_DURATION,
6-
DEFAULT_HOOK_DURATION,
7-
DEFAULT_SPONSOR_DURATION,
8-
FPS,
9-
SPONSOR_INSERT_SECONDS,
10-
TRANSITION_DURATION,
11-
} from "../constants";
12-
import { HookScene } from "../components/HookScene";
13-
import { Scene } from "../components/Scene";
14-
import { CTAScene } from "../components/CTAScene";
15-
import { SponsorSlot } from "../components/SponsorSlot";
3+
import { VideoComposition } from "./VideoComposition";
164

17-
export const MainVideo: React.FC<VideoInputProps> = ({
18-
audioUrl,
19-
audioDurationInSeconds,
20-
hook,
21-
scenes,
22-
cta,
23-
sponsor,
24-
hookDurationInFrames,
25-
ctaDurationInFrames,
26-
sponsorDurationInFrames,
27-
}) => {
28-
const hookDuration = hookDurationInFrames ?? DEFAULT_HOOK_DURATION;
29-
const ctaDuration = ctaDurationInFrames ?? DEFAULT_CTA_DURATION;
30-
const sponsorDuration = sponsor
31-
? (sponsorDurationInFrames ?? DEFAULT_SPONSOR_DURATION)
32-
: 0;
33-
34-
const totalFrames = Math.ceil(audioDurationInSeconds * FPS);
35-
36-
// Calculate available frames for content scenes.
37-
// With cross-fade overlap, each transition saves TRANSITION_DURATION frames.
38-
// Transitions: hook→scene1, scene1→scene2, ..., sceneN→CTA
39-
// Total transitions = scenes.length + 1 (hook-to-first-scene + between-scenes + last-scene-to-CTA)
40-
const numTransitions = scenes.length + 1;
41-
const overlapSavings = numTransitions * TRANSITION_DURATION;
42-
const scenesAvailableFrames = totalFrames - hookDuration - ctaDuration + overlapSavings;
43-
44-
// Distribute scene frames evenly across all scenes
45-
const perSceneFrames = Math.max(
46-
FPS, // minimum 1 second per scene
47-
Math.floor(scenesAvailableFrames / scenes.length)
48-
);
49-
50-
// Sponsor insertion point in frames (~15s mark)
51-
const sponsorInsertFrame = SPONSOR_INSERT_SECONDS * FPS;
52-
53-
// --- Build timeline with cross-fade overlaps ---
54-
// Each scene starts TRANSITION_DURATION frames before the previous one ends.
55-
// Hook starts at 0.
56-
const hookStart = 0;
57-
58-
// First content scene overlaps with the end of the hook
59-
const firstSceneStart = hookStart + hookDuration - TRANSITION_DURATION;
60-
61-
// Calculate scene start positions
62-
const sceneStarts = scenes.map((_, index) =>
63-
firstSceneStart + index * (perSceneFrames - TRANSITION_DURATION)
64-
);
65-
66-
// CTA overlaps with the end of the last content scene
67-
const lastSceneEnd = sceneStarts.length > 0
68-
? sceneStarts[sceneStarts.length - 1] + perSceneFrames
69-
: hookStart + hookDuration;
70-
const ctaStart = lastSceneEnd - TRANSITION_DURATION;
71-
72-
return (
73-
<AbsoluteFill style={{ backgroundColor: "#000" }}>
74-
{/* Audio track — plays for the entire composition */}
75-
<Audio src={audioUrl} />
76-
77-
{/* Hook Scene */}
78-
<Sequence
79-
from={hookStart}
80-
durationInFrames={hookDuration}
81-
name="Hook"
82-
>
83-
<HookScene
84-
hook={hook}
85-
durationInFrames={hookDuration}
86-
isVertical={false}
87-
/>
88-
</Sequence>
89-
90-
{/* Content Scenes — with cross-fade overlap */}
91-
{scenes.map((scene, index) => (
92-
<Sequence
93-
key={`scene-${index}`}
94-
from={sceneStarts[index]}
95-
durationInFrames={perSceneFrames}
96-
name={`Scene ${index + 1}`}
97-
>
98-
<Scene
99-
narration={scene.narration}
100-
bRollUrl={scene.bRollUrl}
101-
visualDescription={scene.visualDescription}
102-
sceneIndex={index}
103-
durationInFrames={perSceneFrames}
104-
isVertical={false}
105-
/>
106-
</Sequence>
107-
))}
108-
109-
{/* Sponsor Slot — overlaid at ~15s mark if sponsor data exists */}
110-
{sponsor && sponsorDuration > 0 && (
111-
<Sequence
112-
from={sponsorInsertFrame}
113-
durationInFrames={sponsorDuration}
114-
name="Sponsor"
115-
>
116-
<SponsorSlot
117-
sponsor={sponsor}
118-
durationInFrames={sponsorDuration}
119-
isVertical={false}
120-
/>
121-
</Sequence>
122-
)}
123-
124-
{/* CTA Scene — cross-fades in from last content scene */}
125-
<Sequence
126-
from={ctaStart}
127-
durationInFrames={ctaDuration}
128-
name="CTA"
129-
>
130-
<CTAScene
131-
cta={cta}
132-
durationInFrames={ctaDuration}
133-
isVertical={false}
134-
/>
135-
</Sequence>
136-
</AbsoluteFill>
137-
);
5+
export const MainVideo: React.FC<VideoInputProps> = (props) => {
6+
return <VideoComposition {...props} isVertical={false} />;
1387
};

0 commit comments

Comments
 (0)