Project and command format
Integrator-owned. The JSON transport and C ABI remain v1; project documents may
use schema 1 to 6. JSON keys are camelCase. Unknown command variants reject.
All times serialize as reduced {value: integer, timescale: positive integer};
safe unreduced input is normalized immediately. Numeric
integers must be in JavaScript’s safe range. Intervals are half-open. No floats
are stored as media time. Positions use finite doubles; transform scale is in (0,16].
Records
Section titled “Records”type Time = { value: number; timescale: number };type Range = { start: Time; end: Time };type SphericalView = { yaw: number; pitch: number; roll: number; verticalFov: number };type Transform = { x: number; y: number; scale: number; fit: "fit" | "fill"; sphere?: SphericalView; reframe?: "subject" };type Locator = { kind: "external" | "relative"; path: string };type Asset = { id: string; name: string; kind: "video" | "image"; locator: Locator; duration: Time; width: number; height: number; hasAudio: boolean; contentHash?: string; projection?: "equirectangular" | "insta360_dual_fisheye";};type MainSegment = { id: string; occurrenceId: string; assetId: string; source: Range; originalSource: Range; omissions: Omission[]; pauses: PauseEdit[]; showVideo: boolean; playAudio: boolean; volume: number; transform: Transform;};type Omission = { id: string; source: Range; transcriptId: string; atomIds: string[] };type PauseEdit = { transcriptId: string; atomId: string; retained: Range };type OverlayContent = | { kind: "media"; assetId: string; sourceStart: Time } | { kind: "text"; text: string; fontSize: number; maxWidth: number; elapsed: Time; typewriterCps?: number };type Overlay = { id: string; parentId: string; anchor: Range; content: OverlayContent; showVideo: boolean; playAudio: boolean; volume: number; transform: Transform;};type Atom = { id: string; text: string; range: Range; kind: "word" | "pause" };type Transcript = { id: string; assetId: string; generation: number; contentHash?: string; engine: string; model: string; boundaryPolicy: string; language: string; atoms: Atom[];};type Correction = { id: string; transcriptId: string; atomIds: string[]; text: string };type CaptionSettings = { enabled: boolean; fontSize: number; x: number; y: number; maxWidth: number };type CaptionOverride = { key: string; x?: number; y?: number; hidden?: boolean };type CaptionBreak = { transcriptId: string; atomId: string };// One of four presets; fps and audioSampleRate never change.type Output = ( | { width: 1080; height: 1920 } // portrait (default) | { width: 1080; height: 1350 } // vertical45 | { width: 1080; height: 1080 } // square | { width: 1920; height: 1080 } // landscape) & { fps: 30; audioSampleRate: 48000 };type OutputPreset = "portrait" | "vertical45" | "square" | "landscape";type Project = { schemaVersion: 1 | 2 | 3 | 4 | 5 | 6; id: string; title: string; revision: number; assets: Asset[]; assetTitles?: Record<string, string>; main: MainSegment[]; overlays: Overlay[]; transcripts: Transcript[]; corrections: Correction[]; captionSettings: CaptionSettings; captionOverrides: CaptionOverride[]; captionBreaks?: CaptionBreak[]; assetColors?: Record<string, number>; output: Output;};type RenderSpan = { id: string; segmentId: string; occurrenceId: string; assetId: string; source: Range; timeline: Range; showVideo: boolean; playAudio: boolean; volume: number; transform: Transform;};type RenderOverlay = { id: string; overlayId: string; parentId: string; assetId: string; source: Range; timeline: Range; showVideo: boolean; playAudio: boolean; volume: number; transform: Transform;};type ProjectedWord = { id: string; occurrenceId: string; segmentId: string; transcriptId: string; atomIds: string[]; text: string; kind: "word" | "pause"; source: Range; timeline: Range; partial: boolean;};type RenderClip = { id: string; occurrenceId: string; assetId: string; originalSource: Range; source: Range; timeline: Range; colorIndex: number };type TranscriptElement = Omit<ProjectedWord, "kind"> & { kind: "word" | "pause" | "deleted"; cutId?: string; pauseBounds?: Range; sentenceId: string;};type RenderSentence = { id: string; label: string; occurrenceId: string; segmentId: string; transcriptId: string; index: number; timeline: Range;};type RenderText = { id: string; overlayId: string; parentId: string; text: string; graphemes: string[]; source: Range; timeline: Range; elapsedAtStart: Time; typewriterCps?: number; fontSize: number; maxWidth: number; transform: Transform; showVideo: boolean;};type CaptionReveal = { at: Time; graphemeCount: number };type CaptionCue = { id: string; text: string; timeline: Range; x: number; y: number; fontSize: number; maxWidth: number; reveals: CaptionReveal[] };type RenderPlan = { revision: number; duration: Time; output: Output; assets: Asset[]; clips: RenderClip[]; spans: RenderSpan[]; overlays: RenderOverlay[]; texts: RenderText[]; captions: CaptionCue[]; words: ProjectedWord[]; elements: TranscriptElement[]; sentences: RenderSentence[];};type Snapshot = { project: Project; renderPlan: RenderPlan; canUndo: boolean; canRedo: boolean; undoAction?: string; redoAction?: string };type Request = { projectId: string; expectedRevision: number; operationId: string; commands: Command[]; undoGroup?: string; actionName?: string };type Reply = { ok: true; value: Snapshot } | { ok: false; error: { code: string; message: string } };Defaults: transform (0.5,0.5,1,fill); captions enabled, size 64, anchor
(0.5,0.82), maxWidth 0.84. Caption x/y denote the cue rectangle’s center in
top-left-origin normalized portrait coordinates. These coordinates also apply
to picture transforms. An image’s asset duration is zero, and its placement
has a real duration. RenderOverlay source range for an image is zero to the
placement duration; it is only a duration carrier, not a sampled movie.
Immutable transcripts are embedded in project.json for v1 so one atomic
replacement captures a complete portable snapshot; raw engine output may be
cached separately. This deliberately simplifies the initially planned sidecar
layout. Runtime job state and caches are not serialized into Project.
Projection or spherical-view fields require project schema 2 (video titles
require schema 3, below). A command introducing these fields promotes the document
atomically; the store preserves its exact prior bytes under
.yapwing/backups/schema-1-<sha256>.json before writing the upgrade. Older apps
cannot open schema 2; restore the backup into a separate project to use one.
Ordinary flat projects remain schema 1. Spherical angles are degrees: yaw/roll
in [-180,180], pitch in [-90,90], verticalFov in [1,179], all finite. Prepared
equirectangular assets use a default (0,0,0,75) view when absent. Raw dual-fisheye
assets may be registered but cannot be placed as main clips or overlays.
assetTitles maps an existing asset ID to a Yapwing-only display title. It
never renames the source file or changes Asset.name, which Locate and relink
still match; an asset without an entry shows its file name without extension.
A stored title is trimmed, nonempty, at most 256 UTF-8 bytes and free of
control characters; at most 10,000 entries. A nonempty assetTitles requires
project schema 3, and the field is omitted when empty. The reader accepts
schemas 1 to 5. After any command the document is promoted atomically to
the lowest schema that can hold its content, and never downgraded: undoing or
clearing the last title leaves it at schema 3. The store preserves the exact
prior bytes under .yapwing/backups/schema-<old>-<sha256>.json before writing
any upgrade, e.g. schema-2-<sha256>.json for 2 to 3. Older apps cannot open
schema 3. Titles never affect the render plan.
renderPlan.sentences lists the spoken sentences of every placed recording
in timeline order, one entry per clip a sentence appears in. A sentence is a
run of the current transcript’s atoms that ends after a word ending in .,
?, ! or … (before closing quotes or parentheses); the pauses after it
belong to it. index counts sentences within the transcript from zero, and
id is derived from the occurrence, transcript and index, so it survives
cuts, restores, splits, reorders, trims, caption edits and removals of other
clips; a new transcript generation or a new placement gives new ids.
label is the short handle agents use (s1, s2, … numbered by recording
order: asset order, then each recording’s earliest source; parts of a
sentence divided by a split are lettered s4a, s4b in timeline order).
Labels are unique in one plan but can renumber when a recording leaves the
reel. Every transcript element names its sentence in sentenceId, taken from
the element’s first atom. Sentences are derived; they are never stored, so
documents of every schema open unchanged.
A text anchor overlapping a media overlay anchor in the same parent requires
project schema 4; schema 1 to 3 documents always kept one child lane, so they open
unchanged and need no rewrite. The promoting command backs up the prior bytes as
above. Older apps cannot open schema 4. An update_overlay may change parentId
(moving text into another clip); the anchor is then in the new parent’s source clock.
captionBreaks lists forced caption cue boundaries, each before one word atom
of an existing transcript; sorted, no duplicates, at most 100,000. Captions
then start a new cue at that word in addition to the usual grouping (6 words,
42 characters, 3 s, cuts and long gaps). A break before the first atom of a
corrected group applies to the shown word; one on any later atom of a merged
group has no effect. A break on a superseded transcript stays valid and inert.
Breaks never change the video or transcript elements, and unaffected cue ids
stay stable.
assetColors maps a placed or previously placed asset ID to a clip palette
index 0 to 7. RenderClip.colorIndex is every clip’s effective color: a stored
entry wins; any other placed source gets, in reel order of first appearance,
the lowest index no other placed source uses, or with all eight in use the
least-used index (ties lowest). Compilation never mutates the project. When an
append leaves (or finds) the reel mixing sources, or the map is already
nonempty, the core stores the effective color of every placed source lacking
an entry, both before and after the insertion, so later splits, reorders,
removals and re-adds never shift a color. Entries are never recolored and are
restored by undo. A single-source reel stores nothing and keeps its schema.
A nonempty captionBreaks or assetColors requires project schema 5; both are
omitted when empty, and promotion and backups follow the rules above. Older
apps cannot open schema 5.
output is one of four presets: portrait 1080x1920 (the default), vertical45
1080x1350, square 1080x1080 and landscape 1920x1080, all at 30 fps and 48000 Hz.
Any other size rejects with invalid_output. transform.reframe: "subject"
asks the host to follow the clip’s main subject on top of x/y/scale; the path is
derived from the source and never stored. Reframe cannot combine with a
spherical view or an equirectangular source, and text cannot reframe. A
non-portrait output or any reframe requires project schema 6; a portrait
project without reframe keeps its schema and bytes. Older apps cannot open
schema 6. Stored text and caption font sizes stay valid when the output
changes, up to 1920 (the longest edge of any preset); the render plan clamps
fontSize to the current output’s longest edge, and a new or changed size
must fit it.
library.json is portable source membership and prepared-media association.
.yapwing/cache/library/ contains derived metadata only,
keyed by source hash and geometry. These caches do not change authored history. Native track timestamps in
metadata encode Int64 values/epochs as decimal strings, separately from the
safe-integer edit-time contract above.
Commands
Section titled “Commands”Every variant has a type tag. A batch creates one authored undo entry and
one monotone revision; an authored batch that leaves the project unchanged
is accepted with no new revision or undo entry. No partial batch commits. Receipt payload hashes and
responses are persisted by the store for idempotent retry after reopening.
| type | Fields |
|---|---|
| register_asset | asset: Asset (metadata-only; no placement or authored undo entry) |
| append | asset: Asset, segmentId: string, occurrenceId: string, index?: number, stillDuration?: Time |
| trim | segmentId: string, source: Range |
| split | segmentId: string, at: Time, newSegmentId: string (source time) |
| remove | id: string (main or overlay) |
| reorder | segmentId: string, toIndex: number (index after removing segment) |
| cut_units | unitIds: string[] (exact visible ProjectedWord.id values) |
| cut_words | occurrenceId: string, atomIds: string[] |
| restore_cut | segmentId: string, cutId: string |
| set_pause_edge | segmentId: string, transcriptId: string, atomId: string, edge: "left" or "right", at: Time (source time) |
| correct_words | occurrenceId: string, atomIds: string[], text: string |
| reset_words | transcriptId: string, atomIds: string[] |
| set_flags | id: string, showVideo: boolean, playAudio: boolean |
| set_volume | id: string, volume: number |
| add_overlay | asset?: Asset, overlay: Overlay |
| add_text | id: string, timeline: TimeRange, content: OverlayContent, transform: Transform (authored, undo name “Add Text”; timeline is a positive range inside the reel and may cross clips; content must be text; becomes one text overlay per clip it covers, the first with id, later ones with derived ids, each later piece’s elapsed is the previous piece’s elapsed plus its anchor’s source duration, as a split makes; no schema promotion) |
| update_overlay | overlay: Overlay |
| set_text | id: string, text: string |
| set_typewriter | id: string, charactersPerSecond: number or null |
| set_text_style | id: string, fontSize?: number, maxWidth?: number (authored, undo name “Text Style”; a text overlay; at least one field; fontSize in output pixels, (0, current output’s longest edge], maxWidth a fraction of output width (0,1]; omitted fields are kept; no schema promotion) |
| set_transform | id: string, transform: Transform |
| set_captions | settings: CaptionSettings (a changed fontSize must fit the current output’s longest edge) |
| set_output | preset: OutputPreset (authored, undo name “Format”; only the four preset names parse; the current preset is a no-op without a revision; stored text and caption sizes are kept; a non-portrait preset promotes to schema 6) |
| set_caption_override | override: CaptionOverride |
| attach_transcript | transcript: Transcript (metadata-only request) |
| identify_asset | assetId: string, contentHash: string (metadata-only request) |
| relink_asset | assetId: string, locator: Locator, contentHash: string (metadata-only; matching bound identity) |
| adopt_external | project: Project (sole command; same identity/revision/assets/transcripts/output) |
| rename | title: string |
| set_asset_title | assetId: string, title: string or null (authored, undo name “Rename Video”; trimmed; null, empty or whitespace clears; promotes to schema 3) |
| set_caption_break | transcriptId: string, atomId: string, enabled: boolean (authored, undo name “Add Caption Break” or “Remove Caption Break”; atom must be a word; unchanged is a no-op without a revision; promotes to schema 5) |
| undo / redo | no extra fields; the only command in its request |
projectId must equal the live project before any retry or mutation. register_asset,
identify_asset, relink_asset and attach_transcript are metadata-only; they may share a batch, but cannot mix
with authored commands. Metadata advances revision and preserves authored undo/redo.
A request normally holds 1–128 commands. To collect a larger library atomically,
129–10,000 commands are accepted only when every command is relink_asset and
each target asset ID occurs once. All targets must exist with the same bound
content identity; one invalid relink rejects the entire transaction. The 32 MiB
request limit still applies. Other metadata, authored or mixed request shapes
remain limited to 128 commands.
A measured lowercase SHA-256 may bind an unidentified asset exactly once; changing
an existing identity rejects. Identify before ASR. Transcript replacement rejects
when corrections, omissions or pause edits in current state or undo/redo depend on
its old generation. Transcript text/timing never changes in place.
Every main record is a durable logical clip. originalSource is its recoverable
source domain and source its current trim. omissions retain exact original word
coverage and identity; pauses retain source-bounded pause handles. Compilation
subtracts omissions and removed pause intervals without replacing the logical clip.
Even a fully cut clip remains in main and clips with a zero-duration timeline.
Still main clips use a virtual clock; append defaults to 5 seconds or accepts a
positive stillDuration. An image Asset still has zero source duration/no audio.
Image trims may expand their synthetic originalSource domain beyond the default
five seconds; image source time does not represent finite recorded samples.
Volume is a finite linear gain in [0,1], independent of playAudio.
adopt_external validates an authored-state candidate while preserving the exact
asset/transcript evidence; it creates one undo entry. New assets or source identity
changes must use typed import/metadata paths, not external adoption.
cut_units targets distinct positive visible ProjectedWord.id values in the
inspected revision, across clips if desired. It creates exact omissions for words
and collapses selected pauses to their current retained midpoint. It preserves
unselected words, pauses, and unrepresented gaps. Correction groups are indivisible.
Unknown/duplicate/empty selection rejects atomically. cut_words is the compatibility
contiguous atom selector; ambiguous partial atoms reject. It now preserves gaps by
the same exact-unit rule. restore_cut removes exactly the named omission after any
other edits; it does not undo subsequent edits or restore a removed logical clip.
set_pause_edge changes only one handle within the original pause/clip domain. A
zero pause is retained and editable. An edge may equal the other; crossing rejects.
No source outside the original pause can be added.
elements contains visible words, every intersecting pause (including zero-length
handles), and deleted-word placeholders in clip/source order. It reuses the word
identity fields. Deleted source is the omitted original interval, timeline a
zero-length point, cutId names its recoverable omission, and text contains the
original immutable transcript words. Pause source is the retained handle range,
pauseBounds its original bounded range. words contains only positive surviving
units for rendering/selection. Deleted content is never captioned or played.
Corrections render only where an entire group survives inside one logical clip;
partial groups fall back to original words marked partial. Explicitly splitting
inside a corrected group therefore shows original partial units on each side.
Overlays are owned by exactly one parentId, anchored in that parent’s source
clock. A child is media or text, and its kind picks its lane: media overlays and text
are separate lanes, derived from content (no stored lane field). Anchors must fit
originalSource; overlapping anchors of one lane in one parent reject even when temporarily hidden by cuts/trims. Reorder carries
children, removal removes them, and cuts/trims hide source coverage without losing
it. Restore re-exposes that exact child coverage. Split partitions parent domains,
omissions, pauses, and children; right media sourceStart/text elapsed advance by
the exact split offset, and right pieces of crossing children receive new IDs. No free-floating overlay can
cross ownership boundaries. A media child requires an existing or supplied matching
asset; text needs none. Text has no audio. Text supports up to 2000 grapheme clusters,
finite fontSize (0,1920] (new text up to the current output’s longest edge),
maxWidth (0,1], and optional typewriterCps (0,240].
Caption cues carry full phrase layout plus ordered absolute reveals; at time t,
show the prefix from the latest reveal with at <= t. graphemeCount includes spaces.
A corrected group reveals its first written word at its first original timestamp;
further written words reveal at equal steps across the group’s own timeline range
(no media time is invented). A corrected group with empty text is hidden from
captions only; its media still plays and the phrase continues across it. Lay out the
complete phrase once so revealing words does not move previously visible words.
Text similarly lays out full content: if animated, at a displayed time t show
min(graphemes.length, 1 + floor(typewriterCps * (elapsedAtStart + t - timeline.start)))
graphemes. elapsedAtStart includes source time skipped by cuts, preserving reveal
continuity. Preview and export must follow the identical rule and grapheme sequence.
uint32_t yapwing_abi_version(void) returns 1.
void *yapwing_session_new(const char *project_json) uses null for a new project.
It returns null for invalid input. yapwing_last_error() returns an owned UTF-8
JSON error buffer after a failed creation. Each session has one serialized caller.
char *yapwing_snapshot(void *session) and
char *yapwing_apply(void *session, const char *request_json) return owned,
NUL-terminated Reply JSON. yapwing_buffer_free(char *) releases every buffer.
yapwing_session_free(void *) releases the session. Catch panics at the boundary.
No video frames or frame-rate calls cross this boundary.
Swift’s Codable records are transport types, not a second validation authority.
Project imports must enter yapwing_session_new before the app consumes a
decoded snapshot or passes a render plan to media. The shared negative fixtures
fixtures/contracts/invalid-unsafe-revision.json and
fixtures/contracts/invalid-reversed-range.json must reject through both the
Rust project/session entry points and the Swift core bridge.
Persistence and agents
Section titled “Persistence and agents”Direct JSON edits are supported while closed. When open, inspect/check/apply routes through the app’s owner-only local endpoint with revision checks. Unexpected file changes suspend autosave and preserve an observed candidate beside the in-memory state; arbitrary uncooperative concurrent raw writers are not supported. Never overwrite a conflict automatically.
The agent bridge (yapwing_agent, the CLI’s agent calls) takes an optional
signals member beside method: host-measured source analysis, never
authored and never written to the project or to a compiled request.
type SourceSignals = { assets?: Record<string, { silences?: Range[]; shots?: Range[]; quality?: { sharpness: number; faceQuality?: number; eyesOpen?: number; stability: number }[];}> };Ranges are in that asset’s source time, ordered and nonoverlapping. quality[i]
describes shots[i], so when present it has one entry per shot; each score is
from 0 to 1 with higher better, and the face scores are absent for a shot
without a face. Signals for assets not in the project are ignored and a
malformed member rejects the call. outline lists the longest silent stretches
that play in the edited video (“Silent stretches … in c1”) and, on a “Shots”
line, the shots that play with their scores; without signals the outline is
unchanged. The planner reads them for edits in the same call.