feat: auto-add extracted/edited clips to the library + library Edit opens the editor (v1.7)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 00:54:43 +02:00
co-authored by Claude Opus 4.8
parent f32d303d26
commit 0e82a82cde
3 changed files with 57 additions and 2 deletions
+6
View File
@@ -55,6 +55,12 @@ All clips are exactly 8 seconds — the standard length for foley sound datasets
- **Ops** — Delete / Silence / Reverse / Trim-to-selection, with undo/redo
- **Audition & Save** — play the result, then Save as (WAV/MP3/FLAC/…)
### Clip library
- **Library tab** — a persistent tab in the scan-results column that collects extracted audio clips (auto-added on extract + editor Save)
- **Add / drag-drop** — import existing audio files
- **Per clip** — Edit (opens the clip editor), Re-export (save-as / transcode), Play, Remove
### Audio scanning
- **Embedding models** — WAV2VEC2 (base/large), HuBERT (base/large/xlarge), BEATs
+26 -2
View File
@@ -4208,6 +4208,7 @@ class AudioEditorDialog(QDialog):
self._versions: list[str] = [path]
self._ver_idx = 0
self._temps: set[str] = set() # rendered version temps to clean up
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
@@ -4431,6 +4432,7 @@ class AudioEditorDialog(QDialog):
finally:
QApplication.restoreOverrideCursor()
if proc is not None and proc.returncode == 0 and os.path.exists(path):
self._last_saved = path
self._set_status(f"Saved: {os.path.basename(path)}")
else:
self._set_status("Save failed")
@@ -5308,6 +5310,10 @@ class MainWindow(QMainWindow):
self._scan_panel.regions_edited.connect(self._on_scan_regions_edited)
self._scan_panel.selection_changed.connect(self._update_scan_export_count)
self._scan_panel.loaded.connect(self._on_scan_panel_loaded)
# lambda re-resolves _open_audio_editor at emit time so tests can
# monkeypatch it (a bare bound-method connect binds at connect time)
self._scan_panel._library.edit_requested.connect(
lambda p: self._open_audio_editor(p))
self._sld_threshold.valueChanged.connect(self._on_threshold_changed)
# Menu bar — wires to the existing handler methods above. Built here,
@@ -5691,13 +5697,21 @@ class MainWindow(QMainWindow):
# ── Changelog ────────────────────────────────────────────
APP_VERSION = "1.6"
APP_VERSION = "1.7"
_SPLIT_HEADER_H = 22 # deck split-column header height (keep both deck spots in sync)
_WAVE_MIN_VIEW = 3.0 # minimum waveform view window (seconds)
_MERGE_CURVES = (("Triangular", "tri"), ("Exponential", "exp"),
("Logarithmic", "log"), ("Quarter sine", "qsin"),
("Half sine", "hsin"))
CHANGELOG: list[tuple[str, list[str]]] = [
("1.7", [
"<b>Clip library</b> — a persistent <b>Library</b> tab in the "
"scan-results column collects your extracted audio clips (and clips "
"you Save from the editor). Add files or drag them in; per clip: "
"<b>Edit</b> (opens the clip editor), <b>Re-export</b> (save-as / "
"transcode), <b>Play</b>, and <b>Remove</b>. Persists across "
"sessions and file switches.",
]),
("1.6", [
"<b>Destructive clip editor</b> — <b>✎ Edit clip…</b> in the Audio "
"tab opens the current area in an editor: drag to select a region "
@@ -7372,8 +7386,17 @@ class MainWindow(QMainWindow):
self._show_status("Could not prepare clip", 3000)
return
self._merge_temps.add(tmp) # reuse the close-time temp sweep
dlg = AudioEditorDialog(tmp, parent=self)
self._open_audio_editor(tmp)
def _open_audio_editor(self, path: str) -> None:
"""Open the destructive editor on *path*; if the user Saves-as, add the
result to the clip library."""
self._stop_audition()
dlg = AudioEditorDialog(path, parent=self)
dlg.exec()
saved = getattr(dlg, "_last_saved", None)
if saved:
self._scan_panel._library.add_clip(saved)
dlg.deleteLater()
def _merge_move(self, delta: int) -> None:
@@ -7743,6 +7766,7 @@ 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._scan_panel._library.add_clip(path)
actual = probe_duration(path)
name = os.path.basename(path)
if actual is not None and actual < dur - 0.1:
+25
View File
@@ -899,3 +899,28 @@ def test_scan_reload_selects_model_tab_not_library(win):
assert sp._tabs.currentIndex() == 1 # a model tab is selected
assert sp._current_table() is not None # regions resolve immediately
assert sp.current_model_name() == "EAT_LARGE"
def test_open_editor_adds_saved_clip_to_library(win, tmp_path, monkeypatch):
import main as m
saved = tmp_path / "edited.wav"; saved.write_bytes(b"")
class FakeDlg:
def __init__(self, path, parent=None):
self._last_saved = str(saved)
def exec(self): return 0
def deleteLater(self): pass
monkeypatch.setattr(m, "AudioEditorDialog", FakeDlg)
lib = win._scan_panel._library
lib._key = "audio_library_l3test" # throwaway, avoids polluting real settings
win._settings.setValue("audio_library_l3test", [])
lib._list.clear()
win._open_audio_editor("/x/in.wav")
assert str(saved) in lib.clips()
def test_library_edit_signal_opens_editor(win, tmp_path, monkeypatch):
import main as m
opened = []
monkeypatch.setattr(win, "_open_audio_editor", lambda p: opened.append(p))
win._scan_panel._library.edit_requested.emit("/some/clip.wav")
assert opened == ["/some/clip.wav"]