roughcut is a command-line for my assistant editor that works with FCP. It reads projects or events (p.e. containing footage from a shoot) exported from FCP as an FCPXML, a paper cut, or a script with shoot comments, and builds FCP projects you import back as FCPXML and finish by hand in the app. It never touches the library or the media: it reads the exports and writes the resulting FCPXML files next to them.
It does two jobs:
| Job | You give it | You get back |
|---|---|---|
roughcut papercut: selects from a paper cut | a stringout (an FCP project with captions, exported as XML) and a paper cut (the lines to keep, with timecodes) | one FCP project per paper-cut section, each a string of selects cut from the stringout, with a marker per select and a report of anything doubtful |
roughcut script: rough cuts from a script | the camera files, the script .docx with the client’s comments, and the event exported from FCP | one FCP project per script, each paragraph cut from the take the comments point to, with every other take of it stacked above (disabled) to audition, and a marker per comment |
Everything it writes is checked against Apple’s FCPXML 1.14 rules before it’s saved; a file
that doesn’t pass is never written as final. It never overwrites a file: a second run writes
_2, _3, and so on, or asks you for a suffix.
1. Requirements
- A Mac with Apple silicon (M1 or later) and a recent macOS. The word-level features
(
--refineandroughcut script transcribe) run Whisper on the Mac’s GPU through MLX, which only exists on Apple silicon. Everything else would run on any Mac, but this guide assumes Apple silicon. - Final Cut Pro 12.3 or later, which reads and writes FCPXML 1.14.
- About 2 GB of free disk for the Whisper model, if you use the word-level features.
- Access to the GitHub repository
rafabenedetti/roughcut(it’s private: ask Rafa to add your GitHub account). - An internet connection for installing. Once installed, nothing leaves the Mac: Whisper runs locally and offline.
What gets installed, and what for:
| Dependency | Needed for | Installed with |
|---|---|---|
| Homebrew | installing ffmpeg | the installer script from brew.sh |
| uv | installing roughcut, Python 3.14 and its libraries | the installer script from astral.sh |
| Python 3.14 | running roughcut | uv, automatically |
| lxml, PyYAML | reading and writing XML and YAML | uv, automatically |
| xmllint | checking every file against the FCPXML 1.14 rules | already part of macOS |
| ffmpeg, ffprobe | reading audio from camera files (--refine, script transcribe) | Homebrew |
| mlx-whisper | word-level transcription (--refine, script transcribe) | uv, as its own tool |
the Whisper model whisper-large-v3-turbo | the same | the Hugging Face hf command |
2. Installing
Open Terminal and run each step in order. Steps 1 to 4 are needed by everyone; steps 5
and 6 only if you’ll use --refine or roughcut script, which is most people.
Step 1: Homebrew
Check whether you already have it:
brew --version
If that prints a version, skip to step 2. If it says command not found, install it:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
At the end the installer prints two or three commands under Next steps that add Homebrew to your shell. Copy and run them, then close Terminal and open a new window.
Step 2: uv
curl -LsSf https://astral.sh/uv/install.sh | sh
Close Terminal and open a new window, then check:
uv --version
You don’t need to install Python yourself: uv downloads Python 3.14 the first time roughcut needs it.
Step 3: ffmpeg
brew install ffmpeg
Check it (both should print a version):
ffmpeg -version
ffprobe -version
xmllint is already on every Mac. Check it with:
xmllint --version
Step 4: roughcut
Get a copy of the code. This puts it in a Developer folder in your home folder; any folder
works, but keep it, because updating means pulling into it:
mkdir -p ~/Developer
git clone https://github.com/rafabenedetti/roughcut.git ~/Developer/roughcut
If Git asks you to install the Command Line Developer Tools, accept, wait for it to finish, and run the clone again. If it asks you to sign in, sign in to the GitHub account that has access to the repository.
Install it as a command:
uv tool install ~/Developer/roughcut
If uv then warns that its tools folder isn’t on your PATH, run the command it suggests, which is usually:
uv tool update-shell
and open a new Terminal window. Check the install:
roughcut --version
It should print roughcut 0.1.0 (FCPXML 1.14 DTD ok). “DTD ok” means the copy of Apple’s
FCPXML rules inside roughcut is intact; if it ever says otherwise, reinstall.
Step 5: mlx-whisper (for --refine and roughcut script transcribe)
mlx-whisper is installed as its own tool, separate from roughcut. roughcut finds it by itself.
uv tool install mlx-whisper
Step 6: the Whisper model (for --refine and roughcut script transcribe)
roughcut runs Whisper offline, so the model has to be downloaded once, beforehand. Install the Hugging Face command-line tool, then use it to fetch the model (about 1.6 GB):
uv tool install huggingface-hub
hf download mlx-community/whisper-large-v3-turbo
The model lands in ~/.cache/huggingface/hub/. You don’t need to do anything else with it.
Updating roughcut
When there’s a new version, pull it and reinstall:
git -C ~/Developer/roughcut pull
uv tool install --reinstall ~/Developer/roughcut
uv tool install installs a snapshot of the code, so changes only reach the roughcut
command after --reinstall.
Uninstalling
uv tool uninstall roughcut
3. The commands
| Command | What it does |
|---|---|
roughcut --version | prints the version and checks the built-in FCPXML rules |
roughcut --print-config | prints every setting with its default and what it does |
roughcut papercut STRINGOUT PAPERCUT | paper cut + stringout → selects projects |
roughcut papercut STRINGOUT PAPERCUT --refine | the same, with cuts placed on the words and the pauses between them |
roughcut papercut --manifest JOBS.yaml | several paper cuts in one run |
roughcut papercut --to-canonical PAPERCUT | converts an older paper cut to the current format |
roughcut script transcribe PROJECT | camera files → word-level transcripts |
roughcut script map PROJECT | script + comments + transcripts → a take map for you to check |
roughcut script cut PROJECT | the checked take map → one FCPXML of rough-cut projects |
Add --help to any of them for its options, e.g. roughcut papercut --help.
Every command ends with an exit code, which also tells you how the run went:
| Exit | Meaning |
|---|---|
| 0 | done, nothing to look at |
| 2 | done, with flags: things worth checking, listed in the output and the report |
| 1 | failed: nothing (or not everything) was written; the message says why |
Paths with spaces need quotes, or drag the file from Finder into Terminal, which quotes it for you.
4. roughcut papercut: selects from a paper cut
What you need
-
The stringout. A project in FCP holding the interview (all the takes you’ll select from, in any order), with captions: roughcut finds each paper-cut line by matching it against the captions, so they have to be there (FCP’s Transcribe to Captions is fine). Multicam clips and plain clips both work. Select the project in the browser, choose File › Export XML…, and keep the default format (FCPXML 1.14). You get a
.fcpxmldbundle; zipping it is fine too. -
The paper cut. The lines you want, grouped into selects, with the stringout timecode of each line. The format:
# Jane Doe profile selects ## PROJECT_v1_JaneDoe-ProfileSelects 0:07:17 – 0:07:19 Design thinking is an approach that allows you to deeply 0:07:19 – 0:07:22 understand people in their context, synthesize // 0:01:38 – 0:01:41 [Jane] I'm Jane Doe, and I am a design strategist, ## Another project from the same interview 0:10:33 – 0:10:37 My wife will be embarrassed that#is the document’s title; it’s ignored.- Each
##heading starts a project, named exactly as written. One file can hold one project or many. - Each line is
start – end text, with the timecode of the stringout’s timeline. //on its own line ends a select. A project’s selects play in the order written.[Name]at the start of a line is a speaker label, not spoken text.
The easiest way to get these lines is from a transcript made by itt-convert, which writes this format: copy the lines you want, add
##and//. Older paper cuts (a Word document with the timecode on its own line, or the August markdown tables) still work;--to-canonicalconverts them to this format (see below).
Running it
roughcut papercut "Jane_stringout.fcpxmld" "Jane_papercut.md"
What it writes, next to the paper cut (or in the folder given with -o):
| File | What it is |
|---|---|
<project>.fcpxmld | one per ## project: import these into FCP |
<paper cut>.report.md | a readable report: each select, where it was found, and any flags |
<paper cut>.report.json | the same, for other tools |
<paper cut>.selects.json | the cut decisions (for troubleshooting) |
failed/ | only if a project couldn’t be made valid: the bundle and its errors |
Import in FCP with File › Import › XML… and pick the .fcpxmld. Each bundle holds one
project cut from the stringout’s own clips, so it links to the media your library already has.
Options:
| Option | What it does |
|---|---|
-o DIR | write the outputs in DIR instead of next to the paper cut |
--suffix _v3 | add _v3 to the project, bundle and report names; use it to run again without overwriting |
--zip | write <project>.fcpxmld.zip instead of the bundle folder |
--refine | place each cut on the words themselves (see below) |
--config FILE | use settings from FILE (see Settings) |
A run never overwrites. If an output already exists it stops before writing; run again with
--suffix.
--refine: cuts on the words
Without it, cuts land where the captions start and end, which is close but can clip a word
or leave a breath. With --refine, roughcut reads the audio around each cut from the camera
files, runs Whisper on it, and puts the in just before the first word and the out in the
quiet after the last one, keeping a breath when the speaker carries on. It follows a few
rules learned from real listens: when in doubt it keeps more material rather than cutting a
word, it never makes a jump cut inside a continuous take, and it reproduces the editor’s own
camera switches exactly. Anything it can’t place with confidence falls back to the
caption-based cut and is flagged.
It needs steps 5 and 6 of the install, and the camera files mounted where they were when the stringout was exported (roughcut reads the media paths inside the export). It takes a few seconds per select.
roughcut papercut "Jane_stringout.fcpxmld" "Jane_papercut.md" --refine --suffix _refined
Several paper cuts at once: --manifest
Write a YAML file listing the jobs. Paths are relative to the manifest file:
- stringout: Jane_stringout.fcpxmld.zip
papercut: Jane_papercut.md
- stringout: Sam_stringout.fcpxmld.zip
papercut: Sam_papercut.md
output: selects # optional: output folder
suffix: _v2 # optional
settings: {speakers: [Tom]} # optional: settings for this job only
roughcut papercut --manifest jobs.yaml
It runs every job even if one fails, and prints a line per job plus a total.
Converting an older paper cut: --to-canonical
roughcut papercut --to-canonical "Jane_papercut.docx"
writes Jane_papercut_canonical.md next to it and checks that it reads back to the same
selects. For the August style, with speaker names inline in the text, list the names in
speakers: first (see Settings) so they’re stripped from the text; the output says how many
were, and you can add [Name] labels back by hand where you want them.
Reading the flags
The report lists flags per select; the terminal shows how many. The ones you’ll meet:
| In the report | Meaning | What to do |
|---|---|---|
tight-tail: … out clamped to the next cue | the out had to stop where the next caption starts | check the end sounds clean |
tail-extra: … | a few words after your last line were kept, rather than cutting mid-caption | trim in FCP if you want |
refine-fallback: … | --refine couldn’t place this cut on the words, so it used the captions | listen to it |
block split into N segments (… at spine cuts) | the select crosses a cut in the stringout, so it became several clips | usually nothing: the stringout’s own cuts are kept |
kept inside the take (…s not in the paper cut): "Right." | a short interjection between your lines was kept, so the take plays without a jump cut | trim it in FCP if you want |
excluded unquoted/partial cues inside anchored span | captions your paper cut skipped were cut out of the select | check the joins |
possible duplicate take …; both kept | two parts of the select seem to be the same words said twice | usually one take split by an FCP cut; check |
ignored-child: … <analysis-marker> not carried | something in the stringout isn’t carried into the selects | nothing |
Paper-cut problems (a line without a timecode, a timecode without text) are flagged too, with the line they’re on; the format section above says what a clean paper cut looks like.
Settings
Every setting has a sensible default. To see them all, with a comment on each:
roughcut --print-config
To change one, put it in a file called roughcut.yaml. roughcut reads, in order, each one
overriding the one before:
- its built-in defaults
~/.config/roughcut/roughcut.yaml(your own defaults)roughcut.yamlin the paper cut’s folder (a good place for a project’sspeakers:)- the file given with
--config - a manifest job’s
settings:
For example, a roughcut.yaml next to an August-style paper cut:
speakers: [Ana, Ben, Kai]
5. roughcut script: rough cuts from a script and the client’s comments
This is for scripted shoots: the talent reads numbered scripts to camera, several takes each, and the client leaves comments on the script (“2nd close-up take was good”, “use the take with the new sentence”). roughcut transcribes every take, works out which takes read which paragraph, reads the comments, proposes a take per paragraph, lets you correct it, and cuts.
It works in three commands, run in order, with a check by you in the middle:
transcribe → map → (you check and edit the take map) → cut → import into FCP
What you need
- The camera files in one folder (e.g.
assets/raw_footage/<shoot>/<camera>/). - The script as a
.docx(Word, or Google Docs › Download › Microsoft Word), with the client’s comments in it. Each script starts with a heading styled as a Heading and writtenScript 2 - Getting Started. A heading like1.1.3 - Introductionabove it is its section, and names the project. A comment on a script’s heading is taken as a note on the whole script. A paragraph entirely in[brackets]is a note, not a line. - The event exported from FCP: import the camera files into an event, select the event in the browser, choose File › Export XML…, and keep FCPXML 1.14. roughcut copies the event’s media references into the rough cut, so it imports linked to the same clips.
The project config
Everything about a shoot lives in one small file, roughcut-project.yaml, in the project
folder or in its tools/ folder. Paths are relative to the project folder, so the file keeps
working if the drive mounts under a different name.
project:
version: v1
script: exports/v1/docs/Scripts - Jane Doe.docx
footage: assets/raw_footage/20260101_PROJECT_v1_Studio_JaneDoe/camera_a
event_xml: exports/v1/xml/20260101_PROJECT_v1_Studio_JaneDoe.fcpxmld
transcribe:
speaker: Jane Doe # helps Whisper spell the name
vocabulary: [wayfinding] # words Whisper might miss; names in the script are found automatically
cut:
event: PROJECT rough cuts v1 # the event the rough cuts appear in, in FCP
Only version, script and footage are required (event_xml is needed for cut). What
roughcut writes, and where, unless the config’s paths: says otherwise ({shoot} is the name
of the footage folder’s parent):
| What | Where |
|---|---|
| audio for transcription | exports/{version}/audio_for_transcripts/ |
| transcripts | exports/{version}/transcripts/{shoot}_transcripts/ |
| take maps | exports/{version}/take_maps/{shoot}_take_map.yaml and .md |
| rough cuts | exports/{version}/xml/{shoot}_roughcuts_{version}.fcpxml |
Other settings you can add:
| Section | Setting | Default | Meaning |
|---|---|---|---|
transcribe | mix | all | which audio to transcribe: all channels mixed, or a list like [1] |
transcribe | spelling | none | extra fixes, e.g. {'\bline shot\b': wide shot} |
transcribe | model, language | turbo, en | the Whisper model and language |
cut | handles | 6 | frames kept each side of a paragraph |
cut | merge_gap_seconds | 2.5 | consecutive paragraphs from one take closer than this stay one clip |
paths | audio, transcripts, take_map, roughcuts | as above | put any of them elsewhere, e.g. where earlier transcripts already are |
In the commands below, PROJECT is the project folder (or the config file itself).
Step 1: roughcut script transcribe
roughcut script transcribe "/Volumes/media/MyProject" --dry-run
--dry-run shows what it will do and writes nothing: how many takes are done and how many
are left, and the vocabulary it found in the script. Then run it for real:
roughcut script transcribe "/Volumes/media/MyProject"
It extracts each camera file’s audio and transcribes it with Whisper, word by word. It’s safe to stop and run again: takes that already have a transcript are skipped. Allow roughly a few seconds per minute of footage on a recent Mac.
Step 2: roughcut script map
roughcut script map "/Volumes/media/MyProject"
It writes the take map, a pair of files in take_maps/:
…_take_map.md, to read: per script, every read of it found in the takes (a pass: a full read, a false start, or a pickup), which framing each is (wide or close-up), each client comment and how it was understood, and the proposed take per paragraph with the reason.…_take_map.yaml, to edit: the same, in a form roughcut reads back.
Framing comes from what the camera operator says on the recording (“starting on the wide”, “let’s go to the close-up”); when a file has no cue, it carries on from the file before. The output lists flags for everything worth checking: takes whose framing is unknown or carried over, comments that point at a take that doesn’t exist, paragraphs no take covers, takes that read no script.
Step 3: check and edit the take map
Open the newest …_take_map.md to read, and edit the newest …_take_map.yaml in any text
editor. Two kinds of edit:
Decisions, at the top of the YAML, fix what roughcut can’t know:
decisions:
false_start_seconds: 20 # a read from the top shorter than this is a false start
framing: # when the operator's cues are missing or wrong
t01: wide
t05: {'0:00': close, '6:43': wide} # close-up, switching to wide at 6:43
false_starts: [t10] # takes (or single passes, like t12.3) never to count or pick
dropped:
10: [5] # script 10, paragraph 5 was cut on set
drop_sentences: # a sentence rewritten on set: drop the version that wasn't said
16: ['The old version of the sentence, exactly as it is in the script.']
skip_scripts: [1] # scripts to leave out
Takes are named by the part of the file name that differs (t05 for
…_ssd22_t05_422.MOV). Times are minutes:seconds from the head of the file, in quotes.
Picks: under each script, each paragraph has a line like
- {para: 3, pass: t03.4, proposed: t03.4, why: Alex}
To choose a different take for that paragraph, change pass: to another pass’s id (they’re
listed under the script’s passes:, and in the .md). Leave proposed: as it is: that’s
how roughcut knows you changed it.
Then run map again. It reads your decisions and picks from the newest take map and writes
the next file (_2, _3, …), so you can compare. Repeat until the flags left are ones you’re
happy with.
Step 4: roughcut script cut
roughcut script cut "/Volumes/media/MyProject" --dry-run
builds and checks everything and writes nothing; without --dry-run it writes
xml/…_roughcuts_v1.fcpxml (and a .selects.json beside it). It always uses the newest take
map; --take-map FILE picks another. It won’t run until a take map exists, so a proposal is
never cut unchecked.
Import it with File › Import › XML…. You get a new event with one project per script,
named by section and script (1.1.3 S02 Getting Started). In each project:
- each paragraph comes from its picked take, trimmed to its words with a few frames of handle,
- every other take of that paragraph sits above it, disabled: enable one to audition it, or swap it in,
- a marker at each paragraph says which take it is and why it was picked, followed by a marker per client comment, with the commenter’s name,
- a paragraph cut on set sits at the end of the project, disabled.
6. Troubleshooting
| Problem | Fix |
|---|---|
roughcut: command not found | open a new Terminal window; if it persists, run uv tool update-shell and open another |
no Whisper Python at … | install mlx-whisper (install step 5) |
| Whisper fails with a message about the model or the network | download the model (install step 6); roughcut never downloads it itself |
--refine flags every select refine-fallback | the camera files aren’t where the stringout says: mount the drive under the same name as when you exported |
would overwrite … | run again with --suffix _v2, or move the old outputs away |
a project lands in failed/ | the errors are in failed/<project>.errors.txt; send them, with the inputs, to Rafa |
no roughcut-project.yaml at … | give the project folder that holds the config (or its tools/ folder), or the config file itself |
project root … isn't a folder (is the drive mounted?) | mount the drive |
… isn't in the event; its passes are left out | the take wasn’t in the exported event: add it to the event, export again, and cut again |
Every file roughcut writes is new; if something looks wrong, delete (or move to the Trash) the outputs of that run and try again. The stringouts, paper cuts, scripts, media and FCP library are only ever read and never modified.