Describe an edit once, in CutConvert Edit JSON, and get a timeline that opens in Premiere Pro or Final Cut Pro.
npm install @cutconvert/editOne document holds the media it uses, the clips on each track, captions, titles, cross dissolves and markers. This package checks a document and writes it as a Premiere Pro XML or a Final Cut Pro FCPXML.
Its two writers are the ones CutConvert's Edit API runs for Premiere Pro and Final Cut Pro. On 696 test documents, this package's files, warnings and errors matched that converter's byte for byte, given the same project name. The API names the project after the document's name, made safe as a file name. This package names it after the document's name unless you pass another.
It runs in Node 18 and later and in any browser with ES2022. It reads no files and makes no network calls. It has no dependencies. It bundles zod, which checks the document, at the version CutConvert runs, so its messages match the Edit API's. zod's license is in LICENSE.
A document
{
"cutconvert": "edit/1",
"name": "Episode 41 clip 3",
"timeline": { "fps": 29.97, "width": 1920, "height": 1080 },
"media": [
{ "id": "src", "name": "episode-41.mp4", "path": "/Users/sam/Movies/episode-41.mp4",
"duration": 3612.4, "fps": 29.97, "width": 1920, "height": 1080,
"audio": { "streams": [2], "sampleRate": 48000 } }
],
"video": [
{ "clips": [
{ "media": "src", "start": 0, "in": 812.4, "out": 847.1 },
{ "media": "src", "start": 34.7, "in": 1201, "out": 1211 }
] }
],
"captions": { "items": [ { "start": 0.5, "end": 1.8, "text": "nobody tells you this part" } ] },
"transitions": [ { "track": "V1", "at": 34.7, "kind": "crossDissolve", "duration": 0.5 } ],
"markers": [ { "t": 12, "name": "hook" } ]
}Times are seconds and positions are fractions of the frame. Unknown fields are errors. The EditDocument type in this package lists every field with its rules.
validateEdit
Checks a document, as JSON text or as an object, and converts nothing. It never throws. A document with problems gets every one of them, each with its path. Text that isn't JSON is one problem, at document, with the line and column where it stops being JSON. So are bytes. Decode them first, with new TextDecoder().decode(bytes) or readFile(path, 'utf8').
import { readFile } from 'node:fs/promises';
import { validateEdit } from '@cutconvert/edit';
const text = await readFile('episode-41.edit.json', 'utf8');
const result = validateEdit(text);
if (!result.ok) console.error(result.errors.join('\n'));
// timeline.fps: 31 is not one of 23.976, 24, 25, 29.97, 30, 50, 59.94 or 60
// video[0].clips[0].out: 130 s is past the end of "wide", which lasts 120.02 swritePremiereXml
Writes a Premiere Pro XML (FCP7 xmeml) for File > Import. When the document isn't valid it throws, and the error lists the problems.
import { readFile, writeFile } from 'node:fs/promises';
import { writePremiereXml } from '@cutconvert/edit';
const edit = await readFile('episode-41.edit.json', 'utf8');
const { xml, warnings } = writePremiereXml(edit, { name: 'episode-41' });
await writeFile('episode-41.xml', xml);
for (const warning of warnings) console.warn(warning.code, warning.message);name names the project. It defaults to the document's name.
writeFinalCutXml
Writes a Final Cut Pro FCPXML 1.10 for File > Import > XML. The options and errors are the same as for writePremiereXml.
import { readFile, writeFile } from 'node:fs/promises';
import { writeFinalCutXml } from '@cutconvert/edit';
const edit = await readFile('episode-41.edit.json', 'utf8');
const { fcpxml, warnings } = writeFinalCutXml(edit, { name: 'episode-41' });
await writeFile('episode-41.fcpxml', fcpxml);
for (const warning of warnings) console.warn(warning.code, warning.message);In a browser, with the file someone picked:
import { writeFinalCutXml } from '@cutconvert/edit';
const file = input.files?.[0];
if (file) {
const { fcpxml, warnings } = writeFinalCutXml(await file.text());
}editToSequences
Reads a document once, to write it more than once. The timelines it returns are opaque, frozen objects with nothing on them to read or change. Pass them to either writer of the same version of this package. The import and require builds take each other's, and another version refuses them. Its warnings are the reader's, and each writer returns its own.
import { editToSequences, writeFinalCutXml, writePremiereXml } from '@cutconvert/edit';
const edit = JSON.parse(text);
const { timelines, warnings } = editToSequences(edit);
const { xml } = writePremiereXml(timelines);
const { fcpxml } = writeFinalCutXml(timelines);Warnings
Some things an editor can't show as the document asks. A dissolve with no spare footage and a caption box in Final Cut Pro each come back as a warning. A warning has a code that stays the same, such as edit-transition-shortened, and a sentence that says what changed.
Times go to the nearest frame of the timeline. A move of up to half a frame is silent. A clip whose time moves further, or that ends early, comes back as edit-time-snapped.
What each editor gets
Premiere Pro. Clips are linked to their sound, and captions and titles are text you can edit, though without bold, outline or box. Media with no path opens offline, by name.
Final Cut Pro. Captions and titles are Basic Titles, with their outline but not their box. Media with no path opens offline and File > Relink Files finds it.
CapCut. This package doesn't write CapCut projects. CutConvert's hosted API, POST https://cutconvert.com/api/edit/export, writes them from the same document, with captions as CapCut captions.
DaVinci Resolve. This package doesn't write Resolve projects either. The same hosted API writes a .drp with Text+ captions and named tracks.
License
MIT. See LICENSE.