Skip to content

Releases: warith-harchaoui/video-helper

v2.4.1

Choose a tag to compare

@warith-harchaoui warith-harchaoui released this 03 Sep 04:44

Fixed: faces.digest atomic manifest write (temp file + os.replace), and video_converter/compress_video's hardware-encode paths now serialize across processes via an flock-based lock, not just an in-process threading.Semaphore — fixes load-average blowup when multiple processes each fall back to a software encode pass concurrently.

v2.4.0

Choose a tag to compare

@warith-harchaoui warith-harchaoui released this 24 Aug 20:37

Added

  • Hardware VideoToolbox encode path on macOS for video_converter's
    cross-container transcode and compress_video (new vcodec= "hevc_videotoolbox" option), a large speed win over the software x264/
    x265 passes on a long source (minutes instead of tens of minutes),
    at a small, acceptable quality-per-bit cost for a web-embedded video —
    not recommended for an archival master. New public hw_vcodec(software, hardware) probes ffmpeg -encoders and returns the hardware codec only
    when the running ffmpeg build actually lists it, falling back to
    software on any probe failure, a non-Darwin platform, or an unlisted
    encoder. Both hardware paths retry once on the software codec if the
    encode itself fails (VideoToolbox has a small, machine-dependent cap on
    concurrent hardware sessions).
  • VIDEO_HELPER_MAX_THREADS (default 4): caps ffmpeg's own thread pool
    on the heavy encode paths — ffmpeg's default (0 = auto) claims every core
    it can see, fine for one isolated process but oversubscribes badly under
    concurrent batch conversions.
  • VIDEO_HELPER_MAX_HW_SESSIONS (default 1): an in-process semaphore
    serializing concurrent VideoToolbox hardware encode attempts — several
    threads requesting a hardware session at once can collide and error out,
    each falling back to a software pass that then monopolizes its thread for
    20-30x longer, which is what actually starves a batch job, not the
    collision itself. Does not coordinate across separate OS processes.

Fixed

  • compress_video's hardware-encode failure path could silently return a
    broken file when called with overwrite=False.
    On a VideoToolbox
    failure it recurses into itself with vcodec="libx265" to retry in
    software — but ffmpeg creates/truncates the output file as part of muxer
    setup before the encode loop runs, so a failed hardware pass can leave a
    partial, invalid file on disk. The recursive call's own
    if not overwrite and file_exists(output_video) early-return then saw
    that leftover file and handed it straight back, never running the
    software fallback at all. Now removes the partial output file before
    recursing.

v2.3.3

Choose a tag to compare

@warith-harchaoui warith-harchaoui released this 21 Aug 04:02

[2.3.3] - 2026-08-21

Fixed

  • faces/sampling.py: shot detection called PySceneDetect's deprecated
    FrameTimecode.get_seconds(); switched to the .seconds property it was
    deprecated in favor of. No behavior change, just silences the warning.
  • _extract_via_pyav's hwaccel fallback: caught av.error.ValueError,
    an attribute that does not exist on PyAV's error module (it roots its
    hierarchy at FFmpegError, not the stdlib exception names). Any real
    hwaccel init/open failure raised AttributeError from inside the except
    clause itself instead of triggering the intended software-decode fallback,
    crashing harder than doing nothing. Now catches av.error.FFmpegError.
  • /extract-frames and /extract-flow API routes: spooling the upload,
    creating the frames directory, and zipping the result all ran outside the
    _cleanup_on_error guard that every other action route uses; a failure at
    any of those steps (not just inside the extraction call) leaked the whole
    temp dir, including the spooled upload, on disk. Both routes now wrap their
    full body in _cleanup_on_error, matching the pattern already used
    elsewhere in api.py.

v2.3.2

Choose a tag to compare

@warith-harchaoui warith-harchaoui released this 17 Aug 07:59

[2.3.2] - 2026-08-17

