branch: elpa/subed commit f9252af5bb4971d674fb10fb1f75a0f824d605c2 Author: Sacha Chua <sa...@sachachua.com> Commit: Sacha Chua <sa...@sachachua.com>
New subed-align-region, subed-retime-subtitles; move duration to subed-common * subed/subed-align.el (subed-align-region): New command. * subed/subed-common.el (subed-retime-subtitles): New command, along with related functions/keymaps/variables. (subed-insert-subtitle-for-whole-file): New command. * README.org (Editing subtitles): Add documentation for aligning and editing timestamps. Move duration calculation from subed-waveform to subed-common * subed/subed-config.el (subed-ffprobe-executable): New option. * subed/subed-waveform.el: Move ffprobe and duration calculation to subed-common.el. * tests/test-subed-common.el, tests/test-subed-waveform.el: Move duration tests from tests-subed-waveform.el to tests-subed-common.el --- README.org | 126 ++++++++++++++++++++------- subed/subed-align.el | 63 ++++++++++++++ subed/subed-common.el | 199 +++++++++++++++++++++++++++++++++++++++++++ subed/subed-config.el | 5 ++ subed/subed-waveform.el | 126 ++------------------------- subed/subed-word-data.el | 7 +- tests/test-subed-common.el | 133 ++++++++++++++++++++++++++++- tests/test-subed-vtt.el | 3 +- tests/test-subed-waveform.el | 113 +----------------------- 9 files changed, 507 insertions(+), 268 deletions(-) diff --git a/README.org b/README.org index aeb7e8357e..6dde2f9883 100644 --- a/README.org +++ b/README.org @@ -262,6 +262,26 @@ manually loading a mode, use those specific format modes instead of ~subed-mode~. ** Some workflow ideas +*** Editing subtitles + +You can use ~subed-mpv-jump-to-current-subtitle~ (~M-j~) to play the current +subtitle and use ~subed-mpv-toggle-pause~ (~M-SPC~) to stop at the right time. +Use ~subed-toggle-loop-over-current-subtitle~ (~C-c C-l~) if you want to keep +looping automatically. + +If you have wdiff installed, you can use +~subed-wdiff-subtitle-text-with-file~ to compare the subtitle text +with a script or another subtitle file. + +*** Writing subtitles from scratch + +One way is to start with one big subtitle that covers the whole media +file, and then split it using ~subed-split-subtitle~ (~M-.~). + +Another way is to type as much of the text as you can without worrying +about timestamps, putting each caption on a separate line. Then you +can use ~subed-align~ to convert it into timestamped captions. + *** Reflowing subtitles into shorter or longer lines You may want to use ~set-fill-column~ and @@ -289,14 +309,6 @@ the end of the subtitle in order to test it. Use keep looping automatically. Use ~subed-mpv-toggle-pause~ (~M-SPC~) to stop at the right time. -~subed-waveform-show-current~ or ~subed-waveform-show-all~ can be -useful for adjusting start and end timestamps. Use -~subed-waveform-set-start~ (~mouse-1~, which is left click) or ~subed-waveform-set-stop~ (~mouse-3~, which is right-click) to adjust only -the current subtitle's timestamps, or use -~subed-waveform-set-start-and-copy-to-previous~ (~S-mouse-1~ or ~M-mouse-1~) or -~subed-waveform-set-stop-and-copy-to-next~ (~S-mouse-3~ or ~M-mouse-3~) to adjust adjacent -subtitles as well. - You can also manually adjust - subtitle start: ~M-[~ / ~M-]~ @@ -304,33 +316,83 @@ You can also manually adjust A prefix argument sets the number of milliseconds (e.g. ~C-u 1000 M-[ M-[ M-[~ decreases start time by 3 seconds). -*** Editing subtitles - -You can use ~subed-mpv-jump-to-current-subtitle~ (~M-j~) to play the current -subtitle and use ~subed-mpv-toggle-pause~ (~M-SPC~) to stop at the right time. -Use ~subed-toggle-loop-over-current-subtitle~ (~C-c C-l~) if you want to keep -looping automatically. - -If you have wdiff installed, you can use -~subed-wdiff-subtitle-text-with-file~ to compare the subtitle text -with a script or another subtitle file. - -*** Writing subtitles from scratch +Rodrigo Morales also has some functions for [[https://rodrigo.morales.pe/2024/11/17/my-subed-configuration-for-adding-subtitles-to-emacsconf-2024/][playing part of the subtitles and changing them by a little bit]]. -One way is to start with one big subtitle that covers the whole media -file, and then split it using ~subed-split-subtitle~ (~M-.~). +You can shift subtitles to start at a specific +timestamp with +~subed-shift-subtitles-to-start-at-timestamp~ . To +use a millisecond offset instead, use +~subed-shift-subtitles~. -Another way is to type as much of the text as you can without worrying -about timestamps, putting each caption on a separate line. Then you -can use ~subed-align~ to convert it into timestamped captions. +**** Waveforms -*** Timing / resynchronizing subtitles +Use ~subed-waveform-show-current~ or ~subed-waveform-show-all~ together with FFmpeg +to display waveforms for subtitles. -If you're using ~subed-waveform-show-current~ or ~subed-waveform-show-all~, you can use ~M-mouse-2~ (Meta-middle-click, ~subed-waveform-shift-subtitles~) to shift the current subtitle and succeeding subtitles so that they start at the position you clicked on. +Use ~subed-waveform-set-start~ (~mouse-1~, which +is left click) or ~subed-waveform-set-stop~ +(~mouse-3~, which is right-click) to adjust only +the current subtitle's timestamps, or use +~subed-waveform-set-start-and-copy-to-previous~ +(~S-mouse-1~ or ~M-mouse-1~) or +~subed-waveform-set-stop-and-copy-to-next~ +(~S-mouse-3~ or ~M-mouse-3~) to adjust adjacent +subtitles as well. -To do this with the keyboard, you can use -~subed-shift-subtitles-to-start-at-timestamp~ if you want to specify a -timestamp or ~subed-shift-subtitles~ to specify a millisecond offset. +You can use ~M-mouse-2~ (Meta-middle-click, ~subed-waveform-shift-subtitles~) to shift the current subtitle and succeeding subtitles so that they start at the position you clicked on. + +**** A transient map for retiming subtitles + +You can use ~subed-retime-subtitles~ to set new +times for subtitles by pressing ~SPC~ when the +current subtitle should stop. It will start with +the current subtitle and then continue until you +press a key that is not in the temporary keymap. + +Keys: + +| ~SPC~ | set stop and move forward | +| ~<left>~ or ~j~ | replay current subtitle | +| ~<right>~ or ~n~ or ~f~ | next | +| ~b~ | back | +| ~p~ | pause | + +**** Aeneas forced alignment tool + +The [[https://www.readbeyond.it/aeneas/][aeneas forced alignment tool]] (Python) can take +a media file and a text file (one cue per line) or +subtitle file, and create a subtitle file with the +timings determined by matching synthesized speech +with the waveforms. + +To use Aeneas to re-time subtitles or text, install +Aeneas and its prerequisites, then call ~M-x +subed-align~ to align the entire buffer. + +You can also select a region and then use ~M-x +subed-align-region~ to recalculate the timestamps +for just that region. One way to use this is: + +1. Determine the last correctly-timed subtitle. We'll call this subtitle A. Go to the beginning of subtitle A and use ~C-SPC~ (~set-mark-command~) to set the mark. +2. Pick a subtitle in the incorrectly-timed section. We'll call this subtitle B. Use ~subed-mpv-jump-to-current-subtitle~ to seek to that position. Play it and listen for the words. If you can't figure out which subtitle matches the position currently being played, choose a different subtitle starting point B until you find one that's recognizable. +3. Reset the playback position by using ~subed-mpv-jump-to-current-subtitle~ on subtitle B. +4. Now look for the subtitle that matches the words you heard at the playback position for subtitle B. We'll call that one subtitle D. +5. Go to the subtitle before subtitle D. We'll call that subtitle C. Use ~C-c ]~ (~subed-copy-player-pos-to-stop-time~) to set the stop time of subtitle C (the one immediately before D) to the playback position, which is the same time as the incorrect starting time for subtitle B. +6. Go to the end of subtitle C. +7. Use ~M-x subed-align-region~ to recalculate the timestamps within that section. + +Aeneas tends to have trouble with subtitle times +where there are long silences, background noises, +inaccurate transcripts (especially where the +speaker has skipped or added many words), +overlapping speakers, and non-English languages. +It may take several tries to figure out a span of +subtitles where Aeneas is more accurate. +Doublechecking with the word timing data can help +quickly verify if the subtitle times are +reasonable. + +**** Word timing data To use word timing data from something like WhisperX, load subed-word-data.el and then use @@ -338,14 +400,14 @@ WhisperX, load subed-word-data.el and then use will then be used when you split subtitles with ~subed-split-subtitle~. -Rodrigo Morales also has some functions for [[https://rodrigo.morales.pe/2024/11/17/my-subed-configuration-for-adding-subtitles-to-emacsconf-2024/][playing part of the subtitles and changing them by a little bit]]. - *** Exporting text for review You can use ~subed-copy-region-text~ to copy the text of the subtitles for pasting into another buffer. Call it with the universal prefix ~C-u~ to copy comments as well. +You can also use ~subed-convert~ to convert subtitles to a text file. + ** Troubleshooting *** subed-mpv: Service name too long diff --git a/subed/subed-align.el b/subed/subed-align.el index c242aeda0c..50eaacc31f 100644 --- a/subed/subed-align.el +++ b/subed/subed-align.el @@ -41,6 +41,69 @@ Ex: task_adjust_boundary_nonspeech_min=0.500|task_adjust_boundary_nonspeech_string=REMOVE will remove silence and other non-speech spans.") +;;;###autoload +(defun subed-align-region (audio-file beg end) + "Align just the given section." + (interactive + (list + (or + (subed-media-file) + (subed-guess-media-file subed-audio-extensions) + (read-file-name "Audio file: ")) + (if (region-active-p) (min (point) (mark)) (point-min)) + (if (region-active-p) (max (point) (mark)) (point-max)))) + (let* ((format (cond + ((derived-mode-p 'subed-vtt-mode) "VTT") + ((derived-mode-p 'subed-srt-mode) "SRT"))) + (temp-text-file + (make-temp-file "subed-align" nil ".txt" + (subed-subtitle-list-text + (subed-subtitle-list beg end)))) + (temp-file + (concat (make-temp-name "subed-align") + "." + (if (buffer-file-name) + (file-name-extension (buffer-file-name)) + (downcase format)))) + (ignore-before (save-excursion + (goto-char beg) + (unless (subed-subtitle-msecs-start) + (subed-forward-subtitle-text)) + (/ (subed-subtitle-msecs-start) 1000.0))) + (process-length (save-excursion + (goto-char end) + (- (/ (subed-subtitle-msecs-stop) 1000.0) + ignore-before))) + results) + (unwind-protect + (progn + (apply + #'call-process + (car subed-align-command) + nil + (get-buffer-create "*subed-aeneas*") + t + (append (cdr subed-align-command) + (list (expand-file-name audio-file) + temp-text-file + (format "is_audio_file_head_length=%.3f|is_audio_file_process_length=%.3f|task_language=%s|os_task_file_format=%s|is_text_type=%s%s" + ignore-before + process-length + subed-align-language + (downcase format) + "plain" + (if subed-align-options (concat "|" subed-align-options) "")) + temp-file))) + ;; parse the subtitles from the resulting output + (setq results (subed-parse-file temp-file)) + (save-excursion + (subed-for-each-subtitle beg end nil + (let ((current (pop results))) + (subed-set-subtitle-time-start (elt current 1)) + (subed-set-subtitle-time-stop (elt current 2)))))) + (delete-file temp-text-file) + (delete-file temp-file)))) + ;;;###autoload (defun subed-align (audio-file text-file format) "Align AUDIO-FILE with TEXT-FILE to get timestamps in FORMAT. diff --git a/subed/subed-common.el b/subed/subed-common.el index 50f642a7f5..97c929e05a 100644 --- a/subed/subed-common.el +++ b/subed/subed-common.el @@ -2427,5 +2427,204 @@ Does not yet take overlapping subtitles into account." (message "%s" (subed-msecs-to-timestamp sum))) sum)) +;;; Experimental retiming workflow + +(defvar subed-retime-subtitles-adjustment-msecs 100 + "Number of msecs to adjust the MPV playback position. +This accounts for reaction time.") + +(defun subed-retime-set-stop-and-move-forward () + "Set the current subtitle's stop time and the next subtitle's start time. +Move to the next subtitle. +Take into account `subed-subtitle-spacing' and +`subed-retime-subtitles-adjustment-msecs'." + (interactive) + (subed-set-subtitle-time-stop + (- subed-mpv-playback-position subed-subtitle-spacing subed-retime-subtitles-adjustment-msecs)) + (subed-forward-subtitle-text) + (subed-set-subtitle-time-start + (- subed-mpv-playback-position subed-retime-subtitles-adjustment-msecs))) + +(defun subed-retime-play-previous () + "Go backward one subtitle and replay." + (interactive) + (subed-backward-subtitle-text) + (subed-mpv-jump-to-current-subtitle)) + +(defun subed-retime-play-next () + "Go backward one subtitle and replay." + (interactive) + (subed-forward-subtitle-text) + (subed-mpv-jump-to-current-subtitle)) + +(defvar subed-retime-subtitles-map + (define-keymap + "SPC" #'subed-retime-set-stop-and-move-forward + "<left>" #'subed-mpv-jump-to-current-subtitle + "j" #'subed-mpv-jump-to-current-subtitle + "<right>" #'subed-retime-play-next + "b" #'subed-retime-play-previous + "f" #'subed-retime-play-next + "n" #'subed-retime-play-next + "p" #'subed-mpv-toggle-pause) + "Some shortcuts for subtitle retiming.") + +;;;###autoload +(defun subed-retime-subtitles () + "Set new stop times for subtitles by pressing SPC when the next subtitle starts." + (interactive) + (subed-disable-loop-over-current-subtitle) + (subed-mpv-unpause) + (subed-mpv-jump-to-current-subtitle) + (set-transient-map + subed-retime-subtitles-map t + nil + ;; todo: support substitute-command-keys + "SPC: set new stop, <left>: replay current, <right>: forward, (b)ack, (f)orward, (p)ause")) + +;;; ffprobe + +(defvar-local subed-file-duration-ms-cache nil + "If non-nil, duration of current file in milliseconds.") + +(defun subed-convert-ffprobe-tags-duration-to-ms (duration) + "Return milliseconds as an integer for DURATION. + +DURATION must be a string of the format HH:MM:SS.MMMM. + +Example: + +00:00:03.003000000 -> 3003 +00:00:03.00370000 -> 3004" + (unless (string-match "\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\)\\.\\([0-9]+\\)" duration) + (error "The duration is not well formatted.")) + (let ((hour (match-string 1 duration)) + (minute (match-string 2 duration)) + (seconds (match-string 3 duration)) + (milliseconds (match-string 4 duration))) + (+ + (* (string-to-number hour) 3600000) + (* (string-to-number minute) 60000) + (* (string-to-number seconds) 1000) + (* (string-to-number (concat "0." milliseconds)) 1000)))) + +(defun subed-ffprobe-duration-ms (filename) + "Use ffprobe to get duration of audio stream in milliseconds of FILENAME." + (let ((json + (json-read-from-string + (with-temp-buffer + (call-process + subed-ffprobe-executable nil t nil + "-v" "error" + "-print_format" "json" + "-show_streams" + "-show_format" + filename) + (buffer-string))))) + ;; Check that the file has at least one audio stream. + (when (eq (seq-find + (lambda (stream) + (equal (alist-get 'codec_type stream) "audio")) + (alist-get 'streams json)) + 0) + (error "The provided file doesn't have an audio stream.")) + (cond + ;; If the file has one stream and it is an audio stream, we can + ;; get the duration from format=duration + ;; + ;; nb_streams equals the number of streams in the media file. + ((and (eq (alist-get 'nb_streams (alist-get 'format json)) 1) + (equal (alist-get + 'codec_type + (seq-first (alist-get 'streams json))) + "audio")) + (* 1000 (string-to-number + (alist-get 'duration (alist-get 'format json))))) + ;; If the file has more than one stream and only one audio + ;; stream, return the duration of the audio stream. + ((and (> (alist-get 'nb_streams (alist-get 'format json)) 1) + (eq (length (seq-filter + (lambda (stream) + (equal (alist-get 'codec_type stream) "audio")) + (alist-get 'streams json))) + 1)) + (cond + ((or + (string-match "\\.mkv\\'" filename) + (string-match "\\.webm\\'" filename)) + (subed-convert-ffprobe-tags-duration-to-ms + (alist-get + 'DURATION + (alist-get + 'tags + (seq-find + (lambda (stream) + (equal (alist-get 'codec_type stream) "audio")) + (alist-get 'streams json)))))) + (t + (* 1000 + (string-to-number + (alist-get + 'duration + (seq-find + (lambda (stream) + (equal (alist-get 'codec_type stream) "audio")) + (alist-get 'streams json)))))))) + ;; TODO: Some media files might have multiple audio streams + ;; (e.g. multiple languages). When the media file has multiple + ;; audio streams, prompt the user for the audio stream. The audio + ;; stream selected by the user must be stored in a buffer-local + ;; variable so that ffmpeg knows the audio stream from which the + ;; waveforms are created. + ))) + +(defun subed-clear-file-duration-ms-cache (&rest _) + "Clear `subed-file-duration-ms-cache'." + (setq subed-file-duration-ms-cache nil)) + +(defun subed-file-duration-ms (&optional filename refresh-cache) + "Return the duration of FILENAME in milliseconds." + (setq filename (or filename (subed-media-file))) + (if refresh-cache (setq subed-file-duration-ms-cache nil)) + (cond + ((numberp subed-file-duration-ms-cache) + (when (> subed-file-duration-ms-cache 0) + subed-file-duration-ms-cache)) + (subed-ffprobe-executable + (setq subed-file-duration-ms-cache + (subed-ffprobe-duration-ms + filename)) + (if (and (numberp subed-file-duration-ms-cache) + (> subed-file-duration-ms-cache 0)) + subed-file-duration-ms-cache + ;; mark as invalid + (warn "Could not get file duration for %s" filename) + (setq subed-file-duration-ms-cache -1) + nil)))) + +(defun subed-insert-subtitle-for-whole-file () + "Insert a subtitle that starts at 0 until the end of the current file. + +This might make it easier to type subtitles from scratch. Use this +function to start with a subtitle for the whole duration. It may be a +good idea to enable pausing while typing with +`subed-toggle-pause-while-typing'. + +As you type each subtitle's worth of text, use `subed-split-subtitle' +to start a new subtitle at the current playback position. + +If there is an error running `subed-ffprobe-executable' points to ffprobe, +use one day as the duration instead." + (interactive) + (when (string= (string-trim (buffer-string)) "") + (subed-auto-insert)) + (subed-append-subtitle + nil + 0 + (condition-case nil + (and (subed-media-file) + (subed-file-duration-ms (subed-media-file))) + (error (* 24 60 60 1000))))) + (provide 'subed-common) ;;; subed-common.el ends here diff --git a/subed/subed-config.el b/subed/subed-config.el index 1397f04a1f..563d187c33 100644 --- a/subed/subed-config.el +++ b/subed/subed-config.el @@ -228,6 +228,11 @@ doing so." "Remembers whether point-to-player was originally enabled by the user. Used when temporarily disabling point-to-player sync.") +(defcustom subed-ffprobe-executable "ffprobe" + "Path to the FFprobe executable used for measuring file duration." + :type 'file + :group 'subed) + (defcustom subed-mpv-socket-dir (concat (temporary-file-directory) "subed") "Path to Unix IPC socket that is passed to mpv's --input-ipc-server option." :type 'file diff --git a/subed/subed-waveform.el b/subed/subed-waveform.el index 3ed7ac6209..ddb3324f55 100644 --- a/subed/subed-waveform.el +++ b/subed/subed-waveform.el @@ -140,11 +140,6 @@ SVG parameters of the displayed bars. Every bar must have a unique :value-type (plist :key-type symbol :value-type string)) :group 'subed-waveform) -(defcustom subed-waveform-ffprobe-executable "ffprobe" - "Path to the FFprobe executable used for measuring file duration." - :type 'file - :group 'subed-waveform) - (defcustom subed-waveform-preview-msecs-before 2000 "Prelude in milliseconds displaying subtitle waveform." :type 'integer @@ -249,121 +244,12 @@ WIDTH and HEIGHT are given in pixels." width height) "[bg][fg]overlay=format=auto,drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=1:color=#9cf42f")) -(defvar-local subed-waveform-file-duration-ms-cache nil "If non-nil, duration of current file in milliseconds.") - -(defun subed-waveform-convert-ffprobe-tags-duration-to-ms (duration) - "Return milliseconds as an integer for DURATION. - -DURATION must be a string of the format HH:MM:SS.MMMM. - -Example: - -00:00:03.003000000 -> 3003 -00:00:03.00370000 -> 3004" - (unless (string-match "\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\)\\.\\([0-9]+\\)" duration) - (error "The duration is not well formatted.")) - (let ((hour (match-string 1 duration)) - (minute (match-string 2 duration)) - (seconds (match-string 3 duration)) - (milliseconds (match-string 4 duration))) - (+ - (* (string-to-number hour) 3600000) - (* (string-to-number minute) 60000) - (* (string-to-number seconds) 1000) - (* (string-to-number (concat "0." milliseconds)) 1000)))) - -(defun subed-waveform-ffprobe-duration-ms (filename) - "Use ffprobe to get duration of audio stream in milliseconds of FILENAME." - (let ((json - (json-read-from-string - (with-temp-buffer - (call-process - subed-waveform-ffprobe-executable nil t nil - "-v" "error" - "-print_format" "json" - "-show_streams" - "-show_format" - filename) - (buffer-string))))) - ;; Check that the file has at least one audio stream. - (when (eq (seq-find - (lambda (stream) - (equal (alist-get 'codec_type stream) "audio")) - (alist-get 'streams json)) - 0) - (error "The provided file doesn't have an audio stream.")) - (cond - ;; If the file has one stream and it is an audio stream, we can - ;; get the duration from format=duration - ;; - ;; nb_streams equals the number of streams in the media file. - ((and (eq (alist-get 'nb_streams (alist-get 'format json)) 1) - (equal (alist-get - 'codec_type - (seq-first (alist-get 'streams json))) - "audio")) - (* 1000 (string-to-number - (alist-get 'duration (alist-get 'format json))))) - ;; If the file has more than one stream and only one audio - ;; stream, return the duration of the audio stream. - ((and (> (alist-get 'nb_streams (alist-get 'format json)) 1) - (eq (length (seq-filter - (lambda (stream) - (equal (alist-get 'codec_type stream) "audio")) - (alist-get 'streams json))) - 1)) - (cond - ((or - (string-match "\\.mkv\\'" filename) - (string-match "\\.webm\\'" filename)) - (subed-waveform-convert-ffprobe-tags-duration-to-ms - (alist-get - 'DURATION - (alist-get - 'tags - (seq-find - (lambda (stream) - (equal (alist-get 'codec_type stream) "audio")) - (alist-get 'streams json)))))) - (t - (* 1000 - (string-to-number - (alist-get - 'duration - (seq-find - (lambda (stream) - (equal (alist-get 'codec_type stream) "audio")) - (alist-get 'streams json)))))))) - ;; TODO: Some media files might have multiple audio streams - ;; (e.g. multiple languages). When the media file has multiple - ;; audio streams, prompt the user for the audio stream. The audio - ;; stream selected by the user must be stored in a buffer-local - ;; variable so that ffmpeg knows the audio stream from which the - ;; waveforms are created. - ))) - -(defun subed-waveform-file-duration-ms (&optional filename) - "Return the duration of FILENAME in milliseconds." - (setq filename (or filename (subed-media-file))) - (cond - (subed-waveform-file-duration-ms-cache - (when (> subed-waveform-file-duration-ms-cache 0) - subed-waveform-file-duration-ms-cache)) - (subed-waveform-ffprobe-executable - (setq subed-waveform-file-duration-ms-cache - (subed-waveform-ffprobe-duration-ms - filename)) - (if (and (numberp subed-waveform-file-duration-ms-cache) - (> subed-waveform-file-duration-ms-cache 0)) - subed-waveform-file-duration-ms-cache - ;; mark as invalid - (warn "Could not get file duration for %s" filename) - (setq subed-waveform-file-duration-ms-cache -1) - nil)))) - -(defun subed-waveform-clear-file-duration-ms-cache (&rest _) - "Clear `subed-waveform-file-duration-ms-cache'." - (setq subed-waveform-file-duration-ms-cache nil)) +(make-obsolete-variable 'subed-waveform-ffprobe-executable 'subed-ffprobe-executable "1.2.22") +(make-obsolete-variable 'subed-waveform-file-duration-ms-cache 'subed-file-duration-ms-cache "1.2.22") +(make-obsolete 'subed-waveform-convert-ffprobe-tags-duration-to-ms 'subed-convert-ffprobe-tags-duration-to-ms "1.2.22") +(make-obsolete 'subed-waveform-ffprobe-duration-ms 'subed-ffprobe-duration-ms "1.2.22") +(make-obsolete 'subed-waveform-file-duration-ms 'subed-file-duration-ms "1.2.22") +(make-obsolete 'subed-waveform-clear-file-duration-ms-cache 'subed-clear-file-duration-ms-cache "1.2.22") ;; This should eventually be replaced with a hook. (with-eval-after-load 'subed-mpv diff --git a/subed/subed-word-data.el b/subed/subed-word-data.el index 1d8dc52952..2bb7f958cc 100644 --- a/subed/subed-word-data.el +++ b/subed/subed-word-data.el @@ -128,9 +128,10 @@ For now, only SRV2 and JSON files are supported." nil nil (lambda (f) - (string-match - "\\.json\\'\\|\\.srv2\\'" - f))))) + (or (file-directory-p f) + (string-match + "\\.json\\'\\|\\.srv2\\'" + f)))))) (subed-word-data--load (if (and (stringp file) (string-match "\\.json\\'" file)) (subed-word-data--extract-words-from-whisperx-json file) diff --git a/tests/test-subed-common.el b/tests/test-subed-common.el index a1d129db09..43d519381e 100644 --- a/tests/test-subed-common.el +++ b/tests/test-subed-common.el @@ -27,6 +27,77 @@ Baz. (subed-srt-mode) (progn ,@body))) +(cl-defun create-sample-media-file (&key + path + duration-video-stream + duration-audio-stream) + "Create a sample media file. + +PATH is the absolute path for the output file. It must be a +string. + +AUDIO-DURATION is the duration in seconds for the audio +stream. It must be a number. + +VIDEO-DURATION is the duration in seconds for the video stream. It +must be a number." + (apply 'call-process + ;; The ffmpeg command shown below can create files with the + ;; extensions shown below (tested using ffmpeg version + ;; 4.4.2-0ubuntu0.22.04.1) + ;; + audio extensions: wav ogg mp3 opus m4a + ;; + video extensions: mkv mp4 webm avi ts ogv" + "ffmpeg" + nil + nil + nil + "-v" "error" + "-y" + (append + ;; Create the video stream + (when duration-video-stream + (list "-f" "lavfi" "-i" (format "testsrc=size=100x100:duration=%d" duration-video-stream))) + ;; Create the audio stream + (when duration-audio-stream + (list "-f" "lavfi" "-i" (format "sine=frequency=1000:duration=%d" duration-audio-stream))) + (list path))) + path) + +(defmacro test-subed-extension (extension &optional has-video) + `(it ,(if has-video + (format "reports the duration of %s even with a longer video stream" extension) + (format "reports the duration of %s" extension)) + (let* (;; `duration-audio-stream' is the duration in seconds for + ;; the media file that is used inside the tests. When + ;; `duration-audio-stream' is an integer, ffprobe might + ;; report a duration that is slightly greater, so we can't + ;; expect the duration reported by ffprobe to be equal to + ;; the duration that we passed to ffmpeg when creating the + ;; sample media file. For this reason, we define the + ;; variables `duration-lower-boundary' and + ;; `duration-upper-boundary' to set a tolerance to the + ;; reported value by ffprobe. + ;; + ;; When `duration-audio-stream' changes, the variables + ;; `duration-lower-boundary' and + ;; `duration-upper-boundary' should be set accordingly." + (duration-audio-stream 3) + (duration-video-stream 5) + (duration-lower-boundary 3000) + (duration-upper-boundary 4000) + (filename (make-temp-file "test-subed-a" nil ,extension)) + (file + (create-sample-media-file + :path filename + :duration-audio-stream duration-audio-stream + :duration-video-stream ,(if has-video + 'duration-video-stream + nil))) + (duration-ms (subed-ffprobe-duration-ms filename))) + (expect duration-ms :to-be-weakly-greater-than duration-lower-boundary) + (expect duration-ms :to-be-less-than duration-upper-boundary) + (delete-file filename)))) + (describe "subed-common" (describe "Iterating over subtitles" (describe "without providing beginning and end" @@ -3951,4 +4022,64 @@ Baz. (find-file file) (expect major-mode :to-equal 'subed-srt-mode) (subed-mpv-kill) - (delete-file file))))) + (delete-file file)))) + + (describe "Creating a subtitle that spans the file" + (it "uses the file duration." + (let* ((filename (make-temp-file "test-subed-a" nil ".opus")) + (file (create-sample-media-file + :path filename + :duration-audio-stream 2))) + (with-temp-srt-buffer + (setq subed-mpv-media-file filename) + (subed-insert-subtitle-for-whole-file) + (let ((list (subed-subtitle-list))) + (expect (length list) :to-equal 1) + (expect (elt (car list) 1) :to-equal 0) + (expect (elt (car list) 2) :to-be-weakly-greater-than 1900) + (expect (elt (car list) 2) :to-be-weakly-less-than 2100))) ; some tolerance + (delete-file filename)))) + + (describe "Get duration in milliseconds of a file with a single audio stream" + (let (;; `duration-audio-stream' is the duration in seconds for + ;; the media file that is used inside the tests. When + ;; `duration-audio-stream' is an integer, ffprobe might + ;; report a duration that is slightly greater, so we can't + ;; expect the duration reported by ffprobe to be equal to + ;; the duration that we passed to ffmpeg when creating the + ;; sample media file. For this reason, we define the + ;; variables `duration-lower-boundary' and + ;; `duration-upper-boundary' to set a tolerance to the + ;; reported value by ffprobe. + ;; + ;; When `duration-audio-stream' changes, the variables + ;; `duration-lower-boundary' and + ;; `duration-upper-boundary' should be set accordingly." + (duration-audio-stream "3") + (duration-lower-boundary 3000) + (duration-upper-boundary 4000)) + (describe "audio file" + (test-subed-extension ".wav") + (test-subed-extension ".ogg") + (test-subed-extension ".mp3") + (test-subed-extension ".opus") + (test-subed-extension ".m4a")) + (describe "video format with just audio" + (test-subed-extension ".mkv") + (test-subed-extension ".mp4") + (test-subed-extension ".webm") + (test-subed-extension ".avi") + (test-subed-extension ".ts") + (test-subed-extension ".ogv")))) + (describe "Get duration in milliseconds of a file with 1 video and 1 audio stream" + ;; In this group of test cases, we want the duration of the audio + ;; stream to be shorter than the duration of the video stream, so + ;; that we can make sure that subed-waveform-ffprobe-duration-ms + ;; specifically gets the duration of the audio stream. + (test-subed-extension ".mkv" t) + (test-subed-extension ".mp4" t) + (test-subed-extension ".webm" t) + (test-subed-extension ".avi" t) + (test-subed-extension ".ts" t) + (test-subed-extension ".ogv" t)) + ) diff --git a/tests/test-subed-vtt.el b/tests/test-subed-vtt.el index f355e0632a..863269aa0a 100644 --- a/tests/test-subed-vtt.el +++ b/tests/test-subed-vtt.el @@ -1723,7 +1723,8 @@ Note this is a test 00:00:01.000 --> 00:00:01.000 another test ") - (expect (elt (car (subed-subtitle-list)) 3) :to-equal "Note this is a test"))))) + (let ((case-fold-search t)) + (expect (elt (car (subed-subtitle-list)) 3) :to-equal "Note this is a test")))))) (describe "Merging with next subtitle" (it "throws an error in an empty buffer." (with-temp-vtt-buffer diff --git a/tests/test-subed-waveform.el b/tests/test-subed-waveform.el index 86de9a6443..507c6cb5c7 100644 --- a/tests/test-subed-waveform.el +++ b/tests/test-subed-waveform.el @@ -2,117 +2,8 @@ (require 'subed-waveform) -(cl-defun create-sample-media-file (&key - path - duration-video-stream - duration-audio-stream) - "Create a sample media file. -PATH is the absolute path for the output file. It must be a -string. - -AUDIO-DURATION is the duration in seconds for the audio -stream. It must be a number. - -VIDEO-DURATION is the duration in seconds for the video stream. It -must be a number." - (apply 'call-process - ;; The ffmpeg command shown below can create files with the - ;; extensions shown below (tested using ffmpeg version - ;; 4.4.2-0ubuntu0.22.04.1) - ;; + audio extensions: wav ogg mp3 opus m4a - ;; + video extensions: mkv mp4 webm avi ts ogv" - "ffmpeg" - nil - nil - nil - "-v" "error" - "-y" - (append - ;; Create the video stream - (when duration-video-stream - (list "-f" "lavfi" "-i" (format "testsrc=size=100x100:duration=%d" duration-video-stream))) - ;; Create the audio stream - (when duration-audio-stream - (list "-f" "lavfi" "-i" (format "sine=frequency=1000:duration=%d" duration-audio-stream))) - (list path))) - path) - -(defmacro test-subed-extension (extension &optional has-video) - `(it ,(if has-video - (format "reports the duration of %s even with a longer video stream" extension) - (format "reports the duration of %s" extension)) - (let* (;; `duration-audio-stream' is the duration in seconds for - ;; the media file that is used inside the tests. When - ;; `duration-audio-stream' is an integer, ffprobe might - ;; report a duration that is slightly greater, so we can't - ;; expect the duration reported by ffprobe to be equal to - ;; the duration that we passed to ffmpeg when creating the - ;; sample media file. For this reason, we define the - ;; variables `duration-lower-boundary' and - ;; `duration-upper-boundary' to set a tolerance to the - ;; reported value by ffprobe. - ;; - ;; When `duration-audio-stream' changes, the variables - ;; `duration-lower-boundary' and - ;; `duration-upper-boundary' should be set accordingly." - (duration-audio-stream 3) - (duration-video-stream 5) - (duration-lower-boundary 3000) - (duration-upper-boundary 4000) - (filename (make-temp-file "test-subed-a" nil ,extension)) - (file - (create-sample-media-file - :path filename - :duration-audio-stream duration-audio-stream - :duration-video-stream ,(if has-video - 'duration-video-stream - nil))) - (duration-ms (subed-waveform-ffprobe-duration-ms filename))) - (expect duration-ms :to-be-weakly-greater-than duration-lower-boundary) - (expect duration-ms :to-be-less-than duration-upper-boundary) - (delete-file filename)))) (describe "waveform" - (describe "Get duration in milliseconds of a file with a single audio stream" - (let (;; `duration-audio-stream' is the duration in seconds for - ;; the media file that is used inside the tests. When - ;; `duration-audio-stream' is an integer, ffprobe might - ;; report a duration that is slightly greater, so we can't - ;; expect the duration reported by ffprobe to be equal to - ;; the duration that we passed to ffmpeg when creating the - ;; sample media file. For this reason, we define the - ;; variables `duration-lower-boundary' and - ;; `duration-upper-boundary' to set a tolerance to the - ;; reported value by ffprobe. - ;; - ;; When `duration-audio-stream' changes, the variables - ;; `duration-lower-boundary' and - ;; `duration-upper-boundary' should be set accordingly." - (duration-audio-stream "3") - (duration-lower-boundary 3000) - (duration-upper-boundary 4000)) - (describe "audio file" - (test-subed-extension ".wav") - (test-subed-extension ".ogg") - (test-subed-extension ".mp3") - (test-subed-extension ".opus") - (test-subed-extension ".m4a")) - (describe "video format with just audio" - (test-subed-extension ".mkv") - (test-subed-extension ".mp4") - (test-subed-extension ".webm") - (test-subed-extension ".avi") - (test-subed-extension ".ts") - (test-subed-extension ".ogv")))) - (describe "Get duration in milliseconds of a file with 1 video and 1 audio stream" - ;; In this group of test cases, we want the duration of the audio - ;; stream to be shorter than the duration of the video stream, so - ;; that we can make sure that subed-waveform-ffprobe-duration-ms - ;; specifically gets the duration of the audio stream. - (test-subed-extension ".mkv" t) - (test-subed-extension ".mp4" t) - (test-subed-extension ".webm" t) - (test-subed-extension ".avi" t) - (test-subed-extension ".ts" t) - (test-subed-extension ".ogv" t))) + ;; moved the duration tests to subed-common. + )