capture_helper.scene module

capture_helper.scene

Scene / configuration model for capture-helper’s live multi-source scene configurator. A scene is a reusable, serialisable description of a live composition: a named canvas of a given pixel size, holding an ordered list of named capture sources (cameras / microphones) each pinned to a rectangle of the canvas with per-source capture parameters.

Why this module exists

The browser GUI (served at GET /gui by capture_helper.api) lets a user visually enumerate devices, drop several onto a canvas, live-preview them, arrange them, and then save the design as a JSON artifact. That artifact is exactly a Scene. The CLI and the HTTP API can then load the same JSON and drive the underlying iterators — the visual design becomes a headless, scriptable capture recipe. This module is the shared, hardware-free core of that round-trip: build → validate → save → load → resolve.

Design constraints

  • Purely additive. Nothing here touches the existing iterator contracts (capture_helper.iter_camera_frames() / iter_mic_audio()). A SceneSource merely records selectors and parameters that are later fed to those functions unchanged.

  • JSON only, no new dependency. Scenes serialise to plain JSON via the standard library so the artifact is trivially diffable, version-controllable, and readable by any language.

  • Best-effort resolution. resolve_scene_sources() matches each recorded source against the current machine’s devices; a scene authored on one machine can be replayed on another as long as the device names / indices line up (or degrade gracefully when they do not).

Usage example

>>> import capture_helper as ch
>>> scene = ch.new_scene("podcast", width=1280, height=720)
>>> scene = ch.add_source(scene, kind="camera", name_substring="FaceTime",
...                       x=0, y=0, w=1280, h=720, label="host cam")
>>> scene = ch.add_source(scene, kind="microphone", name_substring="Built-in",
...                       x=0, y=0, w=0, h=0, label="host mic")
>>> ch.save_scene(scene, "podcast.scene.json")
>>> reloaded = ch.load_scene("podcast.scene.json")
>>> reloaded["name"]
'podcast'

Author

Warith Harchaoui, Ph.D. — https://linkedin.com/in/warith-harchaoui/

class capture_helper.scene.ResolvedSceneSource[source]

Bases: TypedDict

A SceneSource paired with the live device it resolved to.

Keys

scene_sourceSceneSource

The original recipe from the scene.

resolvedSource | None

The live capture_helper.Source matched on this machine, or None when no device satisfied the recipe’s selectors.

errorstr | None

Human-readable reason the recipe could not be resolved, or None on success.

error: str | None
resolved: Source | None
scene_source: SceneSource
class capture_helper.scene.Scene[source]

Bases: TypedDict

A named, serialisable multi-source composition.

Keys

format_versionint

On-disk schema version (SCENE_FORMAT_VERSION).

namestr

Human name for the scene (also the suggested filename stem).

width, heightint

Canvas size in pixels. Source tiles are positioned within this box.

sourceslist[SceneSource]

Ordered list of placed sources (draw order for cameras is by z, ties broken by list order).

format_version: int
height: int
name: str
sources: list[SceneSource]
width: int
class capture_helper.scene.SceneSource[source]

Bases: TypedDict

One source placed on a scene canvas.

A SceneSource is a recipe, not a live handle: it records how to re-select the device (kind + optional name_substring / index), where it sits on the canvas (x/y/w/h/z), and the capture parameters to pass to the iterators when the scene is run.

Keys

idstr

Stable, GUI-generated identifier (uuid4 hex) so the front-end can address a placed source across drag / resize / delete without relying on list order.

kind"camera" | "microphone"

Which iterator this source drives.

labelstr

Human-friendly name shown in the GUI (e.g. "host cam"). Free text.

name_substringstr | None

Case-insensitive substring used to re-resolve the device via capture_helper.pick_source(). None = no name constraint.

indexint | None

Exact device index used to re-resolve the device. None = no index constraint. When both are None the first device of kind is used.

x, yint

Top-left position of the source’s tile on the canvas, in pixels.

w, hint

Tile size in pixels. For microphones (no visual) these may be 0.

zint

Stacking order — higher draws on top. Cameras only.

paramsdict

Free-form capture parameters forwarded to the iterator. For cameras: fps, output_width, output_height, pad_color. For microphones: target_sample_rate, frame_ms, to_mono. Unknown keys are ignored by the runner, so the GUI can round-trip extras safely.

h: int
id: str
index: int | None
kind: Literal['camera', 'microphone']
label: str
name_substring: str | None
params: dict[str, Any]
w: int
x: int
y: int
z: int
capture_helper.scene.add_source(scene, *, kind, name_substring=None, index=None, label=None, x=0, y=0, w=0, h=0, z=0, params=None, source_id=None)[source]

Return a copy of scene with one additional placed source.

