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:
+6
-3
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user