annie.media.convert

Re-encode an audio/video dataset to a consistent, validated form (service).

Some source clips are subtly broken: their container metadata disagrees with the real frame count (torchcodec’s approximate mode derives num_frames from duration × fps, so a clip can report more frames than it has and blow up decoding near the end). Re-encoding every file to constant-frame-rate H.264 — and validating the result with torchcodec — makes the dataset consistent for Annie and for downstream dataloaders / feature extraction.

The module is split into pure, unit-tested pieces (ffmpeg command builders, stem pairing, length math, the batch runner with an injected work function) and thin impure wrappers (run_ffmpeg, validate_video, convert_*_file) that need a system FFmpeg and the media extra and are exercised manually.

Audio/video length contract when muxing: the audio filter apad pads the audio with silence and -shortest trims the output to the (finite) video stream, so the muxed audio is exactly the video’s duration — short audio is padded, long audio is cut — which is what audio_video_consistent() checks.

Attributes

AUDIO_INPUT_SUFFIXES

Accepted source-audio suffixes (lower-case, dotted).

VIDEO_INPUT_SUFFIXES

Accepted source-video suffixes (lower-case, dotted).

AUDIO_OUTPUT_FORMATS

Offered audio output containers/codecs.

VIDEO_OUTPUT_FORMATS

Offered video containers/codecs.

COMMON_SAMPLE_RATES

Sample rates the UI offers as quick picks.

COMMON_FPS

Quick-pick constant frame rates.

BLACK_DEFAULT_SIZE

Black-frame fallback size for a synthesised audio-only video.

BLACK_DEFAULT_FPS

Fps of the synthesised black video when the source has none.

ConvertStatus

Lifecycle state of a batch conversion run.

Classes

AudioOptions

Audio re-encode settings.

VideoOptions

Video re-encode settings.

AudioItem

One planned audio conversion (source → destination).

VideoItem

One planned video conversion.

ConvertProgress

A snapshot of a running (or finished) conversion batch.

ConversionRunner

Runs one conversion batch at a time on a background thread, with progress.

Functions

audio_command(→ list[str])

Build the ffmpeg argv to re-encode one audio file.

video_filters(→ str)

Build the -vf filter chain string for the given video options.

video_command(→ list[str])

Build the ffmpeg argv to re-encode one video (constant frame rate H.264).

black_video_command(→ list[str])

Build the ffmpeg argv for an audio-only item: a black-frame video + audio.

plan_audio(→ list[AudioItem])

Plan the audio batch: one AudioItem per input file.

plan_video(→ list[VideoItem])

Plan the video batch: one VideoItem per input file.

expected_frame_count(→ int)

Return the number of frames a duration_s clip at fps should have.

expected_sample_count(→ int)

Return the number of audio samples a duration_s clip at sample_rate has.

audio_video_consistent(→ bool)

Whether an audio track and a video stream have matching durations.

run_ffmpeg(→ None)

Run an ffmpeg command, raising RuntimeError with its tail on failure.

decodable_frames(→ int)

Return the torchcodec frame count, verified by actually decoding, or 0.

validate_video(→ tuple[bool, str])

Validate a re-encoded video with torchcodec.

convert_audio_file(→ None)

Re-encode one audio file, skipping it if a non-empty output already exists.

convert_video_file(→ None)

Re-encode one video (or synthesise a black-frame one), then validate it.

export_ids(→ pathlib.Path)

Write a one-column CSV (id) of the given filenames' stems, sorted.

export_failures(→ pathlib.Path)

Write a two-column CSV (id,error) for the failed files, sorted by id.

Module Contents

annie.media.convert.AUDIO_INPUT_SUFFIXES: tuple[str, ...] = ('.wav', '.mp3', '.flac', '.m4a', '.aac', '.ogg', '.opus')

Accepted source-audio suffixes (lower-case, dotted).

annie.media.convert.VIDEO_INPUT_SUFFIXES: tuple[str, ...] = ('.mp4', '.avi', '.mov', '.mkv', '.webm', '.m4v')

Accepted source-video suffixes (lower-case, dotted).

annie.media.convert.AUDIO_OUTPUT_FORMATS: tuple[str, ...] = ('wav', 'flac', 'mp3')

Offered audio output containers/codecs.

annie.media.convert.VIDEO_OUTPUT_FORMATS: tuple[str, ...] = ('mp4',)

Offered video containers/codecs.

annie.media.convert.COMMON_SAMPLE_RATES: tuple[int, ...] = (8000, 16000, 22050, 44100, 48000)

Sample rates the UI offers as quick picks.

annie.media.convert.COMMON_FPS: tuple[float, ...] = (10, 24, 25, 30)

Quick-pick constant frame rates.

class annie.media.convert.AudioOptions[source]

Audio re-encode settings.

out_format

Output container/codec ("wav" default, PCM 16-bit).

sample_rate

Target sample rate in Hz.

channels

Channel count (1 = mono, the default).

class annie.media.convert.VideoOptions[source]

