Reaperxxxx commited on
Commit
2a92e96
·
verified ·
1 Parent(s): 0fdb688

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +36 -24
server.js CHANGED
@@ -783,10 +783,19 @@ async function buildVideo(questions, audioFiles, framesDir, videosDir, onProgres
783
  );
784
 
785
  // ------------------------------------------------------------------
786
- // 3. Render frames and build the concat list using real durations.
 
 
 
 
 
 
787
  // ------------------------------------------------------------------
788
- const framesList = path.join(framesDir, "frames_list.txt");
789
- const concatList = [];
 
 
 
790
 
791
  for (let i = 0; i < usedQuestions.length; i++) {
792
  const q = usedQuestions[i];
@@ -798,20 +807,29 @@ async function buildVideo(questions, audioFiles, framesDir, videosDir, onProgres
798
  const rFrame = path.join(framesDir, `q${i + 1}_result.png`);
799
  await renderFrame(q, "result", rFrame);
800
 
801
- concatList.push(`file '${qFrame}'\nduration ${questionDuration}`);
802
- concatList.push(`file '${rFrame}'\nduration ${resultDuration}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
803
 
804
  if (onProgress) onProgress(i + 1, usedQuestions.length);
805
  }
806
 
807
- // ffmpeg concat requires the last entry repeated without a duration
808
- const lastResult = path.join(framesDir, `q${usedQuestions.length}_result.png`);
809
- concatList.push(`file '${lastResult}'`);
810
- fs.writeFileSync(framesList, concatList.join("\n"));
811
 
812
  // ------------------------------------------------------------------
813
- // 4. Concatenate only the audio files we're actually using.
814
- // audioDir is derived from the files themselves (parent directory).
815
  // ------------------------------------------------------------------
816
  const audioDir = path.dirname(usedAudio[0]);
817
  const combinedAudio = path.join(audioDir, "combined.mp3");
@@ -819,9 +837,8 @@ async function buildVideo(questions, audioFiles, framesDir, videosDir, onProgres
819
 
820
  const videoPath = path.join(videosDir, `wyr_${Date.now()}.mp4`);
821
 
822
- // Probe the combined audio duration so we can set an explicit output duration.
823
- // This is more reliable than -shortest, which cuts at whichever stream ends first —
824
- // and with the concat demuxer the video stream duration is often reported wrong.
825
  const audioDurationSecs = await new Promise((resolve, reject) => {
826
  ffmpeg.ffprobe(combinedAudio, (err, meta) => {
827
  if (err) return reject(err);
@@ -834,18 +851,13 @@ async function buildVideo(questions, audioFiles, framesDir, videosDir, onProgres
834
 
835
  await new Promise((resolve, reject) => {
836
  ffmpeg()
837
- .input(framesList)
838
- // No -r on input: the concat demuxer ignores it and uses its own 25fps default.
839
- // Duration is controlled by the duration lines in frames_list.txt.
840
- .inputOptions(["-f concat", "-safe 0"])
841
  .input(combinedAudio)
842
- // No -r on input: the concat demuxer ignores it for image inputs.
843
- // Duration is driven purely by the 'duration' lines in frames_list.txt.
844
- // -filter:v fps=30 resamples the variable-rate concat output to constant 30fps,
845
- // which libx264 requires. -t pins the output to the real audio length so we
846
- // don't get cut short or run long regardless of what concat reports.
847
  .outputOptions([
848
- "-filter:v fps=30",
849
  "-c:v libx264",
850
  "-pix_fmt yuv420p",
851
  "-preset fast",
 
783
  );
784
 
785
  // ------------------------------------------------------------------
786
+ // 3. Render frames and build a sequential image list at 30fps.
787
+ // The concat demuxer ignores 'duration' lines for PNG inputs on
788
+ // this ffmpeg build (Duration: N/A, treats them as raw 25fps stream).
789
+ // Solution: use the image2 demuxer at a fixed 30fps and write each
790
+ // frame as a symlink repeated the correct number of times.
791
+ // Frame count = round(durationSeconds * FPS) — no ffmpeg duration
792
+ // parsing needed, timing is mathematically exact.
793
  // ------------------------------------------------------------------
794
+ const FPS = 30;
795
+ const seqDir = path.join(framesDir, "seq");
796
+ fs.mkdirSync(seqDir, { recursive: true });
797
+
798
+ let seqIndex = 0; // global counter across all frames
799
 
800
  for (let i = 0; i < usedQuestions.length; i++) {
801
  const q = usedQuestions[i];
 
807
  const rFrame = path.join(framesDir, `q${i + 1}_result.png`);
808
  await renderFrame(q, "result", rFrame);
809
 
810
+ // Write symlinks for question phase
811
+ const qFrameCount = Math.max(1, Math.round(questionDuration * FPS));
812
+ for (let f = 0; f < qFrameCount; f++) {
813
+ const linkPath = path.join(seqDir, `frame${String(seqIndex).padStart(6, "0")}.png`);
814
+ fs.symlinkSync(qFrame, linkPath);
815
+ seqIndex++;
816
+ }
817
+
818
+ // Write symlinks for result phase
819
+ const rFrameCount = Math.max(1, Math.round(resultDuration * FPS));
820
+ for (let f = 0; f < rFrameCount; f++) {
821
+ const linkPath = path.join(seqDir, `frame${String(seqIndex).padStart(6, "0")}.png`);
822
+ fs.symlinkSync(rFrame, linkPath);
823
+ seqIndex++;
824
+ }
825
 
826
  if (onProgress) onProgress(i + 1, usedQuestions.length);
827
  }
828
 
829
+ console.log(`🖼 Total frames written: ${seqIndex} @ ${FPS}fps = ${(seqIndex / FPS).toFixed(1)}s`);
 
 
 
830
 
831
  // ------------------------------------------------------------------
832
+ // 4. Concatenate audio files.
 
833
  // ------------------------------------------------------------------
834
  const audioDir = path.dirname(usedAudio[0]);
835
  const combinedAudio = path.join(audioDir, "combined.mp3");
 
837
 
838
  const videoPath = path.join(videosDir, `wyr_${Date.now()}.mp4`);
839
 
840
+ // Probe audio so we can set -t explicitly (guards against any off-by-one
841
+ // between frame count and audio length).
 
842
  const audioDurationSecs = await new Promise((resolve, reject) => {
843
  ffmpeg.ffprobe(combinedAudio, (err, meta) => {
844
  if (err) return reject(err);
 
851
 
852
  await new Promise((resolve, reject) => {
853
  ffmpeg()
854
+ // image2 demuxer at fixed 30fps — reads frame000000.png, frame000001.png, ...
855
+ // Each file appears exactly the right number of times so timing is exact.
856
+ .input(path.join(seqDir, "frame%06d.png"))
857
+ .inputOptions(["-f image2", `-r ${FPS}`])
858
  .input(combinedAudio)
 
 
 
 
 
859
  .outputOptions([
860
+ `-r ${FPS}`,
861
  "-c:v libx264",
862
  "-pix_fmt yuv420p",
863
  "-preset fast",