muvid.montage.analysis¶
Measure the song and the pool: beat grid, bars, sections, and media strength.
Everything the planner needs is MEASURED here, once, and handed over as plain
records — the planner (muvid.montage.plan) is pure and never touches a
file. Three measurements:
Beat grid. mixing.audio.beat_grid (librosa, ISC) is the house beat
backend and is used when it is importable; mixing is a core dependency but
its [beats] extra is not, so on a bare install this module falls back to
its own small numpy estimator — spectral-flux onset strength, an
autocorrelation tempo with a log-normal prior around 120 BPM, and a comb-filter
phase search — which yields a fixed-tempo grid. That is honest for the
material montages are cut to and it is recorded in Analysis.beat_source so
nobody mistakes it for a tracked grid. Only ImportError is caught, and only
on the auto path: a mixing regression must surface as a traceback, not
as a quietly different grid (the mixing rule in the design record).
Bars and sections. librosa has no downbeat tracker, so downbeats are the
beat phase (0..beats_per_bar-1) with the most onset energy — a closed-form vote.
Sections, when the caller does not supply them, are derived from per-bar RMS
energy: a stretch louder than the median is a chorus, quieter is a verse, the
leading and trailing quiet runs are intro/outro. Coarse, deterministic, and
labelled section_source="energy" so a caller knows what it got.
Media strength. A photo’s strength is a closed-form function of its pixel count and luma contrast (a 64x64 grey thumbnail decoded by ffmpeg — no Pillow). The reuse policy reserves the strongest for the final chorus.
Module scope is stdlib-only; numpy and ffmpeg are reached inside functions.
Functions
|
Measure |
|
|
|
Coarse sections from per-bar energy. |
|
Downbeats as the beat phase carrying the most onset energy. |
|
Measure each file: dimensions, duration (clips) and strength. |
|
Caller-supplied sections, clamped to the song and gap-filled with verses. |
Classes
|
What the planner knows about the song. |
|
One pool item, measured. |
|
A labelled stretch of the song, in seconds. |
- class muvid.montage.analysis.Analysis(*, duration, tempo_bpm, beats, downbeats, beats_per_bar=4, sections=(), beat_source='', section_source='', bar_energy_db=(), notes=<factory>)[source]¶
Bases:
objectWhat the planner knows about the song.
- classmethod from_dict(d)[source]¶
The inverse of
to_dict()(as rounded) — so a measured song can be kept.- Return type:
>>> a = Analysis(duration=4.0, tempo_bpm=120.0, beats=(0.0, 0.5), downbeats=(0.0,), ... sections=(Section(label='verse', start=0.0, end=4.0),)) >>> Analysis.from_dict(a.to_dict()) == a True
- class muvid.montage.analysis.Media(*, index, path, kind, width, height, duration=None, strength=0.0)[source]¶
Bases:
objectOne pool item, measured.
kindisphoto,cliporcover;durationisNonefor a still;strengthis the closed-form score the reuse policy ranks on.
- class muvid.montage.analysis.Section(*, label, start, end, energy_db=None)[source]¶
Bases:
objectA labelled stretch of the song, in seconds.
- muvid.montage.analysis.analyze(audio, *, beats='auto', beats_per_bar=4, sections=None)[source]¶
Measure
audio: duration, beat grid, downbeats, bar energy, sections.- Return type:
- muvid.montage.analysis.beat_grid(audio, *, source='auto', duration=None)[source]¶
(beats, tempo_bpm, onset_env, onset_hop_s, source_name, note).source="auto"triesmixing.audio.beat_gridand falls back to the numpy estimator only onImportError(librosa absent). Naming a source never falls back.
- muvid.montage.analysis.derive_sections(bar_energy_db, bars, duration, *, min_section_bars=4)[source]¶
Coarse sections from per-bar energy. Loud = chorus, quiet = verse.
The threshold splits the bar energies in two (
_two_means_threshold()); interior runs shorter thanmin_section_bars(scaled down for short songs) are absorbed into the longer neighbour; a leading/trailing quiet run of at mostMAX_INTRO_BARSbars is an intro/outro. A dynamically flat song is one verse — reported as such rather than invented.>>> bars = [(i * 2.0, (i + 1) * 2.0) for i in range(8)] >>> e = [-20, -20, -20, -20, -10, -10, -20, -20] >>> [(s.label, s.start, s.end) for s in derive_sections(e, bars, 16.0)] [('intro', 0.0, 8.0), ('chorus', 8.0, 12.0), ('outro', 12.0, 16.0)] >>> [s.label for s in derive_sections([-20] * 8, bars, 16.0)] ['verse']
- muvid.montage.analysis.downbeats_from_beats(beats, onset_at_beats, beats_per_bar)[source]¶
Downbeats as the beat phase carrying the most onset energy.
A closed-form vote over
beats_per_barcandidate phases; ties go to phase 0 (the first beat), which is also the answer when there is nothing to vote with.>>> downbeats_from_beats([0, .5, 1, 1.5, 2, 2.5, 3, 3.5], [1, 0, 0, 0, 1, 0, 0, 0], 4) (0, 2) >>> downbeats_from_beats([0, .5, 1, 1.5, 2, 2.5, 3, 3.5], [0, 0, 1, 0, 0, 0, 1, 0], 4) (1, 3)
- muvid.montage.analysis.probe_media(paths, *, kind, start_index=0)[source]¶
Measure each file: dimensions, duration (clips) and strength.
A file with no video stream is refused with its path in the message — a montage that silently skipped a photo would be a different montage.
- muvid.montage.analysis.sections_from_labels(raw, duration)[source]¶
Caller-supplied sections, clamped to the song and gap-filled with verses.
>>> [(s.label, s.start, s.end) for s in sections_from_labels( ... [{'label': 'chorus', 'start': 4, 'end': 8}], 10.0)] [('verse', 0.0, 4.0), ('chorus', 4.0, 8.0), ('verse', 8.0, 10.0)]