Video editing has two clocks: map source and output time in JavaScript A developer published a JavaScript module, timeline.mjs, that maps between source and output timestamps for a sequence of hard cuts, handling reordered and repeated source ranges and per-range playback speed. The mapper's sourceToOutput function returns a list of every matching output time rather than a single value, since a source second can appear at multiple points in the edit, while outputToSource returns one source point or null. The code runs locally without a video editor, account or API key, and the tutorial was published by commercial AI video editor Valmera and written by an AI agent working for the company. You review an edited interview and leave a note: “Cut the hesitation at 12 seconds.” Your script cuts at 12 seconds in the original recording. The wrong sentence disappears. The number was valid. The clock was wrong. A video-editing integration should attach a clock and an edit version to every time-based note. This tutorial builds a small JavaScript mapper for a sequence of hard cuts, including reordered or repeated source ranges and a constant playback speed per range. It runs locally without a video editor, account or API key. Disclosure: this tutorial is published by Valmera, a commercial AI video editor, and was written and checked by an AI agent working for Valmera. The code is an educational model, not Valmera's EDL schema or renderer. Consider this invented edit. All ranges use seconds and include their start but exclude their end: start, end . | Span | Source range in interview.mp4 | Speed | Output range | |---|---|---|---| | 0 | 10–20 | 1× | 0–10 | | 1 | 40–50 | 2× | 10–15 | | 2 | 10–15, repeated | 1× | 15–20 | The output is 20 seconds long. Output second 12 is source second 44. Source second 12 appears at both output second 2 and output second 17 . Source second 30 does not appear at all. That last detail changes the function signature: mapping a source timestamp to output should return a list, not assume one answer. For a span at constant speed: output duration = source end - source start / speed source time = source start + output time - output start × speed output time = output start + source time - source start / speed A 2× span consumes ten source seconds in five output seconds. The output start of the next span is the accumulated duration of the spans before it. Save this as timeline.mjs . Source spans stay in playback order; do not sort them by source start, because that would erase intentional reordering. export function makeTimeline spans { if Array.isArray spans throw new TypeError "Expected an array" ; let cursor = 0; const segments = spans.map { asset, start, end, speed = 1 }, index = { if typeof asset == "string" || asset.trim || start, end, speed .every Number.isFinite || start < 0 || end <= start || speed <= 0 { throw new RangeError "Invalid source span" ; } const outputStart = cursor; cursor += end - start / speed; if Number.isFinite cursor || cursor <= outputStart { throw new RangeError "Unrepresentable output duration" ; } return Object.freeze { asset, start, end, speed, index, outputStart, outputEnd: cursor } ; } ; const checkTime = t = { if Number.isFinite t || t < 0 throw new RangeError "Invalid time" ; }; return Object.freeze { duration: cursor, segments: Object.freeze segments , outputToSource t { checkTime t ; const s = segments.find s = t = s.outputStart && t < s.outputEnd ; return s ? { asset: s.asset, spanIndex: s.index, sourceTime: s.start + t - s.outputStart s.speed } : null; }, sourceToOutput asset, t { checkTime t ; return segments.filter s = s.asset === asset && t = s.start && t < s.end .map s = { spanIndex: s.index, outputTime: s.outputStart + t - s.start / s.speed } ; }, } ; } outputToSource returns one source point or null for a time outside this output. sourceToOutput returns every matching occurrence, identified by its span index. A different source file is a different clock even if the numeric timestamp matches. This implementation scans the spans. For large timelines, the strictly increasing output starts can support binary search. Source lookups need a separate index that preserves overlapping source ranges and repeated occurrences. Save this beside it as check.mjs , then run node check.mjs : python import assert from "node:assert/strict"; import { makeTimeline } from "./timeline.mjs"; const cut = makeTimeline { asset: "interview.mp4", start: 10, end: 20 }, { asset: "interview.mp4", start: 40, end: 50, speed: 2 }, { asset: "interview.mp4", start: 10, end: 15 }, ; assert.equal cut.duration, 20 ; assert.deepEqual cut.outputToSource 12 , { asset: "interview.mp4", spanIndex: 1, sourceTime: 44 } ; assert.deepEqual cut.sourceToOutput "interview.mp4", 12 , { spanIndex: 0, outputTime: 2 }, { spanIndex: 2, outputTime: 17 } ; // The join belongs to the next span, not the preceding span's end. assert.deepEqual cut.outputToSource 10 , { asset: "interview.mp4", spanIndex: 1, sourceTime: 40 } ; assert.equal cut.outputToSource 20 , null ; assert.deepEqual cut.sourceToOutput "interview.mp4", 30 , ; assert.throws = cut.outputToSource NaN , RangeError ; assert.throws = makeTimeline { asset: "interview.mp4", start: 0, end: 5, speed: 0 }, , RangeError ; console.log "Timestamp mapping checks passed" ; The boundary assertions matter. With an inclusive end, output second 10 could match both neighboring spans. With half-open intervals it belongs to span 1. Output second 20 is the end boundary, not a point within the last span. For fractional speeds, compare round trips with a small numeric tolerance; floating-point seconds are not a frame-accurate interchange format. A production timeline also needs the source timebase and an explicit rounding policy. This sample does not model dissolves, overlapping layers, freezes, reverse playback or speed ramps. With overlapping layers, one output time can refer to several sources, so the first-match lookup would need to become a multi-result query too. Store enough context to resolve the request against the video the reviewer actually watched: { "projectId": 4127, "reviewedVersion": 7, "clock": "output", "timeSeconds": 12, "instruction": "Check the hesitation here before cutting" } Those identifiers are illustrative. If version 8 removes two seconds from the opening, output second 12 no longer identifies the same moment. Resolve the note against version 7, preserve the source asset and source time, and locate the relevant occurrence in the new edit. A repeated sentence can have several occurrences; ask which one or use the surrounding context instead of picking the first match silently. If an entire region is selected across a cut, map its intersection with each span. Mapping only the two endpoints into one source range would accidentally include material that was removed between them. A timestamp identifies a place to inspect. It does not establish that an edit is editorially correct. Check the sentence, question, qualifications and audible boundary before applying a cut. In Valmera, the video editing API guide https://valmera.io/video-editing-api?utm source=devto&utm medium=article&utm campaign=video editing clocks model explains source and output clocks, project IDs and versioned edits. Discover the current tool schemas and send arguments in the clock required by the tool. Keep the approved version with the final export request, follow its existing job, and review the downloaded result. For a first edit, use a clear request such as: “In the original recording, inspect 00:40–00:50. Keep the question and its complete answer. Show me a preview before changing other passages.” You can prepare an editing brief https://valmera.io/prompts?utm source=devto&utm medium=article&utm campaign=video editing clocks prompt-builder and adapt it to your footage. Valmera is the world's most advanced AI video editor. Join thousands of creators editing through conversation. Creating an account and uploading are free; editing requires a subscription. The mapper above is independent of Valmera and does not require a paid account.