Cadre Project Format — Offline Editing Contract (v1)
Understand Cadre's .screencraft project directory, project.json schema, runtime-only fields and the safe boundary between live and offline editing.
The MCP tools (tools.md) are the recommended way to edit,
because they route through the running editor's undo history and autosave. This
document describes the offline alternative: editing a project's files
directly on disk when Cadre is not editing that project.
The one rule that keeps you safe: Only edit a project's files while it is closed in the Cadre editor. While a project is open, the renderer's in-memory state — not the file on disk — is the source of truth, and Cadre autosaves it ~3 s after any edit. An external write to
project.jsonduring that window is silently overwritten. There is no file-watch, no reload, and no conflict detection. See §4.
1. The .screencraft directory
Composition compatibility
The optional style.frame.layout object stores the whole-video arrangement:
{ mode, speakerSide, speakerSize, gap, supportHeight }. Modes are default
(camera overlay), fullscreen (camera only), hide (screen only) and split
(side by side, stacked in portrait). Side is left or right; speaker size is
0.18–0.45; gap is 0–80 reference pixels; supporting height is 0–0.35 of the
available height, with zero disabled. Preview and export resolve the same geometry.
edits.layoutSegments[].layout optionally overrides those settings for that
recording-time span. The segment's existing mode, position, customSize and
enabled fields retain their meaning. Segment mode wins over nested layout mode.
The whole-video arrangement resumes in gaps. Disabled segments reserve their
span but do not change the rendered layout. Cuts and speeds do not rewrite these
recording-time ranges.
style.webcam.reframe optionally stores { x, y, zoom }, with normalized crop
positions 0–1 and zoom 1–3. Its default is { x: 0.5, y: 0.5, zoom: 1 }.
edits.overlaySegments[].region: "support" attaches a text overlay to the current
supporting section while retaining the text block's independent timing. Omit
region for ordinary canvas placement. Unsupported region values are removed
during manifest repair. Non-text overlays use ordinary canvas placement.
Old manifests need no migration: an absent frame layout preserves legacy webcam flags and rendering, and absent camera reframing preserves the original crop. New fields round-trip in the live project manifest and through undo/autosave. Full field validation is in composition settings.
A project is a directory named <Name>.screencraft. Cadre scaffolds these
subdirectories (PROJECT_SUBDIRS in src/shared/constants.ts):
MyDemo.screencraft/
├── project.json # THE manifest — the whole edit state (see §2)
├── recording/
│ ├── video.mp4 # first finalized source-video segment
│ ├── video_seg1.mp4 … # later rolling / pause-resume segments
│ ├── audio_system.wav # first system-audio segment, 48kHz float32
│ ├── audio_system_seg1.wav … # later system-audio windows
│ ├── audio_mic.wav # first microphone segment
│ ├── audio_mic_seg1.wav … # later microphone windows
│ ├── audio_mic_enhanced.wav # voice-enhanced mic pass (if run)
│ ├── webcam.webm # webcam capture (if any)
│ ├── interactions.jsonl # cursor/click/scroll/key events (JSONL)
│ ├── metadata.json # RecordingMetadata (resolution, duration, paths)
│ ├── segments.json # atomic finalized-video crash-recovery journal
│ └── music/ # imported music files
├── edits/ # scaffolded but UNUSED for edit data (see §3)
├── analysis/
│ └── transcript.json # derived, non-visual agent speech context (if generated)
├── style/
│ └── assets/
│ ├── <background-image> # imported background images
│ └── animations/<uuid>/ # managed 8-bit RGBA PNG animation frames
├── cache/
│ ├── waveform.json # waveform peaks cache
│ └── thumbnails/ # timeline thumbnail strips
└── exports/ # export output destination (user-chosen names)What is actually read/written: project.json, everything under
recording/, analysis/transcript.json, cache/waveform.json,
style/assets/*, and exports/. The
edits/ and style/ subdirectories exist on disk but hold no edit data —
see §3.
2. project.json — the manifest
project.json is the entire project state, JSON.parsed whole on load and
pretty-printed (JSON.stringify(project, null, 2)) on save. Its declared type
is Project (src/shared/types/project.types.ts):
interface Project {
id: string; // UUID v4
name: string;
version: string; // schema version, e.g. "1.0.0"
createdAt: string; // ISO 8601
modifiedAt: string; // ISO 8601
recording: RecordingMetadata; // source paths, resolution, duration
style: StyleConfig; // background / frame / cursor / keyboard / motion / webcam
edits: EditState; // cuts / speedSegments / captions / zoomKeyframes / ...
exportHistory: ExportRecord[];
dismissedTypingSuggestionIds?: string[];
}All timeline edits live nested inside project.json, under edits and
style. The edits object's declared type is:
interface EditState {
splitPoints?: number[];
systemAudioGain?: number;
micAudioGain?: number;
micNoiseReduction?: boolean;
cuts: Cut[];
speedSegments: SpeedSegment[];
captions: CaptionTrack | null;
zoomKeyframes?: ZoomKeyframe[];
maskSegments?: MaskSegment[];
overlaySegments?: OverlaySegment[];
layoutSegments?: LayoutSegment[];
musicTrack?: MusicTrack | null;
}2.1 Section markers and audio settings
EditState declares these persisted fields. Save and export assemble their
current values through buildLiveProject() in the renderer project store.
| Field | Type | Meaning | Default |
|---|---|---|---|
edits.splitPoints |
number[] |
Section boundaries in recording-time ms. Joining a boundary keeps the footage. | [] |
edits.systemAudioGain |
number |
System-audio gain multiplier (0–4). |
1 |
edits.micAudioGain |
number |
Microphone gain multiplier (0–4). |
1 |
edits.micNoiseReduction |
boolean |
Local microphone noise reduction, including keyboard clicks, in preview and export. Original audio is preserved. | true |
Older projects that omit these fields use the defaults above. Explicit saved
gains are preserved. The Audio panel changes these settings through undoable
store actions. Derived clean microphone WAV files live under cache/audio/
and can be regenerated; the recording remains the source of truth. A requested
noise reduction failure is visible, with a choice to retry or use original audio.
2.2 Entity shapes
The nested entity shapes (identical to what the MCP tools return — see
tools.md shared shapes):
Cut = { id, startTime, endTime, type: 'remove' }
SpeedSegment = { id, startTime, endTime, speed, rampIn, rampOut }
ZoomKeyframe = { id, startTime, endTime, sourceRect: {x,y,width,height},
zoomLevel, trigger, isUserModified, isUserCreated }
CaptionSegment = { id, startTime, endTime, text, confidence, isUserEdited }
MusicTrack = { trackId, filePath, name, volume, fadeInMs, fadeOutMs, loop, durationMs }
CaptionTrack = { language, modelUsed, generatedAt, segments: CaptionSegment[], style }All times are milliseconds from the start of the source recording. Ids are UUID v4 strings you generate yourself for new entities.
Native and rendered motion graphics persist in the same
edits.overlaySegments array as text and SVG overlays. The timeline snapshot,
buildLiveProject(), autosave, and export copy that array as a whole, so the
new variants need no manifest migration:
OverlaySegment = {
id, type: 'text' | 'svg' | 'graphic' | 'animation', startTime, endTime,
position: { x, y }, width, rotationDeg, opacity, animation, keyframes?,
text?, textStyle?, // text only
svg?, // SVG only
graphic?, // graphic only; MotionGraphicSpec below
animationSource? // animation only; AnimationOverlaySource below
}
MotionGraphicSpec =
| { kind: 'counter'; title; subtitle; theme; accentColor; delayMs; durationMs;
easing; from; to; decimals; prefix; suffix }
| { kind: 'bar-chart'; title; subtitle; theme; accentColor; delayMs; durationMs;
easing; bars: Array<{ label; value }>; maxValue?; suffix }
| { kind: 'progress-ring'; title; subtitle; theme; accentColor; delayMs;
durationMs; easing; value; decimals }
AnimationOverlaySource = {
assetId; frameCount; fps; width; height; loop
}Stored graphic objects are canonical: the common presentation and timing
fields are present even when the Agent call omitted them. delayMs and
durationMs are offsets in recording-time milliseconds from the overlay's
startTime; the final visual state holds after delayMs + durationMs until
the segment ends. Cuts and speeds map the recording clock onto output time.
Shortening endTime can clip an animation, but never rescales its internal
timing. See add_graphic_overlay for exact
bounds and defaults.
A rendered animation owns canonical 8-bit RGBA PNGs at
style/assets/animations/<assetId>/frame-000000.png through its declared
frameCount. fps selects frames using local recording time; loop: false
holds the last frame and true wraps. These are managed project assets: copy
the directory with the project and never replace assetId with an absolute
source path. add_animation_overlay is the
safe importer for PNG directories and alpha video; it validates, normalises,
and atomically publishes the sequence. Offline writers must supply an existing
contained asset directory matching the descriptor exactly.
2.3 Segmented media and A/V alignment
Modern recordings are finalized as independently decodable chunks. The
recording object in project.json is authoritative and can carry these
optional, index-aligned arrays:
videoSegments?: string[];
videoSegmentDurationsMs?: number[];
systemAudioSegments?: string[];
systemAudioSegmentDurationsMs?: number[];
systemAudioSegmentStartOffsetsMs?: number[];
micAudioSegments?: string[];
micAudioSegmentDurationsMs?: number[];
micAudioSegmentStartOffsetsMs?: number[];Every path is project-relative and normally sits under recording/. The rules
for consuming or preserving these fields are:
- When
videoSegmentsis absent, the video source is the legacy single filerecording/video.mp4. When it is present, its ordered paths are the video source; do not prependvideo.mp4implicitly. A sole recovered later segment is therefore represented explicitly. videoSegmentDurationsMsis aligned one-for-one withvideoSegmentsand describes each finalized container on the continuous recording-time axis. New manifests reconcile the array to the canonical overallrecording.duration.- Each audio
*SegmentDurationsMsarray is aligned one-for-one with its own*Segmentsarray. A value describes the complete recording-time window for that slot, not merely the samples that survived in the WAV. - Each audio
*SegmentStartOffsetsMsvalue is the signed delta from that window's first video frame to audio sample zero. Positive means the window begins with silence before the WAV starts. Negative means the WAV began early, so its head is trimmed. Zero is neutral. - A declared audio file may be missing after a device or persistence failure. Keep its path, duration and offset slot in place: preview, waveforms, captions and export preserve that entire window as silence. Compacting the arrays would shift every later audio segment earlier and break sync.
The three arrays for a track must have matching lengths when durations and offsets are present. Load-time repair removes incomplete or misaligned hint arrays rather than guessing boundaries. An offline writer should preserve unknown compatibility fields and never reorder, compact or independently edit one of these arrays.
recording/metadata.json is a compatibility and recovery sidecar. It may fill
an optional segmented-media field only when project.json.recording omits that
field; it must not override an array already present in the manifest.
recording/segments.json, when present, is an atomically replaced
crash-recovery journal containing only fully finalized video chunks:
{
"version": 1,
"recordingId": "b2a69e3e-…",
"updatedAt": "2026-08-22T12:34:56.789Z",
"completedSegments": [
{ "filePath": "recording/video.mp4", "durationMs": 30000 },
{ "filePath": "recording/video_seg1.mp4", "durationMs": 30000 }
]
}Cadre uses this journal to recover the longest safe finalized prefix after an abrupt exit; an unsettled tail may exist on disk without appearing in it. The journal is recovery evidence, not edit state. Do not hand-edit it to add a partially written MP4 or use it in place of the manifest arrays.
recording.webcam (null when no webcam was captured) carries the webcam
track info: { filePath, durationMs } plus, on recordings made with webcam A/V
sync, startOffsetMs (wall-clock ms between the webcam's first frame and the
screen video's first frame — positive means the webcam started later; preview
and export seek the webcam to timelineMs - startOffsetMs) and the applied
capture settings width / height / codec / bitrateBps. All of these are
optional and absent on older recordings — treat a missing startOffsetMs as 0.
2.4 A minimal offline edit
To add a cut offline: load project.json, append a Cut to edits.cuts, bump
modifiedAt, write the file back with 2-space indentation. Example diff to
edits:
"edits": {
"cuts": [
{ "id": "c-8f1e2a...", "startTime": 4200, "endTime": 6100, "type": "remove" }
],
"speedSegments": [],
"captions": null,
"zoomKeyframes": [],
"splitPoints": [],
"systemAudioGain": 1,
"micAudioGain": 1,
"micNoiseReduction": true
}3. What's on disk but dead
analysis/transcript.json is not edit state. It is derived, phrase-compacted
Whisper output used by get_recording_context; deleting it does not change the
video and only means an agent must run generate_transcript again. Its times
are recording-time milliseconds. It is written atomically and is deliberately
separate from edits.captions, because generating private agent context must
not make captions appear in the exported video.
analysis/visual-context.json is likewise disposable perception data. It holds
only bounded Apple Vision OCR text, normalized rectangles, timestamps, and
status — never raw frames. Deleting it only makes the next
analyze_visual_context call re-extract and re-read its small frame shortlist.
The explicit get_video_frame fallback uses a temporary WebP and removes it
after returning the single MCP image block.
PROJECT_FILES (in constants.ts) declares paths like
edits/zoom_keyframes.json, edits/cuts.json, edits/captions.json,
edits/masks.json, style/style.json, etc. These are declared but never
read or written — verified by a repo-wide grep (zero references outside the
constant). Do not write edit data to them; nothing loads it. All edit and
style data lives inside project.json (§2). The only files under style/ that
carry real data are imported background images in style/assets/.
Imported background images retain their original encoded bytes. The manifest
stores style.background.imagePath relative to the project, for example
style/assets/<id>-wallpaper.png, so copying or moving the project preserves
the image. Legacy absolute paths to the same managed asset normalize on load
and save. Export requires the selected image to load successfully.
4. The live-vs-offline boundary
There is no live-reload mechanism in Cadre (confirmed: no fs.watch,
chokidar, or watchFile anywhere in src/main or src/renderer).
project.json enters renderer memory in exactly two ways:
project:load— on explicit user action (open from the welcome screen) or right after a recording completes.buildLiveProject()— the in-memory Zustand snapshot that is what actually gets passed to save and export.
Consequences for an offline editor:
- Editor open on this project → your write loses. The next autosave (~3 s, debounced) or an explicit save writes the in-memory state back over your file. No merge, no conflict detection, no warning.
- Editor closed → your write wins. The next
project:loadreads your raw JSON. This is the safe, supported offline path: generate a recording, script the edits intoproject.jsonwhile nothing has it open, then let the user open it once. - Load-time validation repairs, it does not reject.
project.jsonisJSON.parsed and then run throughvalidateAndRepairProject(src/shared/utils/project-validation.ts). Wrong-typed fields are reset to documented defaults, out-of-range numbers are clamped, and individually invalid timeline entries (a cut with an inverted range, a speed segment withspeed: 0) are dropped — so a hand-edited manifest that is almost right will open, but not necessarily as written. Every change is logged, and the original file is copied aside asproject.json.corrupt-<timestamp>.bakbefore the editor can autosave over it. - Two things are not repairable. A manifest with no
id, or with no usablerecording.resolutionin eitherproject.jsonorrecording/metadata.json, fails to open with an explicit error rather than loading into a broken editor. Unknown fields are always preserved verbatim.
The SQLite index
Cadre also keeps a projects index at
~/Library/Application Support/Cadre/database.sqlite (id, name, path,
duration, thumbnail, source type, resolution, size). This index — not the
.screencraft directories — drives the welcome-screen recent list. A project
you create purely on disk will not appear in the recent list until Cadre
learns about it (it is added when Cadre records/creates it, or when opened by
path). To open an off-index project via the Agent API, pass its absolute path
to open_project directly.
5. Recommendation
| Situation | Use |
|---|---|
| Project is open in Cadre / you want live feedback | MCP tools (tools.md) — they share the editor's undo + autosave. |
| Batch-scripting edits for a project no one has open | Offline project.json edit (§2), then have the user open it. |
| Anything time-sensitive or co-edited with a human | MCP tools, never offline — there is no safe concurrent-write story. |
When in doubt, prefer the MCP tools: open_project → get_timeline → mutate →
verify. The offline path exists for headless/batch generation, not for editing
alongside a human.