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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user