feat: disable/enable all clips in a subcategory folder at once
- Sub menu now has per-folder "Disable all" / "Enable all" buttons with live counts - relocate_video_clips accepts filename=None to move every video's clips in a folder - get_all_folder_counts returns profile-wide per-folder counts (incl _disabled) - Disable-all confirms before moving; both refresh markers + playlist counts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+33
-9
@@ -552,12 +552,30 @@ class ProcessedDB:
|
|||||||
counts[folder] = counts.get(folder, 0) + 1
|
counts[folder] = counts.get(folder, 0) + 1
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
def relocate_video_clips(self, filename: str, profile: str,
|
def get_all_folder_counts(self, profile: str = "default") -> dict[str, int]:
|
||||||
|
"""Return clip counts per export folder across all videos in *profile*.
|
||||||
|
|
||||||
|
Includes ``_disabled`` folders so callers can offer enable/disable.
|
||||||
|
"""
|
||||||
|
if not self._enabled:
|
||||||
|
return {}
|
||||||
|
rows = self._con.execute(
|
||||||
|
"SELECT output_path FROM processed WHERE profile = ?",
|
||||||
|
(profile,),
|
||||||
|
).fetchall()
|
||||||
|
counts: dict[str, int] = {}
|
||||||
|
for (op,) in rows:
|
||||||
|
folder = os.path.basename(os.path.dirname(os.path.dirname(op)))
|
||||||
|
counts[folder] = counts.get(folder, 0) + 1
|
||||||
|
return counts
|
||||||
|
|
||||||
|
def relocate_video_clips(self, filename: "str | None", profile: str,
|
||||||
src_folder_name: str,
|
src_folder_name: str,
|
||||||
dst_folder_name: str) -> int:
|
dst_folder_name: str) -> int:
|
||||||
"""Move *filename*'s clips from one export folder to a sibling folder.
|
"""Move clips from one export folder to a sibling folder.
|
||||||
|
|
||||||
Matches rows whose grandparent dir basename == *src_folder_name*,
|
Matches rows whose grandparent dir basename == *src_folder_name*
|
||||||
|
(restricted to *filename* when given, else every video in *profile*),
|
||||||
then moves each clip (and any ``.wav`` sidecar) on disk into a sibling
|
then moves each clip (and any ``.wav`` sidecar) on disk into a sibling
|
||||||
folder named *dst_folder_name*, migrates its dataset.json annotation,
|
folder named *dst_folder_name*, migrates its dataset.json annotation,
|
||||||
and rewrites output_path in the DB. Returns the number of clips moved.
|
and rewrites output_path in the DB. Returns the number of clips moved.
|
||||||
@@ -567,11 +585,17 @@ class ProcessedDB:
|
|||||||
import shutil
|
import shutil
|
||||||
from .annotations import remove_clip_annotation, upsert_clip_annotation
|
from .annotations import remove_clip_annotation, upsert_clip_annotation
|
||||||
|
|
||||||
rows = self._con.execute(
|
if filename is None:
|
||||||
"SELECT id, output_path, label FROM processed"
|
rows = self._con.execute(
|
||||||
" WHERE filename = ? AND profile = ?",
|
"SELECT id, output_path, label FROM processed WHERE profile = ?",
|
||||||
(filename, profile),
|
(profile,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
else:
|
||||||
|
rows = self._con.execute(
|
||||||
|
"SELECT id, output_path, label FROM processed"
|
||||||
|
" WHERE filename = ? AND profile = ?",
|
||||||
|
(filename, profile),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
moves: list[tuple[str, str]] = [] # (old_path, new_path)
|
moves: list[tuple[str, str]] = [] # (old_path, new_path)
|
||||||
updates: list[tuple[str, int]] = [] # (new_path, id)
|
updates: list[tuple[str, int]] = [] # (new_path, id)
|
||||||
@@ -628,7 +652,7 @@ class ProcessedDB:
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
_log(f"Relocated {len(updates)} clip(s) of {filename}: "
|
_log(f"Relocated {len(updates)} clip(s) of {filename or 'all videos'}: "
|
||||||
f"{src_folder_name} -> {dst_folder_name}")
|
f"{src_folder_name} -> {dst_folder_name}")
|
||||||
return len(updates)
|
return len(updates)
|
||||||
|
|
||||||
|
|||||||
@@ -5477,6 +5477,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._btn_hide_subcats.rect().bottomLeft()))
|
self._btn_hide_subcats.rect().bottomLeft()))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
counts = self._db.get_all_folder_counts(self._profile)
|
||||||
container = QWidget()
|
container = QWidget()
|
||||||
layout = QVBoxLayout(container)
|
layout = QVBoxLayout(container)
|
||||||
layout.setContentsMargins(8, 4, 8, 4)
|
layout.setContentsMargins(8, 4, 8, 4)
|
||||||
@@ -5492,10 +5493,33 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
checkboxes: list[tuple[str, QCheckBox]] = []
|
checkboxes: list[tuple[str, QCheckBox]] = []
|
||||||
for name in folders:
|
for name in folders:
|
||||||
|
row = QHBoxLayout()
|
||||||
cb = QCheckBox(name)
|
cb = QCheckBox(name)
|
||||||
cb.setChecked(name not in self._hidden_subcats)
|
cb.setChecked(name not in self._hidden_subcats)
|
||||||
cb.toggled.connect(lambda checked, n=name: self._on_subcat_toggled(n, checked))
|
cb.toggled.connect(lambda checked, n=name: self._on_subcat_toggled(n, checked))
|
||||||
layout.addWidget(cb)
|
row.addWidget(cb, 1)
|
||||||
|
btn_dis = QPushButton("Disable all")
|
||||||
|
btn_en = QPushButton("Enable all")
|
||||||
|
btn_dis.setFlat(True)
|
||||||
|
btn_en.setFlat(True)
|
||||||
|
|
||||||
|
def refresh_states(n=name, bd=btn_dis, be=btn_en):
|
||||||
|
c = self._db.get_all_folder_counts(self._profile)
|
||||||
|
active = c.get(n, 0)
|
||||||
|
disabled = c.get(n + "_disabled", 0)
|
||||||
|
bd.setEnabled(active > 0)
|
||||||
|
be.setEnabled(disabled > 0)
|
||||||
|
bd.setText(f"Disable all ({active})" if active else "Disable all")
|
||||||
|
be.setText(f"Enable all ({disabled})" if disabled else "Enable all")
|
||||||
|
|
||||||
|
btn_dis.clicked.connect(
|
||||||
|
lambda _=False, n=name, rs=refresh_states: self._disable_all_in_folder(n, rs))
|
||||||
|
btn_en.clicked.connect(
|
||||||
|
lambda _=False, n=name, rs=refresh_states: self._enable_all_in_folder(n, rs))
|
||||||
|
refresh_states()
|
||||||
|
row.addWidget(btn_dis)
|
||||||
|
row.addWidget(btn_en)
|
||||||
|
layout.addLayout(row)
|
||||||
checkboxes.append((name, cb))
|
checkboxes.append((name, cb))
|
||||||
|
|
||||||
def set_all(visible: bool):
|
def set_all(visible: bool):
|
||||||
@@ -5511,6 +5535,41 @@ class MainWindow(QMainWindow):
|
|||||||
menu.exec(self._btn_hide_subcats.mapToGlobal(
|
menu.exec(self._btn_hide_subcats.mapToGlobal(
|
||||||
self._btn_hide_subcats.rect().bottomLeft()))
|
self._btn_hide_subcats.rect().bottomLeft()))
|
||||||
|
|
||||||
|
def _disable_all_in_folder(self, folder: str, refresh_states=None) -> None:
|
||||||
|
"""Disable every video's clips in *folder* (move to {folder}_disabled)."""
|
||||||
|
active = self._db.get_all_folder_counts(self._profile).get(folder, 0)
|
||||||
|
if not active:
|
||||||
|
return
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self, "Disable all",
|
||||||
|
f"Disable all {active} clip(s) in '{folder}'?\n\n"
|
||||||
|
f"Files move to '{folder}_disabled' and are excluded from training.",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
|
)
|
||||||
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
|
return
|
||||||
|
n = self._db.relocate_video_clips(
|
||||||
|
None, self._profile, folder, folder + "_disabled")
|
||||||
|
if self._file_path:
|
||||||
|
self._refresh_markers()
|
||||||
|
self._refresh_playlist_checks()
|
||||||
|
if refresh_states:
|
||||||
|
refresh_states()
|
||||||
|
self._show_status(f"Disabled {n} clip(s) in {folder}", 4000)
|
||||||
|
|
||||||
|
def _enable_all_in_folder(self, folder: str, refresh_states=None) -> None:
|
||||||
|
"""Re-enable every video's clips for *folder* (move back from _disabled)."""
|
||||||
|
src = folder + "_disabled"
|
||||||
|
if not self._db.get_all_folder_counts(self._profile).get(src, 0):
|
||||||
|
return
|
||||||
|
n = self._db.relocate_video_clips(None, self._profile, src, folder)
|
||||||
|
if self._file_path:
|
||||||
|
self._refresh_markers()
|
||||||
|
self._refresh_playlist_checks()
|
||||||
|
if refresh_states:
|
||||||
|
refresh_states()
|
||||||
|
self._show_status(f"Re-enabled {n} clip(s) in {folder}", 4000)
|
||||||
|
|
||||||
def _on_subcat_toggled(self, name: str, checked: bool) -> None:
|
def _on_subcat_toggled(self, name: str, checked: bool) -> None:
|
||||||
if checked:
|
if checked:
|
||||||
self._hidden_subcats.discard(name)
|
self._hidden_subcats.discard(name)
|
||||||
|
|||||||
Reference in New Issue
Block a user