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.

lantern-festival.opus 37 comments
$ ffprobe -v error -show_entries stream_tags -of default=nw=1 meeting.opus
CASSINI_AUDIO_CHANNELS=1
CASSINI_AUDIO_DURATION_MS=239713
CASSINI_AUDIO_MATCH_POLICY=exact-opus-audio-v1
CASSINI_AUDIO_OPUS_SHA256=8e1f7499c6d5fba88c3bd9b69ecd3de1b07ae0cff65152c942c5e99062d01cbc
CASSINI_AUDIO_SAMPLE_COUNT=11506248
CASSINI_AUDIO_SAMPLE_RATE=48000
CASSINI_CREATED_AT=2026-04-15T09:12:00Z
CASSINI_DECODE_HINT=Concatenate CASSINI_PAYLOAD_000..N for the manifest; for a transcript body concatenate CASSINI_T…
CASSINI_FORMAT=org.cassini.portable-meeting/1
CASSINI_MEETING_ID=mtg_8e1f7499c6d5fba88c3bd9b69ecd3de1b07ae0cff65152c942c5e99062d01cbc
CASSINI_PAYLOAD_000=H4sIAAAAAAAC_61W227jNhD9lYFe2mItR5Jt-bJPTnYL…
CASSINI_PAYLOAD_CHUNK_COUNT=1
CASSINI_PAYLOAD_ENCODING=base64url+gzip+utf8json
CASSINI_PAYLOAD_GZIP_BYTES=1134

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.

bash
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 .
the manifest that comes out
{
  "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": {}
  }
}
and a transcript body, one item per word
{
  "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.

fetch the .opus
walk the Ogg pages, find OpusTags
reassemble CASSINI_PAYLOAD_000..N
gunzip, parse, check SHA-256
resolve the default transcript
0:00 / 0:00

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.

github.com/codemyriad/gocassini ↗

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.

Read one · Write one · the whole spec in one file

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.