feat: extract audio alongside WebP image sequence
Adds build_audio_extract_command and runs it in ExportWorker after the frame sequence completes. Audio written to <sequence_dir>.wav (lossless pcm_s16le). Extraction failure (no audio stream) is silently ignored. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,20 @@ def build_ffmpeg_command(
|
|||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
def build_audio_extract_command(input_path: str, start: float, sequence_dir: str) -> list[str]:
|
||||||
|
"""Return an ffmpeg command that extracts audio to <sequence_dir>.wav."""
|
||||||
|
audio_path = sequence_dir + ".wav"
|
||||||
|
return [
|
||||||
|
"ffmpeg", "-y",
|
||||||
|
"-ss", str(start),
|
||||||
|
"-i", input_path,
|
||||||
|
"-t", "8",
|
||||||
|
"-vn",
|
||||||
|
"-c:a", "pcm_s16le",
|
||||||
|
audio_path,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def build_mask_output_dir(video_path: str) -> str:
|
def build_mask_output_dir(video_path: str) -> str:
|
||||||
"""Return path of mask output directory: <stem>_masks/ next to the video."""
|
"""Return path of mask output directory: <stem>_masks/ next to the video."""
|
||||||
p = Path(video_path)
|
p = Path(video_path)
|
||||||
@@ -232,6 +246,13 @@ class ExportWorker(QThread):
|
|||||||
)
|
)
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
|
if self._image_sequence:
|
||||||
|
audio_cmd = build_audio_extract_command(
|
||||||
|
self._input, self._start, self._output
|
||||||
|
)
|
||||||
|
subprocess.run(audio_cmd, capture_output=True, text=True, timeout=60)
|
||||||
|
# Audio extraction failure (e.g. no audio stream) is ignored —
|
||||||
|
# the frame sequence is the primary output.
|
||||||
self.finished.emit(self._output)
|
self.finished.emit(self._output)
|
||||||
else:
|
else:
|
||||||
self.error.emit(result.stderr[-500:])
|
self.error.emit(result.stderr[-500:])
|
||||||
|
|||||||
+24
-1
@@ -1,5 +1,5 @@
|
|||||||
import tempfile, os
|
import tempfile, os
|
||||||
from main import build_export_path, format_time, build_ffmpeg_command, build_mask_output_dir, build_sequence_dir
|
from main import build_export_path, format_time, build_ffmpeg_command, build_mask_output_dir, build_sequence_dir, build_audio_extract_command
|
||||||
from main import _normalize_filename, ProcessedDB
|
from main import _normalize_filename, ProcessedDB
|
||||||
|
|
||||||
|
|
||||||
@@ -184,6 +184,29 @@ def test_mask_output_dir_nested():
|
|||||||
assert build_mask_output_dir("/a/b/c/shot_042.mp4") == "/a/b/c/shot_042_masks"
|
assert build_mask_output_dir("/a/b/c/shot_042.mp4") == "/a/b/c/shot_042_masks"
|
||||||
|
|
||||||
|
|
||||||
|
# --- build_audio_extract_command ---
|
||||||
|
|
||||||
|
def test_audio_extract_output_path():
|
||||||
|
cmd = build_audio_extract_command("/in/v.mp4", 0.0, "/out/clip_001")
|
||||||
|
assert cmd[-1] == "/out/clip_001.wav"
|
||||||
|
|
||||||
|
def test_audio_extract_no_video():
|
||||||
|
cmd = build_audio_extract_command("/in/v.mp4", 0.0, "/out/clip_001")
|
||||||
|
assert "-vn" in cmd
|
||||||
|
|
||||||
|
def test_audio_extract_lossless_codec():
|
||||||
|
cmd = build_audio_extract_command("/in/v.mp4", 0.0, "/out/clip_001")
|
||||||
|
assert "-c:a" in cmd
|
||||||
|
assert cmd[cmd.index("-c:a") + 1] == "pcm_s16le"
|
||||||
|
|
||||||
|
def test_audio_extract_timing():
|
||||||
|
cmd = build_audio_extract_command("/in/v.mp4", 12.5, "/out/clip_001")
|
||||||
|
assert "-ss" in cmd
|
||||||
|
assert cmd[cmd.index("-ss") + 1] == "12.5"
|
||||||
|
assert "-t" in cmd
|
||||||
|
assert cmd[cmd.index("-t") + 1] == "8"
|
||||||
|
|
||||||
|
|
||||||
def test_build_sequence_dir_basic():
|
def test_build_sequence_dir_basic():
|
||||||
assert build_sequence_dir("/out", "clip", 1) == "/out/clip_001"
|
assert build_sequence_dir("/out", "clip", 1) == "/out/clip_001"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user