Fixed

  • dump_frames: silently swapped red and blue on every frame it wrote —
    an internal RGB2BGR conversion treated the input as RGB while every other
    part of video-helper (including extract_frames, which dump_frames is
    documented as the inverse of) uses BGR uint8. dump_frames now writes
    frames as-is, matching its documented contract and the OpenCV convention
    used throughout the library. extract_optical_flow's .mp4 visualization
    output (the only in-repo caller of dump_frames) is updated to match
    (_flow_to_bgr, was _flow_to_rgb) so its rendered colors are unaffected.
  • [faces] extra: dropped the dead onnxruntime dependency — nothing in
    video_helper.faces ever imported it. YuNet detection and SFace
    recognition load their .onnx weight files through OpenCV's own bundled
    DNN wrappers (cv2.FaceDetectorYN, cv2.FaceRecognizerSF), and the
    accurate ASD engine (LightASD) runs entirely in PyTorch. Also fixed
    docstrings in faces/__init__.py, faces/asd.py, and
    scripts/seed_model_mirror.py that incorrectly claimed the ASD engine ran
    "through ONNX Runtime" (it never did) and that the seeding script should
    produce a light_asd.onnx file (the runtime looks for light_asd.pth).
  • compress CLI/API surfaces: compress_video's vcodec="copy" remux
    escape hatch (no re-encode) was a real, working library feature but
    unreachable from either CLI — both --vcodec choices lists only allowed
    libx265/libx264. The FastAPI /compress route accepted copy but
    silently accepted any string, forwarding invalid values straight to
    ffmpeg instead of a clean 400. Added "copy" to both CLI choice lists,
    added the same 400 validation /extract-flow already had for
    output_format, and documented vcodec="copy" in compress_video's
    docstring and the README/LISEZMOI reference tables.
  • extract-flow API surface: method, dis_preset, and raft_variant
    were unconstrained on POST /extract-flow (plain str = Form(...))
    though both CLIs already restrict them via choices=. Bad values still
    failed correctly (the library itself validates and raises ValueError,
    mapped to a 400), but with a generic wrapped message and no OpenAPI enum
    documented for the field. Added the same explicit if x not in (...): raise HTTPException(400, ...) pattern already used for output_format
    and compress's vcodec, for a clean, schema-visible error on all three
    fields.
  • /concat API route: clobbered spooled uploads that shared the same
    extension. _spool() always wrote to a fixed upload<ext> path;
    concat() called it once per file then renamed, so a later file with the
    same extension overwrote an earlier one on disk before its own rename
    ever ran. Each upload now writes directly to its position-derived name,
    matching the pattern /overlay and /mux-audio already use for their
    second upload.
  • active_speaker_map's docstring contradicted its own return contract:
    it claimed uncertain clusters (margin and coverage below threshold) are
    omitted, but the finalize step only ever re-checks coverage; a cluster
    with a lower margin but sufficient coverage is intentionally still
    returned, with its real margin exposed as a caller-facing certainty
    signal rather than an internal filter. Docstring now states the actual,
    intentional behavior; the code was already correct.

Documentation

  • Glossed dense optical flow for a first-time reader in README.md /
    LISEZMOI.md and flow.py's module docstring; punctuation-dash cleanup
    across the docs and every touched module's opening docstring.

v2.3.1

Choose a tag to compare

@warith-harchaoui warith-harchaoui released this 15 Aug 03:36

[2.3.1] - 2026-08-15

Fixed

  • Packaging: pyproject.toml's description (PyPI summary metadata)
    exceeded PyPI's 512-character limit, which rejected the upload outright.
    Trimmed to the same content, more concisely worded.

[2.3.0] - 2026-08-15

