From 4445f0e7f4237fb4f661640ec7be4467f43fe250 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 2 Jul 2026 00:07:35 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20audio=20extract=20honored=20a=20silent?= =?UTF-8?q?=20length=20clamp=20=E2=80=94=2030s=20near=20the=20end=20became?= =?UTF-8?q?=203s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _on_extract_audio clamped the duration to (timeline_duration - cursor), so with the playhead within the requested length of the end (or any under-reported duration) a 30s request was silently truncated to whatever remained — the user asked for 30s and got 3s with no indication why. Drop the clamp: pass the requested length straight to ffmpeg, which stops cleanly at end-of-file if the source is shorter. Then ffprobe the result and, when it comes up short, say so ("Saved 3.0s — source ended before 30.0s requested") instead of silently shrinking. When there's room, 30s now yields exactly 30s. Adds core.ffmpeg.probe_duration(). Verified end-to-end: a fitting request returns the exact length; a genuine near-end request returns the available audio (rc=0) and is reported as truncated. Co-Authored-By: Claude Opus 4.8 --- core/ffmpeg.py | 15 +++++++++++++++ main.py | 20 +++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core/ffmpeg.py b/core/ffmpeg.py index ae5995d..b0a8845 100644 --- a/core/ffmpeg.py +++ b/core/ffmpeg.py @@ -186,6 +186,21 @@ _AUDIO_CODEC_BY_EXT: dict[str, list[str]] = { } +def probe_duration(path: str) -> float | None: + """Return the media duration in seconds via ffprobe, or None on failure.""" + try: + r = subprocess.run( + [_bin("ffprobe"), "-v", "error", "-show_entries", "format=duration", + "-of", "default=nw=1:nk=1", path], + capture_output=True, text=True, timeout=30, + ) + if r.returncode == 0 and r.stdout.strip(): + return float(r.stdout.strip()) + except Exception: + pass + return None + + def build_audio_clip_command(input_path: str, start: float, duration: float, out_path: str) -> list[str]: """ffmpeg command to extract exactly *duration* seconds of audio starting diff --git a/main.py b/main.py index 79af109..9d4e3df 100755 --- a/main.py +++ b/main.py @@ -36,7 +36,7 @@ from core.paths import _bin, _log, build_export_path, build_sequence_dir, format from core.ffmpeg import ( _RATIOS, resolve_keyframe, apply_keyframes_to_jobs, build_ffmpeg_command, build_audio_extract_command, build_audio_clip_command, - detect_hw_encoders, + probe_duration, detect_hw_encoders, ) from core.db import ProcessedDB from core.annotations import remove_clip_annotation, upsert_clip_annotation @@ -6403,8 +6403,9 @@ class MainWindow(QMainWindow): return start = self._cursor dur = self._spn_audio_len.value() - if start + dur > self._timeline._duration + 0.05: - dur = max(0.05, self._timeline._duration - start) + # No clamping: pass the requested length straight to ffmpeg. It stops + # cleanly at end-of-file if the source is shorter, and we report the + # actual length afterwards so any truncation is visible, not silent. stem = os.path.splitext(os.path.basename(self._file_path))[0] default_name = f"{stem}_{start:.2f}-{start + dur:.2f}s.wav" default_dir = (self._settings.value("audio_extract_dir", "") @@ -6432,8 +6433,17 @@ class MainWindow(QMainWindow): self._btn_extract_audio.setEnabled(True) if proc is not None and proc.returncode == 0 and os.path.exists(path): self._settings.setValue("audio_extract_dir", os.path.dirname(path)) - self._show_status(f"Saved audio: {os.path.basename(path)}", 5000) - _log(f"Audio extracted: {path} ({dur:.2f}s @ {start:.2f}s)") + actual = probe_duration(path) + name = os.path.basename(path) + if actual is not None and actual < dur - 0.1: + self._show_status( + f"Saved {actual:.2f}s — source ended before {dur:.2f}s " + f"requested ({name})", 7000) + else: + self._show_status( + f"Saved audio: {name} ({(actual or dur):.2f}s)", 5000) + _log(f"Audio extracted: {path} (requested {dur:.2f}s @ {start:.2f}s, " + f"actual {actual if actual is not None else '?'})") else: err = (proc.stderr.strip().splitlines()[-1] if proc and proc.stderr else (err if proc is None else "ffmpeg failed"))