The ChapterScript format

Every chapter is a folder of plain files: one JSON document plus your media. It's open, readable, and diffable, so nothing about your work is locked inside Chapter Vision.

The bundle

A project is a .chapterscript directory bundle:

MyStory.chapterscript/
├── chapter.json      the entire document
└── assets/           your media, unmodified
    ├── intro.mp4
    ├── ambience.m4a
    └── lantern.usdz

chapter.json describes everything the player needs; assets/ holds the referenced files byte-for-byte as imported. Copy the folder and you've copied the project. Chapter Studio, Chapter Vision, and the player all read and write this same format, on disk and over the wire.

chapter.json, top to bottom

The document (format version 2) opens with identity (an experience id, a display name, and the id of the default segment to play first), then five main collections:

Segments, steps, actions

Each segment carries a presentation (immersive, mixed, or windowed), an optional immersive backdrop — either a video skybox with layout (mono, side-by-side, over-under, or multiview HEVC), field of view (180° or 360°), and loop flag, or a USDZ scene — and its steps.

A step is a timed beat: a duration, an optional gate (holds the step until the audience interacts or a timeout passes), a list of actions that fire at the step's start, and scheduled actions that fire at second offsets within the step. Actions are the verbs: reveal, hide, fade, move, scale, play video, play audio, effects, and so on, each a small typed JSON object.

Segments also own animation tracks: one track per animated entity, with up to ten scalar channels (position x/y/z, rotation x/y/z, scale x/y/z, opacity). Keys sit at absolute seconds from the segment's start and carry Bézier tangent handles (or linear/stepped modes) for easing; rotations are continuous Euler degrees with an explicit rotate order, so a two-turn spin is stored as 720° — no shortest-path surprises.

A condensed example

{
  "formatVersion": 2,
  "id": "experience-9b21c4d0",
  "displayName": "Lantern Walk",
  "defaultSegmentId": "segment_intro",
  "segments": [
    {
      "id": "segment_intro",
      "name": "Intro",
      "presentation": "immersive",
      "immersiveBackdrop": {
        "kind": "video",
        "file": "forest_360.mp4",
        "layout": "mono", "field": "field360", "loop": true
      },
      "steps": [
        {
          "id": "step_1", "duration": 8.0,
          "actions": [
            { "revealEntity": { "entity": "lantern.usdz", "fadeIn": 0.5 } }
          ],
          "scheduledActions": [
            { "at": 2.0, "action": { "playVideo": {
                "file": "intro.mp4", "channel": "video-intro",
                "sourceIn": 12.5, "sourceOut": 47.25 } } }
          ]
        },
        {
          "id": "step_2", "duration": 10.0,
          "gate": { "type": "tap", "prompt": "Tap the lantern" },
          "actions": []
        }
      ],
      "animationTracks": [
        {
          "entity": "lantern.usdz",
          "rotateOrder": "xyz",
          "channels": {
            "ty": [
              { "t": 0.0, "v": 1.0, "interp": "bezier",
                "inTangent": { "dt": -0.4, "dv": 0.0 },
                "outTangent": { "dt": 0.4, "dv": 0.0 } },
              { "t": 4.0, "v": 1.6, "interp": "bezier" }
            ]
          }
        }
      ]
    }
  ],
  "entities": [
    { "id": "lantern.usdz", "kind": "model",
      "transform": { "position": [0, 1, -1.5] }, "initiallyEnabled": false }
  ],
  "environment": { "lightingPreset": "dusk", "fogDensity": 0.02 },
  "particlePresets": [],
  "manifest": { "entries": [
    { "id": "intro.mp4", "relativePath": "intro.mp4",
      "byteSize": 48211930, "sha256": "9f31ab…" }
  ] }
}

Note. This example is condensed for reading; a real document spells out every field, and the apps write more metadata (probe dimensions, tint colors, and so on) than shown here.

Trims are metadata, not surgery

Notice sourceIn/sourceOut on the video action above: that's a non-destructive trim. The player cues the master at sourceIn, stops (or loops) at sourceOut, and the file in assets/ is never re-encoded or modified. Several clips can window the same master differently. Omitting the fields means "play the whole file." Only the explicit Consolidate — Overwrite Master File… command actually rewrites a master, and it rebases every clip's window so nothing shifts.

Built for version control

Saves are deterministic: pretty-printed JSON with sorted keys, every time, on every device. Save without changing anything and the bytes are identical; change one fade duration and the diff is one line. That makes .chapterscript bundles genuinely pleasant in git: diffs are reviewable, history is meaningful, and non-colliding edits merge cleanly.

Forward compatibility

Decoding is tolerant by design. Unknown fields are ignored, missing newer fields take defaults, and documents from older versions are migrated on open (legacy step-embedded animation, for example, is hoisted into segment animation tracks). A newer app opens your old chapters; an older player degrades gracefully on newer ones. Migration happens in memory, and your file on disk changes only when you save.