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
14 changes: 11 additions & 3 deletions .github/workflows/phase6-full-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ on:
workflow_dispatch:
inputs:
song_query:
description: Song query (used only when song_id is empty)
description: Song query (leave empty to auto-pick a trending query)
required: false
default: Aaj Ki Raat
default: ''
type: string
song_id:
description: Optional direct JioSaavn song ID
Expand All @@ -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 @@ -59,8 +61,10 @@ jobs:
echo "[phase-6] Starting Phase 3"
if [ -n "$SONG_ID" ]; then
npm run phase3:build -- --song-id="$SONG_ID" --out=data/phase3-lyrics.json
else
elif [ -n "$SONG_QUERY" ]; then
npm run phase3:build -- --query="$SONG_QUERY" --out=data/phase3-lyrics.json
else
npm run phase3:build -- --auto-trending --out=data/phase3-lyrics.json
fi

- name: Phase 5 - Render template lyrics video
Expand All @@ -76,6 +80,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
14 changes: 11 additions & 3 deletions .github/workflows/phase7-youtube-upload.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ on:
workflow_dispatch:
inputs:
song_query:
description: Song query (used only when song_id is empty)
description: Song query (leave empty to auto-pick a trending query)
required: false
default: Aaj Ki Raat
default: ''
type: string
song_id:
description: Optional direct JioSaavn song ID
Expand All @@ -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 @@ -83,8 +85,10 @@ jobs:
echo "[phase-7] Starting Phase 3"
if [ -n "$SONG_ID" ]; then
npm run phase3:build -- --song-id="$SONG_ID" --out=data/phase3-lyrics.json
else
elif [ -n "$SONG_QUERY" ]; then
npm run phase3:build -- --query="$SONG_QUERY" --out=data/phase3-lyrics.json
else
npm run phase3:build -- --auto-trending --out=data/phase3-lyrics.json
fi

- name: Phase 5 - Render video
Expand All @@ -100,6 +104,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
6 changes: 6 additions & 0 deletions .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 @@ -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 defaultPropsWithLogo = {
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={defaultPropsWithLogo}
/>

<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={defaultPropsWithLogo}
/>

<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={defaultPropsWithLogo}
/>
</>
);
Expand Down
69 changes: 62 additions & 7 deletions scripts/phase3-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ const path = require('node:path');
const templateSongData = require('../remotion/template-song-data.json');

const PROJECT_ROOT = path.resolve(__dirname, '..');
const TRENDING_FALLBACK_QUERY = String(process.env.PHASE3_TRENDING_FALLBACK_QUERY || 'Aaj Ki Raat').trim() || 'Aaj Ki Raat';
const TRENDING_FETCH_TIMEOUT_MS = Number(process.env.PHASE3_TRENDING_FETCH_TIMEOUT_MS || 5000);
const APPLE_MUSIC_TRENDING_URL = 'https://rss.applemarketingtools.com/api/v2/in/music/most-played/25/songs.json';

const parseArgs = (argv) => {
const args = {
out: path.join(PROJECT_ROOT, 'data', 'phase3-lyrics.json'),
songId: '',
query: '',
autoTrending: false,
};

for (const arg of argv) {
Expand All @@ -23,6 +27,8 @@ const parseArgs = (argv) => {
args.songId = arg.slice('--song-id='.length).trim();
} else if (arg.startsWith('--query=')) {
args.query = arg.slice('--query='.length).trim();
} else if (arg === '--auto-trending') {
args.autoTrending = true;
}
}

Expand All @@ -31,32 +37,81 @@ const parseArgs = (argv) => {

const cloneJson = (value) => JSON.parse(JSON.stringify(value));

const buildTemplatePayload = ({songId, query}) => {
const fetchTrendingSongQuery = async () => {
try {
const response = await fetch(APPLE_MUSIC_TRENDING_URL, {
signal: AbortSignal.timeout(TRENDING_FETCH_TIMEOUT_MS),
});
if (!response.ok) {
return {query: TRENDING_FALLBACK_QUERY, source: 'fallback'};
}

const payload = await response.json();
const firstSong = payload?.feed?.results?.[0];
const title = String(firstSong?.name || '').trim();
const artist = String(firstSong?.artistName || '').trim();

if (!title || !artist) {
return {query: TRENDING_FALLBACK_QUERY, source: 'fallback'};
}

return {
query: `${title} ${artist}`,
source: 'apple-music-trending',
};
} catch (_error) {
return {query: TRENDING_FALLBACK_QUERY, source: 'fallback'};
}
};

const buildTemplatePayload = ({songId, query, songSelectionMethod, trendingSource}) => {
const payload = cloneJson(templateSongData);

payload.metadata = payload.metadata || {};
payload.metadata.generatedAt = new Date().toISOString();
payload.metadata.phase = 3;
payload.metadata.pipelineMode = 'template-only';
payload.metadata.songSelectionMethod = songSelectionMethod || payload.metadata.songSelectionMethod || 'template-default';

if (songId) {
payload.song = payload.song || {};
payload.song.id = songId;
payload.metadata.songSelectionMethod = 'direct-id-template';
}

if (query) {
payload.metadata.query = query;
if (!songId) {
payload.metadata.songSelectionMethod = 'query-template';
}
}

if (trendingSource) {
payload.metadata.trendingSource = trendingSource;
}

return payload;
};

const runPhase3Build = async ({songId, query, out}) => {
const payload = buildTemplatePayload({songId, query});
const runPhase3Build = async ({songId, query, out, autoTrending}) => {
const normalizedSongId = String(songId || '').trim();
let normalizedQuery = String(query || '').trim();
let songSelectionMethod = 'template-default';
let trendingSource = '';

if (normalizedSongId) {
songSelectionMethod = 'direct-id-template';
} else if (normalizedQuery) {
songSelectionMethod = 'query-template';
} else if (autoTrending || String(process.env.PHASE3_AUTO_TRENDING || '').trim() === '1') {
const trending = await fetchTrendingSongQuery();
normalizedQuery = trending.query;
trendingSource = trending.source;
songSelectionMethod = 'auto-trending-template';
}

const payload = buildTemplatePayload({
songId: normalizedSongId,
query: normalizedQuery,
songSelectionMethod,
trendingSource,
});

await fs.mkdir(path.dirname(out), {recursive: true});
await fs.writeFile(out, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
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'),
concurrency: Number(process.env.PHASE5_RENDER_CONCURRENCY || 2),
crf: Number(process.env.PHASE5_RENDER_CRF || 20),
};

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`,
);
};

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