fix: bug audit — broken test imports, training data overlap, cleanup
- Fix test_utils.py importing build_annotation_json_path from main instead of core.annotations (all 59 tests pass now) - Fix get_training_data double-counting clips at same start_time in both positive and soft sets — subtract positive from soft - Add cancel_flag to train_classifier so training can be interrupted between videos (TrainWorker passes self as cancel_flag) - Remove orphaned core/export.py (was for deleted server API) - Remove stale Dockerfile and docker-compose.yml (referenced server) - Clean up leftover server/__pycache__ and client/ build artifacts - Add torch to requirements.txt (was only mentioned in comments) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
-13
@@ -1,13 +0,0 @@
|
|||||||
FROM nvidia/cuda:12.6.3-runtime-ubuntu24.04
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
python3 python3-pip ffmpeg \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY core/ core/
|
|
||||||
COPY server/ server/
|
|
||||||
RUN pip install --no-cache-dir --break-system-packages fastapi uvicorn[standard]
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
||||||
+6
-1
@@ -240,7 +240,8 @@ def train_classifier(video_infos: list[tuple[str, list[float], list[float]]],
|
|||||||
model_path: str | None = None,
|
model_path: str | None = None,
|
||||||
tolerance: float = 12.0,
|
tolerance: float = 12.0,
|
||||||
neg_margin: float = 120.0,
|
neg_margin: float = 120.0,
|
||||||
embed_model: str | None = None) -> dict:
|
embed_model: str | None = None,
|
||||||
|
cancel_flag: object = None) -> dict:
|
||||||
"""Train a classifier from labeled videos.
|
"""Train a classifier from labeled videos.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -248,6 +249,7 @@ def train_classifier(video_infos: list[tuple[str, list[float], list[float]]],
|
|||||||
model_path: if given, save model to this path
|
model_path: if given, save model to this path
|
||||||
tolerance/neg_margin: labeling parameters
|
tolerance/neg_margin: labeling parameters
|
||||||
embed_model: embedding model name (e.g. "HUBERT_BASE", "BEATS"), defaults to WAV2VEC2_BASE
|
embed_model: embedding model name (e.g. "HUBERT_BASE", "BEATS"), defaults to WAV2VEC2_BASE
|
||||||
|
cancel_flag: object with _cancel attribute; if set, training aborts early
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict with 'classifier', 'embed_model', and metadata, or None on failure.
|
dict with 'classifier', 'embed_model', and metadata, or None on failure.
|
||||||
@@ -257,6 +259,9 @@ def train_classifier(video_infos: list[tuple[str, list[float], list[float]]],
|
|||||||
all_X, all_y = [], []
|
all_X, all_y = [], []
|
||||||
|
|
||||||
for vi, (vpath, gt_intense, gt_soft) in enumerate(video_infos):
|
for vi, (vpath, gt_intense, gt_soft) in enumerate(video_infos):
|
||||||
|
if cancel_flag and getattr(cancel_flag, '_cancel', False):
|
||||||
|
_log("audio_scan: training cancelled")
|
||||||
|
return None
|
||||||
_log(f"audio_scan: training [{vi+1}/{len(video_infos)}] {os.path.basename(vpath)}")
|
_log(f"audio_scan: training [{vi+1}/{len(video_infos)}] {os.path.basename(vpath)}")
|
||||||
y, _ = librosa.load(vpath, sr=_SR, mono=True)
|
y, _ = librosa.load(vpath, sr=_SR, mono=True)
|
||||||
|
|
||||||
|
|||||||
@@ -283,6 +283,11 @@ class ProcessedDB:
|
|||||||
else:
|
else:
|
||||||
soft_by_video.setdefault(fn, set()).add(st)
|
soft_by_video.setdefault(fn, set()).add(st)
|
||||||
|
|
||||||
|
# Remove positive times from soft to avoid conflicting labels
|
||||||
|
for fn in pos_by_video:
|
||||||
|
if fn in soft_by_video:
|
||||||
|
soft_by_video[fn] -= pos_by_video[fn]
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for fn in pos_by_video:
|
for fn in pos_by_video:
|
||||||
sp = source_by_filename.get(fn, "")
|
sp = source_by_filename.get(fn, "")
|
||||||
|
|||||||
-127
@@ -1,127 +0,0 @@
|
|||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import threading
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from .ffmpeg import build_ffmpeg_command, build_audio_extract_command
|
|
||||||
from .paths import _log
|
|
||||||
|
|
||||||
|
|
||||||
class ExportRunner:
|
|
||||||
"""Run ffmpeg export jobs in a background thread pool.
|
|
||||||
|
|
||||||
Callbacks:
|
|
||||||
on_clip_done(path: str)
|
|
||||||
on_all_done()
|
|
||||||
on_error(msg: str)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
input_path: str,
|
|
||||||
jobs: list[tuple[float, str, str | None, float]],
|
|
||||||
short_side: int | None = None,
|
|
||||||
image_sequence: bool = False,
|
|
||||||
max_workers: int | None = None,
|
|
||||||
encoder: str = "libx264",
|
|
||||||
on_clip_done: Callable[[str], None] | None = None,
|
|
||||||
on_all_done: Callable[[], None] | None = None,
|
|
||||||
on_error: Callable[[str], None] | None = None,
|
|
||||||
):
|
|
||||||
self._input = input_path
|
|
||||||
self._jobs = jobs
|
|
||||||
self._short_side = short_side
|
|
||||||
self._image_sequence = image_sequence
|
|
||||||
self._max_workers = max_workers
|
|
||||||
self._encoder = encoder
|
|
||||||
self._on_clip_done = on_clip_done
|
|
||||||
self._on_all_done = on_all_done
|
|
||||||
self._on_error = on_error
|
|
||||||
self._cancel = False
|
|
||||||
self._procs: list[subprocess.Popen] = []
|
|
||||||
self._procs_lock = threading.Lock()
|
|
||||||
self._thread: threading.Thread | None = None
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
||||||
self._thread.start()
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
self._cancel = True
|
|
||||||
with self._procs_lock:
|
|
||||||
for proc in self._procs:
|
|
||||||
try:
|
|
||||||
proc.kill()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def is_running(self) -> bool:
|
|
||||||
return self._thread is not None and self._thread.is_alive()
|
|
||||||
|
|
||||||
def _run_one(self, start: float, output: str,
|
|
||||||
portrait_ratio: str | None, crop_center: float) -> str:
|
|
||||||
if self._cancel:
|
|
||||||
raise RuntimeError("cancelled")
|
|
||||||
if self._image_sequence:
|
|
||||||
os.makedirs(output, exist_ok=True)
|
|
||||||
cmd = build_ffmpeg_command(
|
|
||||||
self._input, start, output,
|
|
||||||
short_side=self._short_side,
|
|
||||||
portrait_ratio=portrait_ratio,
|
|
||||||
crop_center=crop_center,
|
|
||||||
image_sequence=self._image_sequence,
|
|
||||||
encoder=self._encoder,
|
|
||||||
)
|
|
||||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
with self._procs_lock:
|
|
||||||
self._procs.append(proc)
|
|
||||||
try:
|
|
||||||
_, stderr = proc.communicate(timeout=120)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
proc.kill()
|
|
||||||
raise RuntimeError("ffmpeg timed out")
|
|
||||||
finally:
|
|
||||||
with self._procs_lock:
|
|
||||||
self._procs.remove(proc)
|
|
||||||
if self._cancel:
|
|
||||||
raise RuntimeError("cancelled")
|
|
||||||
if proc.returncode != 0:
|
|
||||||
msg = stderr.decode(errors='replace')[-500:] if stderr else "ffmpeg failed"
|
|
||||||
raise RuntimeError(msg)
|
|
||||||
if self._image_sequence:
|
|
||||||
audio_cmd = build_audio_extract_command(self._input, start, output)
|
|
||||||
audio_result = subprocess.run(audio_cmd, capture_output=True, text=True, timeout=60)
|
|
||||||
if audio_result.returncode != 0:
|
|
||||||
msg = (audio_result.stderr or "audio extraction failed")[-500:]
|
|
||||||
raise RuntimeError(msg)
|
|
||||||
return output
|
|
||||||
|
|
||||||
def _run(self):
|
|
||||||
cap = self._max_workers or (os.cpu_count() or 2)
|
|
||||||
workers = min(len(self._jobs), cap)
|
|
||||||
try:
|
|
||||||
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
||||||
futures = {
|
|
||||||
pool.submit(self._run_one, s, o, pr, cc): o
|
|
||||||
for s, o, pr, cc in self._jobs
|
|
||||||
}
|
|
||||||
for fut in as_completed(futures):
|
|
||||||
if self._cancel:
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
path = fut.result()
|
|
||||||
if self._on_clip_done:
|
|
||||||
self._on_clip_done(path)
|
|
||||||
except Exception as e:
|
|
||||||
if "cancelled" not in str(e) and self._on_error:
|
|
||||||
self._on_error(str(e))
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
if self._on_error:
|
|
||||||
self._on_error(str(e))
|
|
||||||
return
|
|
||||||
if self._cancel:
|
|
||||||
return
|
|
||||||
if self._on_all_done:
|
|
||||||
self._on_all_done()
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
services:
|
|
||||||
8cut:
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
volumes:
|
|
||||||
- /path/to/videos:/videos:ro
|
|
||||||
- /path/to/exports:/exports
|
|
||||||
- 8cut-data:/data
|
|
||||||
environment:
|
|
||||||
MEDIA_DIRS: /videos
|
|
||||||
EXPORT_DIR: /exports
|
|
||||||
DB_PATH: /data/8cut.db
|
|
||||||
CACHE_DIR: /data/cache
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
count: all
|
|
||||||
capabilities: [gpu]
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
8cut-data:
|
|
||||||
@@ -350,6 +350,7 @@ class TrainWorker(QThread):
|
|||||||
self._video_infos,
|
self._video_infos,
|
||||||
model_path=self._model_path,
|
model_path=self._model_path,
|
||||||
embed_model=self._embed_model,
|
embed_model=self._embed_model,
|
||||||
|
cancel_flag=self,
|
||||||
)
|
)
|
||||||
if self._cancel:
|
if self._cancel:
|
||||||
return
|
return
|
||||||
|
|||||||
+3
-2
@@ -9,8 +9,9 @@ scikit-learn>=1.3
|
|||||||
joblib>=1.3
|
joblib>=1.3
|
||||||
soundfile>=0.12
|
soundfile>=0.12
|
||||||
|
|
||||||
# Deep learning (torch installed separately for CUDA support)
|
# Deep learning — install via setup_env.sh for correct CUDA version,
|
||||||
# torch and torchaudio are installed via --index-url in setup_env.sh
|
# or manually: pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu128
|
||||||
|
torch>=2.0
|
||||||
torchaudio>=2.0
|
torchaudio>=2.0
|
||||||
|
|
||||||
# Object detection
|
# Object detection
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
import tempfile, os, json
|
import tempfile, os, json
|
||||||
from main import build_export_path, format_time, build_ffmpeg_command, build_sequence_dir, build_audio_extract_command, build_annotation_json_path, upsert_clip_annotation, resolve_keyframe, apply_keyframes_to_jobs
|
from main import build_export_path, format_time, build_ffmpeg_command, build_sequence_dir, build_audio_extract_command, resolve_keyframe, apply_keyframes_to_jobs
|
||||||
|
from core.annotations import build_annotation_json_path, upsert_clip_annotation
|
||||||
from main import ProcessedDB
|
from main import ProcessedDB
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user