SRT real test - run: 2026-10-26 01:51 UTC host: Linux 6.8.0-139-generic x86_64 ffmpeg: ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers === 1. source clip (Wikimedia Commons, CC0: Mandelbrot Set Color Cycling Video 1080p 4) === $ ffprobe -v error -show_entries format=duration,size,format_name -show_entries stream=codec_name,codec_type,width,height,r_frame_rate -of default=noprint_wrappers=1 '/home/ubuntu/rt/srt2/source.webm' codec_name=vp8 codec_type=video width=1920 height=1080 r_frame_rate=30/1 format_name=matroska,webm duration=8.000000 size=22643616 exit=0 === 2. build the demo file (8.0 s clip, last frame held for 6 s, + our narration) === $ ffmpeg -v error -i '/home/ubuntu/rt/srt2/source.webm' -i '/home/ubuntu/rt/srt2/narration.wav' -vf 'tpad=stop_mode=clone:stop_duration=6.0,scale=640:360:force_original_aspect_ratio=decrease,pad=640:360:(ow-iw)/2:(oh-ih)/2,format=yuv420p' -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k -ar 24000 -ac 1 demo.mp4 -y exit=0 $ ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height,r_frame_rate,nb_frames -of default=noprint_wrappers=1 demo.mp4 codec_name=h264 width=640 height=360 r_frame_rate=30/1 nb_frames=420 codec_name=aac r_frame_rate=0/0 nb_frames=327 duration=14.000000 size=2797818 exit=0 === 3. measured cue boundaries (ffmpeg silencedetect on the narration) === $ ffmpeg -v info -i '/home/ubuntu/rt/srt2/narration.wav' -af 'silencedetect=n=-38dB:d=0.12' -f null - ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers built with gcc 13 (Ubuntu 13.2.0-23ubuntu3) configuration: --prefix=/usr --extra-version=3ubuntu5 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --disable-omx --enable-gnutls --enable-libaom --enable-libass --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libglslang --enable-libgme --enable-libgsm --enable-libharfbuzz --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-openal --enable-opencl --enable-opengl --disable-sndio --enable-libvpl --disable-libmfx --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-ladspa --enable-libbluray --enable-libjack --enable-libpulse --enable-librabbitmq --enable-librist --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libx264 --enable-libzmq --enable-libzvbi --enable-lv2 --enable-sdl2 --enable-libplacebo --enable-librav1e --enable-pocketsphinx --enable-librsvg --enable-libjxl --enable-shared libavutil 58. 29.100 / 58. 29.100 libavcodec 60. 31.102 / 60. 31.102 libavformat 60. 16.100 / 60. 16.100 libavdevice 60. 3.100 / 60. 3.100 libavfilter 9. 12.100 / 9. 12.100 libswscale 7. 5.100 / 7. 5.100 libswresample 4. 12.100 / 4. 12.100 libpostproc 57. 3.100 / 57. 3.100 [aist#0:0/pcm_s16le @ 0x5bb608df6f00] Guessed Channel Layout: mono Input #0, wav, from '/home/ubuntu/rt/srt2/narration.wav': Metadata: encoder : Lavf60.16.100 Duration: 00:00:13.91, bitrate: 384 kb/s Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 24000 Hz, 1 channels, s16, 384 kb/s Stream mapping: Stream #0:0 -> #0:0 (pcm_s16le (native) -> pcm_s16le (native)) Press [q] to stop, [?] for help Output #0, null, to 'pipe:': Metadata: encoder : Lavf60.16.100 Stream #0:0: Audio: pcm_s16le, 24000 Hz, mono, s16, 384 kb/s Metadata: encoder : Lavc60.31.102 pcm_s16le size= 0kB time=00:00:00.00 bitrate=N/A speed= 0x [silencedetect @ 0x5bb608dfe480] silence_start: 2.59429 [silencedetect @ 0x5bb608dfe480] silence_end: 3.05129 | silence_duration: 0.457 [silencedetect @ 0x5bb608dfe480] silence_start: 4.35079 [silencedetect @ 0x5bb608dfe480] silence_end: 4.82958 | silence_duration: 0.478792 [silencedetect @ 0x5bb608dfe480] silence_start: 6.69379 [silencedetect @ 0x5bb608dfe480] silence_end: 7.02804 | silence_duration: 0.33425 [silencedetect @ 0x5bb608dfe480] silence_start: 8.56821 [silencedetect @ 0x5bb608dfe480] silence_end: 9.0285 | silence_duration: 0.460292 [silencedetect @ 0x5bb608dfe480] silence_start: 10.5889 [silencedetect @ 0x5bb608dfe480] silence_end: 10.9675 | silence_duration: 0.378625 [out#0/null @ 0x5bb608dd04c0] video:0kB audio:652kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown size=N/A time=00:00:13.82 bitrate=N/A speed=2.02e+03x exit=0 === 4. the subtitle file we ship (cue times above are measured, not typed) === 1 00:00:00,000 --> 00:00:04,351 A color cycling animation of the Mandelbrot set, a famous fractal shape. 2 00:00:04,830 --> 00:00:08,568 The colors repeat in smooth bands while the picture slowly moves. 3 00:00:09,029 --> 00:00:13,905 This clip is public domain, released under CC0 on Wikimedia Commons. === 5. burn the file into the video (ffmpeg subtitles filter / libass) === $ ffmpeg -v error -i demo.mp4 -vf 'subtitles=narration.srt:force_style=Fontsize=20' -c:v libx264 -crf 23 -preset medium -c:a copy burned.mp4 -y exit=0 $ ffprobe -v error -show_entries format=duration,size -of default=noprint_wrappers=1 burned.mp4 duration=14.000000 size=2697736 exit=0 === 6. did the caption really appear at the right time? (caption band vs source) === The caption sits in the bottom 44 px of the frame. For each second we compare that band in the burned video with the same band in the caption-free source: mean absolute difference per pixel (0 to 255). Near zero means nothing was drawn at that second. t=1.000s mean absolute difference from the caption-free source, caption band = 25.44 t=3.000s mean absolute difference from the caption-free source, caption band = 25.26 t=4.500s mean absolute difference from the caption-free source, caption band = 3.57 t=5.500s mean absolute difference from the caption-free source, caption band = 18.78 t=8.000s mean absolute difference from the caption-free source, caption band = 19.65 t=9.500s mean absolute difference from the caption-free source, caption band = 20.48 t=12.000s mean absolute difference from the caption-free source, caption band = 20.48 exported caption-frame.png (t=1.0 s, inside cue 1) and gap-frame.png (t=4.5 s, inside the measured gap) === 7. broken variants: what does each one do? === --- variant: bom.srt $ ffmpeg -v error -i demo.mp4 -vf 'subtitles=bom.srt' -c:v libx264 -crf 23 -preset veryfast -c:a copy out-bom.mp4 -y exit=0 rendered. difference from source in the caption band: at t=1.0 s = 26.32, at t=5.0 s = 23.71 --- variant: crlf.srt $ ffmpeg -v error -i demo.mp4 -vf 'subtitles=crlf.srt' -c:v libx264 -crf 23 -preset veryfast -c:a copy out-crlf.mp4 -y exit=0 rendered. difference from source in the caption band: at t=1.0 s = 26.32, at t=5.0 s = 23.71 --- variant: dotms.srt $ ffmpeg -v error -i demo.mp4 -vf 'subtitles=dotms.srt' -c:v libx264 -crf 23 -preset veryfast -c:a copy out-dotms.mp4 -y exit=0 rendered. difference from source in the caption band: at t=1.0 s = 26.32, at t=5.0 s = 23.71 --- variant: spacedot.srt $ ffmpeg -v error -i demo.mp4 -vf 'subtitles=spacedot.srt' -c:v libx264 -crf 23 -preset veryfast -c:a copy out-spacedot.mp4 -y [Parsed_subtitles_0 @ 0x5f9b006c6c80] Unable to open spacedot.srt [AVFilterGraph @ 0x5f9b0066f940] Error initializing filters [vost#0:0/libx264 @ 0x5f9b005f73c0] Error initializing a simple filtergraph Error opening output file out-spacedot.mp4. Error opening output files: Invalid data found when processing input exit=183 NO OUTPUT FILE - the renderer refused this file --- variant: noblank.srt $ ffmpeg -v error -i demo.mp4 -vf 'subtitles=noblank.srt' -c:v libx264 -crf 23 -preset veryfast -c:a copy out-noblank.mp4 -y exit=0 rendered. difference from source in the caption band: at t=1.0 s = 26.32, at t=5.0 s = 23.71 --- variant: noindex.srt $ ffmpeg -v error -i demo.mp4 -vf 'subtitles=noindex.srt' -c:v libx264 -crf 23 -preset veryfast -c:a copy out-noindex.mp4 -y [Parsed_subtitles_0 @ 0x593b2c2aac80] Unable to open noindex.srt [AVFilterGraph @ 0x593b2c253940] Error initializing filters [vost#0:0/libx264 @ 0x593b2c1db3c0] Error initializing a simple filtergraph Error opening output file out-noindex.mp4. Error opening output files: Invalid data found when processing input exit=183 NO OUTPUT FILE - the renderer refused this file --- variant: overlap.srt $ ffmpeg -v error -i demo.mp4 -vf 'subtitles=overlap.srt' -c:v libx264 -crf 23 -preset veryfast -c:a copy out-overlap.mp4 -y exit=0 rendered. difference from source in the caption band: at t=1.0 s = 26.32, at t=5.0 s = 4.58 --- variant: late.srt $ ffmpeg -v error -i demo.mp4 -vf 'subtitles=late.srt' -c:v libx264 -crf 23 -preset veryfast -c:a copy out-late.mp4 -y exit=0 rendered. difference from source in the caption band: at t=1.0 s = 26.32, at t=5.0 s = 23.71 === 8. SRT to VTT and back (ffmpeg reference for the site's converter tools) === $ ffmpeg -v error -i narration.srt -c:s webvtt narration.vtt -y exit=0 $ ffmpeg -v error -i narration.vtt -c:s srt narration-roundtrip.srt -y exit=0 diff (first 30 lines): --- narration.srt 2026-10-26 01:51:25.267577863 +0000 +++ narration-roundtrip.srt 2026-10-26 01:51:38.239463051 +0000 @@ -9,3 +9,4 @@ 3 00:00:09,029 --> 00:00:13,905 This clip is public domain, released under CC0 on Wikimedia Commons. + === 9. what we ship === -rw-rw-r-- 1 ubuntu ubuntu 2697736 Oct 26 01:51 burned.mp4 -rw-rw-r-- 1 ubuntu ubuntu 2797818 Oct 26 01:51 demo.mp4 -rw-rw-r-- 1 ubuntu ubuntu 306 Oct 26 01:51 narration.srt -rw-rw-r-- 1 ubuntu ubuntu 290 Oct 26 01:51 narration.vtt === done === === 10. the site's own tools on the same file (Playwright, headless Chromium, production site koldflux.com), run 2026-10-26 === $ node scenario.cjs https://koldflux.com/tools/subtitles/subtitle-stats narration.srt 3 cues read | WORDS 34 | CHARACTERS 205 | 15.8 chars/sec | LONGEST LINE 72 chars | OVERLAPPING CUES 0 | LAST CUE ENDS AT 00:00:13,905 | CUES FASTER THAN 20 CHARS/SEC 0 overlap.srt 3 cues read | 14.9 chars/sec | OVERLAPPING CUES 1 dotms.srt 3 cues read | 15.8 chars/sec | OVERLAPPING CUES 0 (plain dot instead of comma) spacedot.srt 0 cues read | each cue skipped: "Skipped a cue with an unreadable timestamp: 00:00:00 .000 --> 00:00:04 .351" noindex.srt 3 cues read | 15.8 chars/sec | OVERLAPPING CUES 0 crlf.srt 3 cues read | 15.8 chars/sec | OVERLAPPING CUES 0 noblank.srt 1 cue read | 62.3 chars/sec | 1 cue faster than 20 chars/s (the block is read as one cue) late.srt 3 cues read | 10.8 chars/sec | LAST CUE ENDS AT 00:00:20,000 (past the end of the 14 s video) bom.srt 3 cues read | 15.8 chars/sec | OVERLAPPING CUES 0