muvid.mcp.footage_tools¶
MCP tools for the footage-aligned music_video genre (thorwhalen/reelee#229).
Module-level tool functions (referenced muvid.mcp.footage_tools:<name>) a host
aggregates via muvid.mcp.register_tools(). All ffmpeg + numpy only, no AI/keys.
A transport, not an implementation. The operations live in
muvid.footage.service; each tool here resolves project_id to the caller’s
stateful FootageWorkspace project (the caller comes
from the OAuth token), calls the operation, and turns its
FootageError into a fastmcp ToolError. What stays here
is what only this surface does: fetching a URL (SSRF-guarded, size/time-bounded, streamed
to disk — the service takes a local file), expanding a shared folder, the per-caller
project listing, and the download claim on a render.
The tool list is derived from the operations catalogue (muvid.mcp._footage_ops):
operations without a hand-written tool here get a GENERATED one, footage_<op>, built
at import from the operation’s own signature and docstring (the named-edit operations:
footage_set_offset, footage_save_edit, footage_set_cut …).
Workflow: create_project(genre='music_video') → set_song → add_footage ×N →
align_footage → (footage_timeline to inspect) → propose_edit(save=true) →
footage_set_cut … → footage_render (or assemble_music_video in one call).
Lifecycle around it (muvid#22): list_music_video_projects finds a project whose id
was lost, and remove_footage takes a clip back out — which invalidates the
alignment, exactly as set_song does.
Functions
|
Add a footage video clip from an http(s) URL (a recording of the song). |
|
Add EVERY clip in a shared folder (Drive / Dropbox / OneDrive) in one call. |
|
Align every uploaded clip to the song by audio, and persist the result. |
|
Assemble the music video — auto (a selection |
|
The song's beat grid — tempo and beat instants on the song timeline — WITHOUT running the scoring job. |
|
Where the beat is in the song or in one video — CONTINUOUS signals, to look at, threshold and bend, not only beat instants. |
|
Forget where I placed this video: remove a hand-declared offset, so the next |
|
Delete a saved edit. |
|
The project as lacing-native standoff annotations, for a multitrack editor. |
|
The saved edits, oldest first: each one's |
|
The DECISION tier's annotations, turned back into an |
|
Why each cut of an edit is where it is — the post-mortem, in plain words. |
|
One video's filmstrip (the same record |
|
Every video's filmstrip — thumbnails to draw each camera's lane. |
|
Fit the moves to the beat: for each cut that shows a video in the part of the song the edit covers (its span) — or the cuts at |
|
One saved edit: its cut list ( |
|
The looks a cut can take — camera moves (punch in, slow push, slow pull, pans) and grades (vivid, black and white, posterize, cartoon) — each with its |
|
Join cut |
|
The song's waveform, to draw under the timeline: |
|
Redo the change |
|
Give an edit a new name — what the edit picker and the renders made from it show. |
|
Render a SAVED edit ( |
|
The finished videos, newest first: each one's |
|
Replace a saved edit's whole cut list — the power tool for rewriting an edit at once. |
|
Save a cut list as a new named edit. |
|
Change one cut of a saved edit ( |
|
Say whether a video has the song in its own sound — overriding what listening found. |
|
Place one video on the song BY HAND: the song time at which the video's own first frame plays (negative = the video starts before the song does). |
|
Choose which part of the song the video covers — where it starts and ends. |
|
Split the cut playing at song time |
|
Your project's song, clips, alignment summary, and renders. |
|
The coverage map: which clips cover which spans of the song (overlaps shown). |
|
Undo the last change to a saved edit (a cut changed, split, joined, the span, a whole replacement — by a person or by the assistant). |
List YOUR music_video (footage) projects, newest-modified first. |
|
The selection strategies available for full-auto assembly. |
|
|
Propose an EDL without rendering it — the cheap half of assembly. |
|
Remove one footage clip from the project — its stored file and its entry. |
|
Set the project's fixed clean song from an http(s) URL. |
- muvid.mcp.footage_tools.add_footage(project_id, *, url, name='')[source]¶
Add a footage video clip from an http(s) URL (a recording of the song). Free.
Accepts a share link as well as a direct media URL. Fetched server-side (streamed to disk; SSRF-guarded, size/duration-capped) and asserted to be media before anything is stored. Returns the assigned
clip_id. Re-runalign_footageafter adding clips.For a whole shoot in one folder, use
add_footage_folder()— a folder link holds many files and is refused here by name.- Return type:
- muvid.mcp.footage_tools.add_footage_folder(project_id, *, url, name_prefix='')[source]¶
Add EVERY clip in a shared folder (Drive / Dropbox / OneDrive) in one call. Free.
A shoot is a folder, not a file — this is the natural unit for music-video footage. The folder link is normalised and downloaded as a single archive server-side, then expanded into one clip per media member.
Members that are skipped — wrong type, over the per-clip size limit, or past the project clip cap — are NAMED in
skippedwith the reason. Nothing is silently truncated: a coverage decision made on quietly-shortened input is worse than one made on a short list.Returns the added clips and the skipped members. Run
align_footageafterwards.- Return type:
- muvid.mcp.footage_tools.align_footage(project_id, *, keep_declared=True)[source]¶
Align every uploaded clip to the song by audio, and persist the result. Free.
Returns each clip’s offset, a confidence in [0,1], its
support(the fraction of the clip that agrees on that offset,nullwhen the aligner took a single whole-clip measurement), and its coverage of the song, plus two lists:low_confidence— clips that matched weakly, for reporting;unreliable— clips whose offset the aligner will NOT vouch for. These stay in the project and stay addressable, and the auto path simply prefers other clips over them: a span another clip covers goes to that clip, and a span only an unreliable clip covers is left as a gap and reported incoverage.excluded(muvid#88).assemble_music_videostill REFUSES an explicitedlthat cuts to one, and still refuses an auto edit when NO clip is trustworthy, unless called withallow_unreliable=true— because a wrong offset does not fail, it renders a video out of sync with the song (muvid#59). Re-align, accept the smaller edit, or opt in deliberately;no_consensus— clips too short to be put to a vote at all (under about 4.5 s). A clip in this list can be marked reliable and still be wrong, and no other field will say so: its offset rests on one measurement, judged by a confidence score that does not rank correctness in this band — measured on the muvid#59 shoot, the WRONG offset scored highest of three (0.834 against 0.566 and 0.621), and on a repeating fixture a 4.4 s clip landing 8 s out is vouched at 0.381. Nothing is refused on this basis, because refusing would take the correct short clips with it. So if a short clip looks out of sync in the render, this list is the first place to look — and muvid#91 is where that trade-off is being decided.
Run this after adding/removing clips and before assembling.
A clip placed by hand (
footage_set_offset) is left as placed and named inkept_declared;keep_declared=falseforgets those first (footage_clear_offset) and measures every clip.- Return type:
- muvid.mcp.footage_tools.assemble_music_video(project_id, *, strategy='', edl=None, preset='', weights=None, config=None, canvas='', allow_unreliable=False, span=None)[source]¶
Assemble the music video — auto (a selection
strategy) or an explicitedl. Free.edl: an explicit edit — a list of{song_start, song_end, clip_id}spans. Must be in order, non-overlapping, and each within its clip’s coverage. A span with no footage is an explicit gap entry (clip_id: null); spans of the song your entries do not reach (head, tail, interior holes) are gap-filled automatically and named in thecoveragereport.each
edlentry may also carrytransition:{"duration_s": 0.4, "curve": "fade"}— blend IN from the previous entry instead of hard-cutting. The blend is CENTRED on the boundary, so a beat-snapped cut stays on the beat: each side suppliesduration_s/2of source beyond its own span, and both must actually have it (a gap side always does — it fades from black). Rejected, not ignored, if it is on the FIRST entry (nothing to blend from), names a curve outsidefade/fadeblack/fadewhite/dissolve/wipe*/slide*/smooth*/circleopen/circleclose, is under 0.04 s, or does not fit. Omit it for a hard cut — the default, and what every entry without the key means.each
edlentry may also carrycrop/crop_end({"x":0,"y":0.25,"w":1,"h":0.5}, fractions of the SOURCE frame — the framing decision;crop_endpans that window and must be the same size) andlook(ONE linear ffmpeg filter chain — the grade, LUT, posterise or in-shot punch-in, applied to the delivery CANVAS after scaling).lookis an allowlist: only the filters named bymuvid.footage.edl.LOOK_FILTERSare accepted, and a chain naming anything else — a second container (movie=), a filter that writes a file (metadata=…:file=,deshake=filename=), a pad label, a graph separator, or an unclosed quote — is refused by name atvalidate_edl, not discovered as an ffmpeg side effect. Its output frame is also bounded: ascale/pad/zoompansize must be a plain pixel count (notiw*80, not-1, nothd720) and no more thanmuvid.footage.edl.MAX_LOOK_SCALEtimes the render canvas — frame size is memory, andscale=8000:8000costs 328 MB against 19 MB for a look that stays at canvas size. On those four filters only options muvid has measured may be set at all —scalew/h/s/size +flags,padw/h/x/y +color,cropw/h/x/y, all ofzoompan— becausepad=aspectandscale=force_original_aspect_ratiomove the frame while declaring no size a bound can read (590 MB and 941 MB measured on a 1920x1080 canvas). Compile one withmuvid.footage.look(punch_in/motion/stylize) rather than hand-writing it.an entry carrying a MOVING look (a punch-in, a pan) should also set
look_time_varying: true. It changes no pixels; it puts a line in the reply’swarningswhen a blended boundary restarts the move’s ramp, which it does because the blend is a separate seek (muvid#73). Leave it off — the default — for a grade, a LUT or a posterise, which never read the clock.an entry may carry
slip_s(seconds, at most one beat either way,muvid.footage.edl.SLIP_MAX_S): the cut shows its video that much LATER (negative: earlier) without moving on the song — a local correction on top of the clip’s alignment, to put a dancer on the beat. The clip must still hold the slipped span.an entry may carry
rate(within1 +- muvid.footage.edl.RATE_MAX_DEV): the cut plays its video that much faster (1.03) or slower, consumingspan * rateseconds of it; the song is untouched. Every optional field survives verbatim in the returnededl.strategy='weighted'(score-driven): the beat-snapped Viterbi selector reads the persisted score tracks (runscore_footagefirst) and the selection config —preset(“energetic”/”contemplative”) and/orweights(per-metric) and/orconfig(lambda_switch/l_min_s/l_max_s/boundary_mode). Re-weighting is cheap: it re-selects from the SAME scores without re-scoring.otherwise full-auto: a registered alignment-only
strategy(seelist_strategies; defaultbest_confidence) builds the edit from the alignments.canvas: render-time override (“landscape”/”portrait”/”square”) — the same edit re-rendered in another shape, no new project needed. Default: the project’s canvas.span:[start_s, end_s]— render only that stretch of the song (a trimmed edit): theedlis gap-filled within it, the video is that long, and the song is cut to it and faded out at the end when it stops before the song does. A render of a trimmed edit records itsspan; pass it back with itsedlto reproduce it. Default: the whole song.A clip the aligner will not vouch for costs its own spans, not the whole edit (muvid#88). On the auto path the strategy prefers a vouched clip wherever one covers the span, so an untrustworthy clip is simply not chosen while any other clip covers that stretch of the song. Where it was the ONLY footage, the span is set aside rather than cut to: it renders as a gap and is named in
coverage.excludedwith the clip, the span, and the confidence/support/margin the verdict rests on. So a five-clip shoot with one bad clip still assembles. The refusal remains for the two cases it was built for: an explicitedlnaming an unvouched clip (you chose it), and an auto edit where NO clip is trustworthy — a black video reported as success is the failure this exists to prevent, so that still comes back as an error naming every clip.allow_unreliable: render even on clips whose alignment the aligner will not vouch for. Off by default and it should stay off: a wrong offset does not fail, it delivers a video out of sync with the song, so the refusal is the only thing standing between a bad measurement and a bad render (muvid#59). Set it when you have checked the offsets yourself, or when a slightly-off cut beats no cut at all — it also turns OFF the set-aside above, since someone who opted in wants that footage rendered rather than gapped.align_footagenames the clips this applies to in itsunreliablelist.
The video is EXACTLY the song’s duration: each cut is trimmed at its aligned in-point, scaled onto the canvas (padded, never stretched), gaps render black, and the CLEAN song audio runs under it all. Returns the render + the same coverage report
propose_editgives.**Read the returned
warningslist.** It is always present and usually empty, and it is what the render PLAN found: a transition that rounded to zero frames at this fps, or a moving look on a blended boundary whose ramp therefore restarts (muvid#73). Neither fails the render —okstays true — so this list is the only place either one is ever said. It exists because these findings used to be Python warnings on the server’s stderr, which a remote caller has no access to.- Return type:
- muvid.mcp.footage_tools.beat_grid(project_id)[source]¶
The song’s beat grid — tempo and beat instants on the song timeline — WITHOUT running the scoring job. Free.
score_footagecomputes this very grid as its first stage and then decodes every clip through cv2 in the background, which is far too much to pay when all a caller wants is where the beats are (thorwhalen/muvid#18 item 5) — a fixed-stride cut grid, a check that the tempo came out right, a cut plan of its own. This is one call tomixing.audio.beat_gridon the song alone (the “compute once on the master” invariant: clips map to it through their offsets, never the other way round), cached under the project asscores/beat_grid.jsonkeyed onsong_hashso the second call is a file read; a project that has already been scored is served from that run’s manifest instead.sourcesays which (computed|cache|scores).Needs the
scoringextra (mixing[beats], i.e. librosa); without it the reply is a clean error naming the install, never a traceback. Needs a song (set_song); no alignment is required.Returns
tempo_bpm,beats(seconds, ascending),n_beats,song_duration(so a caller can close the last bar) andsource.downbeatsis present only when the estimator measured any — the librosa backend has no downbeat tracker, and an empty list would read as “this song has no downbeats”, a measurement nobody made (gate, don’t zero).- Return type:
- muvid.mcp.footage_tools.footage_beat_signals(project_id, *, source='song', max_points=1000)¶
Where the beat is in the song or in one video — CONTINUOUS signals, to look at, threshold and bend, not only beat instants.
sourceis"song"or a clip id. The song gets its sound (audio_onset: the onset envelope the beat grid is estimated from). A video gets its own soundtrack’saudio_onsetwhen it has one, and three visual signals:motion(how much the people in the picture move, the camera’s own move taken out),motion_stops(moves stopping dead and turning — the movement accents) andmotion_stops_local(the same, place by place).Each signal is in the media’s OWN time: sample
iis att0 + i * hop_ss of the song, or of the clip (song timeoffset + t). Values are unnormalised, withmin,maxandp99beside them;Noneis a sample that was not measured.max_pointspools each signal to at most that many samples by their maximum, so a peak survives (0 = every sample; an editor drawing it wants that).Measured once per media and kept (a video’s first call reads every frame and takes tens of seconds; a second call for the same video waits for the first rather than measuring again). Needs the
scoringextra.Returns
{source, kind: audio|video, duration_s, tempo_bpm, beats, signals: {name: {name, label, domain, t0, hop_s, n, min, max, p99, values}}}—beatsandtempo_bpmare the soundtrack’s ([]/Nonewithout one).- Return type:
- muvid.mcp.footage_tools.footage_clear_offset(project_id, *, clip_id)¶
Forget where I placed this video: remove a hand-declared offset, so the next
align_footagemeasures the clip by its audio instead.Only a DECLARED offset can be forgotten (a measured one is replaced by aligning again); an unknown clip, or one with no declared offset, is refused. Until
align_footageruns again the clip has no place on the song, and footage scores made with the old offset are dropped.- Return type:
- muvid.mcp.footage_tools.footage_delete_edit(project_id, *, edit_id)¶
Delete a saved edit. Videos already rendered from it are kept (they still name the edit they came from). An unknown
edit_idis refused, naming the edits.- Return type:
- muvid.mcp.footage_tools.footage_editor_document(project_id)[source]¶
The project as lacing-native standoff annotations, for a multitrack editor. Free.
One tier per clip (its
clip-alignment/v1+, once scored, itsclip-score-track/v1curves) plus aDECISIONtier holding the current default proposal asmusic-video-edl/v1entries — everything referenced to the song by content hash, on one shared song-time axis (thorwhalen/reelee-web#203). Needs theeditorextra (lacing); requires alignment (runalign_footagefirst).After a human edits the DECISION tier, feed its annotations back to
assemble_music_videoviafootage_edl_from_annotations.- Return type:
- muvid.mcp.footage_tools.footage_edits(project_id)¶
The saved edits, oldest first: each one’s
edit_id,name, how it was made, how many cuts it has, andproblem— why it would not validate against the current alignment (nullwhen it does).unreliablenames clips it cuts to whose offsets rendering would refuse.- Return type:
- muvid.mcp.footage_tools.footage_edl_from_annotations(project_id, *, annotations)[source]¶
The DECISION tier’s annotations, turned back into an
edl=argument. Free.The timeline-to-EDL half: pass
footage_editor_document’sDECISIONtier (after whatever an editor did to it) and get back plain{song_start, song_end, clip_id}dicts, ready forassemble_music_video(edl=...)orpropose_edit— a faithful read, not a re-selection. Annotations referencing a song other than this project’s are refused, not read (muvid#35), so a clipboard from another project fails saying so instead of splicing in the wrong spans.- Return type:
- muvid.mcp.footage_tools.footage_explain_edit(project_id, *, edit_id, start_s=None, end_s=None)¶
Why each cut of an edit is where it is — the post-mortem, in plain words.
For every cut: where it lands (on which beat of which bar, or how far off one), how the picture changes (a hard cut, or a fade and where that fade really starts), what picture it shows and why that stretch (its picture changes on the beats inside the cut; a photo’s camera move; a video synced where it was filmed), and
flagsfor the ones worth a look (straddles_the_beat,off_the_beat,long_hold,repeated_stretch). Plus asummaryof the whole edit (fades vs cuts, the song’s pace and shape, the longest shot).Every clock time in a sentence is listed in
times({label, s}) so a screen can link it to the player.start_s/end_skeep only the cuts overlapping that part of the song (the summary stays whole). Explains the edit as it stands now, hand changes included. Free; reads only.- Return type:
- muvid.mcp.footage_tools.footage_filmstrip(project_id, *, clip_id)¶
One video’s filmstrip (the same record
footage_filmstripsgives per clip, with itsclip_idandfps).- Return type:
- muvid.mcp.footage_tools.footage_filmstrips(project_id)¶
Every video’s filmstrip — thumbnails to draw each camera’s lane.
Per clip: sprite sheets of
frame_wxframe_hframes (colsxrowsto a sheet, left to right then down), sampled atfpsframes per second of the CLIP’s own time — frameiis the clip ati / fpss, which sits at song timeoffset + i / fps. Each sheet is anartifact_id(when the project is hosted), with itsfirst_frameandn_frames. Made once per clip and kept; a clip that has none yet takes a few seconds the first time.Returns
{fps, clips: {clip_id: {duration_s, n_frames, frame_w, frame_h, sheets: [{artifact_id, cols, rows, first_frame, n_frames}]}}}.- Return type:
- muvid.mcp.footage_tools.footage_fit_to_beat(project_id, *, edit_id, indices=None, min_z=2.5, apply=True)¶
Fit the moves to the beat: for each cut that shows a video in the part of the song the edit covers (its span) — or the cuts at
indices— find the slip and speed that put its video’s movement accents on the song’s beat, and apply it where the evidence is strong.Per cut, every slip within +-0.25 s (a frame at 60 fps apart) and every speed from x0.92 to x1.08 (1 % apart) is tried; the best is scored against a null of the same search on the video’s accents with their relation to the beat destroyed (
muvid.footage.beat_fit), giving az. A cut changes only whenz >= min_zAND the fit is better than its current timing — and the video still holds the footage it then reads. Cuts shorter than three beats, gaps and videos with nothing measurable are left alone, each with its reason.apply=Falsereports without changing anything. All the changes are ONE step of the edit’s history, so onefootage_undo_edittakes them all back.How much to expect. On three phone videos of a crowd dancing, the accents locked to the beat on one of them only; each video’s own SOUND locked on all three. So on footage like that most cuts will be left as they are, which is the right answer when the picture does not show the beat. It works best on a clearly visible dancer.
Needs the
scoringextra: the first call measures each video’s movement (tens of seconds a video; kept for next time). Returns the edit (footage_get_edit’s shape) plusfit:cuts({index, slip_s, rate, z, applied, reason}per cut looked at),fitted,keptandmin_z.- Return type:
- muvid.mcp.footage_tools.footage_get_edit(project_id, *, edit_id)¶
One saved edit: its cut list (
edl, every span of the song, gaps asclip_id: null), its name and history, and acoveragereport. Cut indexes infootage_set_cut/footage_merge_cutrefer to positions in thisedl.- Return type:
- muvid.mcp.footage_tools.footage_looks(project_id)¶
The looks a cut can take — camera moves (punch in, slow push, slow pull, pans) and grades (vivid, black and white, posterize, cartoon) — each with its
params_schema. Give one tofootage_set_cutaslook={"name": ..., **params}.- Return type:
- muvid.mcp.footage_tools.footage_merge_cut(project_id, *, edit_id, index, into='previous')¶
Join cut
indexto its neighbour: the neighbour (into“previous” or “next”) takes over its span, so the neighbour’s video must cover it. The joined cut keeps the neighbour’s video, framing and look. Returns the changed edit.- Return type:
- muvid.mcp.footage_tools.footage_peaks(project_id, *, n=2000)¶
The song’s waveform, to draw under the timeline:
nequal slices of the song, each the loudest moment in it (mono), scaled so the loudest slice is 1.0.Returns
{duration_s, n, peaks: [0..1, ...]}; sliceicovers song timei * duration_s / nto(i + 1) * duration_s / n. Kept per song andn.- Return type:
- muvid.mcp.footage_tools.footage_redo_edit(project_id, *, edit_id)¶
Redo the change
footage_undo_editlast took back. A new change after an undo discards what could be redone. Returns the edit as it now is.- Return type:
- muvid.mcp.footage_tools.footage_rename_edit(project_id, *, edit_id, name)¶
Give an edit a new name — what the edit picker and the renders made from it show.
Only the name changes; the cuts, the span and the edit’s id stay as they are, and the rename can be undone like any other change. Returns the edit.
- Return type:
- muvid.mcp.footage_tools.footage_render(project_id, *, edit_id, canvas='', allow_unreliable=False)[source]¶
Render a SAVED edit (
propose_edit(save=true)/footage_save_edit) into a music video. Free, minutes.Same render, same refusal and same reply as
assemble_music_videowith that edit’s cut list asedl— plusedit_id, so the video says which edit it came from.canvasre-renders the same edit as “landscape”/”portrait”/”square”. Refused when the edit cuts to a clip whose offset the aligner will not vouch for, unlessallow_unreliable(seeassemble_music_video). Read the returnedwarnings.- Return type:
- muvid.mcp.footage_tools.footage_renders(project_id)¶
The finished videos, newest first: each one’s
render_id, speakableref, theedit_idit was made from, itslabel, canvas,ok, the number ofwarnings, andartifact_idto play it by when the project is hosted.- Return type:
- muvid.mcp.footage_tools.footage_replace_edit(project_id, *, edit_id, edl)¶
Replace a saved edit’s whole cut list — the power tool for rewriting an edit at once. The new list is checked exactly as
footage_save_editchecks one; on refusal the edit is left as it was. The previous list is not kept.- Return type:
- muvid.mcp.footage_tools.footage_save_edit(project_id, *, edl, name='', how_made='by hand', edit_id='', span=None)¶
Save a cut list as a new named edit.
edlis a list of{song_start, song_end, clip_id}spans (plus optionaltransition/crop/crop_end/look/look_time_varying), in the same formfootage_get_editreturns andpropose_editproduces. Holes are filled with gap entries; the list is checked (order, overlap, every span inside its clip’s coverage) and refused with the reason if it does not hold.edit_idfixes the id (an existing one is refused — usefootage_replace_edit).span([start_s, end_s]) makes the edit cover only that part of the song — its render is that long, the song cut to it; default the whole song. Returns the saved edit.- Return type:
- muvid.mcp.footage_tools.footage_set_cut(project_id, *, edit_id, index, clip_id=None, song_start=None, song_end=None, look=None, look_time_varying=None, slip_s=None, rate=None)¶
Change one cut of a saved edit (
indexis its position infootage_get_edit’s edl).clip_id: show another video over this span (""makes it a gap). The new video must cover the span. Its framing (crop) is dropped, since it was chosen for the old video’s frame; itslookis kept. A video cut to the music (or a photo) is shown from its start; a video with the song in it, where it was filmed.song_start/song_end: move the cut’s boundaries. The neighbouring cut’s boundary moves with it, so the edit stays one continuous timeline; a move that would swallow a neighbour whole is refused (join them withfootage_merge_cut).look: a NAMED look fromfootage_looks—{"name": "slow_push", "zoom": 1.08}, compiled for this cut’s length and the project’s canvas and kept on the cut aslook_spec(with every parameter’s value) so it can be shown and changed — or, for power users, one raw ffmpeg filter chain (allowlisted; setlook_time_varyingfor one that moves).""removes it.slip_s: show a slightly different moment of the same video over the same span —0.1reads the footage 0.1 s later — to put a dancer’s moves on the beat where the clip’s alignment is right overall but a little off here. At most one beat either way (SLIP_MAX_S);0removes it. A new video (clip_id) starts unslipped.rate: play this cut’s video a little faster or slower —1.03shows 3 % more footage over the same span, so the moves run 3 % faster (the song is never touched). Within1 +- RATE_MAX_DEV;1removes it. The video must hold the footage the cut then reads. A new video starts at speed 1.
Parameters left out are unchanged. The changed edit is checked and saved; returns it.
- Return type:
- muvid.mcp.footage_tools.footage_set_has_song(project_id, *, clip_id, has_song)¶
Say whether a video has the song in its own sound — overriding what listening found.
"yes": it is a recording of the song (a concert, a dance); it is synced to the song and never cut freely to the music."no": it is not (a day out, b-roll); it is always cut to the music."auto"(the default) believes the listening: a videoalign_footageplaced confidently is synced, any other is cut to the music. A photo is always cut to the music. Like a hand-placed offset, the choice survives listening again. Takes effect at the nextpropose_edit.- Return type:
- muvid.mcp.footage_tools.footage_set_offset(project_id, *, clip_id, offset_s)¶
Place one video on the song BY HAND: the song time at which the video’s own first frame plays (negative = the video starts before the song does).
Use it when
align_footagegets a clip wrong — on long, repetitive songs it can land a whole chorus away (muvid#59) — or when you already know the offset. The offset is recorded assource: "declared"and trusted for rendering (a person vouched for it); how much of the song the clip covers is computed from the two durations.align_footagekeeps it unless told otherwise. Changing an offset makes the footage scores stale, so they are dropped.- Return type:
- muvid.mcp.footage_tools.footage_set_span(project_id, *, edit_id, start_s, end_s)¶
Choose which part of the song the video covers — where it starts and ends.
Trimming loses nothing. The span is a window on the edit, not a cut of it: every cut is kept whole, and only what is RENDERED is limited to
start_s..``end_s`` (the song cut to match, faded out at the end when it stops before the song does; cuts across an edge are shortened in the render only). Widening the span again —start_s=0andend_s= the song’s length is the whole song — brings back exactly what was there. Returns the edit, with itsspan.- Return type:
- muvid.mcp.footage_tools.footage_split_cut(project_id, *, edit_id, at_s)¶
Split the cut playing at song time
at_sinto two cuts of the same video.The two halves keep the cut’s video, framing and look; a moving framing (a pan) is divided where it was at
at_s. Refused on a boundary (nothing to split). Returns the changed edit;changedis the index of the second half.- Return type:
- muvid.mcp.footage_tools.footage_status(project_id)[source]¶
Your project’s song, clips, alignment summary, and renders. Free.
Also: each clip’s offset and whether it was measured or declared (
alignments), the savededits, andnext_step— the operation that moves the project on.- Return type:
- muvid.mcp.footage_tools.footage_timeline(project_id)[source]¶
The coverage map: which clips cover which spans of the song (overlaps shown). Free.
The surface for choosing which parts to use before
assemble_music_video. Built from the persisted alignment (runalign_footagefirst).- Return type:
- muvid.mcp.footage_tools.footage_undo_edit(project_id, *, edit_id)¶
Undo the last change to a saved edit (a cut changed, split, joined, the span, a whole replacement — by a person or by the assistant). Returns the edit as it now is;
footage_redo_editputs the change back. Up to 100 changes are kept per edit.- Return type:
- muvid.mcp.footage_tools.list_music_video_projects()[source]¶
List YOUR music_video (footage) projects, newest-modified first. Free.
The way back to a lost
project_id(muvid#22):list_projectsspans both muvid genres but says nothing about a footage project’s progress, and every other footage tool needs the id first. Each row carries where the project is in the workflow —has_song,n_clips,aligned,n_renders— so the next call reads off the listing without afootage_statusper project:alignedis true only when the persisted alignment covers every current clip. Anadd_footageorremove_footageafteralign_footagemakes it false again until you re-align.n_renderscounts whatfootage_statuslists underrenders;0is a positive answer (“no cut yet”), never an omitted project.
[]means you have no footage projects; a visualizer project is not one and shows up inlist_projectsinstead.- Return type:
- muvid.mcp.footage_tools.list_strategies()[source]¶
The selection strategies available for full-auto assembly. Free.
- Return type:
- muvid.mcp.footage_tools.propose_edit(project_id, *, strategy='', preset='', weights=None, config=None, save=False, name='', pace='')[source]¶
Propose an EDL without rendering it — the cheap half of assembly. Free, seconds.
Selection and rendering are separate concerns, and only one of them costs an encode. This returns the edit an
assemble_music_videocall would have produced, so a caller can compare several strategies/weightings, read the coverage report, edit the list by hand, and only then pay for a render — passing the chosen EDL straight back toassemble_music_video(project_id, edl=...).Returns the
edl(ready to feed back verbatim — spans the WHOLE song, with spans no footage covers as explicit gap entries,clip_id: null, rendered as black), thestrategyactually used, and acoveragereport naming every uncovered span of the song and every segment that made the cut despite weak alignment. Same arguments asassemble_music_video’s auto path, and the same edit it would build — including thecoverage.excludedrecovery described there.save=truealso keeps it as a named edit (name, default “Edit N”) and returns itsedit_id— change it cut by cut withfootage_set_cut/footage_split_cut/footage_merge_cutand render it withfootage_render.Footage without the song in it is cut to the music. Only videos that are recordings of the song are synced; every span they leave is filled from the other videos, cut on the song’s beats with each video at the stretch whose picture changes land on the beat (
musicin the reply).rolessays how each clip was used; overrule one withfootage_set_has_song. Every video must have been listened to (align_footage) or declared.pace(slow/steady/driving/frantic) sets how often the cuts to the music come.- Return type:
- muvid.mcp.footage_tools.remove_footage(project_id, *, clip_id)[source]¶
Remove one footage clip from the project — its stored file and its entry. Free.
Irreversible for the clip (re-add it from its URL if it was a mistake); existing renders are untouched. Removal INVALIDATES the alignment and every persisted score track, exactly as
set_songdoes (muvid#22) — the alignment describes the clip set it was measured on, and a removed clip’s offset must not outlive the clip. So runalign_footageagain beforepropose_edit/assemble_music_video; until thenfootage_statusreports the project unaligned and this project’salignedflag inlist_music_video_projectsis false.An unknown
clip_idis refused, naming the clip ids the project holds, and a refusal changes nothing on disk. Returns what was removed, the clips that remain, and whether an alignment / score tracks were actually dropped.- Return type:
- muvid.mcp.footage_tools.set_song(project_id, *, url)[source]¶
Set the project’s fixed clean song from an http(s) URL. Free.
Accepts a share link (Google Drive / Dropbox / OneDrive) as well as a direct media URL — the link is normalised before fetching, and the downloaded bytes are checked to be media, so a private-file sign-in page is refused with that diagnosis rather than stored.
This is the reference every uploaded clip is aligned to and whose audio the final video uses. Replaces any previous song. Duration/size-capped.
- Return type: