muvid.montage.spec¶
The montage treatment spec — what a director (human or model) decides, as data.
Two layers, the same split as muvid.lyricvid.spec and for the same
reason: the half a model is good at is separated from the half it is not.
directionWHAT and WHY — a mood, the accent colour and grade, how the cutting should feel (a multiplier over the archetype’s own pacing), and the reuse policy that decides how a small pool carries a long song.
scenesHOW — an archetype from a closed set applied to named sections, plus a small closed set of parameters. Never a cut time, never a crop rectangle.
Three rules follow:
No timings from the model. Every cut sits on a beat, a bar or a subdivision of the song’s measured grid; the spec names a density, Python applies it (
muvid.montage.plan).No geometry from the model. Crops and moves come from a closed set of variants, chosen by the planner’s reuse policy so a revisited photo shows a different framing. A model that emitted rectangles would emit wrong ones.
Closed vocabularies everywhere, so an invalid spec can be projected onto the valid space (
repair()) rather than bounced back for a retry.
This module is stdlib-only and import-safe on purpose: it is on the listing path of the plugin surface.
Module Attributes
How the pool is cut to the song. |
|
The parameters each archetype accepts, as a small JSON Schema per key. |
|
How the cutting should feel. |
|
Multiplier on beats-per-cut per cut feel. |
|
A colour grade applied to every frame. |
|
How a still moves within a slot. |
|
How a slot arrives. |
|
Section labels the planner knows how to pace. |
Functions
|
Take whatever a caller or a model produced and return a plannable spec. |
The JSON Schema for a |
|
|
The treatment a caller gets from |
|
Project |
|
Return a list of human-readable problems. |
Every closed vocabulary, for prompts and UI. |
Classes
|
The song-level creative decision. |
|
Colours, as |
|
The reuse POLICY — how a small pool carries a long song. |
|
One archetype, applied to part of the song. |
|
A complete, plannable treatment. |
- muvid.montage.spec.ARCHETYPES: dict[str, str] = {'ballad_dissolve': 'Cut every 2-4 bars on a downbeat, one-beat crossfades, a slow Ken Burns drift on each still. For slow songs and quiet sections.', 'beat_cut': "Hard cuts on the grid — every beat or half-bar in a chorus, every bar in a verse — with a punch-zoom on each cut. The 'photo beat sync' look.", 'grid': 'A 2x2 grid of tiles; one tile swaps on every beat. Dense and busy; wants a pool of eight or more.', 'stop_motion': 'Stills held for a beat subdivision with no motion at all — a flip-book. Mechanical, playful.'}¶
How the pool is cut to the song. The planner owns the timing and geometry; the spec only names the family and its knobs.
- muvid.montage.spec.ARCHETYPE_PARAMS: dict[str, dict[str, dict[str, Any]]] = {'ballad_dissolve': {'bars_per_cut': {'default': 4, 'description': 'Bars between cuts in a verse; a chorus halves it.', 'maximum': 16, 'minimum': 1, 'type': 'number'}, 'drift': {'default': 0.08, 'description': 'Ken Burns amplitude as a fraction of the frame.', 'maximum': 0.3, 'minimum': 0, 'type': 'number'}, 'fade_beats': {'default': 1, 'description': 'Crossfade length in beats. 0 is a hard cut.', 'maximum': 4, 'minimum': 0, 'type': 'number'}}, 'beat_cut': {'beats_per_cut': {'default': 4, 'description': 'Beats between cuts in a verse; a chorus halves it.', 'maximum': 16, 'minimum': 1, 'type': 'number'}, 'punch': {'default': 0.12, 'description': 'Punch-zoom amount on each cut (0 disables).', 'maximum': 0.3, 'minimum': 0, 'type': 'number'}}, 'grid': {'beats_per_swap': {'default': 2, 'description': 'Beats between tile swaps in a verse; a chorus halves it.', 'maximum': 16, 'minimum': 1, 'type': 'number'}}, 'stop_motion': {'subdivision': {'default': 2, 'description': 'Holds per beat (2 = eighth notes). A chorus doubles it, up to 4.', 'maximum': 4, 'minimum': 1, 'type': 'integer'}}}¶
The parameters each archetype accepts, as a small JSON Schema per key. A key outside this table is a validation error and is dropped by
repair(); a value outside its range is clamped. Closed, so a plugin UI can render it.
- muvid.montage.spec.CUT_FEELS: dict[str, str] = {'driving': 'Twice as many cuts. Pushes forward.', 'frantic': "Four times as many cuts, floored at the archetype's minimum.", 'slow': "Half as many cuts as the archetype's default. Contemplative.", 'steady': "The archetype's own pacing."}¶
How the cutting should feel. A MULTIPLIER over each archetype’s own pacing, so the same treatment reads “slow” on a ballad and “driving” on a banger without the model naming a number of beats.
- muvid.montage.spec.CUT_FEEL_FACTORS: dict[str, float] = {'driving': 0.5, 'frantic': 0.25, 'slow': 2.0, 'steady': 1.0}¶
Multiplier on beats-per-cut per cut feel. Smaller is more cuts.
- class muvid.montage.spec.Direction(*, mood='', palette=<factory>, cut_feel='steady', grade='none', reuse=<factory>, rationale='')[source]¶
Bases:
objectThe song-level creative decision.
- muvid.montage.spec.GRADES: dict[str, str] = {'mono': 'Desaturate to black and white.', 'none': 'The pool as shot.', 'tint': "Blend the palette's accent colour into the frame (a duotone-ish wash)."}¶
A colour grade applied to every frame. Closed because the grade is EXECUTABLE ffmpeg; the palette’s accent parameterises it, never a filter string.
- muvid.montage.spec.MOTIONS: dict[str, str] = {'none': 'Held still.', 'pan_left': 'A slow drift leftward.', 'pan_right': 'A slow drift rightward.', 'punch': 'Arrives zoomed in and relaxes to rest within half a beat.', 'zoom_in': 'A slow push in.', 'zoom_out': 'A slow pull out.'}¶
How a still moves within a slot. Assigned by the ARCHETYPE, never by the spec — listed here so the plan’s vocabulary is closed and documented.
- class muvid.montage.spec.Palette(*, bg='#000000', accent='#e0533d')[source]¶
Bases:
objectColours, as
#rrggbb.accentparameterises thetintgrade;bgis the letterbox/pad colour when a tile does not fill its region.
- class muvid.montage.spec.Reuse(*, min_gap=6, reserve_for_finale=True)[source]¶
Bases:
objectThe reuse POLICY — how a small pool carries a long song.
min_gapis how many cuts must pass before an image may return (it shrinks automatically topool - 1for a small pool). Every return uses a different crop/move variant.reserve_for_finaleholds the strongest quarter of the pool back for the last chorus.
- muvid.montage.spec.SECTION_LABELS: dict[str, str] = {'bridge': 'Chorus density.', 'chorus': 'Dense cuts; the last chorus gets the strongest images.', 'intro': 'Sparse cuts.', 'outro': 'Sparse cuts.', 'verse': "The archetype's verse pacing."}¶
Section labels the planner knows how to pace. Others are paced as a verse.
- class muvid.montage.spec.Scene(*, applies_to=('*', ), archetype='beat_cut', params=<factory>)[source]¶
Bases:
objectOne archetype, applied to part of the song.
applies_tonames sections by label, or"*"for the whole song. A named scene wins its section; sections no scene names fall back to the first"*"scene, so a one-scene spec is valid and complete.
- muvid.montage.spec.TRANSITIONS: dict[str, str] = {'cut': 'A hard cut.', 'dissolve': 'A noisy dissolve.', 'fade': 'A crossfade.', 'fadeblack': 'Dip to black.'}¶
How a slot arrives. A curated subset of ffmpeg’s
xfadetransitions, the same posture asmuvid.footage.edl.TRANSITION_CURVES: a name outside it is refused here rather than discovered as an ffmpeg error three stages later.
- class muvid.montage.spec.TreatmentSpec(*, spec_version='1.0', title='', direction=<factory>, scenes=<factory>)[source]¶
Bases:
objectA complete, plannable treatment.
>>> s = TreatmentSpec() >>> validate(s) [] >>> TreatmentSpec.from_dict(s.to_dict()) == s True- classmethod from_dict(d)[source]¶
Build from a plain mapping — TOTAL over what a model or a remote caller sends.
Missing keys default; wrong-shaped values are coerced toward the field’s type rather than raised on; unknown keys are dropped. What cannot be coerced falls to the default and
repair()reports the vocabulary-level substitutions afterwards.- Return type:
>>> s = TreatmentSpec.from_dict({'scenes': [None, {'applies_to': 'chorus', ... 'archetype': 'grid', 'params': 'fast'}], ... 'direction': {'palette': {'foo': 1, 'accent': '#00ff00'}, ... 'reuse': {'min_gap': '3'}}}) >>> s.scenes[1].applies_to, s.scenes[1].archetype, dict(s.scenes[1].params) (('chorus',), 'grid', {}) >>> s.direction.palette.accent, s.direction.reuse.min_gap ('#00ff00', 3)
- muvid.montage.spec.coerce(obj)[source]¶
Take whatever a caller or a model produced and return a plannable spec.
Accepts a
TreatmentSpec, a mapping, or a JSON string — and repairs it. This is the one entry point production code should use.- Return type:
>>> spec, notes = coerce('{"scenes": [{"archetype": "grid"}]}') >>> spec.scenes[0].archetype, notes ('grid', [])
- muvid.montage.spec.json_schema()[source]¶
The JSON Schema for a
TreatmentSpec.>>> s = json_schema() >>> s['properties']['scenes']['items']['properties']['archetype']['enum'][0] 'ballad_dissolve'
- muvid.montage.spec.one_scene(archetype='beat_cut', **direction)[source]¶
The treatment a caller gets from
archetype=alone.- Return type:
>>> one_scene('grid', cut_feel='driving').scenes[0].archetype 'grid'
- muvid.montage.spec.repair(spec)[source]¶
Project
speconto the valid space. Returns(spec, notes).A model that names a grade that does not exist has made a small, mechanical mistake; projecting it onto the default renders something good now, where a retry costs a round trip. Every substitution is reported.
- Return type:
>>> fixed, notes = repair(TreatmentSpec(scenes=(Scene(archetype='swirl'),))) >>> fixed.scenes[0].archetype 'beat_cut' >>> notes ["scenes[0].archetype 'swirl' -> 'beat_cut'"] >>> fixed, notes = repair(TreatmentSpec(scenes=( ... Scene(archetype='beat_cut', params={'punch': 9, 'nope': 1}),))) >>> dict(fixed.scenes[0].params), notes ({'punch': 0.3}, ['scenes[0].params.punch 9 -> 0.3', "scenes[0].params dropped unknown ['nope']"])
- muvid.montage.spec.validate(spec)[source]¶
Return a list of human-readable problems. Empty means plannable.
>>> validate(TreatmentSpec(scenes=(Scene(archetype='nope'),))) ["scenes[0].archetype 'nope' is not one of the known archetypes"] >>> validate(TreatmentSpec(scenes=(Scene(archetype='grid', params={'punch': 1}),))) ["scenes[0].params 'punch' is not a parameter of 'grid' (allowed: ['beats_per_swap'])"]