fix: use exact MP3 duration for word-timing sync instead of byte estimate

Parses the generated TTS audio file with music-metadata to get the real
duration before computing word timings, eliminating desync between text
overlay and voice playback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Sonnet 4.6 committed 2026-03-24 19:22:28 +01:00
1 parent 9145bc5941
commit 32c308c2af
1 file changed
+12 -4
+12 -4
View File
@@ -6,6 +6,7 @@ import { bundle } from "@remotion/bundler";
import { renderMedia, selectComposition } from "@remotion/renderer"; import { renderMedia, selectComposition } from "@remotion/renderer";
import { ffmpegService } from "../services/ffmpeg"; import { ffmpegService } from "../services/ffmpeg";
import { storage as dbStorage } from "../storage"; import { storage as dbStorage } from "../storage";
import * as musicMetadata from "music-metadata";
export const remotionRouter = Router(); export const remotionRouter = Router();
@@ -158,12 +159,19 @@ remotionRouter.post("/render", upload.fields([{ name: "images", maxCount: 4 }, {
if (ttsResult.success && ttsResult.audioBase64) { if (ttsResult.success && ttsResult.audioBase64) {
const audioFilename = `tts-${Date.now()}.mp3`; const audioFilename = `tts-${Date.now()}.mp3`;
const audioPath = path.join(uploadDir, audioFilename); const audioPath = path.join(uploadDir, audioFilename);
fs.writeFileSync(audioPath, Buffer.from(ttsResult.audioBase64, "base64")); const audioBuffer = Buffer.from(ttsResult.audioBase64, "base64");
fs.writeFileSync(audioPath, audioBuffer);
audioUrl = `${host}/uploads/temp/${audioFilename}`; audioUrl = `${host}/uploads/temp/${audioFilename}`;
// Estimate audio duration (MP3 at 128kbps ~ 16KB/s) // Get exact audio duration from MP3 metadata
const audioBytes = Buffer.from(ttsResult.audioBase64, "base64").length; try {
estimatedAudioDuration = Math.max(audioBytes / 16000, ttsText.split(/\s+/).length * 0.5); const meta = await musicMetadata.parseFile(audioPath);
estimatedAudioDuration = meta.format.duration ?? 0;
} catch {
// Fallback: rough estimate from byte size (128kbps = 16KB/s)
estimatedAudioDuration = audioBuffer.length / 16000;
}
estimatedAudioDuration = Math.max(estimatedAudioDuration, ttsText.split(/\s+/).length * 0.35);
wordTimings = computeWordTimings(overlayText, estimatedAudioDuration, FPS, 0); wordTimings = computeWordTimings(overlayText, estimatedAudioDuration, FPS, 0);
console.log(`✅ TTS generated, ~${estimatedAudioDuration.toFixed(1)}s, ${wordTimings.length} words`); console.log(`✅ TTS generated, ~${estimatedAudioDuration.toFixed(1)}s, ${wordTimings.length} words`);