The input scene is not mutated — a shallow copy with a new sources list is returned, so callers holding the old scene keep it intact (matches the immutable-update convention used across the suite’s config helpers).

Parameters:
  • scene (Scene) – The scene to extend.

  • kind ("camera" | "microphone") – Source kind.

  • name_substring (str, optional) – Case-insensitive device-name filter recorded for later resolution.

  • index (int, optional) – Exact device-index filter recorded for later resolution.

  • label (str, optional) – Display name. Defaults to a generated "<kind> N" label.

  • x (int) – Canvas placement (see SceneSource).

  • y (int) – Canvas placement (see SceneSource).

  • w (int) – Canvas placement (see SceneSource).

  • h (int) – Canvas placement (see SceneSource).

  • z (int) – Canvas placement (see SceneSource).

  • params (dict, optional) – Capture parameters forwarded to the iterator at run time.

  • source_id (str, optional) – Explicit id (uuid4 hex). Generated when omitted — the GUI usually supplies its own so the round-trip is stable.

Returns:

A new scene including the added source.

Return type:

Scene

Examples

>>> s = new_scene("demo")
>>> s2 = add_source(s, kind="camera", label="cam", w=1280, h=720)
>>> len(s["sources"]), len(s2["sources"])
(0, 1)
capture_helper.scene.load_scene(path)[source]

Load and validate a scene from a JSON file.

Parameters:

path (str or Path) – Path to a scene JSON file.

Returns:

The parsed, validated scene.

Return type:

Scene

Raises:

Examples

>>> import tempfile, os
>>> p = os.path.join(tempfile.mkdtemp(), "demo.scene.json")
>>> _ = save_scene(new_scene("demo"), p)
>>> load_scene(p)["name"]
'demo'
capture_helper.scene.new_scene(name='untitled', *, width=1280, height=720)[source]

Create an empty scene with a named canvas.

Parameters:
  • name (str, default "untitled") – Human name for the scene; also the suggested filename stem.

  • width (int) – Canvas size in pixels. Defaults to 1280x720.

  • height (int) – Canvas size in pixels. Defaults to 1280x720.

Returns:

A scene with no sources yet.

Return type:

Scene

Raises:

ValueError – If width or height is not a positive integer.

Examples

>>> s = new_scene("demo", width=640, height=360)
>>> s["width"], s["height"], s["sources"]
(640, 360, [])
capture_helper.scene.resolve_scene_sources(scene)[source]

Match every scene source against the current machine’s live devices.

A scene authored elsewhere (or before a device was plugged in) may not fully resolve here. Rather than raising on the first miss, this returns one ResolvedSceneSource per recipe so the GUI / CLI can report exactly which tiles are live and which are dangling.

Parameters:

scene (Scene) – The scene to resolve.

Returns:

One entry per source in scene["sources"], in the same order.

Return type:

list[ResolvedSceneSource]

Examples

>>> res = resolve_scene_sources(new_scene("empty"))
>>> res
[]
capture_helper.scene.save_scene(scene, path)[source]

Serialise a scene to a JSON file on disk.

Parameters:
  • scene (Scene) – The scene to persist. Validated before writing.

  • path (str or Path) – Destination path. Parent directories are created if missing.

Returns:

The absolute path the scene was written to.

Return type:

str

Raises:

ValueError – If scene fails validate_scene().

Examples

>>> import tempfile, os
>>> p = os.path.join(tempfile.mkdtemp(), "demo.scene.json")
>>> _ = save_scene(new_scene("demo"), p)
>>> os.path.exists(p)
True
capture_helper.scene.scene_from_available_devices(name='auto')[source]

Build a starter scene from whatever devices this machine reports.

Convenience used by the GUI’s “auto-populate” button and by the docs: grabs the first camera (full-canvas) and the first microphone (if any) so the user has something live to arrange instead of a blank canvas.

Parameters:

name (str, default "auto") – Name for the generated scene.

Returns:

A scene seeded with up to one camera and one microphone. Empty of sources when the host reports no devices (e.g. headless CI).

Return type:

Scene

Examples

>>> isinstance(scene_from_available_devices(), dict)
True
capture_helper.scene.validate_scene(scene)[source]

Validate an in-memory object as a well-formed Scene.

Cheap structural checks only — device availability is NOT verified here (that is resolve_scene_sources()’ job, and depends on the host). This guards the save/load boundary so a corrupt or hand-edited file surfaces a clear error instead of an obscure KeyError deep in the runner.

Parameters:

scene (Any) – Candidate object (typically freshly parsed JSON).

Returns:

The same object, once every invariant holds (returned for chaining).

Return type:

Scene

Raises:

ValueError – If any required key is missing or has the wrong type / value.

Examples

>>> validate_scene(new_scene("ok"))["name"]
'ok'