NLE Timeline

Non-linear-editor timeline and scene compositor for Dash — frame-accurate scrubbing, multi-track clips, waveforms, and a DOM+Canvas preview stage.

📚 Full documentation: github.com/pip-install-python/dash-nle-timeline — complete API reference and examples in the repository. This page is the quick-start overview.

Installation

Visit GitHub Repo · PyPI

pip install dash-nle-timeline

Introduction

A calendar or scheduler widget thinks in dates. A video editor needs an integer frame grid, a real draggable playhead, snapping to clip edges / markers / grid, audio waveforms, and edits expressed as data. dash-nle-timeline is that — a purpose-built non-linear-editor (NLE) timeline for Dash. Only small JSON travels over callbacks (the control plane); your media stays on the media plane (a <video>, an audio engine, a server render) driven off the timeline's outputs.

The package ships two components:

Quick Start

Frames — not seconds — are the canonical unit (seconds = frame / fps). Give the timeline an explicit height via style, define tracks, then place clips on them by trackId. Audio lanes paint waveforms from precomputed peaks — the component paints bars only, it never decodes audio.

# File: docs/dash_nle_timeline/quick_start.py

import math

import dash_nle_timeline as nle
import dash_mantine_components as dmc

# Precomputed audio peaks (magnitude format: one positive value per bucket, 0..1).
# In a real app you would compute these server-side from the audio file.
PEAKS = [abs(math.sin(i / 7)) * 0.8 + 0.15 for i in range(220)]

component = dmc.Paper(
    withBorder=True,
    p="md",
    children=dmc.Stack(
        [
            dmc.Text("A static three-track timeline — scrub the playhead, "
                     "drag clips, trim edges, or press S to split.", size="sm", c="dimmed"),
            nle.DashNleTimeline(
                id="nle-quick-start-tl",
                fps=30,
                duration=300,
                pixelsPerSecond=90,
                tracks=[
                    {"id": "v1", "kind": "video", "label": "V1"},
                    {"id": "v2", "kind": "video", "label": "V2"},
                    {"id": "a1", "kind": "audio", "label": "A1"},
                ],
                clips=[
                    {"id": "c1", "trackId": "v1", "start": 0, "duration": 90,
                     "label": "intro.mp4", "color": "indigo"},
                    {"id": "c2", "trackId": "v1", "start": 100, "duration": 120,
                     "label": "main.mp4", "color": "teal"},
                    {"id": "c3", "trackId": "v2", "start": 60, "duration": 80,
                     "label": "overlay.png", "color": "orange"},
                    {"id": "c4", "trackId": "a1", "start": 0, "duration": 220,
                     "label": "music.wav",
                     "peaks": PEAKS, "peaksFormat": "magnitude", "peaksPerFrame": 1},
                ],
                markers=[
                    {"id": "m1", "frame": 100, "label": "cut", "color": "gold"},
                ],
                style={"width": "100%", "height": "260px"},
            ),
        ],
        gap="sm",
    ),
)

Python Owns Clips — Apply lastEdit in a Callback

This is the core pattern of the component. Every committed edit (move, resize, trim, split, delete, select, seek) is reported as a single lastEdit dict. It carries a timestamp, so a Dash Input on it always fires — even for repeated identical edits.

The component applies edits to its own optimistic copy of clips (so they persist visually without a round-trip), but Python is the source of truth: a callback reads lastEdit, applies it to the clip list, and writes clips back. Writing back is how you validate, override, or persist edits — reject an edit by returning the unmodified list.

# File: docs/dash_nle_timeline/edits_callback.py

import json

import dash_nle_timeline as nle
import dash_mantine_components as dmc
from dash import callback, Input, Output, State, no_update

component = dmc.Paper(
    withBorder=True,
    p="md",
    children=dmc.Stack(
        [
            dmc.Text("Move, resize, trim, or split a clip — every committed edit "
                     "arrives in Python as one `lastEdit` dict.", size="sm", c="dimmed"),
            nle.DashNleTimeline(
                id="nle-edits-tl",
                fps=30,
                duration=240,
                tracks=[
                    {"id": "v1", "kind": "video", "label": "V1"},
                    {"id": "v2", "kind": "video", "label": "V2"},
                ],
                clips=[
                    {"id": "c1", "trackId": "v1", "start": 0, "duration": 70,
                     "label": "shot A", "color": "indigo"},
                    {"id": "c2", "trackId": "v1", "start": 90, "duration": 80,
                     "label": "shot B", "color": "teal"},
                    {"id": "c3", "trackId": "v2", "start": 40, "duration": 60,
                     "label": "title", "color": "orange"},
                ],
                style={"width": "100%", "height": "200px"},
            ),
            dmc.Text("Last edit event:", size="sm", fw=500),
            dmc.Code(id="nle-edits-out", block=True,
                     children="(interact with the timeline)"),
        ],
        gap="sm",
    ),
)


