Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/phase6-full-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ on:
options:
- 16x9-logo
- 16x9-clean
- 4k-logo
- 4k-clean
max_seconds:
description: Clip duration cap in seconds (0 = full song)
required: false
Expand Down Expand Up @@ -76,6 +78,10 @@ jobs:

if [ "$RENDER_PRESET" = "16x9-clean" ]; then
npm run phase5:render:16x9 -- --lyrics=data/phase3-lyrics.json --out=output/phase6-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
elif [ "$RENDER_PRESET" = "4k-clean" ]; then
npm run phase5:render:4k -- --lyrics=data/phase3-lyrics.json --out=output/phase6-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
elif [ "$RENDER_PRESET" = "4k-logo" ]; then
npm run phase5:render:4k:logo -- --lyrics=data/phase3-lyrics.json --out=output/phase6-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
else
npm run phase5:render:16x9:logo -- --lyrics=data/phase3-lyrics.json --out=output/phase6-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
fi
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/phase7-youtube-upload.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ on:
options:
- 16x9-logo
- 16x9-clean
- 4k-logo
- 4k-clean
max_seconds:
description: Clip duration cap (0 means full song)
required: false
Expand Down Expand Up @@ -100,6 +102,10 @@ jobs:

if [ "$RENDER_PRESET" = "16x9-clean" ]; then
npm run phase5:render:16x9 -- --lyrics=data/phase3-lyrics.json --out=output/phase7-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
elif [ "$RENDER_PRESET" = "4k-clean" ]; then
npm run phase5:render:4k -- --lyrics=data/phase3-lyrics.json --out=output/phase7-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
elif [ "$RENDER_PRESET" = "4k-logo" ]; then
npm run phase5:render:4k:logo -- --lyrics=data/phase3-lyrics.json --out=output/phase7-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
else
npm run phase5:render:16x9:logo -- --lyrics=data/phase3-lyrics.json --out=output/phase7-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
fi
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/test-render.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ on:
options:
- 16x9-logo
- 16x9-clean
- 4k-logo
- 4k-clean
max_seconds:
description: Clip duration cap in seconds
required: false
Expand Down Expand Up @@ -41,7 +43,7 @@ jobs:
- name: Build test lyrics input
run: npm run phase3:build -- --song-id=TEST_TEMPLATE --query="template test render" --out=data/test-lyrics.json

- name: Render test video (16:9)
- name: Render test video
env:
RENDER_PRESET: ${{ inputs.render_preset }}
PHASE5_MAX_SECONDS: ${{ inputs.max_seconds }}
Expand All @@ -53,6 +55,10 @@ jobs:

if [ "$RENDER_PRESET" = "16x9-clean" ]; then
npm run phase5:render:16x9 -- --lyrics=data/test-lyrics.json --out=output/test-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
elif [ "$RENDER_PRESET" = "4k-clean" ]; then
npm run phase5:render:4k -- --lyrics=data/test-lyrics.json --out=output/test-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
elif [ "$RENDER_PRESET" = "4k-logo" ]; then
npm run phase5:render:4k:logo -- --lyrics=data/test-lyrics.json --out=output/test-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
else
npm run phase5:render:16x9:logo -- --lyrics=data/test-lyrics.json --out=output/test-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
fi
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"phase5:render": "node scripts/phase5-render.js",
"phase5:render:16x9": "node scripts/phase5-render-16x9.js",
"phase5:render:16x9:logo": "node scripts/phase5-render-16x9-logo.js",
"phase5:render:4k": "node scripts/phase5-render-4k.js",
"phase5:render:4k:logo": "node scripts/phase5-render-4k-logo.js",
"phase7:upload": "node scripts/phase7-upload.js",
"render:test": "node scripts/render-test.js",
"remotion:studio": "remotion studio remotion/index.jsx"
Expand Down
32 changes: 27 additions & 5 deletions remotion/Root.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import templateSongData from './template-song-data.json';
const FPS = 30;
const WIDTH_16X9 = 1920;
const HEIGHT_16X9 = 1080;
const WIDTH_4K = 3840;
const HEIGHT_4K = 2160;
const DEFAULT_DURATION_SECONDS = Number(templateSongData.song?.duration || 205);
const DEFAULT_DURATION_FRAMES = Math.max(1, Math.round(DEFAULT_DURATION_SECONDS * FPS));

