diff --git a/main.py b/main.py index 7e7db51..671c65e 100755 --- a/main.py +++ b/main.py @@ -39,7 +39,7 @@ from core.ffmpeg import ( build_ffmpeg_command, build_audio_extract_command, build_audio_clip_command, build_crossfade_merge_command, build_audio_delete_command, build_audio_silence_command, - build_audio_reverse_command, + build_audio_reverse_command, build_audio_heal_delete_command, audio_edit_filters, probe_duration, detect_hw_encoders, ) from core.db import ProcessedDB @@ -4248,8 +4248,10 @@ class AudioEditorDialog(QDialog): self._last_saved = None # path of the last Save-as (for library) self._play_proc = None # ffplay audition process self._clip_dur = 0.0 # true duration of the current version + self._join_preview: tuple[float, float] | None = None self._wave = AudioWaveform() + self._btn_heal_cut = QPushButton("Heal Cut") self._btn_delete = QPushButton("Delete") self._btn_silence = QPushButton("Silence") self._btn_reverse = QPushButton("Reverse") @@ -4260,7 +4262,8 @@ class AudioEditorDialog(QDialog): self._btn_play.setCheckable(True) self._btn_save_as = QPushButton("Save as…") self._btn_close = QPushButton("Close") - for b, slot in ((self._btn_delete, self._on_delete), + for b, slot in ((self._btn_heal_cut, self._on_heal_cut), + (self._btn_delete, self._on_delete), (self._btn_silence, self._on_silence), (self._btn_reverse, self._on_reverse), (self._btn_trim, self._on_trim), @@ -4274,6 +4277,7 @@ class AudioEditorDialog(QDialog): outer = QVBoxLayout(self) outer.addWidget(self._wave) ops = QHBoxLayout() + ops.addWidget(self._btn_heal_cut) for b in (self._btn_delete, self._btn_silence, self._btn_reverse, self._btn_trim): ops.addWidget(b) @@ -4326,7 +4330,8 @@ class AudioEditorDialog(QDialog): return None return (s, e) - def _apply_op(self, build_fn, whole_clip_ok: bool = True) -> None: + def _apply_op(self, build_fn, whole_clip_ok: bool = True, + after_success=None) -> None: se = self._selection_secs() if se is None: self._set_status("Select a region first") @@ -4354,6 +4359,8 @@ class AudioEditorDialog(QDialog): self._ver_idx += 1 self._temps.add(tmp) self._reload() + if after_success is not None: + after_success(s) self._set_status("") else: try: @@ -4362,6 +4369,13 @@ class AudioEditorDialog(QDialog): pass self._set_status("Edit failed") + def _prepare_join_preview(self, seam_t: float) -> None: + self._join_preview = (max(0.0, seam_t - 1.0), seam_t + 1.0) + + def _on_heal_cut(self): + self._apply_op(build_audio_heal_delete_command, whole_clip_ok=False, + after_success=self._prepare_join_preview) + def _on_delete(self): self._apply_op(build_audio_delete_command, whole_clip_ok=False) diff --git a/tests/test_ui_structure.py b/tests/test_ui_structure.py index 1918875..d6bb841 100644 --- a/tests/test_ui_structure.py +++ b/tests/test_ui_structure.py @@ -795,8 +795,8 @@ def test_audio_editor_dialog_scaffold(win): assert dlg._current() == "/nonexistent.wav" # widgets present assert dlg._wave is not None - for name in ("_btn_delete", "_btn_silence", "_btn_reverse", "_btn_trim", - "_btn_undo", "_btn_redo", "_btn_save_as"): + for name in ("_btn_heal_cut", "_btn_delete", "_btn_silence", "_btn_reverse", + "_btn_trim", "_btn_undo", "_btn_redo", "_btn_save_as"): assert isinstance(getattr(dlg, name), QPushButton) # undo disabled at the base version, redo disabled with no forward history assert not dlg._btn_undo.isEnabled() @@ -821,6 +821,28 @@ def test_editor_delete_builds_command(win, tmp_path, monkeypatch): assert seen["inp"] == str(src) and seen["s"] == 1.0 and seen["e"] == 3.0 +def test_editor_heal_cut_builds_command(win, tmp_path, monkeypatch): + import main as m, pytest + src = tmp_path / "v0.wav" + src.write_bytes(b"") + dlg = m.AudioEditorDialog(str(src), parent=win) + dlg._clip_dur = 6.0 + dlg._wave.set_view(0.0, 6.0) + dlg._wave.set_selection(2.0, 3.0) + seen = {} + class _Stop(Exception): + pass + def fake(inp, s, e, out): + seen.update(inp=inp, s=s, e=e, out=out) + raise _Stop + monkeypatch.setattr(m, "build_audio_heal_delete_command", fake) + with pytest.raises(_Stop): + dlg._on_heal_cut() + assert seen["inp"] == str(src) + assert seen["s"] == 2.0 and seen["e"] == 3.0 + assert seen["out"].endswith(".wav") + + def test_editor_op_no_region_is_safe(win, tmp_path, monkeypatch): import main as m src = tmp_path / "v0.wav"; src.write_bytes(b"")