fix: 6 bugs — profile isolation, export stashing, auto-negative guard

- Stash profile and crop_center at export start for async safety
- Scope get_group/delete_group by profile to prevent cross-profile leaks
- Guard auto-negative sampling when no markers exist (prevents flood)
- Wrap ffmpeg subprocess with clean timeout error message
- Fix scan-all panel reload to use stashed profile, not live value
- Remove dead warnings import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 16:28:51 +02:00
parent 6870e5aaf3
commit 5a9e068903
3 changed files with 34 additions and 23 deletions
+6 -3
View File
@@ -3,7 +3,6 @@
import hashlib
import os
import subprocess
import warnings
import numpy as np
from .paths import _bin, _log
@@ -22,7 +21,10 @@ def _load_audio_ffmpeg(path: str, sr: int = _SR) -> np.ndarray:
"-loglevel", "error",
"pipe:1",
]
try:
proc = subprocess.run(cmd, capture_output=True, timeout=300)
except subprocess.TimeoutExpired:
raise RuntimeError(f"ffmpeg timed out (300s) on {os.path.basename(path)}")
if proc.returncode != 0:
raise RuntimeError(f"ffmpeg failed: {proc.stderr.decode().strip()}")
return np.frombuffer(proc.stdout, dtype=np.float32)
@@ -240,10 +242,11 @@ def _extract_w2v_targeted(y: np.ndarray, sr: int, gt_intense: list[float],
# Don't let manual negatives overlap with positives
manual_neg_times -= pos_times
# Auto negative windows: every 4s, far from any marker (skip if margin <= 0)
# Auto negative windows: every 4s, far from any marker (skip if margin <= 0 or no markers)
neg_times = set()
if all_gt and neg_margin > 0:
for t in range(0, int(duration - _WINDOW), 4):
if neg_margin > 0 and min((abs(t - g) for g in all_gt), default=9999) > neg_margin:
if min(abs(t - g) for g in all_gt) > neg_margin:
neg_times.add(t)
all_times = sorted(pos_times | neg_times | manual_neg_times)
+17 -13
View File
@@ -159,43 +159,47 @@ class ProcessedDB:
self._con.execute("DELETE FROM processed WHERE output_path = ?", (output_path,))
self._con.commit()
def get_group(self, output_path: str) -> list[str]:
"""Return all output_paths sharing the same (filename, start_time) as *output_path*."""
def get_group(self, output_path: str, profile: str = "") -> list[str]:
"""Return all output_paths sharing the same (filename, start_time, profile) as *output_path*."""
if not self._enabled:
return []
row = self._con.execute(
"SELECT filename, start_time FROM processed WHERE output_path = ?",
"SELECT filename, start_time, profile FROM processed WHERE output_path = ?",
(output_path,),
).fetchone()
if not row:
return []
filename, start_time, row_profile = row
p = profile or row_profile
rows = self._con.execute(
"SELECT output_path FROM processed"
" WHERE filename = ? AND start_time = ? ORDER BY output_path",
(row[0], row[1]),
" WHERE filename = ? AND start_time = ? AND profile = ? ORDER BY output_path",
(filename, start_time, p),
).fetchall()
return [r[0] for r in rows]
def delete_group(self, output_path: str) -> list[str]:
"""Delete all rows sharing the same (filename, start_time) as *output_path*.
def delete_group(self, output_path: str, profile: str = "") -> list[str]:
"""Delete all rows sharing the same (filename, start_time, profile) as *output_path*.
Returns list of deleted output_paths."""
if not self._enabled:
return []
with self._lock:
row = self._con.execute(
"SELECT filename, start_time FROM processed WHERE output_path = ?",
"SELECT filename, start_time, profile FROM processed WHERE output_path = ?",
(output_path,),
).fetchone()
if not row:
return []
filename, start_time = row
filename, start_time, row_profile = row
p = profile or row_profile
paths = [r[0] for r in self._con.execute(
"SELECT output_path FROM processed WHERE filename = ? AND start_time = ?",
(filename, start_time),
"SELECT output_path FROM processed"
" WHERE filename = ? AND start_time = ? AND profile = ?",
(filename, start_time, p),
).fetchall()]
self._con.execute(
"DELETE FROM processed WHERE filename = ? AND start_time = ?",
(filename, start_time),
"DELETE FROM processed WHERE filename = ? AND start_time = ? AND profile = ?",
(filename, start_time, p),
)
self._con.commit()
return paths
+8 -4
View File
@@ -3068,7 +3068,7 @@ class MainWindow(QMainWindow):
filename, profile, model_label, regions)
# If this is the currently loaded file, update the panel
if self._file_path and os.path.basename(self._file_path) == filename:
self._scan_panel.load_for_file(filename, self._profile)
self._scan_panel.load_for_file(filename, profile)
self._timeline.set_scan_regions(regions)
self._scan_all_next()
@@ -3303,11 +3303,13 @@ class MainWindow(QMainWindow):
short_side = self._spn_resize.value() or None
self._export_short_side = short_side
self._export_portrait = "Off"
self._export_crop_center = 0.5
self._export_format = fmt
self._export_clip_count = 1
self._export_spread = 0
self._export_folder = folder
self._export_folder_suffix = ""
self._export_profile = self._profile
hw_on = self._chk_hw.isChecked() and self._hw_encoders
encoder = self._hw_encoders[0] if hw_on else "libx264"
@@ -3354,7 +3356,7 @@ class MainWindow(QMainWindow):
fmt=self._export_format,
clip_count=1,
spread=0,
profile=self._profile,
profile=self._export_profile,
source_path=self._file_path,
)
upsert_clip_annotation(self._export_folder, path, label)
@@ -3544,11 +3546,13 @@ class MainWindow(QMainWindow):
self._export_cursor = self._cursor
self._export_short_side = short_side
self._export_portrait = self._cmb_portrait.currentText()
self._export_crop_center = self._crop_center
self._export_format = fmt
self._export_clip_count = self._spn_clips.value()
self._export_spread = self._spn_spread.value()
self._export_folder = folder
self._export_folder_suffix = folder_suffix
self._export_profile = self._profile
self._btn_export.setEnabled(False)
self._set_subprofile_btns_enabled(False)
@@ -3595,11 +3599,11 @@ class MainWindow(QMainWindow):
category=category,
short_side=self._export_short_side,
portrait_ratio=portrait,
crop_center=self._crop_center,
crop_center=self._export_crop_center,
fmt=self._export_format,
clip_count=self._export_clip_count,
spread=self._export_spread,
profile=self._profile,
profile=self._export_profile,
source_path=self._file_path,
)
upsert_clip_annotation(self._export_folder, path, label)