video_helper package
Subpackages
- video_helper.faces package
- Submodules
- Module contents
Submodules
- video_helper.api module
- video_helper.cli_argparse module
- video_helper.cli_click module
- video_helper.flow module
- video_helper.gui module
- video_helper.main module
- video_helper.main
black_video()burn_subtitles()compress_video()concat_videos()dump_frames()extract_audio_track()extract_frames()extract_unique_colors()extract_video_chunk()image_loop_to_video()is_valid_video_file()mux_audio_video()overlay_image()srt2vtt()to_editing_intermediate()video_converter()video_dimensions()video_duration()
- video_helper.mcp module
Module contents
video_helper
Multi-backend frame extraction (VidGear / PyAV / ffmpeg-pipe), video conversion, subtitle muxing, and lightweight image-ops glue for the AI Helpers suite.
Multi-surface exposure
Every public function below is reachable from four surfaces:
Python library import (this module).
Argparse CLI:
video-helper(stdlib-only, always installed).Click CLI:
video-helper-click(needs the[cli]extra).FastAPI HTTP:
video_helper.api(needs the[api]extra), which also serves a minimal browser “video bench” GUI atGET /gui.
Usage Example
>>> import video_helper as vh
>>> for frame in vh.extract_frames("clip.mp4", frame_interval=1.0):
... # frame.shape == (H, W, 3) — BGR uint8 (OpenCV convention)
... do_something(frame)
See EXAMPLES.md at the repo root for the full cookbook (sparse
access, torch / pil destinations, batched yields, hwaccel, http_headers
for yt-dlp-resolved sources, scale-fit-and-pad, …).
- video_helper.black_video(duration, width, height, output_video, frame_rate=30)[source]
Generate a silent solid-black video of
durationseconds.- Parameters:
duration (float) – Output duration in seconds.
width (int) – Output frame width in pixels (rounded down to even — H.264 yuv420p requires even dimensions).
height (int) – Output frame height in pixels (rounded down to even).
output_video (str) – Path to the output video file (.mp4 recommended).
frame_rate (int, optional) – Output frame rate (default 30).
- Return type:
None
Notes
Useful as a “buffer” / breathing clip between two visuals in a montage, or as a placeholder when an asset is missing. Encoded as H.264 yuv420p with no audio track.
Examples
>>> black_video(0.5, 1920, 1080, "buffer.mp4")
- video_helper.burn_subtitles(input_video, subtitles_file, output_video, force_style=None)[source]
Burn subtitles from an .srt / .vtt / .ass file into the video frames.
- Parameters:
input_video (str) – Path to the input video file.
subtitles_file (str) –
Path to a subtitles file in one of the formats libass understands:
.srt — plain SubRip. Renders in the libass default style;
<font color="…">tags are honored..vtt — WebVTT. Cue-class colors (
<c.red>…</c>) and any inline::cuerules from aSTYLEblock are honored. The companionsrt2vtt()writes both pieces in one shot..ass / .ssa — Advanced SubStation Alpha. All per-cue formatting (font, color, outline, position) is honored as authored.
output_video (str) – Path to the output video file (.mp4 recommended).
force_style (str, optional) – ASS-style override forwarded to the
subtitlesfilter’sforce_styleargument — e.g."FontName=Helvetica,FontSize=24,PrimaryColour=&H00FFFFFF&". Useful for SRT (which has no native styling) or to override a global property of a VTT/ASS file without editing it. Per-cue colors from VTT/ASS still win againstforce_stylekeys they explicitly set.
- Return type:
None
Notes
A single backend (libass through ffmpeg’s
subtitlesfilter) handles all three formats, so there is no need for separateburn_srt/burn_vtt/burn_assfunctions. The filter mounts the file by path; we escape:to\:and'to\'so absolute paths on macOS / Windows behave. Video is re-encoded (the filter rewrites every frame), audio is copied if present.Examples
Plain SRT, default style:
>>> burn_subtitles("clip.mp4", "subs.srt", "captioned.mp4")
Colored WebVTT (cue classes carry their own colors):
>>> burn_subtitles("clip.mp4", "subs.vtt", "captioned.mp4")
Force a font + size on top of any source format:
>>> burn_subtitles("clip.mp4", "subs.vtt", "captioned.mp4", ... force_style="FontName=Helvetica,FontSize=28,Outline=2")
- video_helper.compress_video(input_video, output_video=None, *, target_size_mb=97.0, audio_bitrate='128k', vcodec='libx265', min_video_bitrate_kbps=200, overwrite=True)[source]
Compress a video to a target file size via two-pass encoding.
Solves for the video bitrate that makes video + audio together fit inside
target_size_mb, given the source duration, then runs a standard ffmpeg two-pass encode at that bitrate. Defaults to HEVC (libx265), taggedhvc1(ffmpeg’s default HEVC tag,hev1, is not recognized by QuickTime / Apple players), and moves the moov atom to the front of the file (+faststart) so playback can start before the download finishes. Built for “the compressed file that gets embedded in a web video player”, not an archival master.- Parameters:
input_video (str) – Path to the source video.
output_video (str, optional) – Path to write the compressed video to. Defaults to
<input>-compressed.mp4next to the source.target_size_mb (float, optional) – Target output file size in megabytes (default 97 — comfortably under the 100 MB caps common on chat apps and code-hosting attachments).
audio_bitrate (str, optional) – Audio bitrate passed to the AAC encoder, ffmpeg syntax (default
"128k"). Subtracted from the size budget before solving for the video bitrate.vcodec (str, optional) – Video codec (default
"libx265"— HEVC compresses noticeably better than H.264 at the same target size). Pass"libx264"for maximum legacy-player compatibility at a larger file for the same quality; thehvc1tag is only applied when encoding HEVC.min_video_bitrate_kbps (int, optional) – Floor on the solved video bitrate (default 200) — guards against a near-zero bitrate when a long source is squeezed into a small target size, which would otherwise silently produce an unwatchable file instead of a clear error.
overwrite (bool, optional) – Overwrite
output_videoif it already exists (default True); when False and the file already exists, that path is returned as-is with no re-encode.
- Returns:
Path to the compressed video (
output_video).- Return type:
- Raises:
AssertionError – If
input_videois not a valid video file.ffmpeg.Error – If either encoding pass fails.
Examples
>>> compress_video("meeting.mp4", "meeting-compressed.mp4", target_size_mb=97) 'meeting-compressed.mp4'
- video_helper.concat_videos(input_videos, output_video, reencode=True, frame_rate=None)[source]
Concatenate
input_videosend-to-end intooutput_video.- Parameters:
input_videos (List[str]) – Ordered list of input video paths.
output_video (str) – Path to the output video file (.mp4 recommended).
reencode (bool, optional) – Whether to re-encode (libx264). Default
True— strongly recommended when the inputs come from different sources, since the concat demuxer’s stream-copy path requires identical codec, timebase, frame rate and resolution; mismatched inputs produce audio/video drift or hard ffmpeg errors. SetFalseonly when the inputs are guaranteed bit-identical containers.frame_rate (int, optional) – Force this output frame rate (only used when
reencode=True).
- Return type:
None
Notes
Uses the ffmpeg
concatdemuxer (text manifest) which is the only correct way to concatenate variable-length clips end-to-end without re-timing artefacts. The temporary manifest is written to a process-temp file and removed automatically.Examples
>>> concat_videos(["intro.mp4", "body.mp4", "outro.mp4"], "final.mp4")
- video_helper.dump_frames(frames_list, output_movie, fps=30)[source]
Save frames to a video file.
- Parameters:
- Return type:
None
Notes
The function uses VidGear to write the frames to a video file.
Usage
>>> frames = [frame1, frame2, frame3] >>> dump_frames(frames, "output.mp4")
- video_helper.extract_audio_track(input_video, output_audio, sample_rate=44100, channels=2, encoding='pcm_s16le')[source]
Extract the audio track of a video file into a standalone audio file.
- Parameters:
input_video (str) – Path to the input video file (any container ffmpeg can read).
output_audio (str) – Path to the output audio file. The extension picks the container;
.wavpairs naturally withencoding="pcm_s16le"for a lossless extract.sample_rate (int, optional) – Output sample rate in Hz (default 44100).
channels (int, optional) – Output channel count (default 2 — stereo). Use
1for mono.encoding (str, optional) – Audio codec (default
"pcm_s16le"). For non-WAV outputs use a codec compatible with the container (e.g."aac"for .m4a,"libmp3lame"for .mp3).
- Return type:
None
Notes
Source-of-truth companion to
audio_helper.sound_converterfor the case where the input is a video —sound_converterrejects video extensions in its input-validation pass, hence the dedicated function here. Drops the video stream (-vn) and re-encodes only the audio.Examples
>>> extract_audio_track("interview.mp4", "interview.wav") >>> extract_audio_track("clip.mov", "clip.mp3", ... encoding="libmp3lame", sample_rate=22050)
- video_helper.extract_frames(video_path, start_index=None, end_index=None, start_instant=None, end_instant=None, stabilize=False, frame_step=1, frame_interval=None, frame_indices=None, frame_times=None, backend='auto', hwaccel=None, http_headers=None, output_width=None, output_height=None, pad_color='black', destination='numpy', device='cpu', batch_size=None, layout='image')[source]
Extract frames from a video, dispatching to the best available backend.
The function picks a backend automatically based on the requested access pattern and what’s installed locally; pass
backend=...to override. Frames are yielded in the user’s preferred form viadestination(numpy array or torch tensor on a chosen device), optionally batched to amortize the host→device transfer.Backends
vidgear— OpenCV+VidGear with a producer thread. Fastest path for full sequential decode up to ~720p on macOS (uses AVFoundation under the hood) and the only backend that supportsstabilize=True. Decodes from t=0 with no real seek.pyav— direct ffmpeg libav bindings. Best default for windowed sequential, sparse reads, and any “torch on GPU” destination thanks to keyframe seek + hwaccel support.ffmpeg-pipe— ffmpeg subprocess fallback. Useful when PyAV isn’t installed. Sequential only; honorshwaccel.
- param video_path:
Path to the input video file.
- type video_path:
str
- param start_index:
Inclusive frame-index bounds. If None, defaults to start-of-file and end-of-file respectively.
- type start_index:
int, optional
- param end_index:
Inclusive frame-index bounds. If None, defaults to start-of-file and end-of-file respectively.
- type end_index:
int, optional
- param start_instant:
Same bounds expressed in seconds. When provided, they override the index form.
- type start_instant:
float, optional
- param end_instant:
Same bounds expressed in seconds. When provided, they override the index form.
- type end_instant:
float, optional
- param stabilize:
If True, runs VidGear’s software stabilizer. Forces
backend="vidgear".- type stabilize:
bool, optional
- param frame_step:
Sampling stride within the range (every Nth frame). Defaults to 1.
- type frame_step:
int, optional
- param frame_interval:
Sampling period in seconds. Overrides
frame_stepwhen given.- type frame_interval:
float, optional
- param frame_indices:
Explicit set of frame indices to read (sparse / random access). When provided, range parameters are ignored.
- type frame_indices:
list[int], optional
- param frame_times:
Same as
frame_indicesbut in seconds; converted internally.- type frame_times:
list[float], optional
- param backend:
"auto"(default),"vidgear","pyav", or"ffmpeg-pipe".- type backend:
str, optional
- param hwaccel:
Hardware-accelerated decoder. Default
None. Pass"auto"to enable platform-appropriate accel ("videotoolbox"on macOS,"cuda"on Linux+NVIDIA), or an explicit value. Honored only bypyavandffmpeg-pipe. Fordestination="torch"with a GPU device,"auto"is enabled by default since the wall-time penalty observed on numpy-destination cells doesn’t apply (the frames go through one numpy stack and then host→device in one shot — see SPEED_ANALYSIS.md).- type hwaccel:
str, optional
- param http_headers:
HTTP headers forwarded to the underlying decoder. Required for URLs that need a specific
User-Agent/Referer/Cookie/Authorization— e.g. yt-dlp-resolved YouTube live streams, members-only / age-gated content, Vimeo private videos, Twitch. Joined into ffmpeg’s-headersCRLF string under the hood. Honored bypyavandffmpeg-pipe; thevidgearbackend logs a warning and ignores them (OpenCV’s HTTP layer doesn’t surface headers cleanly).- type http_headers:
dict[str, str], optional
- param output_width:
Exact output frame size in pixels. Behavior:
Both set → scale-fit (aspect-preserving) then pad with
pad_colorso the output is exactlyoutput_width × output_height. Typical for ML pipelines that need a fixed input shape.Only one set → scale to that dimension preserving aspect ratio; the other dimension is derived. No padding.
Neither set (default) → frame keeps its native dimensions.
The transform runs in numpy via
cv2.resize+cv2.copyMakeBorderpost-decode. Same behavior across all backends (vidgear / pyav / ffmpeg-pipe).- type output_width:
int, optional
- param output_height:
Exact output frame size in pixels. Behavior:
Both set → scale-fit (aspect-preserving) then pad with
pad_colorso the output is exactlyoutput_width × output_height. Typical for ML pipelines that need a fixed input shape.Only one set → scale to that dimension preserving aspect ratio; the other dimension is derived. No padding.
Neither set (default) → frame keeps its native dimensions.
The transform runs in numpy via
cv2.resize+cv2.copyMakeBorderpost-decode. Same behavior across all backends (vidgear / pyav / ffmpeg-pipe).- type output_height:
int, optional
- param pad_color:
Padding color when scale-fit-and-pad applies (i.e. both
output_widthandoutput_heightare set, and the source’s aspect ratio differs from the target). Accepts:common names:
"black"(default),"white","red","green","blue","yellow","cyan","magenta","gray"/"grey""#RRGGBB"hex"transparent"raisesValueError: it would require 4-channel BGRA output, breaking the(H, W, 3)contract; not implemented.
- type pad_color:
str, optional
- param destination:
Where frames land. Default
"numpy"."numpy"— BGR uint8np.ndarrayin OpenCV’s channels-last layout."torch"— RGB uint8torch.Tensorin PyTorch’s channels-first layout. PyTorch imported lazily."pil"—PIL.Image.Image(mode"RGB",size=(W, H)per Pillow convention). Pillow imported lazily.batch_sizenot supported (Pillow has no batched type).
See the layout table for exact shapes.
- type destination:
str, optional
- param device:
Target device when
destination="torch"."cpu"(default),"mps"(Apple Silicon),"cuda"(NVIDIA), or"auto"(cuda > mps > cpu). Ignored whendestination="numpy".- type device:
str, optional
- param batch_size:
If provided, yield a batched tensor / array per batch instead of one frame at a time. The last batch may be smaller. Strongly recommended with
destination="torch"+ GPU device: one host→device transfer per batch instead of one per frame (typical 5-20× win).- type batch_size:
int, optional
- param layout:
Axis convention for batched yields (ignored when
batch_sizeis None and fordestination="pil")."image"(default) — each batch is a stack of independent images;"video"— each batch is a video clip with a time axis.Concrete shapes per (destination, layout, batch_size):
destination
layout
batch_size
yield
"numpy"any
None
(H, W, 3)HWC, BGR uint8"numpy""image"N
(N, H, W, 3)NHWC, BGR uint8"numpy""video"N
(N, H, W, 3)THWC, BGR uint8 (same mem; T == N)"torch"any
None
(3, H, W)CHW, RGB uint8"torch""image"N
(N, 3, H, W)NCHW, RGB uint8 (batch of images)"torch""video"N
(3, N, H, W)CTHW, RGB uint8 (video clip; T == N)"pil"n/a
forbidden
PIL.Imagemode=``”RGB”, size=``(W, H)- type layout:
str, optional
- Yields:
numpy.ndarray – Successive frames as
(H, W, 3)BGR uint8 arrays — same convention as OpenCV and the previous VidGear-only implementation.
Examples
>>> # Sequential time range — dispatcher picks PyAV (windowed) >>> for frame in extract_frames("clip.mp4", start_instant=10, end_instant=20, frame_step=5): ... process(frame)
>>> # Sparse access at specific times — routed to PyAV >>> list(extract_frames("clip.mp4", frame_times=[1.5, 12.0, 47.0]))
>>> # Stream as torch tensors on Apple Silicon, batched for one transfer per 32 frames >>> for batch in extract_frames("clip.mp4", ... destination="torch", device="mps", batch_size=32): ... # batch.shape == (N, H, W, 3); N == 32 for all but the last batch ... model(batch)
- Parameters:
video_path (str)
start_index (int | None)
end_index (int | None)
start_instant (float | None)
end_instant (float | None)
stabilize (bool)
frame_step (int)
frame_interval (float | None)
backend (str)
hwaccel (str | None)
http_headers (dict | None)
output_width (int | None)
output_height (int | None)
pad_color (str)
destination (str)
device (str)
batch_size (int | None)
layout (str)
- Return type:
- video_helper.extract_optical_flow(input_video, output_path=None, *, method='dis', dis_preset='fast', raft_variant='small', device='cpu', clip_flow=None, start_instant=None, end_instant=None, frame_step=1, frame_interval=None, fps=None, output_width=None, output_height=None, wavelet='db2', overwrite=True)[source]
Compute dense optical flow over a video file and write it to disk.
File-level convenience wrapper around
iter_frame_optical_flow()for the common “just give me flow for this video” case (CLI / API surfaces need a single input path and a single output path, not a frame iterator). The output kind is inferred fromoutput_path’s extension, mirroring howvideo_helper.main.video_converter()infers its container:.npy— the raw flow-only array,(T, H, W, 2)float32 (vx,vy), one entry per input frame (frame 1 is all zeros).anything else (default
.mp4) — an HSV-color-wheel visualization video (direction -> hue, magnitude -> value, see_flow_to_rgb()), viewable directly without loading numpy.
- Parameters:
input_video (str) – Path to the source video.
output_path (str, optional) – Where to write the result. Defaults to
<input>-flow.mp4next to the source. Extension controls the output kind (see above).method ({"dis", "farneback", "raft"}, default "dis") – Optical-flow backend — see
iter_frame_optical_flow().dis_preset ({"ultrafast", "fast", "medium"}, default "fast") – Speed/quality preset for
method="dis". Ignored otherwise.raft_variant ({"small", "large"}, default "small") – RAFT network variant. Only used for
method="raft".device (str, default "cpu") – Torch device for
method="raft"only.clip_flow (float or None, default None) – Symmetric pixel clip for outlier suppression — see
iter_frame_optical_flow().start_instant (float, optional) – Start time in seconds (forwarded to
video_helper.main.extract_frames()).end_instant (float, optional) – End time in seconds (forwarded to
video_helper.main.extract_frames()).frame_step (int, default 1) – Take every Nth frame (forwarded to
video_helper.main.extract_frames()).frame_interval (float, optional) – Sample one frame every N seconds (forwarded to
video_helper.main.extract_frames(); mutually exclusive withframe_stepthere).fps (float, optional) – Frame rate for the
.mp4visualization output. Defaults to the source video’s probed frame rate divided byframe_step(ignored for.npyoutput, and only a rough estimate whenframe_intervalis used instead offrame_step).output_width (int, optional) – Resize the flow field (not the source frames) to this width before writing — e.g. a smaller
.npyfor storage, or a smaller visualization video. Must be given together withoutput_height. Goes throughresize_flow()(wavelet-based, magnitude-rescaled, discontinuity-aware) rather than plain interpolation.output_height (int, optional) – Resize the flow field to this height. See
output_width.wavelet (str, default "db2") – PyWavelets wavelet name forwarded to
resize_flow(). Ignored unlessoutput_width/output_heightare set.overwrite (bool, default True) – Overwrite
output_pathif it already exists; when False and the file already exists, that path is returned as-is with no recompute.
- Returns:
Path to the written file (
output_path).- Return type:
- Raises:
AssertionError – If
input_videois not a valid video file.ValueError – If
method,dis_preset, orraft_variantis not supported (propagated fromiter_frame_optical_flow()).ImportError – If
method="raft"is requested buttorchvisionis not installed, or ifoutput_width/output_heightare requested butPyWaveletsis not installed.
Examples
>>> extract_optical_flow("clip.mp4", "clip-flow.mp4", method="dis") 'clip-flow.mp4' >>> extract_optical_flow("clip.mp4", "clip-flow.npy", method="dis") 'clip-flow.npy'
- video_helper.extract_unique_colors(srt_file_path)[source]
Extract all unique hex color codes from an SRT file.
- Parameters:
srt_file_path (str) – Path to the input .srt file.
- Returns:
Set[str] – A set of unique hex color codes found in the .srt file.
Usage
—–
>>> srt_file = “subtitles.srt”
>>> unique_colors = extract_unique_colors(srt_file)
>>> print(unique_colors)
{‘#FF0000’, ‘#00FF00’, ‘#0000FF’}
- Return type:
- video_helper.extract_video_chunk(input_video, sample_start, sample_end, output_video, *, copy=False)[source]
Extract a chunk of video from the specified start to end time and save it to a new file.
- Parameters:
input_video (str) – Path to the input video file.
sample_start (float) – Start time in seconds for the extraction.
sample_end (float) – End time in seconds for the extraction.
output_video (str) – Path to save the extracted video chunk.
copy (bool, optional) – Stream-copy the cut instead of re-encoding (default
False, the safe choice for an arbitrary input). Fast and lossless, but only frame-accurate when every frame ofinput_videois already a keyframe — i.e.input_videocame fromto_editing_intermediate(). On an ordinary delivery-encoded input,copy=Truesilently snaps the cut to the nearest keyframe instead of the exact requested timestamp.Usage
-----
extract_video_chunk("input.mp4" (>>>)
10.0
20.0
"output_chunk.mp4")
extract_video_chunk("intermediate.mp4" (>>>)
10.0
20.0
"chunk.mp4"
copy=True)
- Return type:
None
- video_helper.image_loop_to_video(image, duration, output_video, frame_rate=30, width=None, height=None)[source]
Loop a still image for
durationseconds into a silent video.- Parameters:
image (str) – Path to the input still (PNG, JPG, …).
duration (float) – Output duration in seconds.
output_video (str) – Path to the output video file (.mp4 recommended).
frame_rate (int, optional) – Output frame rate (default 30).
width (int, optional) – If both provided, the image is letterboxed (scale + pad with black) to the target viewport. Width and height are rounded down to even.
height (int, optional) – If both provided, the image is letterboxed (scale + pad with black) to the target viewport. Width and height are rounded down to even.
- Return type:
None
Notes
Common in title cards, screenshot scenes, slide-style montages. Encoded as H.264 yuv420p with no audio track.
Examples
>>> image_loop_to_video("title.png", 3.0, "title.mp4", ... width=1920, height=1080)
- video_helper.is_valid_video_file(video_file)[source]
Check that
video_fileexists, has a known video extension, and contains a video stream.Combines an extension check (against
video_extensions) with anffprobeinvocation so both a fake.mp4(no video stream) and a real video renamed to.xyzare rejected.HTTP / HTTPS URLs short-circuit to
True: the only way to truly validate a remote URL is to spend bandwidth fetching part of the stream, and ffmpeg will surface a clear error downstream if the URL is bad. Callers passing a yt-dlp-resolved direct URL (withhttp_headers) wouldn’t even get past this gate otherwise.
- video_helper.iter_frame_optical_flow(frames, *, method='dis', dis_preset='fast', raft_variant='small', device='cpu', clip_flow=None, grayscale=False, output_width=None, output_height=None, wavelet='db2')[source]
Re-yield a BGR frame stream with 2 extra dense-optical-flow channels.
Wraps any
(H, W, 3)BGR uint8 frame iterator —video_helper.extract_frames()output,capture_helper.iter_camera_framesoutput, or any other source sharing that contract — and yields either(H, W, 5)float32 arrays (default: BGR frame + flow) or(H, W, 3)float32 arrays (grayscale=True: single-channel intensity + flow). In both layouts the last 2 channels are always per-pixel flowvx/vyrelative to the previous frame.- Parameters:
frames (Iterator[numpy.ndarray]) – Source frames, each
(H, W, 3)BGR uint8 (OpenCV convention).method ({"dis", "farneback", "raft"}, default "dis") – Optical-flow backend.
"dis"and"farneback"use onlyopencv-python(already a core dependency, no extra install)."raft"is a deep-learning estimator that needs the[flow]extra (pip install "video-helper[flow]") and is quality-first / GPU-recommended — CPU RAFT is not expected to run in real time.dis_preset ({"ultrafast", "fast", "medium"}, default "fast") – Speed/quality preset for
method="dis". Ignored otherwise.raft_variant ({"small", "large"}, default "small") –
raft_small(speed-favoring) orraft_large(quality-favoring). Only used formethod="raft". RAFT’s correlation pyramid needs feature maps at least 16px wide after an internal 8x downsample, so frames smaller than ~128x128 raise inside torchvision — not a video-helper limitation, but worth knowing before wrapping small crops/thumbnails.device (str, default "cpu") – Torch device for
method="raft"only:"cpu","mps","cuda", or"auto"(best available). Ignored for"dis"/"farneback", which are CPU-only OpenCV calls.clip_flow (float or None, default None) – When set, symmetrically clip both
vxandvyto[-clip_flow, clip_flow]pixels — suppresses rare outlier vectors (e.g. at scene cuts) without changing the array shape/dtype.grayscale (bool, default False) – When
True, yield(H, W, 3)arrays (grayscale intensity + flow) instead of the default(H, W, 5)(BGR + flow) — a smaller, motion-focused representation for callers that don’t need color (e.g. feeding a flow-only model). RAFT still computes flow from the full-color frame pair regardless of this flag; it only changes what gets written to the non-flow output channel(s).output_width (int, optional) – Resize each yielded frame to this width. Must be given together with
output_height(no aspect-preserving/padding mode here — for that, pre-resizeframesitself viaextract_frames(output_width=..., output_height=...)so flow is computed directly at the target resolution). This parameter is for the different case of resizing an already-computed flow field — e.g. computing flow at full quality then shrinking for storage, or computing cheaply at low resolution and upsampling for display. The image channel(s) are resized with standard bilinear interpolation (no discontinuity concern for color/intensity); the flow channels go throughresize_flow()(wavelet-based, magnitude-rescaled, discontinuity-aware).output_height (int, optional) – Resize each yielded frame to this height. See
output_width.wavelet (str, default "db2") – PyWavelets wavelet name forwarded to
resize_flow(). Ignored unlessoutput_width/output_heightare set.
- Yields:
numpy.ndarray –
grayscale=False(default):(H, W, 5)float32 array per input frame.[..., :3]is the BGR frame cast to float32 (values 0-255 —.astype(np.uint8)recovers the plain image);[..., 3]isvx,[..., 4]isvy.grayscale=True:(H, W, 3)float32;[..., 0]is grayscale intensity,[..., 1]isvx,[..., 2]isvy. Either way flow is signed pixel displacement, and the first yielded frame has zero flow (no previous frame yet), keeping frame count 1:1 withframes.- Raises:
ValueError – If
method,dis_preset, orraft_variantis not a supported value, or if exactly one ofoutput_width/output_heightis given without the other.ImportError – If
method="raft"is requested buttorchvisionis not installed, or ifoutput_width/output_heightare requested butPyWaveletsis not installed (install either withpip install "video-helper[flow]").
- Return type:
Iterator[ndarray]
Examples
>>> import video_helper as vh >>> frames = vh.extract_frames("clip.mp4", frame_step=1) >>> for flow_frame in vh.iter_frame_optical_flow(frames, method="dis"): ... bgr = flow_frame[..., :3].astype("uint8") ... vx, vy = flow_frame[..., 3], flow_frame[..., 4] ... break >>> for flow_frame in vh.iter_frame_optical_flow(frames, method="dis", grayscale=True): ... gray = flow_frame[..., 0].astype("uint8") ... vx, vy = flow_frame[..., 1], flow_frame[..., 2] ... break
Notes
Composability is the point: this function takes a generic frame iterator rather than a video path, so it works identically wrapping
video_helper.extract_frames(...)(file) orcapture_helper.iter_camera_frames(...)(live camera) — both already share the same(H, W, 3)BGR uint8 contract.
- video_helper.mux_audio_video(input_video, input_audio, output_video, audio_codec='aac', audio_bitrate='192k', shortest=False)[source]
Mux a separate audio track onto a (typically silent) video.
- Parameters:
input_video (str) – Path to the video file. Any existing audio track is replaced.
input_audio (str) – Path to the audio file (WAV, MP3, AAC, …).
output_video (str) – Path to the output video file (.mp4 recommended).
audio_codec (str, optional) – Audio codec for the output stream (default
"aac"). Use"copy"if the input audio is already in a container-compatible codec.audio_bitrate (str, optional) – Audio bitrate when re-encoding (default
"192k"); ignored whenaudio_codec="copy".shortest (bool, optional) – If
True, the output stops when the shorter of the two streams ends. IfFalse(default), the output keeps the video length and the audio is padded with silence (or truncated) by the muxer.
- Return type:
None
Notes
Video stream is copied — no re-encoding — so the muxing is fast and lossless on the video side. Use this after assembling a silent
visuals.mp4and a separatenarration.wavtrack.Examples
>>> mux_audio_video("silent.mp4", "voice.wav", "final.mp4")
- video_helper.overlay_image(input_video, image, output_video, x='0', y='0', scale_width=None)[source]
Overlay a still image (PNG with alpha works) on top of a video.
- Parameters:
input_video (str) – Path to the base video.
image (str) – Path to the overlay image (PNG with alpha is the typical case — cursors, watermarks, logos).
output_video (str) – Path to the output video file (.mp4 recommended).
x (str, optional) – Overlay positions. Plain integers (
"10") place the image statically; ffmpeg overlay expressions ("if(lt(t,1.0),0,100)","W/2-w/2", …) move the overlay over time. Default"0","0"(top-left).y (str, optional) – Overlay positions. Plain integers (
"10") place the image statically; ffmpeg overlay expressions ("if(lt(t,1.0),0,100)","W/2-w/2", …) move the overlay over time. Default"0","0"(top-left).scale_width (int, optional) – If provided, scale the overlay to this width keeping aspect ratio — useful for cursor PNGs that come at a different size than the target frame.
- Return type:
None
Notes
Time-varying expressions are evaluated per-frame (
eval=frame) so animations stay smooth at any framerate. The underlying video stream is re-encoded (libx264) and the original audio track, if any, is preserved.Examples
>>> overlay_image("clip.mp4", "cursor.png", "out.mp4", ... x="if(lt(t,2),100,400)", y="200", ... scale_width=24)
- video_helper.resize_flow(flow, output_width, output_height, *, wavelet='db2')[source]
Resize a dense optical-flow field, preserving motion discontinuities.
Applies
_wavelet_resize_channel()tovxandvyindependently, then rescales the displacement magnitudes by the same factor as the spatial resize — a flow field encodes physical pixel displacement, so “5px right” at the original resolution must become “10px right” after a 2x upsample, exactly like rescaling a velocity when changing units. Skipping this step is a common, silent correctness bug in flow-resizing code (frame-only resizing tools likecv2.resizedon’t know to do it, since a plain image has no such physical meaning).This is specifically for the 2-channel
vx/vyflow itself — for the BGR/grayscale image channels, standard interpolation (e.g.extract_frames’s ownoutput_width/output_height) is the right tool; there is no discontinuity-preservation concern for color.- Parameters:
flow (numpy.ndarray) – Flow field
(H, W, 2)float, channels[vx, vy]— e.g. the last 2 channels of aniter_frame_optical_flow()output frame.output_width (int) – Target width in pixels.
output_height (int) – Target height in pixels.
wavelet (str, default "db2") – A PyWavelets wavelet name.
"db2"(Daubechies-2) is a reasonable default — smoother reconstruction than"haar"(which is blocky) without the longer-filter ringing of higher-order wavelets.
- Returns:
(output_height, output_width, 2)float32, magnitude-rescaled.- Return type:
numpy.ndarray
- Raises:
ValueError – If
flowis not(H, W, 2), oroutput_width/output_heightis not a positive integer.ImportError – If
PyWaveletsis not installed (install withpip install "video-helper[flow]").
Examples
>>> import numpy as np >>> flow = np.zeros((64, 64, 2), dtype=np.float32) >>> flow[:, 32:, 0] = 5.0 # a hard motion boundary: 5px/frame rightward >>> resized = resize_flow(flow, output_width=32, output_height=32) >>> resized.shape (32, 32, 2)
Notes
Only the portion of the resize that is an integer power-of-two ratio (in both axes together) goes through the wavelet transform; the remainder (always < 2x, and the whole resize for a mixed up/down-sample across axes) falls back to a single nearest-neighbor snap — see
_wavelet_resize_channel()for the full rationale.
- video_helper.srt2vtt(srt_file_path, vtt_file_path=None, css_file_path=None)[source]
Convert an SRT subtitle file to WebVTT, preserving font colors and emitting a companion CSS file.
Any
<font color="#RRGGBB">…</font>tag in the SRT is rewritten as a WebVTT<c.<hex_lowercase>>…</c>cue class, and a stylesheet binding each class to its color is written next to the VTT.- Parameters:
- Return type:
None
Examples
>>> srt2vtt("subtitles.srt") >>> srt2vtt("subtitles.srt", "out.vtt", "out.css")
- video_helper.video_converter(input_video, output_video=None, frame_rate=None, width=None, height=None, without_sound=False)[source]
Convert a video file to a new format with specified options.
- Parameters:
input_video (str) – Path to the input video file.
output_video (str) – Path to the output video file.
frame_rate (int, optional) – Frame rate of the output video file.
width (int, optional) – Width of the output video file. If only width is specified, aspect ratio is maintained. If width is odd, it is reduced by 1 (ffmpeg reasons).
height (int, optional) – Height of the output video file. If only height is specified, aspect ratio is maintained. If height is odd, it is reduced by 1 (ffmpeg reasons).
without_sound (bool, optional) – Remove audio from the output video file.
- Return type:
None
Notes
The output video file will be in the same format as the input, unless an output file with a different format is specified.
Examples
>>> video_converter("input.mp4", "output.mp4", frame_rate=30, width=640, height=480) >>> video_converter("input.mp4", "output.mp4", without_sound=True)
- video_helper.video_dimensions(video_file, http_headers=None)[source]
Get the dimensions of a video file (or URL) using
ffmpeg-python.Returned keys:
width,height,duration,frame_rate,has_sound.- Parameters:
video_file (str) – Path to the input video file, OR an HTTP / HTTPS URL (e.g. a yt-dlp-resolved direct media URL).
http_headers (dict[str, str], optional) – HTTP headers (User-Agent, Referer, Cookie, …) forwarded to ffprobe via
-headers. Required whenvideo_fileis a URL that needs specific headers — e.g. yt-dlp-resolved YouTube live, members-only, age-gated content. Ignored whenvideo_fileis a local path.
- Returns:
{"width": int, "height": int, "duration": float, "frame_rate": float, "has_sound": bool}.- Return type:
Examples
>>> d = video_dimensions("video.mp4") >>> print(d) {'width': 1920, 'height': 1080, 'duration': 10.0, 'frame_rate': 30.0, 'has_sound': True}
Notes
Uses
ffmpeg.probe()(a thin wrapper overffprobe) for metadata extraction.http_headersare passed through ffprobe’s-headersflag so URL-protected streams probe correctly.
- video_helper.video_duration(input_video)[source]
Return the duration (seconds) of a video file.
- Parameters:
input_video (str) – Path to the input video file.
- Returns:
Duration in seconds.
- Return type:
Notes
Mirror of
audio_helper.get_audio_durationfor the video side. Usesvideo_dimensionsunder the hood, which already callsffmpeg.probe— kept as a top-level convenience so callers don’t have to remember the dict key.Examples
>>> video_duration("clip.mp4") 12.34