feat: add Delete button to remove exports from disk, DB, and dataset.json

Targets last export by default; targets marker-selected clip when one is
active (shown as "Delete <name>"). Clears on cursor move if no last export.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 14:23:16 +02:00
parent 8bc42cabd9
commit 47ea6199fa
+60
View File
@@ -6,6 +6,7 @@ import sys
import os import os
import re import re
import json import json
import shutil
import sqlite3 import sqlite3
import subprocess import subprocess
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -101,6 +102,23 @@ def build_annotation_json_path(folder: str) -> str:
return os.path.join(folder, "dataset.json") return os.path.join(folder, "dataset.json")
def remove_clip_annotation(folder: str, clip_path: str) -> None:
"""Remove the entry for *clip_path* from <folder>/dataset.json if present."""
json_path = build_annotation_json_path(folder)
if not os.path.exists(json_path):
return
abs_path = os.path.abspath(clip_path)
with open(json_path, "r", encoding="utf-8") as f:
try:
entries = json.load(f)
except (json.JSONDecodeError, ValueError):
return
entries = [e for e in entries if e.get("path") != abs_path]
with open(json_path, "w", encoding="utf-8") as f:
json.dump(entries, f, indent=2, ensure_ascii=False)
f.write("\n")
def upsert_clip_annotation(folder: str, clip_path: str, label: str) -> None: def upsert_clip_annotation(folder: str, clip_path: str, label: str) -> None:
"""Insert or update one entry in <folder>/dataset.json. """Insert or update one entry in <folder>/dataset.json.
@@ -1244,6 +1262,11 @@ class MainWindow(QMainWindow):
self._btn_export.setEnabled(False) self._btn_export.setEnabled(False)
self._btn_export.clicked.connect(self._on_export) self._btn_export.clicked.connect(self._on_export)
self._btn_delete = QPushButton("Delete")
self._btn_delete.setEnabled(False)
self._btn_delete.setToolTip("Delete last export (or selected marker) from disk, DB, and dataset.json")
self._btn_delete.clicked.connect(self._on_delete_export)
# Settings dialog # Settings dialog
self._settings_dialog = SettingsDialog(self) self._settings_dialog = SettingsDialog(self)
self._settings_dialog.venv_installed.connect(self._on_venv_installed) self._settings_dialog.venv_installed.connect(self._on_venv_installed)
@@ -1277,6 +1300,7 @@ class MainWindow(QMainWindow):
transport_row.addWidget(self._cmb_category) transport_row.addWidget(self._cmb_category)
transport_row.addWidget(self._lbl_next) transport_row.addWidget(self._lbl_next)
transport_row.addWidget(self._btn_export) transport_row.addWidget(self._btn_export)
transport_row.addWidget(self._btn_delete)
# Row 2 — output path + encoding settings (bottom) # Row 2 — output path + encoding settings (bottom)
settings_row = QHBoxLayout() settings_row = QHBoxLayout()
@@ -1424,10 +1448,41 @@ class MainWindow(QMainWindow):
def _on_marker_clicked(self, start_time: float, output_path: str) -> None: def _on_marker_clicked(self, start_time: float, output_path: str) -> None:
self._overwrite_path = output_path self._overwrite_path = output_path
self._lbl_next.setText(f"{os.path.basename(output_path)}") self._lbl_next.setText(f"{os.path.basename(output_path)}")
self._btn_delete.setEnabled(True)
self._btn_delete.setText(f"Delete {os.path.basename(output_path)}")
self.statusBar().showMessage( self.statusBar().showMessage(
f"Overwrite mode: {os.path.basename(output_path)} — export to replace", 5000 f"Overwrite mode: {os.path.basename(output_path)} — export to replace", 5000
) )
def _on_delete_export(self) -> None:
target = self._overwrite_path or self._last_export_path
if not target:
return
name = os.path.basename(target)
# Delete from disk
if os.path.isdir(target):
shutil.rmtree(target, ignore_errors=True)
wav = target + ".wav"
if os.path.exists(wav):
os.remove(wav)
elif os.path.exists(target):
os.remove(target)
# Remove from DB and dataset.json
self._db.delete_by_output_path(target)
folder = self._txt_folder.text()
remove_clip_annotation(folder, target)
# Reset state
if self._overwrite_path:
self._overwrite_path = ""
if self._last_export_path == target:
self._last_export_path = ""
self._export_counter = max(1, self._export_counter - 1)
self._btn_delete.setEnabled(False)
self._btn_delete.setText("Delete")
self._update_next_label()
self._refresh_markers()
self.statusBar().showMessage(f"Deleted: {name}")
def _on_portrait_ratio_changed(self, text: str) -> None: def _on_portrait_ratio_changed(self, text: str) -> None:
ratio = None if text == "Off" else text ratio = None if text == "Off" else text
self._crop_bar.set_portrait_ratio(ratio) self._crop_bar.set_portrait_ratio(ratio)
@@ -1454,6 +1509,9 @@ class MainWindow(QMainWindow):
if self._overwrite_path: if self._overwrite_path:
self._overwrite_path = "" self._overwrite_path = ""
self._update_next_label() self._update_next_label()
if not self._last_export_path:
self._btn_delete.setEnabled(False)
self._btn_delete.setText("Delete")
if self._mpv.is_playing(): if self._mpv.is_playing():
self._mpv.play_loop(t, t + 8.0) self._mpv.play_loop(t, t + 8.0)
else: else:
@@ -1594,6 +1652,8 @@ class MainWindow(QMainWindow):
self._export_counter += 1 self._export_counter += 1
self._update_next_label() self._update_next_label()
self._btn_export.setEnabled(True) self._btn_export.setEnabled(True)
self._btn_delete.setEnabled(True)
self._btn_delete.setText("Delete")
self.statusBar().showMessage(f"Exported: {os.path.basename(path)}") self.statusBar().showMessage(f"Exported: {os.path.basename(path)}")
self._refresh_markers() self._refresh_markers()
self._playlist.mark_done(self._file_path) self._playlist.mark_done(self._file_path)