feat: add Remotion project scaffolding and subtitle tools

Remotion project:
- package.json with Remotion 4.0.522, React 19, Tailwind v4
- tsconfig.json (strict, ES2018, Bundler module resolution)
- remotion.config.ts (rspack, jpeg image format)
- ESLint flat config, Prettier config
- Root component and MyComposition placeholder (1280x720, 30fps, 60 frames)

Subtitle tools:
- src/lrc2srt.ts — LRC to SRT converter (minimal, no dependencies)
- ffmpeg.sh — helper for baking ASS subtitles or static-image+audio bypass
This commit is contained in:
2026-09-09 17:35:35 +09:00
parent 0de9c41c7d
commit 74b70967a1
12 changed files with 270 additions and 20 deletions
+25
View File
@@ -0,0 +1,25 @@
import { CalculateMetadataFunction, Composition } from "remotion";
type Props = {};
const calculateMetadata: CalculateMetadataFunction<Props> = () => {
return {};
};
export const MyComposition = () => {
return (
<Composition
id="MyComp"
component={MyComponent}
durationInFrames={60}
fps={30}
width={1280}
height={720}
calculateMetadata={calculateMetadata}
/>
);
};
export const MyComponent: React.FC<Props> = () => {
return null;
};
+10
View File
@@ -0,0 +1,10 @@
import "./index.css";
import { MyComposition } from "./Composition";
export const RemotionRoot: React.FC = () => {
return (
<>
<MyComposition />
</>
);
};
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+4
View File
@@ -0,0 +1,4 @@
import { registerRoot } from "remotion";
import { RemotionRoot } from "./Root";
registerRoot(RemotionRoot);
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env node
// lrc→srt converter — minimal, no external dependencies
//
// Usage:
// node lrc2srt.ts <input.lrc> [output.srt]
//
// If output is omitted, writes to stdout.
import * as fs from "fs";
import * as path from "path";
function lrcTimeToSeconds(line: string): number | null {
const match = line.match(/\[(\d{1,2}):(\d{2})\.(\d{2,3})\]/);
if (!match) return null;
const minutes = parseInt(match[1], 10);
const seconds = parseInt(match[2], 10);
const centis = match[3].padEnd(3, "0").substring(0, 3);
const ms = parseInt(centis, 10);
return minutes * 60 + seconds + ms / 1000;
}
function secondsToSrtTime(t: number): string {
const hours = Math.floor(t / 3600);
const minutes = Math.floor((t % 3600) / 60);
const secs = Math.floor(t % 60);
const ms = Math.round((t - Math.floor(t)) * 1000);
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")},${String(ms).padStart(3, "0")}`;
}
function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.error("Usage: lrc2srt.ts <input.lrc> [output.srt]");
process.exit(1);
}
const input = fs.readFileSync(args[0], "utf-8");
const lines = input.split(/\r?\n/);
const entries: { start: number; end: number; text: string }[] = [];
let currentStart: number | null = null;
let currentText: string | null = null;
for (const raw of lines) {
const trimmed = raw.trim();
if (!trimmed) continue;
const timeMatch = trimmed.match(/\[(\d{1,2}):(\d{2})\.(\d{2,3})\]/);
const text = trimmed.replace(/\[\d{1,2}:\d{2}\.\d{2,3}\]/g, "").trim();
if (timeMatch && !currentStart) {
currentStart = lrcTimeToSeconds(raw);
currentText = text;
} else if (currentStart && currentText) {
// If there's a time tag, the current entry ends here
entries.push({ start: currentStart, end: currentStart + 1, text: currentText });
currentStart = lrcTimeToSeconds(raw);
currentText = text;
} else if (currentStart && !currentText) {
currentText = text;
} else if (!currentStart) {
// line without time tag, skip
}
}
if (currentStart && currentText) {
entries.push({ start: currentStart, end: currentStart + 1, text: currentText });
}
// Fix end times: each entry ends when the next one starts
for (let i = 0; i < entries.length; i++) {
if (i + 1 < entries.length) {
entries[i].end = entries[i + 1].start;
} else {
entries[i].end = entries[i].start + 5; // default 5s for last line
}
}
const srtBlocks = entries.map((e, idx) => {
return `${idx + 1}\n${secondsToSrtTime(e.start)} --> ${secondsToSrtTime(e.end)}\n${e.text}`;
});
const output = srtBlocks.join("\n\n") + "\n";
const outPath = args[1];
if (outPath) {
fs.writeFileSync(outPath, output, "utf-8");
} else {
process.stdout.write(output);
}
}
main();