org.cassini.portable-meeting/1 · audio/ogg · no new extension
An ordinary audio file that carries its own transcript.
A Cassini file is a normal .opus recording: Ogg Opus at 48 kHz, and any player
plays it. It also carries who spoke, what they said word by word with timestamps, and what
produced that text, in the same place TITLE and ARTIST live.
Nothing about it looks unusual until you go looking.
Real output, with the payload chunks hidden. Tags sort ASCII: that is the order the producer writes them.
Three commitments
01
It degrades to audio
Understand nothing and you still play the recording. Understand the tags and you get everything. The worst case is hearing a meeting without seeing the words.
02
Keep what a better model could use
The raw ASR words survive the cleanup that rewrites them, next to provenance naming the engine, model and device. The cleanup of 2026 will look bad in 2028. The original words and their timings stay available for whatever replaces it.
03
The digest binds transcript to recording
A SHA-256 over the Opus packets, answering one question: is this transcript describing this recording? It catches accidents, not adversaries. A join key, not a seal.
See for yourself
Two layers, on purpose. The first is plain tags any tool shows: title, date, speaker count,
the digests, and a decode hint explaining the second layer in one sentence. Someone with ffprobe and no documentation should be able to work this out alone.
The second is the payload: compact UTF-8 JSON, gzipped, base64url-encoded, split across numbered tags small enough to keep the comment header readable. It decodes in one pipeline, using nothing you don't already have.
The awk line re-pads: the producer writes unpadded base64url and GNU basenc demands padding. A real wart, written down rather than hidden.
ffprobe -v error -show_entries stream_tags -of json meeting.opus \
| jq -r '[.streams[0].tags | to_entries[]
| select(.key | test("^CASSINI_PAYLOAD_[0-9]{3}$"))]
| sort_by(.key) | map(.value) | join("")' \
| awk '{ p=(4-length($0)%4)%4; printf "%s",$0; for(i=0;i<p;i++) printf "=" }' \
| basenc --base64url -d | gunzip | jq .{
"kind": "cassini-portable-meeting",
"version": 1,
"profile": "ogg-opus",
"meeting": {
"id": "mtg_8e1f7499c6d5fba88c3bd9b69ecd3de1b07ae0cff65152c942c5e99062d01cbc",
"title": "Lantern Festival Booth Run-through",
"recordedAtLocal": "2026-04-15T11:12:00",
"durationMs": 239713
},
"audio": {
"container": "ogg",
"codec": "opus",
"sampleRate": 48000,
"channels": 1,
"sampleCount": 11506248,
"durationMs": 239713
},
"integrity": {
"matchPolicy": "exact-opus-audio-v1",
"opusAudioSha256": "8e1f7499c6d5fba88c3bd9b69ecd3de1b07ae0cff65152c942c5e99062d01cbc"
},
"speakers": [
{
"id": "spk_mira",
"label": "Mira Chen"
},
{
"id": "spk_leo",
"label": "Leo Rossi"
},
"… 4 more"
],
"transcripts": [
{
"id": "script",
"role": "scripted",
"default": true,
"format": "cassini.words.v1",
"payloadRef": {
"prefix": "CASSINI_TX_SCRIPT_PAYLOAD_",
"chunkCount": 3,
"sha256": "bcfe6a717171545ef264e1bcf66aba6f014bc25445267c35a206179c12a8c68d"
}
}
],
"provenance": {
"speechToText": {}
}
}{
"format": "cassini.words.v1",
"items": [
{
"speaker": "spk_mira",
"startMs": 900,
"endMs": 1410,
"text": "Morning."
},
{
"speaker": "spk_mira",
"startMs": 1410,
"endMs": 1774,
"text": "Sorry,"
},
{
"speaker": "spk_mira",
"startMs": 1774,
"endMs": 1992,
"text": "two"
},
{
"speaker": "spk_mira",
"startMs": 1992,
"endMs": 2356,
"text": "goals"
}
]
}Try it on the actual file
Nothing is loaded from anywhere else. The player fetches the same .opus you can
download, walks its Ogg pages in JavaScript, reassembles the chunks, inflates them with DecompressionStream, checks the manifest and transcript digests, and plays. The audio
digest is not checked in the browser, so the reader says unverified, as the spec
requires. If the format works, this works.
The words are the script the voices read, so the transcript's role is scripted.
Segment timings are measured from the render; word timings inside a segment are interpolated,
and the file says so.
decoding Lantern Festival Booth Run-through…
Who writes and reads these
gocassini
The reference implementation: records Nextcloud Talk calls, transcribes, packs. Producer and reader, AGPL-3.0.
Three readers and a producer, in this repo
A Python extractor over ffprobe, one that needs no external tools at all, a
browser reader with no dependencies, and a complete producer in stdlib Python. All CC0. The
producer exists because a spec you cannot implement from is not a spec.
The honest answer to "who else implements this" is nobody. If you build something that reads or writes these files, I would like to hear about it.
What it costs
0.5%
of the demo file is transcript: 9 KB on 1.81 MB of audio.
669
word-timed items, 6 speakers, 4:00 of speech, in 37 Vorbis comments.
0
new extensions, outer media types or codecs. The file is audio/ogg and stays
that. The two JSON bodies inside carry their own application/vnd.cassini.*+json names.
What it doesn't do
Writing these down saves every implementer a wrong guess.
It doesn't model transcript history
A reprocessed file replaces its predecessor. Provenance records what made the current transcript, not what came before.
It doesn't survive audio edits
Cut the audio in an editor that has never heard of Cassini and the digest stops matching. What is left is a plain recording with stale metadata.
It doesn't prove authenticity
Anyone who rewrites the transcript can recompute every hash and the file still verifies.
It doesn't claim a name of its own
No new extension, media type or magic bytes. It has to keep working in software that will never be updated for it.