@callback(
    Output("nle-edits-out", "children"),
    Input("nle-edits-tl", "lastEdit"),
    prevent_initial_call=True,
)
def show_edit(edit):
    return json.dumps(edit, indent=2)


# Python owns `clips`: apply each reported edit to the list and feed it back.
# The component edits its own copy optimistically, so writing clips back is how
# you validate, override, or persist the result.
@callback(
    Output("nle-edits-tl", "clips"),
    Input("nle-edits-tl", "lastEdit"),
    State("nle-edits-tl", "clips"),
    prevent_initial_call=True,
)
def apply_edit(edit, clips):
    if not edit:
        return no_update
    by_id = {c["id"]: dict(c) for c in clips}
    edit_type = edit.get("type")
    clip = by_id.get(edit.get("clipId"))

    if edit_type == "move" and clip:
        clip["start"] = edit["start"]
        clip["trackId"] = edit.get("trackId", clip["trackId"])
    elif edit_type in ("resize", "trim") and clip:
        for key in ("start", "duration", "inPoint"):
            if key in edit:
                clip[key] = edit[key]
    elif edit_type == "delete" and clip:
        del by_id[clip["id"]]
    else:
        # select / seek / split / paste etc. — nothing to write back here
        return no_update
    return list(by_id.values())

Playback and the Playhead

Setting playing=True starts a requestAnimationFrame loop that advances playhead at fps (writes throttled by playheadThrottleMs). The component plays no media itself — drive a real <video>, an audio engine, or a DashNleScene off the playhead output. The playhead prop round-trips: the component writes it on scrub/seek/playback, and a callback may write it to move the playhead programmatically. loop=True wraps back to frame 0 at duration.

# File: docs/dash_nle_timeline/playback.py

import dash_nle_timeline as nle
import dash_mantine_components as dmc
from dash import callback, Input, Output, State

FPS = 30
DURATION = 240

component = dmc.Paper(
    withBorder=True,
    p="md",
    children=dmc.Stack(
        [
            dmc.Text("Playback is a requestAnimationFrame loop driven by the "
                     "`playing` prop — the component plays no media itself, it just "
                     "advances `playhead` at the configured fps.", size="sm", c="dimmed"),
            nle.DashNleTimeline(
                id="nle-playback-tl",
                fps=FPS,
                duration=DURATION,
                loop=True,
                tracks=[
                    {"id": "v1", "kind": "video", "label": "V1"},
                ],
                clips=[
                    {"id": "c1", "trackId": "v1", "start": 0, "duration": 100,
                     "label": "scene 1", "color": "indigo"},
                    {"id": "c2", "trackId": "v1", "start": 110, "duration": 110,
                     "label": "scene 2", "color": "teal"},
                ],
                style={"width": "100%", "height": "160px"},
            ),
            dmc.Group(
                [
                    dmc.Button("Play", id="nle-playback-btn", color="teal", size="sm"),
                    dmc.Text("frame 0  |  0.000s", id="nle-playback-out",
                             size="sm", ff="monospace", c="dimmed"),
                ],
                gap="md",
            ),
        ],
        gap="sm",
    ),
)


@callback(
    Output("nle-playback-tl", "playing"),
    Output("nle-playback-btn", "children"),
    Input("nle-playback-btn", "n_clicks"),
    State("nle-playback-tl", "playing"),
    prevent_initial_call=True,
)
def toggle_play(_n, playing):
    now_playing = not bool(playing)
    return now_playing, ("Pause" if now_playing else "Play")


@callback(
    Output("nle-playback-out", "children"),
    Input("nle-playback-tl", "playhead"),
)
def show_playhead(frame):
    frame = frame or 0
    return f"frame {frame}  |  {frame / FPS:.3f}s"

Keyboard shortcuts (enabled by default via keyboard): Space play/pause, arrows step (Shift = 1s), S split at playhead, Delete, ⌘/Ctrl+C/V/D copy/paste/duplicate, ⌘/Ctrl+A select all, Home/End, +/- zoom. The imperative command prop dispatches the same actions from Python (play, pause, seek, splitAtPlayhead, zoomToFit, …), de-duped by a unique id.


DashNleScene

DashNleScene is the preview side: a compositor stage that renders, at the current playhead, a background — a dash-leaflet2 Map in children and/or a native XYZ tileset — then overlay layers (image / video / text) with per-layer [start, end) visibility windows and a screen-space camera. Sync playhead and playing from a DashNleTimeline, and drive the tileset.viewport from a camera-kind track's cameraKeyframes to fly the map while compositing video on top.

import dash_nle_timeline as nle

scene = nle.DashNleScene(
    id="nle-scene",
    fps=30,
    playhead=0,                       # sync from DashNleTimeline
    resolution=[1280, 720],
    tileset={
        "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
        "viewport": {"center": [48.8566, 2.3522], "zoom": 12},
    },
    layers=[
        {"id": "title", "kind": "text", "text": "Paris flyover",
         "rect": [0.05, 0.05, 0.9, 0.2], "start": 0, "end": 90},
    ],
    style={"width": "100%"},
)

