fix: audio extract honored a silent length clamp — 30s near the end became 3s
_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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
def build_audio_clip_command(input_path: str, start: float, duration: float,
|
||||||
out_path: str) -> list[str]:
|
out_path: str) -> list[str]:
|
||||||
"""ffmpeg command to extract exactly *duration* seconds of audio starting
|
"""ffmpeg command to extract exactly *duration* seconds of audio starting
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from core.paths import _bin, _log, build_export_path, build_sequence_dir, format
|
|||||||
from core.ffmpeg import (
|
from core.ffmpeg import (
|
||||||
_RATIOS, resolve_keyframe, apply_keyframes_to_jobs,
|
_RATIOS, resolve_keyframe, apply_keyframes_to_jobs,
|
||||||
build_ffmpeg_command, build_audio_extract_command, build_audio_clip_command,
|
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.db import ProcessedDB
|
||||||
from core.annotations import remove_clip_annotation, upsert_clip_annotation
|
from core.annotations import remove_clip_annotation, upsert_clip_annotation
|
||||||
@@ -6403,8 +6403,9 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
start = self._cursor
|
start = self._cursor
|
||||||
dur = self._spn_audio_len.value()
|
dur = self._spn_audio_len.value()
|
||||||
if start + dur > self._timeline._duration + 0.05:
|
# No clamping: pass the requested length straight to ffmpeg. It stops
|
||||||
dur = max(0.05, self._timeline._duration - start)
|
# 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]
|
stem = os.path.splitext(os.path.basename(self._file_path))[0]
|
||||||
default_name = f"{stem}_{start:.2f}-{start + dur:.2f}s.wav"
|
default_name = f"{stem}_{start:.2f}-{start + dur:.2f}s.wav"
|
||||||
default_dir = (self._settings.value("audio_extract_dir", "")
|
default_dir = (self._settings.value("audio_extract_dir", "")
|
||||||
@@ -6432,8 +6433,17 @@ class MainWindow(QMainWindow):
|
|||||||
self._btn_extract_audio.setEnabled(True)
|
self._btn_extract_audio.setEnabled(True)
|
||||||
if proc is not None and proc.returncode == 0 and os.path.exists(path):
|
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._settings.setValue("audio_extract_dir", os.path.dirname(path))
|
||||||
self._show_status(f"Saved audio: {os.path.basename(path)}", 5000)
|
actual = probe_duration(path)
|
||||||
_log(f"Audio extracted: {path} ({dur:.2f}s @ {start:.2f}s)")
|
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:
|
else:
|
||||||
err = (proc.stderr.strip().splitlines()[-1] if proc and proc.stderr
|
err = (proc.stderr.strip().splitlines()[-1] if proc and proc.stderr
|
||||||
else (err if proc is None else "ffmpeg failed"))
|
else (err if proc is None else "ffmpeg failed"))
|
||||||
|
|||||||
Reference in New Issue
Block a user