emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[nongnu] elpa/subed f9252af5bb 3/5: New subed-align-region, subed-retime


From: ELPA Syncer
Subject: [nongnu] elpa/subed f9252af5bb 3/5: New subed-align-region, subed-retime-subtitles; move duration to subed-common
Date: Fri, 13 Dec 2024 10:02:35 -0500 (EST)

branch: elpa/subed
commit f9252af5bb4971d674fb10fb1f75a0f824d605c2
Author: Sacha Chua <sacha@sachachua.com>
Commit: Sacha Chua <sacha@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.
+  )



reply via email to

[Prev in Thread] Current Thread [Next in Thread]