feat: add counting bench, triage, and dataset modules

This commit includes major additions and updates to the frontend and backend architectures, introducing new dataset management, live counting features, batch processing, and triage logic. Includes new UI pages, components, and API routes.
This commit is contained in:
asus committed 2026-08-14 16:28:52 +07:00
1 parent 8285400254
commit 5c7c122105
80 files changed
+20074 -1412

No files matched your search

+79 -16
View File
@@ -9,6 +9,7 @@ import json
import os
import shutil
import subprocess
import tempfile
from typing import Callable, List, Optional
VIDEO_EXTS = (".mp4", ".mkv", ".mov", ".avi", ".webm", ".m4v")
@@ -44,7 +45,7 @@ def probe(path: str) -> dict:
result = subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=width,height,avg_frame_rate",
"-show_entries", "stream=width,height,avg_frame_rate,codec_name",
"-show_entries", "format=duration",
"-of", "json", path],
capture_output=True, text=True,
@@ -59,12 +60,14 @@ def probe(path: str) -> dict:
"width": int(streams[0].get("width") or 0),
"height": int(streams[0].get("height") or 0),
"fps": _parse_fps(streams[0].get("avg_frame_rate")),
"codec_name": str(streams[0].get("codec_name") or ""),
"size": stat.st_size,
}
_probe_cache[key] = info
return info
def _parse_fps(value: Optional[str]) -> float:
# ffprobe reports "30000/1001", and "0/0" for streams it cannot work out.
if not value or "/" not in value:
@@ -111,23 +114,30 @@ def extract_frames(
"-vf", f"fps={fps}", "-q:v", "2",
os.path.join(out_dir, "%06d.jpg"),
]
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# stderr goes to a file, not a pipe: a damaged recording emits decode errors
# without end, and nothing reads the pipe until ffmpeg exits — so a 64KB
# buffer would fill and block ffmpeg forever, hanging the whole job queue.
with tempfile.TemporaryFile("w+") as stderr_log:
process = subprocess.Popen(command, stdout=subprocess.DEVNULL,
stderr=stderr_log, text=True)
expected = frame_count(start_sec, end_sec, fps)
while process.poll() is None:
if should_stop is not None and should_stop():
process.terminate()
process.wait(timeout=10)
raise VideoError("cancelled")
if on_progress is not None:
on_progress(min(_written(out_dir), expected))
try:
process.wait(timeout=1)
except subprocess.TimeoutExpired:
pass
expected = frame_count(start_sec, end_sec, fps)
while process.poll() is None:
if should_stop is not None and should_stop():
process.terminate()
process.wait(timeout=10)
raise VideoError("cancelled")
if on_progress is not None:
on_progress(min(_written(out_dir), expected))
try:
process.wait(timeout=1)
except subprocess.TimeoutExpired:
pass
if process.returncode != 0:
raise VideoError((process.stderr.read() or "ffmpeg failed").strip().splitlines()[-1])
if process.returncode != 0:
stderr_log.seek(0)
lines = stderr_log.read().strip().splitlines()
raise VideoError(lines[-1] if lines else "ffmpeg failed")
return sorted(name for name in os.listdir(out_dir) if name.endswith(".jpg"))
@@ -137,3 +147,56 @@ def _written(out_dir: str) -> int:
return sum(1 for name in os.listdir(out_dir) if name.endswith(".jpg"))
except OSError:
return 0
def convert_to_h264(
input_path: str,
output_path: Optional[str] = None,
crf: int = 18,
preset: str = "medium",
) -> str:
"""Transcode input video to browser-supported H.264 / AAC MP4.
Uses CRF 18 for visually lossless compression (no perceptible visual loss,
significantly reduced file size), yuv420p for universal browser playback,
and +faststart for instant web streaming.
"""
if not available():
raise VideoError("ffmpeg is not installed in this environment")
if not os.path.isfile(input_path):
raise VideoError(f"Input video file not found: {input_path}")
if not output_path:
base, _ = os.path.splitext(input_path)
output_path = f"{base}_h264.mp4"
temp_output_path = output_path.replace(".mp4", "_tmp.mp4") if output_path.endswith(".mp4") else output_path + ".tmp.mp4"
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
command = [
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
"-i", input_path,
"-c:v", "libx264",
"-crf", str(crf),
"-preset", preset,
"-pix_fmt", "yuv420p",
"-movflags", "+faststart",
"-c:a", "aac",
"-b:a", "128k",
"-f", "mp4",
temp_output_path,
]
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode != 0:
if os.path.isfile(temp_output_path):
os.remove(temp_output_path)
err_msg = result.stderr.strip().splitlines()[-1] if result.stderr else "ffmpeg conversion failed"
raise VideoError(err_msg)
os.replace(temp_output_path, output_path)
return output_path