§ Guide · Scoring to picture

Score a film with an AI agent

Published October 2026

An agent can watch a cut, write a cue sheet, and compose and orchestrate a cue. It cannot make an orchestra play, do drop-frame timecode math reliably by hand, or hand an editor a session. Those are the steps Doseedo's MCP tools cover: doseedo_tempo_map turns hit points into a tempo map that lands each one on a downbeat, doseedo_render_score plays every part on its instrument model and returns a stem per part, a mix and a receipt saying whether each part played its notes, doseedo_generate_sound makes the hits that aren't notes, and doseedo_session_from_stems builds a Logic Pro or Ableton Live project. ffmpeg does the last mile. This is the whole loop, with real numbers.

Who does what

StepThe agentDoseedo
1. SpotFinds cuts, reads frames for mood, maps dialogue, writes the cue sheetOptional: doseedo_separate_stems pulls the dialogue out of a mixed track
2. Hit pointsPicks the hits and the tempodoseedo_tempo_map: a tempo map that lands every hit on a downbeat. Free, no key.
3. WriteThemes, harmony, orchestration, on that beat griddoseedo_instruments: what can play, with ranges
4. PlayChecks the receipts, listens, picksdoseedo_render_score: every part on its instrument, a stem each, a mix, a fit receipt per part
5. Sound designDescribes the sounddoseedo_generate_sound: impacts, whooshes, risers
6. DeliverHands over the projectdoseedo_session_from_stems: a .logicx or .als with audio, MIDI and the tempo map
7. PictureDucks under dialogue, normalizes, muxes with ffmpegNothing. The final mux stays on your machine.
claude mcp add --transport http --scope user doseedo https://api.doseedo.com/mcp

The server publishes this sequence itself: doseedo_recipes {name:"score-a-film"} returns it to any agent, step by step.

1. Spot the picture

The agent works from the cut. Three commands give it what a music editor would mark up:

# cuts: every scene change with its time
ffmpeg -i film.mp4 -vf "select=gt(scene\,0.3),showinfo" -f null - 2>&1 | grep pts_time

# a frame every two seconds, for the agent to look at (mood, action, light)
mkdir -p frames && ffmpeg -i film.mp4 -vf fps=1/2,scale=640:-1 frames/%04d.jpg

# where the dialogue is: spans of sound vs. silence on the dialogue track
ffmpeg -i dialogue.wav -af silencedetect=noise=-35dB:d=0.5 -f null - 2>&1 | grep silence_

Without a clean dialogue track, doseedo_separate_stems on the production audio returns a vocals stem to run the last command on. The result is a cue sheet, the document every later step reads:

CueInHitsFeel
1M101:02:10:00door slam 01:02:17:18 · she turns 01:02:27:12 · title card 01:02:40:12Quiet dread, strings low, the horn arrives when she turns, full on the title

2. Land the hits: doseedo_tempo_map

A composer scoring to picture bends the tempo so a bar line falls on each hit: between two hits Δt seconds apart, a whole number of bars at bpm = bars × beats per bar × 60 / Δt, picked to stay nearest the cue's tempo. The tool does exactly that, in timecode, at the film's frame rate, including drop-frame (;) at 29.97 and 59.94:

doseedo_tempo_map {
  "bpm": 96,
  "fps": 24,
  "start_timecode": "01:02:10:00",
  "hits": [
    { "timecode": "01:02:17:18", "label": "door slam" },
    { "timecode": "01:02:27:12", "label": "she turns" },
    { "timecode": "01:02:40:12", "label": "title card" }
  ]
}

Its answer for that cue sheet, trimmed:

{
  "tempo_map": [
    { "beat": 0,  "bpm": 92.9032 },
    { "beat": 12, "bpm": 98.4615 },
    { "beat": 28, "bpm": 92.3077 },
    { "beat": 48, "bpm": 96 }
  ],
  "hits": [
    { "label": "door slam",  "time_s": 7.75, "beat": 12, "bar": 4,  "beat_in_bar": 1 },
    { "label": "she turns",  "time_s": 17.5, "beat": 28, "bar": 8,  "beat_in_bar": 1 },
    { "label": "title card", "time_s": 30.5, "beat": 48, "bar": 13, "beat_in_bar": 1 }
  ],
  "bpm_range": [ 84.48, 107.52 ],
  "max_tempo_deviation_bpm": 3.69,
  "timing_error_ms": 0.006,
  "cue_start": { "seconds": 3730, "timecode": "01:02:10:00" }
}

Every hit is on beat 1 of a bar, and the tempo never moves more than 3.7 bpm from 96. The default range is ±12%, and bpm_range widens or narrows it. When no whole number of bars fits between two hits, the hit lands on a beat instead and notes says so. When nothing fits at all, the tool refuses and says to widen the range or play the hit off the grid. It is pure math: free, instant, no key.

3. Write the parts on that grid

Now the agent composes, in beats from bar 1 on the grid it was just given: the accent, the stinger or the change goes on hits[].beat. doseedo_instruments lists what can play, each with its range in concert pitch (a horn is 41–77, a cello 36–81) and its exact call. The parts can be note rows the agent writes, a multi-track MIDI file from pretty_midi or music21, or a MusicXML export from Dorico, Sibelius, Finale or MuseScore, in which case dynamics, hairpins, staccato and accents, pizz. and con sord. are performed and transposing parts are read at concert pitch.

4. Play every part: doseedo_render_score

