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()). ASceneSourcemerely 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'
- class capture_helper.scene.ResolvedSceneSource[source]
Bases:
TypedDictA
SceneSourcepaired with the live device it resolved to.Keys
- scene_sourceSceneSource
The original recipe from the scene.
- resolvedSource | None
The live
capture_helper.Sourcematched on this machine, orNonewhen no device satisfied the recipe’s selectors.- errorstr | None
Human-readable reason the recipe could not be resolved, or
Noneon success.
- scene_source: SceneSource
- class capture_helper.scene.Scene[source]
Bases:
TypedDictA 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).
- sources: list[SceneSource]
- class capture_helper.scene.SceneSource[source]
Bases:
TypedDictOne source placed on a scene canvas.
A
SceneSourceis a recipe, not a live handle: it records how to re-select the device (kind+ optionalname_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 areNonethe first device ofkindis 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.
- 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
scenewith one additional placed source.The input scene is not mutated — a shallow copy with a new
sourceslist 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:
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:
- Raises:
FileNotFoundError – If
pathdoes not exist.ValueError – If the file is not valid JSON or not a well-formed scene.
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:
- Returns:
A scene with no sources yet.
- Return type:
- Raises:
ValueError – If
widthorheightis 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
ResolvedSceneSourceper 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:
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:
- Returns:
The absolute path the scene was written to.
- Return type:
- Raises:
ValueError – If
scenefailsvalidate_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:
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 obscureKeyErrordeep 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:
- Raises:
ValueError – If any required key is missing or has the wrong type / value.
Examples
>>> validate_scene(new_scene("ok"))["name"] 'ok'