Skip to content

Editing

Once your shots are rendered, editing is where you turn them into a finished cut — trimming, joining clips with transitions, grading the colour, adding captions, music and sound, and putting on cinematic letterbox bars. It's a separate, lightweight layer: pure ffmpeg, so it runs without Blender and is fast.

The Edit DSL assembles rendered shots into a final cut. It is pure ffmpeg — no Blender — so it runs under plain python3 (captions also need Pillow).

from dsl.edit_dsl import Edit

edit = Edit("bridge_sequence")

with edit.clip(shot1.render, name="setup") as c:
    c.trim(0.2, 3.5)
    c.fade_in(0.4)
    c.caption("Canal Bridge — Dusk", start=0.2, end=2.0)
    c.look("cold")

with edit.clip(shot2.render, name="move") as c:
    c.slow_motion(0.7)
    c.sound("sfx/bird_tweet", at="start")

edit.between("setup", "move").crossfade(0.5)
edit.music("bgm/atmospheric_pad", volume=0.12)
edit.letterbox("2.35")
edit.save("final.mp4")             # → edits/bridge_sequence/final.mp4

Per-clip operations

Group Methods
Picture trim, cut_start, cut_end, duration, speed, slow_motion, reverse, freeze, fade_in, fade_out
Sound mute, sound, music, ambience
Text / look caption, overlay, look, letterbox
Beats beat("reveal" / "shock" / "romantic_pause" / "memory_flash")

Time expressions for at/start/end: a number, "start", "end", "end-0.5", "start+0.3".

Transitions

cut · smash · crossfade(duration) · wipe(direction, duration) · fade_to_black(duration)

edit.between("a", "b").crossfade(0.4)
edit.between("b", "c").fade_to_black(0.5)

Parameter reference & tuning

Every editing dial, its unit and default. Time expressions (at / start / end): a number of seconds, "start", "end", "end-0.5", "start+0.3".

Picture (per clip)

Method Dial(s) Default Effect
trim(start, end) seconds 0.0, end keep start…end
cut_start / cut_end(s) seconds shave N s off the head / tail
duration(s) seconds force a hard length
speed(factor) × >1 faster, <1 slower
slow_motion(factor) × 0.6 slow-mo shortcut
freeze(at, duration) time, seconds —, 0.5 hold a single frame
fade_in / fade_out(s) seconds 0.4 dip from / to black

Sound (per clip / whole cut)

