[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.
+ )
- [nongnu] elpa/subed updated (a80297b9c1 -> 00d9568b27), ELPA Syncer, 2024/12/13
- [nongnu] elpa/subed 3a0f267d58 2/5: Fix compiler warnings, ELPA Syncer, 2024/12/13
- [nongnu] elpa/subed 37a671b816 1/5: VTT: Require newlines before NOTE so that it can be within cue text, ELPA Syncer, 2024/12/13
- [nongnu] elpa/subed 00d9568b27 5/5: update README note, ELPA Syncer, 2024/12/13
- [nongnu] elpa/subed 545f170170 4/5: Show directories when doing subed-word-data-load-from-file, ELPA Syncer, 2024/12/13
- [nongnu] elpa/subed f9252af5bb 3/5: New subed-align-region, subed-retime-subtitles; move duration to subed-common,
ELPA Syncer <=