annie.media.rendering

Render pipeline: burn annotations into a browser-playable preview clip.

A render is submitted as a job and runs on a background thread, so the unpredictable, CPU-bound work of decoding and re-encoding never stalls the UI. The submit → poll → path contract is identical whether the work is backed by a thread pool (now) or an external queue (later), so the seam costs nothing today and upgrades cleanly.

The job tracker (submit / status / sweep) is pure Python and unit-tested with an injected render function. The default render function (burn_clip()) needs the media extra and a system FFmpeg; it decodes frames, draws the overlay (annie.color.draw_overlay()), pipes raw RGB into an ffmpeg subprocess (libx264), and muxes the source audio back. Outputs land in a temp dir that a TTL sweeper purges, so rendered clips never accumulate on disk.

Attributes

RenderFn

Signature of a render function.

CRF

x264 quality for rendered previews (lower is better; ffmpeg's default is 23).

PRESET

x264 speed/compression trade-off. The reviewer is waiting on this render, so

FASTSTART

-movflags value that relocates the MP4 moov atom ahead of the media data.

Classes

JobStatus

Lifecycle states of a render job.

RenderJob

A single render job and its current state.

RenderService

Thread-pool-backed render-job tracker with a TTL temp sweeper.

Functions

burn_clip(→ None)

Render a video's combined annotations into a browser-playable clip.

Module Contents

annie.media.rendering.RenderFn

Signature of a render function.

Given a video entry and an output path, produce a browser-playable annotated clip.

annie.media.rendering.CRF = 26

x264 quality for rendered previews (lower is better; ffmpeg’s default is 23).

These clips are throwaway review aids swept within minutes, watched in a box a few hundred pixels wide, so preview-grade quality buys a materially smaller file and a faster render without affecting frame count or timing.

annie.media.rendering.PRESET = 'veryfast'

x264 speed/compression trade-off. The reviewer is waiting on this render, so spend the time budget on latency rather than on the last few percent of bitrate.

annie.media.rendering.FASTSTART = '+faststart'

-movflags value that relocates the MP4 moov atom ahead of the media data.

Without it the atom lands at the end of the file and a browser must fetch the whole clip before the first frame can play.

class annie.media.rendering.JobStatus[source]

Bases: enum.StrEnum

Lifecycle states of a render job.

Initialize self. See help(type(self)) for accurate signature.

class annie.media.rendering.RenderJob[source]

A single render job and its current state.

job_id

Unique opaque id used to poll for status.

row_key

The Browse row this render belongs to.

status

Current JobStatus.

output_path

Path to the finished clip once status is DONE.

error

Error message when status is FAILED.

created_at

Monotonic-ish wall-clock submit time (epoch seconds).

class annie.media.rendering.RenderService(render_fn: RenderFn | None = None, *, temp_dir: str | pathlib.Path | None = None, max_workers: int | None = None, ttl_seconds: int | None = None)[source]

Thread-pool-backed render-job tracker with a TTL temp sweeper.

Parameters:
  • render_fn – The function that actually produces a clip. Defaults to burn_clip(); tests inject a fake to exercise the lifecycle without a media backend.

  • temp_dir – Directory for rendered clips. Defaults to the configured temp dir.

  • max_workers – Maximum concurrent renders. Defaults to the configured value.

  • ttl_seconds – Age after which a finished clip is eligible for sweeping.

submit(entry: annie.core.models.VideoEntry) → str[source]

Enqueue a render for entry and return its job id immediately.

Parameters:

entry – The video to render. Must have a video file to decode.

Returns:

The job id to poll with get().

Raises:

ValueError – If the entry has no video path to render.

get(job_id: str) → RenderJob | None[source]

Return the job for job_id, or None if unknown.

Parameters:

job_id – The id returned by submit().

Returns:

The RenderJob, or None.

jobs() → list[RenderJob][source]

Return a snapshot of all tracked jobs.

Returns:

A list copy of the current jobs.

sweep(*, now: float | None = None) → int[source]

Delete rendered clips whose file is older than the TTL; forget their jobs.

File-based (by modification time) rather than job-based, so it also reclaims clips orphaned by a page refresh or disconnect — where the job is still tracked but the UI element that showed it is gone — and any .mp4 left by a previous process. A running job’s output is never touched.

Parameters:

now – Override for the current epoch time (for testing).

Returns:

The number of clip files deleted.

clear_all() → tuple[int, int][source]

Delete every finished clip and forget all non-running jobs.

Unlike sweep(), this ignores the TTL and also removes any orphaned .mp4 files in temp_dir that are not tracked by a job (e.g. left over from a previous process). Running jobs are left untouched so an in-flight render is not corrupted.

Returns:

A (jobs_cleared, files_deleted) pair — the number of jobs removed from the tracker and the number of files actually unlinked from disk.

shutdown() → None[source]

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

annie.media.rendering.burn_clip(entry: annie.core.models.VideoEntry, output_path: pathlib.Path) → None[source]

Render a video’s combined annotations into a browser-playable clip.

Decodes every frame of the source (a frame with no detections is written through unchanged, so the render always has the same frame count as the original) and draws, on each: the vdet boxes in flat blue, every track in its own stable unique colour (never blue/green), and the active / protagonist track in green. Frames are piped as raw RGB into an ffmpeg libx264 subprocess, and the source audio is muxed back (the encode is not -shortest, so a shorter audio track never truncates the video).

The output is H.264 in MP4 — already the most broadly playable web format, so there is nothing to switch to. What makes it web-friendly rather than merely web-compatible is FASTSTART-style muxing (the moov atom is moved to the front, letting the browser start playing before the file has fully downloaded) plus a preview-grade CRF. Neither touches frame count, duration, resolution, or frame timing — only how many bits each frame costs — so the render stays frame-for-frame aligned with the original.

Parameters:
  • entry – The video to render; entry.video_path must be set and at least one of vdet/tracks should be present for boxes to appear.

  • output_path – Where to write the encoded .mp4.

Raises:
  • annie.decode.MediaUnavailableError – If the media extra is absent.

  • ValueError – If the entry has no video path.

  • RuntimeError – If the ffmpeg subprocess fails.