video_helper package
Submodules
- video_helper.api module
- video_helper.cli_argparse module
- video_helper.cli_click module
- video_helper.gui module
- video_helper.main module
- video_helper.main
black_video()burn_subtitles()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()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 five 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.MCP server:
video-helper-mcp(needs the[api,mcp]extras).
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.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"raisesValueErrorin v1.5.0 — it would require 4-channel BGRA output, breaking the(H, W, 3)contract. Planned for v1.6.0.
- 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_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)[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.
Usage
-----
extract_video_chunk("input.mp4" (>>>)
10.0
20.0
"output_chunk.mp4")
- 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.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.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