Video re-encode settings.

width

Target width in px, or None to keep the source width.

height

Target height in px, or None to keep the source height.

grayscale

Desaturate to black-and-white when True (still yuv420p).

fps

Target constant frame rate, or None to keep the source fps.

add_audio

Mux the matching-stem audio into the output when True.

min_frames

Reject (skip) any source whose output would have fewer than this many decodable frames. 2 (default) drops degenerate single-frame “videos” (audio-only files that carry one thumbnail-like frame); 1 only requires that at least one frame decodes.

out_format

Output container ("mp4").

property keep_resolution: bool

Whether the source resolution is kept (no scaling).

annie.media.convert.BLACK_DEFAULT_SIZE = 256

Black-frame fallback size for a synthesised audio-only video.

Used when the options keep the original size/fps (of which there is none).

annie.media.convert.BLACK_DEFAULT_FPS = 25.0

Fps of the synthesised black video when the source has none.

annie.media.convert.audio_command(src: str | pathlib.Path, dst: str | pathlib.Path, opts: AudioOptions) → list[str][source]

Build the ffmpeg argv to re-encode one audio file.

Parameters:
  • src – Source audio (or a media file with an audio stream).

  • dst – Destination path.

  • opts – Audio settings.

Returns:

The ffmpeg argv list (no shell).

annie.media.convert.video_filters(opts: VideoOptions) → str[source]

Build the -vf filter chain string for the given video options.

Parameters:

opts – Video settings.

Returns:

A comma-joined filter chain, or "" when no spatial filter is needed. A single given dimension scales the other by -2 (keeps aspect, even).

annie.media.convert.video_command(src: str | pathlib.Path, dst: str | pathlib.Path, opts: VideoOptions, audio_path: str | pathlib.Path | None = None) → list[str][source]

Build the ffmpeg argv to re-encode one video (constant frame rate H.264).

Audio handling: when opts.add_audio and audio_path is given, the audio is muxed and matched to the video length via apad + -shortest; otherwise the output has no audio (-an).

Parameters:
  • src – Source video.

  • dst – Destination .mp4.

  • opts – Video settings.

  • audio_path – Matching audio file to mux, or None.

Returns:

The ffmpeg argv list (no shell).

annie.media.convert.black_video_command(dst: str | pathlib.Path, audio: str | pathlib.Path, opts: VideoOptions) → list[str][source]

Build the ffmpeg argv for an audio-only item: a black-frame video + audio.

The black lavfi source is infinite and -shortest trims the output to the (finite) audio, so the synthesised video has exactly the audio’s duration. The frame size / fps come from opts or fall back to BLACK_DEFAULT_SIZE / BLACK_DEFAULT_FPS when the options keep the (non-existent) original.

Parameters:
  • dst – Destination .mp4.

  • audio – The audio to carry.

  • opts – Video settings (used for size / fps).

Returns:

The ffmpeg argv list (no shell).

class annie.media.convert.AudioItem[source]

One planned audio conversion (source → destination).

property label: str

Display name for progress (the source filename).

class annie.media.convert.VideoItem[source]

One planned video conversion.

src is the source video, or None for an audio-only item that should be synthesised as a black-frame video carrying audio.

property label: str

Display name for progress (the source filename, or the output name).

property is_black: bool

Whether this item synthesises a black-frame video (no source video).

annie.media.convert.plan_audio(in_dir: str | pathlib.Path | None, out_dir: str | pathlib.Path, opts: AudioOptions) → list[AudioItem][source]

Plan the audio batch: one AudioItem per input file.

Parameters:
  • in_dir – Folder of source audio files.

  • out_dir – Destination folder.

  • opts – Audio settings (its out_format sets the destination suffix).

Returns:

The planned items (empty if the input folder is missing).

annie.media.convert.plan_video(in_dir: str | pathlib.Path | None, out_dir: str | pathlib.Path, opts: VideoOptions, audio_dir: str | pathlib.Path | None = None) → list[VideoItem][source]

Plan the video batch: one VideoItem per input file.

When opts.add_audio and audio_dir is given, each video is paired by stem with an audio file of any supported format in audio_dir. When opts.audio_only_black is also set, an audio file with no matching video becomes a black-frame video carrying that audio; otherwise such audio yields no video at all (it stays only in the audio folder).

Parameters:
  • in_dir – Folder of source videos.

  • out_dir – Destination folder.

  • opts – Video settings (its out_format sets the destination suffix).

  • audio_dir – Folder of audio to mux (typically the audio output folder).

Returns:

The planned items (empty if the input folder is missing).

annie.media.convert.expected_frame_count(duration_s: float, fps: float) → int[source]

Return the number of frames a duration_s clip at fps should have.

annie.media.convert.expected_sample_count(duration_s: float, sample_rate: int) → int[source]

Return the number of audio samples a duration_s clip at sample_rate has.

annie.media.convert.audio_video_consistent(num_frames: int, fps: float, num_samples: int, sample_rate: int, *, tolerance_s: float = 0.04) → bool[source]

