Checking caption timing by skimming with Emacs Lisp or JS
| js, emacs, subedSometimes automatic subtitle timing tools like Aeneas can get confused by silences, extraneous sounds, filler words, mis-starts, and text that I've edited out of the raw captions for easier readability. It's good to quickly check each caption. I used to listen to captions at 1.5x speed, watching carefully as each caption displayed. This took a fair bit of time and focus, so… it usually didn't happen. Sampling the first second of each caption is faster and requires a little less attention.
Skimming with subed.el
Here's a function that I wrote to play the first second of each subtitle.
(defvar my-subed-skim-msecs 1000 "Number of milliseconds to play when skimming.") (defun my-subed-skim-starts () (interactive) (subed-mpv-unpause) (subed-disable-loop-over-current-subtitle) (catch 'done (while (not (eobp)) (subed-mpv-jump-to-current-subtitle) (let ((ch (read-char "(q)uit? " nil (/ my-subed-skim-msecs 1000.0)))) (when ch (throw 'done t))) (subed-forward-subtitle-time-start) (when (and subed-waveform-minor-mode (not subed-waveform-show-all)) (subed-waveform-refresh)) (recenter))) (subed-mpv-pause))
Now I can read the lines as the subtitles play, and I can press any key to stop so that I can fix timestamps.
Skimming with Javascript
I also want to check the times on the Web in case there have been caching issues. Here's some Javascript to skim the first second of each cue in the first text track for a video, with some code to make it easy to process the first video in the visible area.
function getVisibleVideo() { const videos = document.querySelectorAll('video'); for (const video of videos) { const rect = video.getBoundingClientRect(); if ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ) { return video; } } return null; } async function skimVideo(video=getVisibleVideo(), msecs=1000) { // Get the first text track (assumed to be captions/subtitles) const textTrack = video.textTracks[0]; if (!textTrack) return; const remaining = [...textTrack.cues].filter((cue) => cue.endTime >= video.currentTime); video.play(); // Play the first 1 second of each visible subtitle for (let i = 0; i < remaining.length && !video.paused; i++) { video.currentTime = remaining[i].startTime; await new Promise((resolve) => setTimeout(resolve, msecs)); } }
Then I can call it with skimVideo();
. Actually,
in our backstage area, it might be useful to add a
Skim button so that I can skim things from my
phone.
function handleSkimButton(event) { const vid = event.target.closest('.vid').querySelector('video'); skimVideo(vid); } document.querySelectorAll('video').forEach((vid) => { const div = document.createElement('div'); const skim = document.createElement('button'); skim.textContent = 'Skim'; div.appendChild(skim); vid.parentNode.insertBefore(div, vid.nextSibling); skim.addEventListener('click', handleSkimButton); });
Results
How much faster is it this way?
Some code to help figure out the speedup
(-let* ((files (directory-files "~/proj/emacsconf/2024/cache" t "--main\\.vtt")) ((count-subs sum-seconds) (-unzip (mapcar (lambda (file) (list (length (subed-parse-file file)) (/ (compile-media-get-file-duration-ms (concat (file-name-sans-extension file) ".webm")) 1000.0))) files))) (total-seconds (-reduce #'+ sum-seconds)) (total-subs (-reduce #'+ count-subs))) (format "%d files, %.1f hours, %d total captions, speed up of %.1f" (length files) (/ total-seconds 3600.0) total-subs (/ total-seconds total-subs)))
It looks like for EmacsConf talks where we typically format captions to be one long line each (< 60 characters), this can be a speed-up of about 4x compared to listening to the video at normal speed. More usefully, it's different enough to get my brain to do it instead of putting it off.
Most of the automatically-generated timestamps are fine. It's just a few that might need tweaking. It's nice to be able to skim them with fewer keystrokes.