Added

  • iter_frame_optical_flow: optional generator that wraps any (H, W, 3) BGR
    frame iterator — extract_frames output, or a live source such as
    capture_helper.iter_camera_frames, sharing the same contract — and
    re-yields (H, W, 5) float32 arrays (frame + vx/vy dense flow vs. the
    previous frame), or (H, W, 3) (grayscale intensity + flow) with the new
    grayscale=True. Three backends: "dis" (default) and "farneback" need
    no new dependency (opencv-python is already core); "raft" is a deep
    RAFT network via torchvision, gated behind the new [flow] extra.
  • extract_optical_flow: file-level convenience wrapper (extract_frames
    → iter_frame_optical_flow → write) with full CLI/API/MCP parity, matching every
    other operation in the suite — video-helper extract-flow /
    video-helper-click extract-flow, POST /extract-flow (auto-published as
    an MCP tool via fastapi-mcp, no extra wiring needed). Output kind is
    inferred from the output path's extension: .npy (raw (T, H, W, 2)
    float32 flow array) or anything else (default .mp4, an HSV-color-wheel
    visualization video).
  • resize_flow: resizes a dense-optical-flow field via wavelet
    decomposition (PyWavelets, part of the [flow] extra) instead of plain
    bilinear/bicubic interpolation, specifically to avoid smearing motion
    discontinuities across a resize, plus correctly rescaling flow magnitude
    by the same factor as the spatial resize (a common silent correctness bug
    in naive flow-resizing code). Wired into iter_frame_optical_flow and
    extract_optical_flow (and their CLI/API surfaces) as an optional
    output_width/output_height pair.

Security

  • The HTTP API leaked a temp directory (including the uploaded video) on
    every failed request
    to any of the 11 action routes (/convert,
    /compress, /chunk, /black, /image-loop, /concat, /overlay,
    /extract-audio, /mux-audio, /burn-subs, /srt2vtt): cleanup was
    only ever scheduled via background.add_task(_cleanup, tmp) on the
    success path, so a client sending deliberately-invalid input repeatedly
    could exhaust server disk — every failure left its whole temp dir behind
    forever. Fixed with a shared _cleanup_on_error context manager that
    reclaims the temp dir immediately on any exception, leaving the
    success-path background cleanup untouched.

Fixed

  • CLI: a library exception now prints one clean Error: ... line to
    stderr and exits 1, instead of a raw Python traceback, on both CLI
    twins. video-helper-click's console-script entry point now points at a
    new cli_click.main() wrapper (was the bare cli group).
  • API: every library exception collapsed into FastAPI's generic 500.
    AssertionError/ValueError (the library's own input-validation
    failures) now map to 400; anything else (RuntimeError, ImportError,
    an ffmpeg.Error) maps to 502.
  • Tests: test_compress_video_rejects_infeasible_target_size asserted
    stale behavior — compress_video was intentionally changed to clamp to
    the floor bitrate with a warning instead of raising (see its own
    min_video_bitrate_kbps docstring), but the test still expected an
    AssertionError. Updated to assert the actual, intended behavior.

v2.3.0

Choose a tag to compare

@warith-harchaoui warith-harchaoui released this 15 Aug 03:30

[2.3.0] - 2026-08-15

Added

  • iter_frame_optical_flow: optional generator that wraps any (H, W, 3) BGR
    frame iterator — extract_frames output, or a live source such as
    capture_helper.iter_camera_frames, sharing the same contract — and
    re-yields (H, W, 5) float32 arrays (frame + vx/vy dense flow vs. the
    previous frame), or (H, W, 3) (grayscale intensity + flow) with the new
    grayscale=True. Three backends: "dis" (default) and "farneback" need
    no new dependency (opencv-python is already core); "raft" is a deep
    RAFT network via torchvision, gated behind the new [flow] extra.
  • extract_optical_flow: file-level convenience wrapper (extract_frames
    → iter_frame_optical_flow → write) with full CLI/API/MCP parity, matching every
    other operation in the suite — video-helper extract-flow /
    video-helper-click extract-flow, POST /extract-flow (auto-published as
    an MCP tool via fastapi-mcp, no extra wiring needed). Output kind is
    inferred from the output path's extension: .npy (raw (T, H, W, 2)
    float32 flow array) or anything else (default .mp4, an HSV-color-wheel
    visualization video).
  • resize_flow: resizes a dense-optical-flow field via wavelet
    decomposition (PyWavelets, part of the [flow] extra) instead of plain
    bilinear/bicubic interpolation, specifically to avoid smearing motion
    discontinuities across a resize, plus correctly rescaling flow magnitude
    by the same factor as the spatial resize (a common silent correctness bug
    in naive flow-resizing code). Wired into iter_frame_optical_flow and
    extract_optical_flow (and their CLI/API surfaces) as an optional
    output_width/output_height pair.

