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 hashlib
import os import os
import subprocess import subprocess
import warnings
import numpy as np import numpy as np
from .paths import _bin, _log from .paths import _bin, _log
@@ -22,7 +21,10 @@ def _load_audio_ffmpeg(path: str, sr: int = _SR) -> np.ndarray:
"-loglevel", "error", "-loglevel", "error",
"pipe:1", "pipe:1",
] ]
try:
proc = subprocess.run(cmd, capture_output=True, timeout=300) 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: if proc.returncode != 0:
raise RuntimeError(f"ffmpeg failed: {proc.stderr.decode().strip()}") raise RuntimeError(f"ffmpeg failed: {proc.stderr.decode().strip()}")
return np.frombuffer(proc.stdout, dtype=np.float32) 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 # Don't let manual negatives overlap with positives
manual_neg_times -= pos_times 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() neg_times = set()
if all_gt and neg_margin > 0:
for t in range(0, int(duration - _WINDOW), 4): 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) neg_times.add(t)
all_times = sorted(pos_times | neg_times | manual_neg_times) 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.execute("DELETE FROM processed WHERE output_path = ?", (output_path,))
self._con.commit() self._con.commit()
def get_group(self, output_path: str) -> list[str]: def get_group(self, output_path: str, profile: str = "") -> list[str]:
"""Return all output_paths sharing the same (filename, start_time) as *output_path*.""" """Return all output_paths sharing the same (filename, start_time, profile) as *output_path*."""
if not self._enabled: if not self._enabled:
return [] return []
row = self._con.execute( 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,), (output_path,),
).fetchone() ).fetchone()
if not row: if not row:
return [] return []
filename, start_time, row_profile = row
p = profile or row_profile
rows = self._con.execute( rows = self._con.execute(
"SELECT output_path FROM processed" "SELECT output_path FROM processed"
" WHERE filename = ? AND start_time = ? ORDER BY output_path", " WHERE filename = ? AND start_time = ? AND profile = ? ORDER BY output_path",
(row[0], row[1]), (filename, start_time, p),
).fetchall() ).fetchall()
return [r[0] for r in rows] return [r[0] for r in rows]
def delete_group(self, output_path: str) -> list[str]: def delete_group(self, output_path: str, profile: str = "") -> list[str]:
"""Delete all rows sharing the same (filename, start_time) as *output_path*. """Delete all rows sharing the same (filename, start_time, profile) as *output_path*.
Returns list of deleted output_paths.""" Returns list of deleted output_paths."""
if not self._enabled: if not self._enabled:
return [] return []
with self._lock: with self._lock:
row = self._con.execute( 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,), (output_path,),
).fetchone() ).fetchone()
if not row: if not row:
return [] 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( paths = [r[0] for r in self._con.execute(
"SELECT output_path FROM processed WHERE filename = ? AND start_time = ?", "SELECT output_path FROM processed"
(filename, start_time), " WHERE filename = ? AND start_time = ? AND profile = ?",
(filename, start_time, p),
).fetchall()] ).fetchall()]
self._con.execute( self._con.execute(
"DELETE FROM processed WHERE filename = ? AND start_time = ?", "DELETE FROM processed WHERE filename = ? AND start_time = ? AND profile = ?",
(filename, start_time), (filename, start_time, p),
) )
self._con.commit() self._con.commit()
return paths return paths
+8 -4
View File
@@ -3068,7 +3068,7 @@ class MainWindow(QMainWindow):
filename, profile, model_label, regions) filename, profile, model_label, regions)
# If this is the currently loaded file, update the panel # If this is the currently loaded file, update the panel
if self._file_path and os.path.basename(self._file_path) == filename: 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._timeline.set_scan_regions(regions)
self._scan_all_next() self._scan_all_next()
@@ -3303,11 +3303,13 @@ class MainWindow(QMainWindow):
short_side = self._spn_resize.value() or None short_side = self._spn_resize.value() or None
self._export_short_side = short_side self._export_short_side = short_side
self._export_portrait = "Off" self._export_portrait = "Off"
self._export_crop_center = 0.5
self._export_format = fmt self._export_format = fmt
self._export_clip_count = 1 self._export_clip_count = 1
self._export_spread = 0 self._export_spread = 0
self._export_folder = folder self._export_folder = folder
self._export_folder_suffix = "" self._export_folder_suffix = ""
self._export_profile = self._profile
hw_on = self._chk_hw.isChecked() and self._hw_encoders hw_on = self._chk_hw.isChecked() and self._hw_encoders
encoder = self._hw_encoders[0] if hw_on else "libx264" encoder = self._hw_encoders[0] if hw_on else "libx264"
@@ -3354,7 +3356,7 @@ class MainWindow(QMainWindow):
fmt=self._export_format, fmt=self._export_format,
clip_count=1, clip_count=1,
spread=0, spread=0,
profile=self._profile, profile=self._export_profile,
source_path=self._file_path, source_path=self._file_path,
) )
upsert_clip_annotation(self._export_folder, path, label) upsert_clip_annotation(self._export_folder, path, label)
@@ -3544,11 +3546,13 @@ class MainWindow(QMainWindow):
self._export_cursor = self._cursor self._export_cursor = self._cursor
self._export_short_side = short_side self._export_short_side = short_side
self._export_portrait = self._cmb_portrait.currentText() self._export_portrait = self._cmb_portrait.currentText()
self._export_crop_center = self._crop_center
self._export_format = fmt self._export_format = fmt
self._export_clip_count = self._spn_clips.value() self._export_clip_count = self._spn_clips.value()
self._export_spread = self._spn_spread.value() self._export_spread = self._spn_spread.value()
self._export_folder = folder self._export_folder = folder
self._export_folder_suffix = folder_suffix self._export_folder_suffix = folder_suffix
self._export_profile = self._profile
self._btn_export.setEnabled(False) self._btn_export.setEnabled(False)
self._set_subprofile_btns_enabled(False) self._set_subprofile_btns_enabled(False)
@@ -3595,11 +3599,11 @@ class MainWindow(QMainWindow):
category=category, category=category,
short_side=self._export_short_side, short_side=self._export_short_side,
portrait_ratio=portrait, portrait_ratio=portrait,
crop_center=self._crop_center, crop_center=self._export_crop_center,
fmt=self._export_format, fmt=self._export_format,
clip_count=self._export_clip_count, clip_count=self._export_clip_count,
spread=self._export_spread, spread=self._export_spread,
profile=self._profile, profile=self._export_profile,
source_path=self._file_path, source_path=self._file_path,
) )
upsert_clip_annotation(self._export_folder, path, label) upsert_clip_annotation(self._export_folder, path, label)