doseedo_render_score {
  "bpm": 96,
  "tempo_map": [ { "beat": 0, "bpm": 92.9032 }, { "beat": 12, "bpm": 98.4615 },
                 { "beat": 28, "bpm": 92.3077 }, { "beat": 48, "bpm": 96 } ],
  "parts": [
    { "name": "Timpani", "instrument": "vibraphone", "timbre_preset": "timpani:preset1",
      "notes": [ { "pitch": 43, "start_beats": 12, "duration_beats": 2, "velocity": 118 },
                 { "pitch": 38, "start_beats": 48, "duration_beats": 4, "velocity": 124 } ] },
    { "name": "Horn",     "instrument": "french_horn",    "notes": [ … ] },
    { "name": "Violin I", "instrument": "violin",         "notes": [ … ] },
    { "name": "Cello",    "instrument": "cello",          "notes": [ … ] },
    { "name": "Piano",    "instrument": "acoustic_piano", "notes": [ … ] }
  ]
}

Before anything renders, the submit reply's prepared block says which instrument plays each part and why (resolved_by: the part's own call, an override, the written name, or the General MIDI program), so a wrong match is caught early. For this call:

"prepared": {
  "parts": [
    { "name": "Timpani", "instrument": "vibraphone", "timbre_preset": "timpani:preset1",
      "catalog_id": "timpani", "resolved_by": "part", "notes": 2, "range": [ 38, 43 ] },
    { "name": "Horn", "instrument": "french_horn", "catalog_id": "french_horn",
      "resolved_by": "part", "notes": 2, "range": [ 55, 60 ] },
    …
  ],
  "duration_s": 42.5, "bpm": 96, "time_signature": "4/4", "tempo_changes": 4
}

The render takes one to four minutes. The result has a stem per part and a mix, as pre-signed links (24 hours, plain curl -OJ):

"parts": [
  { "index": 0, "name": "Timpani", "instrument": "vibraphone", "url": "https://…",
    "fit": { "verdict": "pass", "coverage": …, "med_offset_ms": … }, "timbre": { … }, "level": { … } },
  …
],
"mix_url": "https://…"

5. The hits that are not notes: doseedo_generate_sound

doseedo_generate_sound { "prompt": "heavy wooden door slamming shut in a stone hallway", "kind": "hit" }

About 20 seconds later it is a 2-second WAV, 10 credits (the sound-effects guide has the real reply). It goes on the picture at the hit's time: 7.75 seconds into this cue.

6. The session: doseedo_session_from_stems

doseedo_session_from_stems { "from_job": "<the render_score job_id>", "target": "logic" }

from_job reads everything from the score render: each part's audio on its own track from bar 1, the written parts as MIDI tracks beside them, and the tempo map. In 30 to 90 seconds it returns a zipped .logicx (or .als with target:"ableton") that a composer or music editor opens and keeps working in. The session build costs no credits. For a cue rendered in several calls, pass stems: [{name, audio_url}, …] from all of them (up to 24), the midi_tracks and the same tempo_map.

7. On the picture: duck, normalize, mux

The last mile is ffmpeg on your machine. This places the cue and the door slam, ducks the music under the dialogue, normalizes to −16 LUFS for the web and muxes it onto the video. If the film file starts at timecode 01:00:00:00, cue 1M1 (01:02:10:00) is 130 seconds in, and the door slam is 7.75 seconds after that:

ffmpeg -i film.mp4 -i mix.wav -i door_slam.wav -filter_complex "
  [1:a]adelay=130000:all=1[m];
  [2:a]adelay=137750:all=1[sfx];
  [0:a]asplit=2[dlg][key];
  [m][key]sidechaincompress=threshold=0.03:ratio=8:attack=20:release=400[duck];
  [dlg][duck][sfx]amix=inputs=3:duration=first:normalize=0,
    loudnorm=I=-16:TP=-1.5:LRA=11[a]" \
  -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 192k film_scored.mp4

Honest limits

Score your first cue

Connect the server, give the agent a cut and three hit points, and ask for a cue in stems and a Logic session.

The Doseedo MCP server →

FAQ

Can an AI agent score a film?

An agent can spot the picture and write the cues; Doseedo supplies what it cannot do itself. doseedo_tempo_map lands hit points on downbeats, doseedo_render_score plays every part on an instrument model (a stem per part plus a mix, each part checked against its notes), doseedo_generate_sound makes the sound design, and doseedo_session_from_stems delivers a Logic Pro or Ableton Live project.

How do I make the music hit the cuts?

Give doseedo_tempo_map the cue's tempo and the hit points, as seconds or SMPTE timecode with the frame rate. It returns a tempo map that puts each hit on a downbeat while staying near your tempo, plus the bar and beat each hit lands on. Pass that tempo_map unchanged to the render and the session.

Which timecode formats are supported?

23.976, 24, 25, 29.97, 30, 50, 59.94 and 60 fps, as HH:MM:SS:FF, with ";" before the frames for drop-frame at 29.97 and 59.94. A start_timecode marks the cue's first downbeat.

Can I get the cue as a Logic or Ableton session?

Yes. doseedo_session_from_stems with from_job set to the render_score job builds a .logicx or .als: every part's audio on its own track, the written parts as MIDI, on the tempo map. It costs no credits.

How many instruments can play in one call?

Up to 8 stems per doseedo_render_score call, with at most one World or custom instrument. For a larger orchestra, render it in calls by section with the same tempo map; every call starts at bar 1, so the stems line up.

What does a cue cost?

Ten credits per 30-second window for each group of up to six parts: a 42-second cue with five parts is 20 credits. The tempo map and the session build are free, and a sound effect is 10 credits. doseedo_quote prices a call before it runs.

The Doseedo MCP server · one URL, every client

By DAW
By AI client
By capability

Related