const defaultProps16x9 = {
const defaultPropsClean = {
songData: templateSongData,
};

const defaultProps16x9Logo = {
const defaultPropsLogo = {
songData: templateSongData,
showAnimatedLogo: true,
logoSrc: staticFile('assets/DTunes-transparent.svg'),
Expand All @@ -29,7 +31,7 @@ export const RemotionRoot = () => {
fps={FPS}
width={WIDTH_16X9}
height={HEIGHT_16X9}
defaultProps={defaultProps16x9Logo}
defaultProps={defaultPropsLogo}
/>

<Composition
Expand All @@ -39,7 +41,7 @@ export const RemotionRoot = () => {
fps={FPS}
width={WIDTH_16X9}
height={HEIGHT_16X9}
defaultProps={defaultProps16x9}
defaultProps={defaultPropsClean}
/>

<Composition
Expand All @@ -49,7 +51,27 @@ export const RemotionRoot = () => {
fps={FPS}
width={WIDTH_16X9}
height={HEIGHT_16X9}
defaultProps={defaultProps16x9Logo}
defaultProps={defaultPropsLogo}
/>

<Composition
id="LyricsTemplateVideo4k"
component={LyricsTemplateVideo}
durationInFrames={DEFAULT_DURATION_FRAMES}
fps={FPS}
width={WIDTH_4K}
height={HEIGHT_4K}
defaultProps={defaultPropsClean}
/>

<Composition
id="LyricsTemplateVideo4kLogo"
component={LyricsTemplateVideo}
durationInFrames={DEFAULT_DURATION_FRAMES}
fps={FPS}
width={WIDTH_4K}
height={HEIGHT_4K}
defaultProps={defaultPropsLogo}
/>
</>
);
Expand Down
60 changes: 60 additions & 0 deletions scripts/phase5-render-4k-logo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env node
'use strict';

const path = require('node:path');

const {runPhase5Render} = require('./phase5-render');

const PROJECT_ROOT = path.resolve(__dirname, '..');

const parsePositiveNumber = (value) => {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 0) {
return 0;
}
return parsed;
};

const parseArgs = (argv) => {
const args = {
lyrics: path.join(PROJECT_ROOT, 'data', 'phase3-lyrics.json'),
out: path.join(PROJECT_ROOT, 'output', 'phase5-video-4k-logo.mp4'),
maxSeconds: parsePositiveNumber(process.env.PHASE5_MAX_SECONDS || '0'),
};

for (const arg of argv) {
if (arg.startsWith('--lyrics=')) {
const value = arg.slice('--lyrics='.length).trim();
args.lyrics = path.isAbsolute(value) ? value : path.join(PROJECT_ROOT, value);
} else if (arg.startsWith('--out=')) {
const value = arg.slice('--out='.length).trim();
args.out = path.isAbsolute(value) ? value : path.join(PROJECT_ROOT, value);
} else if (arg.startsWith('--max-seconds=')) {
args.maxSeconds = parsePositiveNumber(arg.slice('--max-seconds='.length).trim());
}
}

return args;
};

const main = async () => {
const args = parseArgs(process.argv.slice(2));

const result = await runPhase5Render({
...args,
compositionId: 'LyricsTemplateVideo4kLogo',
});

console.log('[phase5-4k-logo] Render complete');
console.log(`[phase5-4k-logo] Output: ${path.relative(PROJECT_ROOT, result.out)}`);
console.log(
`[phase5-4k-logo] Video: ${result.width}x${result.height} @ ${result.fps}fps, ${result.durationInSeconds.toFixed(2)}s (${result.durationInFrames} frames)`,
);
};

if (require.main === module) {
main().catch((error) => {
console.error(`[phase5-4k-logo] Failed: ${error.message || error}`);
process.exit(1);
});
}
60 changes: 60 additions & 0 deletions scripts/phase5-render-4k.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env node
'use strict';

const path = require('node:path');

const {runPhase5Render} = require('./phase5-render');

const PROJECT_ROOT = path.resolve(__dirname, '..');

const parsePositiveNumber = (value) => {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 0) {
return 0;
}
return parsed;
};

const parseArgs = (argv) => {
const args = {
lyrics: path.join(PROJECT_ROOT, 'data', 'phase3-lyrics.json'),
out: path.join(PROJECT_ROOT, 'output', 'phase5-video-4k.mp4'),
maxSeconds: parsePositiveNumber(process.env.PHASE5_MAX_SECONDS || '0'),
};

for (const arg of argv) {
if (arg.startsWith('--lyrics=')) {
const value = arg.slice('--lyrics='.length).trim();
args.lyrics = path.isAbsolute(value) ? value : path.join(PROJECT_ROOT, value);
} else if (arg.startsWith('--out=')) {
const value = arg.slice('--out='.length).trim();
args.out = path.isAbsolute(value) ? value : path.join(PROJECT_ROOT, value);
} else if (arg.startsWith('--max-seconds=')) {
args.maxSeconds = parsePositiveNumber(arg.slice('--max-seconds='.length).trim());
}
}

return args;
};

const main = async () => {
const args = parseArgs(process.argv.slice(2));

const result = await runPhase5Render({
...args,
compositionId: 'LyricsTemplateVideo4k',
});

console.log('[phase5-4k] Render complete');
console.log(`[phase5-4k] Output: ${path.relative(PROJECT_ROOT, result.out)}`);
console.log(
`[phase5-4k] Video: ${result.width}x${result.height} @ ${result.fps}fps, ${result.durationInSeconds.toFixed(2)}s (${result.durationInFrames} frames)`,
);
};

if (require.main === module) {
main().catch((error) => {
console.error(`[phase5-4k] Failed: ${error.message || error}`);
process.exit(1);
});
}