Implementation guide · writing
Create a Cassini file
Take existing audio and timed words through packaging, checking and sharing. Try the complete example, then use your own inputs.
On this page
Use this guide to turn existing audio and timed words into a file someone else can open. First package the example, then substitute your own inputs and check the result before sharing it.
You need Python 3 and curl for the first example. The producer uses only the Python standard library. Recording, speech recognition and any audio conversion happen before this step.
Write your first file
In a fresh directory, download the CC0 producer, the example audio and its matching word data:
curl -fLO https://raw.githubusercontent.com/codemyriad/cassini-format/main/tools/cassini-pack.py
curl -fLO https://format.gocassini.com/demo/cassini-final-moments.opus
curl -fLO https://format.gocassini.com/demo/cassini-final-moments.words.json
python3 cassini-pack.py cassini-final-moments.opus cassini-final-moments.words.json meeting.opus \
--title "My portable recording" --created-at 2026-09-09T09:00:00ZThe result is meeting.opus, containing 120 words and
4 speakers. The command prints audio digest unchanged
after reading back the output and comparing the compressed audio with the input.
Open the result to read and listen to your newly packaged file.
The sample source already has Cassini metadata. This exercise uses its audio and supplies the words again. The packer writes a new manifest from the supplied inputs; it does not merge existing transcripts or other metadata from the source.
Use your own inputs
Replace the audio and JSON paths, title and date in the command. Your source must be Ogg Opus. Your JSON supplies the speaker table and the words that actually describe that recording:
{
"speakers": [{ "id": "sam", "label": "Sam" }],
"items": [
{ "speaker": "sam", "startMs": 900, "endMs": 1300, "text": "Hello." }
]
}Times are integer milliseconds from the start of the audio. Each word's
speaker identifies an entry in speakers. Keep words in speaker-turn order,
including overlaps; sorting the whole list by time changes how conversations
are reconstructed. See the word format for exact rules.
The packer also accepts segments instead of items:
{
"speakers": [{ "id": "sam", "label": "Sam" }],
"segments": [
{ "speaker": "sam", "words": [
{ "startMs": 900, "endMs": 1300, "text": "Hello." }
] }
]
}Use these exact field names and millisecond units when adapting speech-recognition output. This is a supported JSON input shape, not a general importer for every transcription service. Speaker attribution and transcription accuracy remain the responsibility of the pipeline that supplied them.
Check and share the result
The producer's self-check establishes that packaging preserved the audio digest. Before sharing, validate the output and open the result in the recipient's software. An ordinary Ogg Opus player can play it; a Cassini reader can also show the embedded transcript.
What you are making
An Ogg Opus file at 48 kHz, mono for speech, with two things in its OpusTags comment header: plain comments a human can read, and a JSON manifest gzipped, base64url-encoded and cut into numbered pieces.
What a producer MUST do is normative and not restated here: the container and digest rules are in Writing a file, and every tag a file has to carry, with its exact value, is in Cassini descriptor tags.
Beyond that, the reference producer writes readable TITLE, DATE and
DESCRIPTION comments, writes a summary tag only when it has a value, and never
puts a room token, join link or internal service URL in a tag, because these
files get mailed to people; see
Summary and mirror tags.
A complete producer
tools/cassini-pack.py builds a valid file with nothing but the Python
standard library. No ffmpeg, no Go. It walks the Ogg pages, computes the digest,
builds the manifest, and rewrites only the OpusTags packet, copying every audio
page across untouched and patching the page sequence numbers and CRCs.
The digest excludes OpusTags and all Ogg framing, so tagging cannot change
it: compute it once.
tools/cassini-opus-digest.py computes exact-opus-audio-v1 from
the digest spec alone.
Check the audio match shows how to download it
and compare the result with the claims in a file. For the downloadable example,
the computed digest and the stored claim agree:
8ce6c6690402af088220183ded389bf6f34662b490869a28c3fcadf5a01f58d8The file contains 3109617 playable samples at 48000 Hz, for a duration of 64783 ms. These values and the digest above are read from the downloadable file when the site is generated.
Checking your work
When you are making files, validate the manifest and the selected word transcript against the schemas. The commands need Python 3, curl and ffprobe (part of FFmpeg), and one Python package in an isolated environment:
python3 -m venv .cassini-check
. .cassini-check/bin/activate
python3 -m pip install jsonschema
curl -fLo cassini_extract.py https://raw.githubusercontent.com/codemyriad/cassini-format/main/tools/cassini-extract.py
curl -fLO https://format.gocassini.com/schema/cassini-portable-meeting-manifest-v1.schema.json
curl -fLO https://format.gocassini.com/schema/cassini-words-v1.schema.json
python3 cassini_extract.py meeting.opus > manifest.json
python3 cassini_extract.py meeting.opus --transcript > body.json
python3 - <<'PY'
import json
from jsonschema import Draft202012Validator, FormatChecker
for document, schema in [
("manifest.json", "cassini-portable-meeting-manifest-v1.schema.json"),
("body.json", "cassini-words-v1.schema.json"),
]:
with open(schema) as f:
validator = Draft202012Validator(json.load(f), format_checker=FormatChecker())
with open(document) as f:
validator.validate(json.load(f))
print(document + ": schema valid")
PYFor multiple transcripts, repeat body validation for each supported word
transcript using --transcript ID.
The schemas check structure. Also review the cross-field rules: one default
per slot, valid sourceTranscriptId references, and agreement between the
manifest and summary tags. Passing a schema or the extractor's --check alone
does not establish those rules. The normative producer requirements
remain the contract.
Before sharing a produced file, open it in the receiving software too. Listen, seek and review the text. No hash or schema can tell you that the recognizer heard the right word.