feat: add heal cut ffmpeg command
This commit is contained in:
@@ -314,6 +314,41 @@ def build_audio_delete_command(input_path: str, start: float, end: float,
|
|||||||
"-filter_complex", fc, "-map", "[out]", *codec, out_path]
|
"-filter_complex", fc, "-map", "[out]", *codec, out_path]
|
||||||
|
|
||||||
|
|
||||||
|
def _auto_heal_crossfade(start: float, end: float,
|
||||||
|
requested: float | None = None) -> float:
|
||||||
|
if end <= start:
|
||||||
|
raise ValueError("heal delete end must be greater than start")
|
||||||
|
if requested is not None:
|
||||||
|
fade = max(0.0, float(requested))
|
||||||
|
else:
|
||||||
|
fade = min(0.25, max(0.04, (end - start) * 0.25))
|
||||||
|
# Without knowing total duration, clamp only to available pre-roll.
|
||||||
|
fade = min(fade, max(0.0, float(start)))
|
||||||
|
return round(fade, 3)
|
||||||
|
|
||||||
|
|
||||||
|
def build_audio_heal_delete_command(input_path: str, start: float, end: float,
|
||||||
|
out_path: str,
|
||||||
|
crossfade: float | None = None) -> list[str]:
|
||||||
|
"""Remove [start, end] and heal the join with a short equal-power crossfade."""
|
||||||
|
if end <= start:
|
||||||
|
raise ValueError("heal delete end must be greater than start")
|
||||||
|
s, e = round(start, 3), round(end, 3)
|
||||||
|
xf = _auto_heal_crossfade(s, e, crossfade)
|
||||||
|
ext = os.path.splitext(out_path)[1].lower()
|
||||||
|
codec = _AUDIO_CODEC_BY_EXT.get(ext, [])
|
||||||
|
if xf <= 0:
|
||||||
|
fc = (f"[0]atrim=end={s},asetpts=PTS-STARTPTS[a];"
|
||||||
|
f"[0]atrim=start={e},asetpts=PTS-STARTPTS[b];"
|
||||||
|
f"[a][b]concat=n=2:v=0:a=1[out]")
|
||||||
|
else:
|
||||||
|
fc = (f"[0]atrim=end={s},asetpts=PTS-STARTPTS[a];"
|
||||||
|
f"[0]atrim=start={e},asetpts=PTS-STARTPTS[b];"
|
||||||
|
f"[a][b]acrossfade=d={xf}:c1=qsin:c2=qsin[out]")
|
||||||
|
return [_bin("ffmpeg"), "-y", "-i", input_path,
|
||||||
|
"-filter_complex", fc, "-map", "[out]", *codec, out_path]
|
||||||
|
|
||||||
|
|
||||||
def build_audio_silence_command(input_path: str, start: float, end: float,
|
def build_audio_silence_command(input_path: str, start: float, end: float,
|
||||||
out_path: str) -> list[str]:
|
out_path: str) -> list[str]:
|
||||||
"""Silence the [start, end] region (volume=0 gated by an enable expr)."""
|
"""Silence the [start, end] region (volume=0 gated by an enable expr)."""
|
||||||
|
|||||||
@@ -231,6 +231,42 @@ def test_audio_delete_empty_head():
|
|||||||
assert "atrim=end=0.0" in fc and "atrim=start=2.0" in fc
|
assert "atrim=end=0.0" in fc and "atrim=start=2.0" in fc
|
||||||
|
|
||||||
|
|
||||||
|
def test_audio_heal_delete_command_crossfades_join():
|
||||||
|
from core.ffmpeg import build_audio_heal_delete_command
|
||||||
|
cmd = build_audio_heal_delete_command("/in.wav", 2.0, 4.0, "/o/o.wav", crossfade=0.1)
|
||||||
|
assert cmd[0] == "ffmpeg"
|
||||||
|
assert cmd.count("-i") == 1
|
||||||
|
fc = cmd[cmd.index("-filter_complex") + 1]
|
||||||
|
assert "atrim=end=2.0" in fc
|
||||||
|
assert "atrim=start=4.0" in fc
|
||||||
|
assert "acrossfade=d=0.1:c1=qsin:c2=qsin[out]" in fc
|
||||||
|
assert cmd[cmd.index("-map") + 1] == "[out]"
|
||||||
|
assert "pcm_s16le" in cmd
|
||||||
|
assert cmd[-1] == "/o/o.wav"
|
||||||
|
|
||||||
|
|
||||||
|
def test_audio_heal_delete_command_auto_crossfade_clamped():
|
||||||
|
from core.ffmpeg import build_audio_heal_delete_command
|
||||||
|
cmd = build_audio_heal_delete_command("/in.wav", 10.0, 12.0, "/o/o.mp3")
|
||||||
|
fc = cmd[cmd.index("-filter_complex") + 1]
|
||||||
|
assert "acrossfade=d=0.25:c1=qsin:c2=qsin[out]" in fc
|
||||||
|
assert "libmp3lame" in cmd
|
||||||
|
|
||||||
|
|
||||||
|
def test_audio_heal_delete_command_near_start_shortens_crossfade():
|
||||||
|
from core.ffmpeg import build_audio_heal_delete_command
|
||||||
|
cmd = build_audio_heal_delete_command("/in.wav", 0.03, 1.0, "/o/o.wav")
|
||||||
|
fc = cmd[cmd.index("-filter_complex") + 1]
|
||||||
|
assert "acrossfade=d=0.03:c1=qsin:c2=qsin[out]" in fc
|
||||||
|
|
||||||
|
|
||||||
|
def test_audio_heal_delete_command_rejects_invalid_region():
|
||||||
|
import pytest
|
||||||
|
from core.ffmpeg import build_audio_heal_delete_command
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
build_audio_heal_delete_command("/in.wav", 3.0, 3.0, "/o/o.wav")
|
||||||
|
|
||||||
|
|
||||||
# --- ProcessedDB ---
|
# --- ProcessedDB ---
|
||||||
|
|
||||||
def test_db_add_and_get_markers():
|
def test_db_add_and_get_markers():
|
||||||
|
|||||||
Reference in New Issue
Block a user