Whether an audio track and a video stream have matching durations.

Parameters:
  • num_frames – Decoded video frame count.

  • fps – Video frame rate.

  • num_samples – Decoded audio sample count (per channel).

  • sample_rate – Audio sample rate.

  • tolerance_s – Allowed absolute difference in seconds (≈ one frame at 25 fps).

Returns:

True if the two durations agree within tolerance_s.

annie.media.convert.run_ffmpeg(cmd: list[str]) → None[source]

Run an ffmpeg command, raising RuntimeError with its tail on failure.

annie.media.convert.decodable_frames(path: str | pathlib.Path) → int[source]

Return the torchcodec frame count, verified by actually decoding, or 0.

A file with a declared video stream but no real frames (an audio-only .mp4 that VLC plays as audio) can still report num_frames >= 1 in its metadata, so the count alone is not enough — we decode the first (and last) frame to confirm they really exist. Any decode failure yields 0.

Parameters:

path – The media file to inspect.

Returns:

The number of decodable frames (0 if the frames cannot be decoded).

annie.media.convert.validate_video(path: str | pathlib.Path, min_frames: int = 1) → tuple[bool, str][source]

Validate a re-encoded video with torchcodec.

Requires at least min_frames frames (so a degenerate single-frame, audio-only result is rejected), decodes every frame in exact mode (so an unreadable frame raises), and confirms the exact and approximate frame counts agree — the discrepancy that makes some source clips fail late in decoding.

Parameters:
  • path – The video to validate.

  • min_frames – Minimum decodable frame count required to pass.

Returns:

An (ok, message) pair.

annie.media.convert.convert_audio_file(item: AudioItem, opts: AudioOptions) → None[source]

Re-encode one audio file, skipping it if a non-empty output already exists.

On any failure the (possibly partial) output is removed, so the output folder only ever contains good files.

annie.media.convert.convert_video_file(item: VideoItem, opts: VideoOptions) → None[source]

Re-encode one video (or synthesise a black-frame one), then validate it.

Grace handling: a source .mp4 is skipped with an error (and never lands in the output folder) when it has no video stream or yields fewer than opts.min_frames decodable frames — which drops degenerate single-frame “videos” (audio-only files carrying one thumbnail-like frame, which VLC plays as audio-only). A stale bad output from a previous run is removed in those cases too. If a torchcodec-valid output already exists it is skipped (resume); any failure removes a partial output.

annie.media.convert.export_ids(path: str | pathlib.Path, ids: list[str]) → pathlib.Path[source]

Write a one-column CSV (id) of the given filenames’ stems, sorted.

Parameters:
  • path – Destination CSV path.

  • ids – Filenames (or ids); their stems are written, de-duplicated and sorted.

Returns:

The path written.

annie.media.convert.export_failures(path: str | pathlib.Path, failures: list[tuple[str, str]]) → pathlib.Path[source]

Write a two-column CSV (id,error) for the failed files, sorted by id.

Parameters:
  • path – Destination CSV path.

  • failures – (filename, error) pairs.

Returns:

The path written.

annie.media.convert.ConvertStatus

Lifecycle state of a batch conversion run.

class annie.media.convert.ConvertProgress[source]

A snapshot of a running (or finished) conversion batch.

title

Human label of the batch (e.g. "Audio" / "Video").

total

Number of files in the batch.

done

Number processed so far (succeeded or failed).

current

The file currently being processed.

succeeded

Filenames that converted (or were skipped as already done).

failed

(filename, error) pairs for files that failed.

started_at

Epoch seconds when the batch started, or None.

finished_at

Epoch seconds when it finished, or None while running.

status

One of ConvertStatus.

property elapsed: float

Seconds elapsed since the batch started (frozen once finished).

property fraction: float

Progress as a fraction in [0, 1].

property eta_seconds: float | None

Estimated seconds remaining, or None if not yet estimable.

property succeeded_count: int

Number of files processed without error.

class annie.media.convert.ConversionRunner[source]

Runs one conversion batch at a time on a background thread, with progress.

The batch loop is generic: it is given a list of items, a work callable that converts one item (raising on failure), and a label callable for display. Failures are recorded per item and never stop the batch.

running() → bool[source]

Whether a batch is currently in progress.

progress() → ConvertProgress[source]

Return a consistent snapshot of the current progress.

cancel() → None[source]

Request cancellation; the batch stops after the current file.

start(title: str, items: list, work: collections.abc.Callable[[object], None], *, label: collections.abc.Callable[[object], str] = lambda it: ...) → concurrent.futures.Future[source]

Begin a batch on the worker thread.

Parameters:
  • title – Human label for the batch.

  • items – The planned items to convert.

  • work – Converts one item, raising on failure.

  • label – Maps an item to its display name.

Returns:

The Future of the batch (mostly for tests).

Raises:

RuntimeError – If a batch is already running.

shutdown() → None[source]

Shut the worker pool down, waiting for an in-flight batch to finish.