For full map interactivity (pan/zoom/controls/flyTo) put a dash-leaflet2 Map in children instead of using tileset.


DashNleTimeline Props

PropertyTypeDefaultDescription
idstring-Component ID for Dash callbacks.
fpsnumber30Frames per second — defines the integer-frame grid and frame↔second conversions.
durationnumber300Total timeline length in integer frames.
playheadnumber-Current playhead position in integer frames. Written on scrub/seek/playback; writable from callbacks.
playingboolean-When True, a rAF loop advances playhead at fps. The component plays no media itself.
loopbooleanFalseLoop back to 0 when the playhead reaches duration.
playheadThrottleMsnumber60Min ms between playhead writes during playback.
trackslist of dicts-Track lanes, top-to-bottom. Keys: id*, kind ('video'/'audio'/'camera'), label, height, locked, muted, color.
clipslist of dicts-Clips on tracks. Keys: id, trackId, start, duration (frames), inPoint, label, color, src, peaks, peaksFormat ('minmax'/'magnitude'), peaksPerFrame, thumbnails, locked. Python is the source of truth.
lastEditdict-[Readonly] Most recent committed edit. Keys: type (move/resize/trim/split/delete/select/seek/copy/paste), timestamp, clipId, clipIds, trackId, start, duration, inPoint, frame, resultIds, snapped.
markerslist of dicts-Labeled time points: visual guides + snap targets. Keys: id, frame, label, color.
cameraKeyframeslist of dicts-Keyframes for 'camera' tracks. Keys: id, trackId, time, center [lat, lng], zoom*, bearing, easing, label.
selectedClipIdslist of strings-Currently selected clip ids. Round-trips; writable from Python.
selectedClipIdstring-[Readonly] The single selected clip id, or None when zero/many selected.
commanddict-Imperative command, de-duped by id. Keys: id, type (play/pause/togglePlay/seek/stepFrame/splitAtPlayhead/deleteSelected/copy/paste/duplicate/selectAll/zoomToFit/zoomTo/scrollToFrame), payload.
pixelsPerSecondnumber120Horizontal zoom as pixels per second.
minPixelsPerSecondnumber8Lower clamp for wheel-zoom.
maxPixelsPerSecondnumber4000Upper clamp for wheel-zoom.
scrollXnumber-Horizontal scroll offset of the lane viewport, in CSS px from frame 0.
snappingbooleanTrueMaster enable for snapping during move/resize/trim.
snapTargetslist['clips','playhead','markers']Snap-eligible line categories: 'playhead', 'grid', 'markers', 'clips'.
snapThresholdnumber8Snap activation distance in screen px.
gridFramesnumber0Grid spacing in frames for 'grid' snapping. 0 = off.
allowCrossTrackMovebooleanTrueAllow dragging a clip onto another lane of the same kind.
multiSelectbooleanTrueAllow shift/cmd-click multi-selection.
keyboardbooleanTrueEnable all keyboard shortcuts.
readOnlybooleanFalseView-only: disable editing (scrub + selection + zoom still work).
controlsbooleanTrueShow the built-in transport bar above the ruler.
themestring'dark'Chrome color theme: 'dark' or 'light'.
headerWidthnumber140Width of the left track-header column in px.
rulerHeightnumber28Height of the time-ruler row in px.
classNamestring-CSS class name(s) for the root element.
styleobject-Inline CSS styles — give the timeline an explicit width/height.

DashNleScene Props

PropertyTypeDefaultDescription
idstring-Component ID for Dash callbacks.
childrennode-Background slot — e.g. a dash-leaflet2 Map. Rendered behind the tileset and overlay layers, filling the stage.
playheadnumber-Current playhead in integer frames (typically synced from a DashNleTimeline). Layers with a [start, end) window show/hide on it.
playingbooleanFalseWhen True, video layers play natively; when False they seek to playhead / fps (frame-accurate scrub). Sync from the timeline.
fpsnumber30Maps the playhead to seconds for video layers.
layerslist of dicts-Overlay layers, bottom-to-top by z then array order. Keys: id*, kind ('image'/'video'/'text'), src, text, rect [x,y,w,h] (0..1), opacity, rotation, start, end, srcStart, z, color, fontSize.
tilesetdict-Native XYZ tileset background. Keys: url, viewport ({center: [lat,lng], zoom}), tileSize (256), maxZoom (19), opacity (1), subdomains (['a','b','c']).
cameradict-Screen-space pan/zoom of the overlay layer group: {x: 0, y: 0, zoom: 1}.
resolutionlist[1280, 720]Output stage resolution [width, height] in px (sets the aspect ratio).
backgroundstring'#000000'Stage background CSS color (behind everything).
classNamestring-CSS class name(s) for the root element.
styleobject-Inline CSS styles for the component.

\ = required key inside the dict.*

Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs: