Repository navigation
Releases: warith-harchaoui/video-helper
Release list
v2.4.1
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
Added
- Hardware VideoToolbox encode path on macOS for
video_converter's
cross-container transcode andcompress_video(newvcodec= "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 publichw_vcodec(software, hardware)probesffmpeg -encodersand 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 withoverwrite=False. On a VideoToolbox
failure it recurses into itself withvcodec="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
[2.3.3] - 2026-08-21
Fixed
faces/sampling.py: shot detection called PySceneDetect's deprecated
FrameTimecode.get_seconds(); switched to the.secondsproperty it was
deprecated in favor of. No behavior change, just silences the warning._extract_via_pyav's hwaccel fallback: caughtav.error.ValueError,
an attribute that does not exist on PyAV's error module (it roots its
hierarchy atFFmpegError, not the stdlib exception names). Any real
hwaccel init/open failure raisedAttributeErrorfrom inside theexcept
clause itself instead of triggering the intended software-decode fallback,
crashing harder than doing nothing. Now catchesav.error.FFmpegError./extract-framesand/extract-flowAPI routes: spooling the upload,
creating the frames directory, and zipping the result all ran outside the
_cleanup_on_errorguard 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 inapi.py.
v2.3.2
[2.3.2] - 2026-08-17
Fixed
dump_frames: silently swapped red and blue on every frame it wrote —
an internalRGB2BGRconversion treated the input as RGB while every other
part of video-helper (includingextract_frames, whichdump_framesis
documented as the inverse of) uses BGR uint8.dump_framesnow writes
frames as-is, matching its documented contract and the OpenCV convention
used throughout the library.extract_optical_flow's.mp4visualization
output (the only in-repo caller ofdump_frames) is updated to match
(_flow_to_bgr, was_flow_to_rgb) so its rendered colors are unaffected.[faces]extra: dropped the deadonnxruntimedependency — nothing in
video_helper.facesever imported it. YuNet detection and SFace
recognition load their.onnxweight 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 infaces/__init__.py,faces/asd.py, and
scripts/seed_model_mirror.pythat incorrectly claimed the ASD engine ran
"through ONNX Runtime" (it never did) and that the seeding script should
produce alight_asd.onnxfile (the runtime looks forlight_asd.pth).compressCLI/API surfaces:compress_video'svcodec="copy"remux
escape hatch (no re-encode) was a real, working library feature but
unreachable from either CLI — both--vcodecchoices lists only allowed
libx265/libx264. The FastAPI/compressroute acceptedcopybut
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-flowalready had for
output_format, and documentedvcodec="copy"incompress_video's
docstring and the README/LISEZMOI reference tables.extract-flowAPI surface:method,dis_preset, andraft_variant
were unconstrained onPOST /extract-flow(plainstr = Form(...))
though both CLIs already restrict them viachoices=. Bad values still
failed correctly (the library itself validates and raisesValueError,
mapped to a 400), but with a generic wrapped message and no OpenAPI enum
documented for the field. Added the same explicitif x not in (...): raise HTTPException(400, ...)pattern already used foroutput_format
andcompress'svcodec, for a clean, schema-visible error on all three
fields./concatAPI route: clobbered spooled uploads that shared the same
extension._spool()always wrote to a fixedupload<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/overlayand/mux-audioalready 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.mdandflow.py's module docstring; punctuation-dash cleanup
across the docs and every touched module's opening docstring.
v2.3.1
[2.3.1] - 2026-08-15
Fixed
- Packaging:
pyproject.toml'sdescription(PyPIsummarymetadata)
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_framesoutput, 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/vydense 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 viafastapi-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 intoiter_frame_optical_flowand
extract_optical_flow(and their CLI/API surfaces) as an optional
output_width/output_heightpair.
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 viabackground.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_errorcontext 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
newcli_click.main()wrapper (was the barecligroup). - 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,
anffmpeg.Error) maps to 502. - Tests:
test_compress_video_rejects_infeasible_target_sizeasserted
stale behavior —compress_videowas intentionally changed to clamp to
the floor bitrate with a warning instead of raising (see its own
min_video_bitrate_kbpsdocstring), but the test still expected an
AssertionError. Updated to assert the actual, intended behavior.
v2.3.0
[2.3.0] - 2026-08-15
Added
iter_frame_optical_flow: optional generator that wraps any(H, W, 3)BGR
frame iterator —extract_framesoutput, 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/vydense 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 viafastapi-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 intoiter_frame_optical_flowand
extract_optical_flow(and their CLI/API surfaces) as an optional
output_width/output_heightpair.
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 viabackground.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_errorcontext 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
newcli_click.main()wrapper (was the barecligroup). - 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,
anffmpeg.Error) maps to 502. - Tests:
test_compress_video_rejects_infeasible_target_sizeasserted
stale behavior —compress_videowas intentionally changed to clamp to
the floor bitrate with a warning instead of raising (see its own
min_video_bitrate_kbpsdocstring), but the test still expected an
AssertionError. Updated to assert the actual, intended behavior.
v2.2.0
[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/libx265by default,
taggedhvc1for 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'soutput_width/output_height/pad_color
parameters (previously library-only) are now also exposed on the CLI
extract-framessubcommand and the/extract-framesAPI 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.txtas its own cached layer. requirements-dev.txtis now derived frompyproject.toml's[dev]
extra instead of maintained by hand.
v2.1.1
Fix: video_converter defaults extensionless output paths to mp4 instead of failing. See CHANGELOG.md.
v2.1.0
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 instandpoint/
vocal-helper/md2star/os-helper. Closes the CLI/API/MCP surface
gap for video-helper flagged inai-helpers/.private/do.md§7.
v1.8.1
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