One file holds the whole storybook.
SSYNC is to storybooks what PDF is to documents: pictures, words, voices, music and timing in one open file. Deliberately not video. The player is plain web — nobody needs our servers, or our app, to hear a story.
Nothing is uploaded — the ZIP is built on this device.
- ZIP · manifest.json + assets/
- UTF-8 JSON · schema v2.0
- AAC-LC in M4A (WAV fallback)
- Plain web · no server required
Taken apart
- Metadata,
metadata: The Brave Little Star · by Island Development Crew · en - Visual,
pages[].illustration: assets/page-3.jpg · The little star among bigger, sleepy stars - Text,
pages[].text.content:"Why do I shine so bright?" she wondered. The bigger stars just yawned. "Stars shine. That's what we do. Don't overthink it."
- Voice,
pages[].text.audioUrl: none on this page — the browser voice reads it - Sound,
pages[].music: Wonder · A major pentatonic · 76 BPM · ducks 12 dB - Behavior,
settings · pages[].timing: autoPause 4s × 1.0 · chime on · words light up - Signature (V2.1 · DRAFT),
signature: nothing signed on this tape yet
A reader that understands only layers 1–3 still shows the story; each layer above adds a sense.
The codec rule
- Narration is AAC-LC in M4A, mono, 48 kHz, about 96 kbps.
- Fall back to 16-bit WAV, 16 kHz mono. The only fallback.
- Opus, WebM or Ogg. Drafts only, never a published tape.
Validator
Write your own player
import { unzipSync, strFromU8 } from 'fflate'; const files = unzipSync(tape); const m = JSON.parse(strFromU8(files['manifest.json'])); for (const page of m.pages) { draw(files[page.illustration?.url], page.text?.content); }
Seven layers in one packet
A story is not a blob of media. It is seven layers stacked in one manifest, each one optional except the first — so a reader can implement as far up the stack as it likes and still play every SSYNC file it meets.
Metadata
Who made it, what it is called, what language it speaks. The only required field in the whole format is metadata.title — everything else is optional, so a one-page story is a legal story.
Visual
Still images, not video: a photograph of a crayon drawing, ≤ 2048px on the long edge, with alt text so the page is describable. An optional entrance animation per page — never required to understand the story.
Text
The words on the page, kept as text rather than baked into the picture, so they can be re-typeset, resized, read aloud, or highlighted word by word as they are spoken.
Voice
The recording of a real person reading this page — the reason the format exists. Published narration is always AAC/M4A (WAV as the only fallback), so the tape a parent records on Android plays on a grandparent's iPad.
Sound
A mood name for the music bed under the voice — Wonder, Calm, Adventure, Hush. The player generates it and ducks it about −12 dB while narration plays. No mood on a page means silence, on purpose.
Behavior
How the story moves: how long to wait after narration ends, whether to chime on the page turn, whether words light up as they are read, and the accessibility timing multiplier a slower reader needs.
Signature
Who owns this tape, what it was remixed from, the share URL it lives at, and the consent record for every recorded voice inside it. Consent travels with the story, not in some server's database. Draft in v2.1 — additive, and v2.0 readers ignore it.
A ZIP you can open with anything
my-story.storysync
├── manifest.json REQUIRED · SSYNC v2 manifest (UTF-8 JSON)
└── assets/ all bundled media, flat
├── cover.jpg
├── page-1.jpg
├── narration-1.m4a AAC-LC · 48 kHz mono · ~96 kbps
├── page-2.jpg
└── narration-2.wav the only permitted fallbackmanifest.jsonMUST sit at the archive root and validate against the v2 schema.- Every
assets/…path the manifest references MUST exist. A reader MUST reject dangling references. - Assets SHOULD be stored uncompressed (deflate level 0) — media is already compressed, so recompression costs time for nothing.
- Unknown files and unknown manifest fields MUST be ignored, never fatal. Forward compatibility is the whole point of a format.
- Reference budget: an 8-page story with ~5 minutes of narration fits in about 15 MB.
{
"id": 3,
"illustration": { "url": "assets/page-3.jpg", "alt": "A drawing of a red boat" },
"text": {
"content": "The little boat sailed on.",
"audioUrl": "assets/narration-3.m4a",
"audioCodec": "aac"
},
"music": "Calm",
"timing": { "autoPause": "3s" }
}The published-audio codec rule
MediaRecorder disagrees with itself across browsers: Safari records AAC/MP4, Chrome and Android record Opus/WebM — and iOS Safari cannot reliably play Opus/WebM. Left alone, a tape recorded on an Android phone is silent on an iPhone: exactly the cross-device moment this format exists to deliver.
- Narration MUST be AAC-LC in an MP4/M4A container (
audioCodec: "aac"). Reference target: mono, 48 kHz, ~96 kbps. - Where AAC cannot be encoded, narration MAY fall back to 16-bit PCM WAV, 16 kHz mono (
audioCodec: "wav"). That is the only permitted fallback. - Narration MUST NOT be Opus, WebM or Ogg. Those are valid only in editor drafts that never leave the authoring device.
| Recorded as | At publish |
|---|---|
| AAC/MP4 — Safari, iOS | Passes through. Already compliant. |
| Opus/WebM — Chrome, Android, with a WebCodecs AAC encoder | Decoded and re-encoded to AAC-LC, 48 kHz mono, ~96 kbps, in M4A. |
| Opus/WebM — no AAC encoder available (Firefox today) | Decoded to 16-bit PCM WAV, 16 kHz mono. Bigger, but it plays everywhere. |
A child’s voice is part of the format’s job
The flagship user is four years old and recording over their own drawings. So the rules are in the spec, not only in a policy page: publishing is gated behind a parental gate, deleting a story deletes all of it — manifest, images, narration, share code — and the player ships zero third-party trackers. The Signature layer exists so the consent record travels inside the tape rather than in somebody’s database.