Method Dial(s) Default Effect
sound(name, at, volume, pan) time · gain · −1…1 "start" · 1.0 · 0 one-shot SFX; raise volume to ~1.3–1.6 over existing audio; pan L↔R
music(name, volume) gain None a music bed; set ~0.6–0.8 (amix halves it — #8)
ambience(name, volume) gain alias of music
mute() drop the clip's own audio

Text · look · transitions

Method Dial(s) Default Effect
caption(content, start, end) text · time · time 0.0, end on-screen text
overlay(name, opacity, start, end) 0–1 · time None image/sting overlay (needs index.json)
look(preset) name colour grade — warm·cold·noir·dream·memory·surveillance
letterbox(aspect) ratio "2.35" cinematic bars
crossfade(d) · wipe(dir, d) · fade_to_black(d) seconds (· direction) 0.4 · 0.5 · 0.5 between-clip transitions
beat(name, at) name · time "end-0.5" editorial preset (needs index.json)

Assembling these toward a finished look: see Production-Quality Tuning.

Color looks

Six color-grade presets that set a mood — applied per clip with c.look("<name>") or to the whole cut with edit.look("<name>"). Shown below on the same daylight Cycles frame so the difference is clear:

original warm cold noir
original warm cold noir
ungraded golden, +saturation cool/blue black & white, high contrast
dream memory surveillance
dream memory surveillance
soft, hazy, brightened heavily faded / nostalgic desaturated + contrasty (CCTV)
with edit.clip(shot.render) as c:
    c.look("cold")          # warm · cold · noir · dream · memory · surveillance
edit.look("noir")           # …or grade the whole assembled cut

A finished cut applies a look in motion, alongside transitions, music, and letterbox (unmute for the music):

Audio assets

Namespaced keys, auto-indexed from the asset packs:

  • Music: "bgm/<name>" — 58 tracks (e.g. bgm/atmospheric_pad, bgm/classical_piano).
  • SFX: "sfx/<name>" — 112 effects (e.g. sfx/bird_tweet, sfx/body_fall).
c.music("bgm/classical_piano", volume=0.18)
c.sound("sfx/bird_tweet", at="start")

Background music (BGM)

Lay a music bed two ways:

Scope Call Effect
Whole cut edit.music("bgm/<name>", volume=…) one track under the entire assembled timeline (edit.ambience(…) is an alias)
One clip c.music("bgm/<name>", volume=…) music under that clip only

Under the hood (add_bgm) the track is looped to cover the video, faded in/out, and mixed under any existing audio (dialogue, SFX).

Set the volume higher than you'd expect

The mix uses ffmpeg's amix, which scales each input by ~½ — so a value like 0.15 comes out near-silent. Use roughly 0.60.8 for an audible-but-subtle bed (≈0.30.4 in the final mix). The Scene 2 sample uses 0.7. Details in Known Issues #11.

The library: 58 tracks under assets/bgm/, auto-indexed as "bgm/<name>" — e.g. bgm/atmospheric_pad, bgm/classical_piano, bgm/action_heroic, bgm/comedic_score, bgm/city_street, bgm/cafe_ambience. List them with assets.list("audio").

Track levels vary — set volume to taste

All 58 tracks load and mix (verified). But their built-in loudness ranges widely — e.g. action_heroic ≈ −15 dB vs atmospheric_pad ≈ −47 dB — so one volume won't suit every track. Rough starting points: punchy tracks ~0.60.8; quiet ambient beds higher.

edit.music("bgm/classical_piano", volume=0.7)   # whole-cut bed, audible under dialogue

Hear it: the Gallery's Sample Scene has a with-music / no-music A/B of the exact same cut.

Sound effects (SFX)

An SFX is a one-shot sound dropped at a moment in a clip (a thud, a whoosh, a gunshot). Adding one is three steps.

1 · Find an effect. Effects are keyed "sfx/<name>"112 of them under assets/sfx/ (e.g. sfx/body_fall, sfx/sword_whoosh, sfx/gunshot, sfx/crowd_cheer, sfx/footsteps_grass). Browse them:

assets.list("audio")                                  # every bgm/* and sfx/* key
assets.search("audio", kind="sfx", tags=["impact"])   # filter by tag

2 · Drop it on a clip with c.sound(...):

with edit.clip(shot.render) as c:
    c.sound("sfx/body_fall", at="end-0.5")     # plays once, near the end of this clip

3 · Place & balance it with the three knobs:

Arg Controls Examples
at when it fires "start", "end", "end-0.5", "start+0.3", or seconds (1.2)
volume how loud 1.0 default — raise to ~1.3–1.6 if dialogue/music is present (it's amix-mixed)
pan left ↔ right -1.0 (left) · 0 (centre) · 1.0 (right)
c.sound("sfx/airplane_flyby", at="start", volume=1.2, pan=-0.6)   # loud, panned left

Per-clip vs. whole-cut

c.sound(...) attaches to the clip you're editing. For a timeline-wide effect, use edit.audio("sfx/<name>", start=…, volume=…) on the assembled cut.

All effects verified

112/112 effects resolve and mix (test_check/sfx_check.py); sampled effects land at a clearly audible −24 to −31 dB at volume=1.0. Note: like BGM, an SFX is amix-mixed under existing audio, so over a clip that already has dialogue/music you may need to raise volume.

SFX in action — effects matched to their moment (unmute, pick a tab):

c.sound("sfx/body_fall", at="end-0.7", volume=1.5)   # thud on the landing

c.sound("sfx/sword_whoosh", at="start+0.3", volume=1.5)

c.sound("sfx/gunshot", at="start", volume=1.3)       # rifle fires early

c.sound("sfx/crowd_cheer", at="start", volume=1.0)

Optional assets

The stock/images overlay categories and the beat() presets reference named stock/sting assets that only exist if you create assets/edit/index.json.