Security

  • The HTTP API leaked a temp directory (including the uploaded video) on
    every failed request
    to any of the 11 action routes (/convert,
    /compress, /chunk, /black, /image-loop, /concat, /overlay,
    /extract-audio, /mux-audio, /burn-subs, /srt2vtt): cleanup was
    only ever scheduled via background.add_task(_cleanup, tmp) on the
    success path, so a client sending deliberately-invalid input repeatedly
    could exhaust server disk — every failure left its whole temp dir behind
    forever. Fixed with a shared _cleanup_on_error context manager that
    reclaims the temp dir immediately on any exception, leaving the
    success-path background cleanup untouched.

Fixed

  • CLI: a library exception now prints one clean Error: ... line to
    stderr and exits 1, instead of a raw Python traceback, on both CLI
    twins. video-helper-click's console-script entry point now points at a
    new cli_click.main() wrapper (was the bare cli group).
  • API: every library exception collapsed into FastAPI's generic 500.
    AssertionError/ValueError (the library's own input-validation
    failures) now map to 400; anything else (RuntimeError, ImportError,
    an ffmpeg.Error) maps to 502.
  • Tests: test_compress_video_rejects_infeasible_target_size asserted
    stale behavior — compress_video was intentionally changed to clamp to
    the floor bitrate with a warning instead of raising (see its own
    min_video_bitrate_kbps docstring), but the test still expected an
    AssertionError. Updated to assert the actual, intended behavior.

v2.2.0

Choose a tag to compare

@warith-harchaoui warith-harchaoui released this 13 Aug 12:35

[2.2.0] - 2026-08-13

Added

  • compress_video: two-pass ffmpeg encode that solves for the video
    bitrate needed to hit a target file size (HEVC/libx265 by default,
    tagged hvc1 for QuickTime/Apple compatibility, +faststart). Ships
    with full CLI (video-helper compress / video-helper-click compress),
    API (POST /compress), and GUI parity, matching every other operation
    in the suite.
  • extract_frames's output_width / output_height / pad_color
    parameters (previously library-only) are now also exposed on the CLI
    extract-frames subcommand and the /extract-frames API endpoint.

Fixed

  • PyAV decoder deadlock risk in _extract_via_pyav.
  • pad_color='transparent' error message hardcoded a stale version string.

Changed

  • Added a conda environment.yaml; the Dockerfile now installs
    requirements.txt as its own cached layer.
  • requirements-dev.txt is now derived from pyproject.toml's [dev]
    extra instead of maintained by hand.

v2.1.1

Choose a tag to compare

@warith-harchaoui warith-harchaoui released this 09 Aug 14:27

Fix: video_converter defaults extensionless output paths to mp4 instead of failing. See CHANGELOG.md.

v2.1.0

Choose a tag to compare

@warith-harchaoui warith-harchaoui released this 08 Aug 06:46

Added

  • MCP surface (video_helper.mcp, [mcp] extra, entry point
    video-helper-mcp): exposes the existing FastAPI app as MCP tools via
    fastapi-mcp, mirroring the pattern already shipped in standpoint /
    vocal-helper / md2star / os-helper. Closes the CLI/API/MCP surface
    gap for video-helper flagged in ai-helpers/.private/do.md §7.

v1.8.1

Choose a tag to compare

@warith-harchaoui warith-harchaoui released this 01 Aug 00:42

Move the agent skill out of the public repo: without an MCP surface, the Claude/OpenCode skill no longer earns its keep as public distribution. Moved skills/ to the gitignored .private/skills/ (kept locally, never published). TRIGGERS.md stays public.

Full changelog: https://github.com/warith-harchaoui/video-helper/blob/main/CHANGELOG.md