NLE Timeline

View NLE Timeline

.. toc::

.. llms_copy::NLE Timeline

πŸ“š Full documentation: [github.com/pip-install-python/dash-nle-timeline](https://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](https://github.com/pip-install-python/dash-nle-timeline) Β· [PyPI](https://pypi.org/project/dash-nle-timeline/)

``bash 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.

.. exec::docs.dash_nle_timeline.quick_start :code: false

```python

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.

.. exec::docs.dash_nle_timeline.edits_callback :code: false

```python

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.

.. exec::docs.dash_nle_timeline.playback :code: false

```python

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.

```python 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

| Property | Type | Default | Description | |:---------|:-----|:--------|:------------| | id | string | - | Component ID for Dash callbacks. | | fps | number | 30 | Frames per second β€” defines the integer-frame grid and frame↔second conversions. | | duration | number | 300 | Total timeline length in integer frames. | | playhead | number | - | Current playhead position in integer frames. Written on scrub/seek/playback; writable from callbacks. | | playing | boolean | - | When True, a rAF loop advances playhead at fps. The component plays no media itself. | | loop | boolean | False | Loop back to 0 when the playhead reaches duration. | | playheadThrottleMs | number | 60 | Min ms between playhead writes during playback. | | tracks | list of dicts | - | Track lanes, top-to-bottom. Keys: id*, kind ('video'/'audio'/'camera'), label, height, locked, muted, color. | | clips | list 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. | | lastEdit | dict | - | [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. | | markers | list of dicts | - | Labeled time points: visual guides + snap targets. Keys: id*, frame*, label, color. | | cameraKeyframes | list of dicts | - | Keyframes for 'camera' tracks. Keys: id*, trackId*, time*, center* [lat, lng], zoom*, bearing, easing, label. | | selectedClipIds | list of strings | - | Currently selected clip ids. Round-trips; writable from Python. | | selectedClipId | string | - | [Readonly] The single selected clip id, or None when zero/many selected. | | command | dict | - | Imperative command, de-duped by id. Keys: id*, type* (play/pause/togglePlay/seek/stepFrame/splitAtPlayhead/deleteSelected/copy/paste/duplicate/selectAll/zoomToFit/zoomTo/scrollToFrame), payload. | | pixelsPerSecond | number | 120 | Horizontal zoom as pixels per second. | | minPixelsPerSecond | number | 8 | Lower clamp for wheel-zoom. | | maxPixelsPerSecond | number | 4000 | Upper clamp for wheel-zoom. | | scrollX | number | - | Horizontal scroll offset of the lane viewport, in CSS px from frame 0. | | snapping | boolean | True | Master enable for snapping during move/resize/trim. | | snapTargets | list | ['clips','playhead','markers'] | Snap-eligible line categories: 'playhead', 'grid', 'markers', 'clips'. | | snapThreshold | number | 8 | Snap activation distance in screen px. | | gridFrames | number | 0 | Grid spacing in frames for 'grid' snapping. 0 = off. | | allowCrossTrackMove | boolean | True | Allow dragging a clip onto another lane of the same kind. | | multiSelect | boolean | True | Allow shift/cmd-click multi-selection. | | keyboard | boolean | True | Enable all keyboard shortcuts. | | readOnly | boolean | False | View-only: disable editing (scrub + selection + zoom still work). | | controls | boolean | True | Show the built-in transport bar above the ruler. | | theme | string | 'dark' | Chrome color theme: 'dark' or 'light'. | | headerWidth | number | 140 | Width of the left track-header column in px. | | rulerHeight | number | 28 | Height of the time-ruler row in px. | | className | string | - | CSS class name(s) for the root element. | | style | object | - | Inline CSS styles β€” give the timeline an explicit width/height. |

DashNleScene Props

| Property | Type | Default | Description | |:---------|:-----|:--------|:------------| | id | string | - | Component ID for Dash callbacks. | | children | node | - | Background slot β€” e.g. a dash-leaflet2 Map. Rendered behind the tileset and overlay layers, filling the stage. | | playhead | number | - | Current playhead in integer frames (typically synced from a DashNleTimeline). Layers with a [start, end) window show/hide on it. | | playing | boolean | False | When True, video layers play natively; when False they seek to playhead / fps (frame-accurate scrub). Sync from the timeline. | | fps | number | 30 | Maps the playhead to seconds for video layers. | | layers | list 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. | | tileset | dict | - | Native XYZ tileset background. Keys: url*, viewport* ({center: [lat,lng], zoom}), tileSize (256), maxZoom (19), opacity (1), subdomains (['a','b','c']). | | camera | dict | - | Screen-space pan/zoom of the overlay layer group: {x: 0, y: 0, zoom: 1}. | | resolution | list | [1280, 720] | Output stage resolution [width, height] in px (sets the aspect ratio). | | background | string | '#000000' | Stage background CSS color (behind everything). | | className | string | - | CSS class name(s) for the root element. | | style | object | - | 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: