Repository navigation
v0.2.0-beta.7
Pre-release
Pre-release
·
1002 commits
to main
since this release
Added
- Access control is now a choice rather than a requirement. Cassini used to have exactly one way to store recordings — a Nextcloud Team folder with per-recording permissions — which needs two Nextcloud apps an external app cannot install for itself, so an instance without them recorded meetings happily and then could not publish any of them. Recordings can now be visible to anyone with a Nextcloud account, in the
cassiniservice account's ownCassiniNoACL/Recordings, needing no extra apps at all; or to meeting participants only, which is exactly today's model in itsCassiniTeam folder. Each has its own folder, so neither can shadow the other's path. - Cassini can set itself up. It offers to make the changes an administrator used to make by hand: the
cassiniservice account and its group, the Team folder, its group mappings, advanced permissions, and the permission-manager delegation. It lists every change before it makes any of them, and performs them as you — Nextcloud asks you to confirm your password with its own dialog, and Cassini never sees, stores or sends it. Theoccrecipe is still there for an administrator who would rather run it, and is generated from the same plan Cassini executes, so the two cannot describe different setups. - Every missing prerequisite is named, together with the
occcommand that fixes it. Cassini andGET /operator/statuslist them all — the service account, each Nextcloud app, the Team folder, its group mappings, its ACL setting and its ACL manager. - A development or CI deployment can still declare the mode, with the
CASSINI_STORAGE_MODEdeploy option (defaultoraccess_controlled). It is not for production installs, and it is believed only where it fits: a declared mode whose prerequisites are missing, or that meets recordings in both folders, or the same recording in both, is refused at enable time and not recorded. It seeds the initial value only, and changing the variable never moves an archive that already exists. - Switching says what it would do before it does it, and can be re-run. The confirmation reports how many recordings would move and between which folders, whether the destination already holds any, and whether an earlier switch left a copy behind. Working that out is read-only, and nothing happens until you confirm. The switch itself copies the archive, checks every recording arrived, records the new audience, and only then empties the old folder — so whichever audience is in force, that folder holds a complete archive, and a switch interrupted by a restart, a timeout or a closed browser tab strands nothing and can simply be asked for again. Cassini names the folder that still holds a leftover copy, offers one action to clear it, and says when recordings are sitting in the audience that is not in force.
cassini meetings summarize <meeting.opus> ...adds a meeting summary to already-published portable files without re-running transcription: the transcript sealed inside each file feeds one LLM call — configured by the sameLLM_BASE_URL/OPENROUTER_API_KEY/SUMMARY_*variables ascassini build— and the file is rewritten in place with its audio bytes untouched.- Files that already carry a summary are skipped;
--forceregenerates and replaces one, and--outwrites a single file's result to a new path instead of in place. A backfilled summary is markedprovenance.meetingSummary.source = "backfill"in the manifest. - You can find words inside the meeting you are reading. A search box above the transcript narrows it to the turns that match, shows how many of them there are, and clearing it brings the whole transcript back. It searches what was actually said, so it still finds a turn whose wording was tidied for display.
- Administrators can tell Cassini the spellings transcription produces for names it gets wrong, in the app's settings beside the existing vocabulary: one name per line, its spellings separated by commas. A search for the name then also finds the recordings where it was written down differently, and each result says whether it matched what you typed or a known mis-hearing. Cassini ships a starting list; anything configured here is added to it, and replaces it for a name that appears in both. It changes no recording — the vocabulary setting is the one that biases new transcriptions.
- You can now search across your meetings. Cassini indexes each transcript as it is published and answers
published/searchwith the moments that match — the meeting, the speaker, and where in the recording they said it. You only ever see meetings you can already open. - Search knows the names transcription gets wrong. Looking for "cassini" also finds the recordings where it was written down as "casino", and each result says whether it matched your words or a known mis-hearing of them.
- Every answer says how much it covered: how many of the meetings you can read were actually searched. A meeting Cassini could not index is reported as outside coverage rather than passing for one with no matches, and an unreachable archive is an error rather than an empty result.
- Cassini's search index lives in its own
search.sqlite3file beside the job database and holds nothing that cannot be rebuilt from the published recordings. Deleting it is safe: it costs a rebuild and never touches recordings or job history. cassini-operator backfill-searchindexes meetings published before the index existed. It is safe to re-run and safe to interrupt, and it indexes a meeting only when the stored transcript matches the recording that was delivered.- Searches are rate limited per account. Each one asks Nextcloud what that person may read, so a client looping on search would put that load on Nextcloud rather than on Cassini; a burst is fine and a loop is asked to slow down, with a reply that says when to retry. The limit is per account, so one runaway client cannot lock anyone else out.
- Pick several meetings in the Cassini app — across rooms, and the picks survive changing the room or the search — and Prepare hands you all of them as one document to copy or download. It is the same document
cassini meetings contextprints for the same meetings in the same order, byte for byte, because the app asks the same producer for it rather than assembling its own. - Before it hands anything over, Prepare says what the bundle will not contain: how many of the picked meetings have no summary, how many were published before the index recorded whether they have one — a different statement, and never counted as the first — how many predate the single-file format, and whether the word count it shows is a total or a floor. All of it is read from the published meeting index, so nothing is downloaded to answer it.
- A meeting that leaves the archive while it is picked is removed from the selection and said so, rather than quietly shrinking the document you were about to hand over.
- A portable export offers no Prepare at all. There is no operator behind an exported site to assemble a bundle, and producing a lookalike in the browser would be a second implementation of a published format.
GET /jobsreturnsroom_name, the conversation's display name as it was when the job ran. The operator now stores the room as real columns on the job rather than only inside its Talk binding, and recovers them for every recording it has already made, so a room-scoped question is an ordinary query rather than a JSON parse. The conversation's token gets a column but is deliberately left out of the response, for the same reason the Talk binding has always been withheld from it.- The meeting list is organised by room. A rooms nav down the left lists every room the archive knows about with its meeting count, alongside "All meetings" and a "No room" group that keeps recordings without a room — an imported file, a simulated run, an old recording — visible rather than hidden.
- Meetings are grouped under month headings, and each row now shows its room, its date, its speaker count and how long it ran.
cassini insight runasks one question of several meetings and writes the answer. It reads the context bundlescassini meetings context --json --outproduces — one--contextper bundle, in the order you name them — runs the summarise workflow over all of them at once, and writes the result to--outor to standard output.--modeloverrides the model for that run alone, and--timestampsshows the model where in each meeting something was said so it can cite it.- The document it writes carries its own provenance as frontmatter: which meetings it was produced from, with their rooms; the workflow's id, version and the content hash of the exact prompt bytes that produced it; the endpoint and model that answered; and when. A summary has always been able to say only that some model wrote it at some point, which is not enough to tell two documents from different prompt versions apart.
--recordwrites the same record as JSON beside the document, so a grading harness reads one pair rather than parsing prose. cassini insight runexits with a different code per outcome — no endpoint configured, the endpoint refused, the model failed or timed out, a bad request — instead of warning and carrying on. Skipping quietly is reasonable for a summary nobody asked for and never for a document somebody did. The codes are listed incassini insight run --help. A failed run still writes its--recordfile, saying what was attempted and why it did not happen.- Transcripts now carry cross-track speaker attribution evidence. Every word records how far the loudest other participant's microphone sat above its own noise floor compared with the speaker's own, so crosstalk the decoder transcribed as speech becomes visible instead of shipping unmarked. Where a meeting's own level distribution shows an unambiguous crosstalk population, affected words are flagged
lowConfidenceSpeaker; the canonical transcript keeps every word, and the evidence travels with them asattributionGapDb. - Generated summaries now exclude words flagged as probable crosstalk. A reader can see and overrule a marked word in the transcript; a summary has no such reader, and a fabricated interjection there becomes a decision somebody supposedly made.
- Speech recognition now runs behind a backend interface, so a second engine can be added by registering a factory rather than by changing the pipeline. Select one with
CASSINI_STT_BACKEND; the bundled sherpa-onnx decoder remains the default, and an unknown selection fails loudly rather than silently transcribing with a different engine. - The viewer now surfaces the crosstalk evidence the transcript carries. A turn built entirely from words the acoustic evidence attributes to somebody else is badged "probably crosstalk", and in the exact-word view individual flagged words are de-emphasised with a dashed underline and a tooltip saying how many dB louder the other microphone was. The words are still shown and still seekable — the reader can listen and decide.
- Added a standalone waveform challenge miner that finds tricky meeting sections, including overlapping speakers, without rerunning speech recognition.
- A published meeting now records how its word ends were decided, as
provenance.wordTimings.endsBoundedByAudioin the portable-meeting manifest. When the key is absent, a consumer may repair an over-long word by clipping it towards the meeting median; when present, the end was measured against audio and must be preserved. - A Settings surface in the app, beside Browse and Operator (admins only). It hosts transcription quality — moved off the operator page — and the new LLM settings: endpoints, whether summaries run and on which endpoint and model (with a fetched model list). Each endpoint carries its own request bounds — timeout and response-token limit — with empty fields meaning the defaults (900s / 4096).
- The app keeps its own LLM settings (
llm-settings.jsonbeside the job database;GET/PUT /settings/llm): named endpoints, and the summary policy. Switching between a hosted provider and a self-hosted model server is one settings change, with no ExApp redeploy.GET /settings/llm/providers/{id}/modelslists what an endpoint serves. API keys are stored server-side and never returned. - Generate an insight from inside the app. Pick meetings in the browse list, open Prepare context, and the panel now offers a Generate card under Copy and Download: choose one of the templates this deployment ships and press Generate. The transcripts go to the AI endpoint you picked and the answer is written into your own Nextcloud files.
- Insight runs report themselves honestly rather than as a spinner. A run appears in the card the moment you press Generate and carries
queued, thenrunning, then a result or a failure — a local model over five meetings takes minutes, and the card says so instead of implying seconds. A failed run names the cause in words you can act on ("No AI endpoint is configured", "The endpoint rejected the request"), carries Retry, and — for an administrator — links straight to the AI providers panel that fixes it. Retry replays the request as you made it, to the endpoint you picked; it falls back to the deployment's configured endpoint only if you picked none, or the one you picked has since been removed — which is what makes adding an endpoint a fix rather than a suggestion. - Ask your own question. One shipped template, "Ask your own question", takes a question you type and answers it from the meetings you picked: the answer in at most three sentences, the evidence with who said what, and what these meetings do not answer. The question box appears for that template and for no other — every other template asks its own question, and a question sent to one of those would be dropped without being asked, so it is refused rather than silently discarded.
cassini insight rungained the matching--questionflag. - Insights are reachable from the browser: the manifest declares three new USER routes —
insights(create a run, list your own),insights/<id>(read one with its document) andinsights/<id>/retry. They are the first routes in Cassini a logged-in non-administrator may POST to, and they are USER rather than ADMIN because a run acts only within what that person could already do by hand: it reads the meetings they picked, as them, and writes the answer into their own Nextcloud Files. On upgrade an existing install picks the new routes up from the release itself — no re-registration needed, because every release bumps<version>(see below). - An insight you ask for in the app is answered by the deployment on your behalf and delivered into your own Nextcloud Files, under a Cassini Insights folder in your home. You own the file outright, so Nextcloud's own sharing works on it and nothing new decides who may read it. It is deliberately not filed under
Cassini/— that name is the shared recordings Team folder, which is read-only for everybody but Cassini's service account — and an insight is never written next to the recordings. - A document is never overwritten. Each file is named for the day, the template and the run it came from, and if something is already at that name the run writes the next free one and records which it used.
- Every meeting an insight covers is fetched as you, over WebDAV, at the moment the run happens — so Nextcloud's own per-file permissions decide what is in the bundle, and a run can only ever be assembled out of meetings you could already open yourself. A retry re-checks that: a meeting you can no longer read stops the run rather than quietly dropping out of the answer.
- A failed run says which of the five things went wrong in words that name the fix — no endpoint configured, the endpoint refused the request, the model did not answer, the answer could not be written, or the request itself — rather than reporting one general failure.
- Cassini now answers
published/meetings-listwith the meetings the calling account may read, narrowed by an optional date range or room. The viewer and thecassini meetingsCLI previously fetched the whole catalogue and filtered it themselves; the same list is now available already filtered, in the catalogue format both already read. - A date-filtered list reports how many meetings it dropped because their published date could not be read, so a short list is visibly partial rather than quietly incomplete.
- Unlike the catalogue it is built from, this list never answers an unreachable Nextcloud with an empty result: a substrate failure is reported as an error, and an empty list means only that the account may read no matching meeting.
- Cassini Admin's Transcription quality panel reports the device and model the
next build will actually use, so a CPU run is stated up front rather than
inferred from a failed build. The device override adds a CPU choice
alongside Auto and CUDA. cassini meetings contexttakes several meeting ids and prints one document holding all of them, so a question can be asked of a set of meetings rather than one at a time. The meetings appear in the order you named them, separated by a---rule, and the--jsonform carries them as ameetingsarray whose every entry is exactly the single-meeting document the command has always produced. An id you cannot read — or one that does not exist, which is answered the same way on purpose — fails the whole run: a bundle that quietly dropped a meeting would answer a question asked of all of them using only some of them, and look right doing it.cassini meetings context --timestampscites each passage's start time, asMM:SSorH:MM:SSpast an hour, beside the speaker. Off by default. The timings were already in the--jsonoutput and had no way of reaching the text an agent actually reads, so nothing asked to say where something was said could point at it.- The context bundle's format has a published schema,
spec/cassini-meetings-context-v1.schema.json, and its output is pinned by fixtures.cassini.meetings.context.v1was a version string with nothing written down behind it; now a change to what the command emits shows up as a diff in a checked-in file. - The published meeting index (
catalog.json, and socassini meetings list --json) now records whether each meeting carries a summary and how many words its transcript holds. Both come out of the file the publish step already reads, so anything listing meetings can say which of them have a summary, and size a selection of them, without downloading a single recording. Meetings published before this update carry neither field, which reads as "not known" — never as "no summary" or "no words"; republishing one fills them in. - The Cassini app can ask for a set of meetings as one document:
GET published/meetings-context?ids=…,…, optionally&format=jsonand×tamps=true. It answers with exactly whatcassini meetings contextprints for the same meetings — the operator resolves what you may read the same way it resolves the meeting list, fetches each recording from Nextcloud Files as you, and runs the CLI over them, so there is one implementation of the document rather than one per reader. An id you may not read is answered404, identically to one that does not exist; a request that cannot be answered is502, never an empty document. At most 20 meetings per request, because each one costs a whole recording download. cassini meetings context --local ./Meeting.opus [...]reads portable.opusfiles off disk instead of fetching meeting ids from Nextcloud, and needs none of the connection settings. Each meeting's id is its file's basename without the.opus, which is how a published archive names one.--catalog ./catalog.jsonsupplies the room id and room name, which live in the catalog rather than in the file — with it, a run over an archive's files produces the same bytes as a run over the same meeting ids; without it a meeting renders with the room id its file was tagged with and no room name.
Ids travel as one comma-separatedids, not as a repeatedid: AppAPI's proxy collapses repeated query parameters to the last value, which would have answered a three-meeting request with a one-meeting document and a200. A caller reaching the operator directly may still repeatid.- Insight templates is a real panel in the Operator: it lists every template this build ships, and each row opens onto the exact instruction that template sends to the model — the prompt itself, not a description of it — beside the id, version and content hash an insight document records. So a document you found a month later can be traced to the prompt that produced it, rather than to a name that may since have meant something else.
- A second template, "Commitments and owners": one section per person who spoke, with what they took on and when they said so, then what was assigned to somebody who never answered, then what nobody claimed.
cassini insight run --workflow todosruns it, and every other workflow the panel lists can be named the same way. cassini insight workflowsprints the same list on the command line, and--jsonadds each workflow's instruction verbatim. It is how you check what a given image will actually send, without reading its source. The Operator's templates panel is this command's output.- LLM settings carry a second step, Insights, beside the meeting summary, so an insight can run on a different provider and model from the one that writes each meeting's summary — a small local model for every published meeting, a larger one for a question you ask by hand. An insight step with no endpoint of its own runs on the summary endpoint, so an existing install gains insights without configuring anything, and the settings panel says which of the two is answering. Each step also names the workflow it runs.
- Per-step request bounds (
SUMMARY_TIMEOUT_SEC/INSIGHT_TIMEOUT_SECand the matching_MAX_TOKENS), because two steps on two endpoints cannot share one timeout: a CPU-bound local model needs a far longer leash than a hosted API. - Each step's workflow field is checked against the templates the image actually ships, and names them when the id is not one of them. The operator cannot know the registry — it lives in the recorder — so a typo used to save cleanly and only fail at the run.
- An insight now keeps a record of itself from the moment it is asked for, moving through queued, running and then succeeded or failed. A run that takes minutes against a local model shows what it is doing rather than nothing, and a failed one says why it failed instead of stopping silently.
- Retrying a failed insight is another attempt on the same insight, not a second one: it keeps its id, workflow, meetings, question and the endpoint you picked, and replays that request. It falls back to the deployment's configured endpoint only if you picked none, or the one you picked has since been removed — so fixing a missing key on your endpoint, or registering one where there was none, is enough to make the retry work. A retry pressed twice at once starts one attempt and refuses the other. A run the operator was restarted out from under is failed with a message asking you to retry it, instead of staying stuck on "running".
- An insight now appears in the browse list beside the meetings it was drawn from, and under every room those meetings came from: a question asked of three rooms is found in all three, not filed under one of them. A Meetings/Insights switch above the list narrows to one kind or the other, and the last kind showing cannot be switched off. Insights that are queued or still running are listed too, saying so — an answer that takes minutes is visible while it is being written rather than appearing from nowhere afterwards — and a listing that fails says it failed instead of reading as "there are none".
- Opening an insight shows the question it was asked, the meetings it read — each of which opens as itself, with the way back to the insight — the answer, and who ran it with which workflow and version. Every source meeting lists the insights that read it, so the link works in both directions. A meeting or an insight you may not read is simply absent from both, with nothing anywhere saying that it existed. The standalone viewer export, which has no operator behind it to have run anything, shows none of this and offers no filter for it.
GET operator/setup, which any logged-in Nextcloud user may read, now also reports what this deployment's AI configuration allows:features.summaries(a recording will be summarised) andfeatures.insights(a question asked of a set of meetings will reach an endpoint). One bit each — never the endpoint, the model or the key, which stay administrator-only. It makes the promise indocs/privacy.md— that nothing leaves your infrastructure unless an endpoint is configured — checkable by the people whose meetings are being recorded.- The Cassini app now explains an unconfigured deployment instead of staying quiet about it: picking meetings on an instance with no AI endpoint says so in the Prepare panel, and an administrator gets a link straight to the AI providers panel that keeps the meeting they had open. Anyone who is not an administrator is told the fact and that only an administrator can change it, rather than being shown a control that would be refused. The standalone viewer export, which has no operator to ask, says nothing at all.
- Pick the AI endpoint and model when you create an insight. Prepare now offers a Provider and a Model beside the template, defaulting to the first configured endpoint and its own default model. It is offered to everyone, not only administrators: which of the configured endpoints sees your transcripts is the asker's decision, so
GET operator/ai/providersis a USER route carrying ids and display names — the base URL, the key and the request bounds stay administrator-only. A retry re-runs what you asked for, falling back to the deployment's endpoint only if the one you chose has since been removed. This adds a route, so the release that ships it must bump<version>inappinfo/info.xml— AppAPI only rewrites an app's route table on a version change. - "Ask your own question", the freeform insight template. Pick it in Prepare, type a question, and the answer comes back in three parts: the answer itself in at most three sentences, the evidence with who said what and when, and — never omitted — what these meetings do not answer. The machinery for a caller-supplied question already existed (
{{QUESTION}},Workflow.TakesQuestion); what was missing was a prompt carrying the placeholder. A template that takes a question is offered wherever one can be typed and withheld where one cannot: it is not offered as the publish pipeline's summary step, which runs unattended with nobody to ask. - The Participant and project vocabulary setting now guides the transcriber itself. Terms are applied as sherpa-onnx contextual biasing, so the recogniser can prefer "Librocco" or "Aire Spaces" where it used to invent a spelling. The vocabulary is a decoder prior rather than a text rewrite; its score therefore stays conservative so it does not overpower weak acoustic evidence.
provenance.speechToText.hintsin the build manifest records how many terms were used, the boost, and the decoding method. When a vocabulary is configured but cannot be applied, it records that and why, so a build that ignored the setting never looks like one that honoured it.CASSINI_STT_HINTS_DISABLEDturns biasing off and restores greedy search, so an operator can get back to the transcription they had before;CASSINI_STT_HINTS_SCOREoverrides the boost. Both are passed through the deployment compose file.- The meeting list's search box now searches what was said in your meetings, not just their names and dates. A matching meeting shows the moments that matched, and clicking one opens the meeting at that point with the transcript already filtered to those lines.
- When search cannot answer — too many searches, an index still building, or an unreachable archive — the list says so instead of showing an empty result that would read as "nothing matched".
- Cross-meeting search results now carry a short quote of what matched, so a result says why it matched rather than only where.
cassini annotatereads and writes the tags and marks a recording carries inside its own.opusfile.showprints them.applytakes a batch of edits — tag the whole meeting or a stretch of it, remove a mark, remove a tag, undo everything one run added, rename a tag — and rewrites the file once, without touching the audio, the transcripts or the summary. Sending the same batch twice changes nothing the second time, and a batch that changes nothing does not rewrite the file.cassini annotate carrymoves the marks from a delivered recording onto a newly sealed one, so re-running a meeting does not lose them. When the new recording's audio differs, the marks are kept and flagged as made against different audio rather than redrawn on it.- A meeting's tags and marks can be read and changed at
annotations/meetings/<id>, by people in the app, the CLI and agents alike. Anyone who can read a meeting can mark it, and every mark is attributed to the Nextcloud account that made it, never to a name the request supplies. The marks are written into the recording itself, so they travel with it when it is downloaded or shared. - Two people marking the same meeting at once no longer overwrite each other: a write that finds the recording changed underneath it re-reads the newer copy and applies its marks to that. A meeting you cannot read answers exactly as one that does not exist.
GET annotations/tagslists the tags on the meetings you can open, each with how many meetings and marks carry it, and says how many of your meetings it could read marks for. A tag that is only on meetings you cannot open never appears and never changes a count.published/searchandpublished/meetings-listtaketag=(a label in any case, or a tag id) and answer only from meetings carrying a mark of that tag. Each search result also lists the tags whose marked stretches it falls inside.- Cassini keeps its tag index in its own
annotations.sqlite3beside the job database. It holds nothing that cannot be rebuilt from the marks inside the recordings, andcassini-operator backfill-annotationsrebuilds it: recordings that have not changed are skipped without a download, and a fresh index keeps the tag namespace the archive already uses. - A recording can carry tags. A tag such as
hiringcan go on the whole meeting or on a stretch of it, and the marks live inside the recording's.opusfile, so a meeting keeps its tags when it is downloaded or shared. The portable format and its schema describe them, and a reader that does not understand them still opens the recording normally. cassini meetings tagslists the tags on the meetings you can read.cassini meetings annotations <id>shows one meeting's marks, andcassini meetings annotate <id> --ops <file>adds or removes them in one batch. Every mark records the Nextcloud account that made it, and whether a person or an agent made it, so an agent's whole run can be undone in one step.cassini meetings searchandcassini meetings listtake--tagto look only at meetings with that tag. Against a Cassini app that does not offer tags, they fail instead of silently returning every meeting.cassini inspectreports a recording's tags: how many there are, and whether they were made against this recording's audio.- You can tag meetings from the app. Each row in the meeting list shows its tags; tag one meeting from its row, or select several and tag them together. A filter beside the rooms narrows the list to one tag or several.
- Inside a meeting, you can tag a section of the transcript: select its words (or, on a wide screen, drag down the rail beside it), choose Tag selection, and set exactly where it begins and ends with the handles in the text or the arrow keys. Tagged sections are underlined in their tag's colour and, on a wide screen, bracketed beside the text with the tag's name; arrows step from one to the next. The last tag you used is offered again beside Tag selection, so tagging several sections alike is one click each. On a phone, a section's tag sits in the text in front of it, and its details and the arrows sit above the player.
- A tag has a colour and, if you want, an icon. You choose them where you create the tag, and change them in Manage tags, where tags can also be renamed, merged and deleted. A change reaches the meetings you can read; meetings in rooms you cannot open keep the tag as it was. Colours are stored for the installation rather than inside the recordings, so recolouring a tag rewrites nothing.
- Each AI provider now carries a default model, set in AI providers. Summaries and insights use it unless the step names its own, and the Prepare panel's model field opens pre-filled with it, so a run can use another model without picking one every time.
- A failed insight can be retried from its card in the browse list and from its document, not only from the panel that started it. A failed run now records only a reason (
reasonon the run, one of fourteen tokens); the app decides the wording, so the words can improve without touching stored runs. - One dialog, once per install. The first administrator to open a fresh Cassini is told that recordings, and the names of the rooms they came from, will be visible to anyone with an account on this Nextcloud, and given one button that creates the
cassiniNextcloud account they are kept in. Nextcloud asks them to confirm their password, and Cassini never sees it; the account's own password is generated, set and discarded. The dialog is recorded as seen by the install rather than by the browser, and only once the account exists: an administrator who leaves it to change the audience first is asked again next time, and so is one whose install cannot create the account at all — where Cassini cannot create it, the dialog says so and points at Operator › Settings instead of claiming to be ready to record. On Nextcloud releases that let an app write to user administration, Cassini creates the account when it is enabled and nobody is asked anything. - Operator › Settings › "Who can see recordings", with two audiences and one sentence each: Everyone with a Nextcloud account and Meeting participants. The one in force is marked, and the line beneath says how many recordings already exist and who can see them, including after a switch, when the recordings from before it stay visible to everyone. Where recordings are stored, the service account and the
occrecipe are behind "Details for administrators". Where thecassiniaccount does not exist yet, the section says so and offers to create it, whichever audience is in force. - The meeting list says who can see the recordings in it, beside the meeting count: "Visible to anyone with a Nextcloud account" or "Visible to meeting participants". Administrators and everybody else see the same chip.
- A switch shows its four steps while it runs, with a count of the recordings copied, and says the page can be closed; one that was interrupted offers Resume. Choosing Meeting participants without Team folders or Everyone Group offers to install them, and says that Everyone Group adds a group called Everyone to the whole of Nextcloud first. The confirmation says what the switch means for new and for existing recordings, in a number, and the direction that widens the audience carries that number in its own button.
- Added
linux/arm64multi-arch support to the portable Cassini ExApp container image. Releases, pushes tomain, and manual workflow runs now publish multi-platform images (linux/amd64andlinux/arm64) with bundled ARM64 sherpa-onnx runtime libraries and Parakeet ASR models. - Recordings a switch left visible to everyone can now be limited to the people who were in the call. Choosing "Meeting participants" on an instance that already has recordings copies them into the Team folder and leaves them readable by every account — Cassini had no way of knowing who was in a past meeting, and said so once, in the page that performed the switch. It now keeps that record while each meeting runs: the accounts, groups and teams with access to the room are captured when a recording starts and again when it stops, and frozen, so a room that gains or loses members later cannot change who may open a recording made before the change. Operator › Settings › "Who can see recordings" lists everything still visible to everyone, shows exactly who each one would be limited to, and limits the ones you tick. Groups and teams are granted as themselves rather than expanded, so reorganising a team is how access moves with it.
- Recordings it cannot limit say why, instead of sitting there unexplained — recorded before Cassini kept the record, everyone in the call was a guest with no account to grant, or no job of this instance owns the file. Recordings of public conversations never appear at all: they are readable by everyone because the conversation was public. Anything needing a bespoke audience is still Files → Advanced permissions, and a recording you want left open can be dismissed from the list and brought back again.
- The meeting list's search box now finds meetings by the names of their tags. Typing part of a tag's name brings up the meetings carrying it, alongside matches on the meeting's own name, date and what was said in it.
- A recording published on its own — in a static export, or embedded in any page — now shows the tags and marked stretches that are inside it, read from the file rather than from a server.
<cassini-meeting src="…">embeds one recording in any page from a single script tag, served fromgocassini.com/embed/v1/viewer.js. The attribute contract iscassini-viewer/ATTRIBUTES.md.- An embed carries a "Recorded with Cassini" footer by default, linking to gocassini.com. Remove it with the
hide-badgeattribute. - Tag colours and icons are now saved in each recording's
cassini.annotations.v1document. Downloaded recordings and the public viewer retain their tag appearance without an installation-local lookup. - Cassini
doctorand the operator/statusdiagnostic endpoint now verify
the native speech engine runtime and report whether the Parakeet v3 reference
frontend optimization (+cassini-parakeet-v3-reference-v1) is active. If an
unpatched library is detected, a non-blocking warning is reported with guidance
on how to enable the optimized runtime. cassini dev meetings pull --out <dir>mirrors the meetings a Nextcloud account may read into a local seed pack —catalog.jsonplusmeetings/<id>.opus, the shape a published site has always had. It pulls everything by default,--limitkeeps the newest N, and--room/--from/--tonarrow it the waycassini meetings listdoes.--dry-runreports the total size before committing to the transfer, and re-running skips what is already on disk at the size the server reports, so an interrupted pull is resumed by repeating the command. Every request is a GET or a HEAD; nothing is written back.cassini dev stack up --seed <dir>loads a seed pack into the harness, andharness/bin/seed-nc-files.sh --pack <dir>does the same against a stack that is already running. A local development stack can now be brought up holding a real archive instead of a synthetic fixture, which is what the meeting list, cross-meeting search and insights actually need to be worked on. Seeding is a filesystem copy plusocc groupfolders:scanrather than a WebDAV upload, so 2 GB of recordings are copied and scanned in about four seconds, followed by a permissions pass of roughly half a second per meeting. The catalog is merged rather than replaced, so meetings a stack recorded itself survive a seed.- Seeded meetings are given an explicit read grant, and are therefore readable by every account on the stack, which the seeder says on every run: production's per-meeting permissions name production accounts and do not travel with the data. They also carry no operator job history. Neither affects the published surfaces, and both are stated so that no access-control change is tested against a seeded archive.
- Recordings now carry a wall-clock anchor per audio track. Each track gains
first_packet_wall_msandfirst_timeline_nsMKV tags: two readings of the instant the track's first packet arrived, one in recorder wall time and one in meeting time. Because both come from the same monotonic clock, a later stage holding a wall-clock instant can convert it to a position on the meeting timeline exactly, without reopening the session artifact. The tags are emitted as a pair or not at all, and a recording that predates them reads as unknown rather than as the epoch. A third tag,first_rtp_timestamp, is recorded for triage only — Janus rewrites the timestamps it relays to each subscriber, so it is not the sender's clock and cannot place a sender-side time. - The selection bar's count opens a list of every meeting you have picked, including the ones the current room or search is hiding. Open a meeting from there, or unpick it.
- The
scriptedtranscript role, for authored text a recording is a performance of: a read script, a song's words. It is not a transcription, so it never names a source transcript. - Recording checks in Operator → Publish pipeline guide administrators through
storage, processing, Talk authentication and handoff, followed by a real Talk
recording and playback confirmation. Existing recordings remain accessible. - Persist the Talk internal secret securely on Cassini's volume, with deployment
configuration taking precedence. Diagnostics expire and remain advisory;
missing local credentials are reported before recording starts. - Provide installation-specific host commands and copyable provider requests,
plus first-ExApp, ARM64/amd64 and AIO persistence guidance.
Changed
- Skip standalone recorder integration jobs on PRs confined to the operator, frontend, microsite, or PR-conflict automation. Shared and unknown inputs still run the scenarios, and main pushes retain full coverage.
- Storage that does not match the recorded audience is reported as a mismatch that refuses to publish and names both ways out. A Team-folder list Cassini cannot read counts as an unanswered question rather than as "there is no Cassini Team folder" — the folder list arrives with every folder on the instance in one response, so a single unrelated folder in an unexpected shape used to make the whole answer undecodable and indistinguishable from an empty one.
- Cassini no longer builds Team folders or their ACL topology on its own. On every enable it checks what is there and reports what is not; it makes those changes only when you ask for them. A half-built setup can no longer end up looking healthy.
- Talk refuses to start a recording when the storage it would be published to is missing a named prerequisite, instead of capturing the whole call and failing afterwards. The moderator sees Talk's own "The recording failed" message; the reason, and the command that fixes it, are in Cassini.
- Installing the two Nextcloud apps is the one step Cassini still cannot take for you, and it now says so instead of implying otherwise. Nextcloud requires an administrator's password on that particular request, which Cassini declines to handle; it attempts the install through its own backend — which works on Nextcloud 32.x, 33.0.0–33.0.5, 34.0.0, and on any release where
allowed_no_password_confirmation_rangesis set — and otherwise links you to Nextcloud's own Apps page. When the App Store refuses, the message says plainly that Nextcloud reports the same error whether the app is unknown, incompatible or the store is unreachable, and that retrying inside five minutes will say the same thing. backfill-nc-filesrefuses to run where recordings are visible to anyone with a Nextcloud account, instead of writing access rules into a folder where they mean nothing. Everything it writes is protected by Team-folder permissions, and those recordings live in thecassiniaccount's own folder where every signed-in account can already read them — so the migration would be at best a no-op with side effects, and at worst would re-derive an audience for recordings an administrator had deliberately left open. The refusal names the audience, writes nothing, and points at the setting that actually moves an archive between the two.- The recordings-permissions, install and privacy documents now describe both audiences. The upgrade section in particular had the Team-folder path collision backwards — a Team folder takes the
Cassinipath and Nextcloud renames the colliding home directory, rather than the mount being remapped — so its instruction to move the old folder out of the way first was unnecessary. - A keyless OpenAI-compatible endpoint now switches meeting summaries on:
LLM_BASE_URLalone is enough, andOPENROUTER_API_KEYis needed only when the endpoint requires one. Previously both had to be set, so a self-hosted model server silently produced no summary. On upgrade, an install that hadLLM_BASE_URLset without a key starts producing summaries through that endpoint from its next build. - With no key configured the recorder omits the
Authorizationheader entirely rather than sending an empty bearer token, which some self-hosted servers reject. New request bounds for slow local models:CASSINI_LLM_TIMEOUT_SEC(default 900) andCASSINI_LLM_MAX_TOKENS(default 4096). cassini meetings searchsays where a speaker id comes from — thespeaker=value a previous result printed — so--speakercan actually be used. The ids are opaque and are not meant to be typed from memory.- The control panel names each recording after its conversation. A recording was labelled by its room token —
Call 3qbs6123vxx— which identifies the call but not which conversation it was. Rows and the detail heading now show the Talk conversation's display name wherever the operator has it, keeping the token label as the fallback for a non-Talk recording, one whose room-name lookup never completed, and anything recorded before this release. - Opening a meeting slides it in over the list from the right, instead of replacing the list with a second column; on a phone it arrives as a bottom sheet. Close it with the × or Escape, and the list is still there, in the same place, with the same room selected.
- The search box moved to the top of the list and filters within the selected room. It still matches a meeting's name or date, and the room and search narrowings each show as a chip you can clear.
- A word can now be flagged
lowConfidenceSpeakeronly when its own track sits well below that speaker's own speech level (the quiet-owner signature). Genuine double-talk between microphones with different signal-to-noise ratios measures large gaps but is never flagged, and the crosstalk threshold is estimated on the quiet-owner population alone. The per-wordattributionGapDbmeasurement itself is unchanged and still annotates every word. - The meeting manifest records what the attribution stage did: whether it ran, the mode (annotate/drop/disabled), words measured, flagged and dropped, and the estimated threshold when one was found. A build that deletes words in drop mode is no longer indistinguishable from one that never saw them.
CASSINI_STT_BACKEND,CASSINI_ATTRIBUTION_DISABLEDandCASSINI_ATTRIBUTION_DROPare documented and passed through the deployment bundle; an unknown backend id now fails the build before any decoding or model download instead of after the mixdown.CASSINI_ATTRIBUTION_DROP=1removes flagged crosstalk words from the transcript itself instead of only marking them, andCASSINI_ATTRIBUTION_DISABLED=1skips the measurement. Marking is the default: real crosstalk overlaps the level range genuine simultaneous speech occupies, so deleting canonical words on a threshold is opt-in.- The word being spoken now lights up instantly instead of fading in. The fade lasted about as long as a short word, so the highlight was always part-way through changing and never quite agreed with what you were hearing; it now tracks the audio exactly. Hovering a word to seek also highlights it immediately.
- Overlapping speech reads as speech again. A turn is now one paragraph, however many pieces the producer cut it into: when two people talk over each other their sentences no longer alternate in one-to-three-word fragments. A short reaction someone made while another person was talking appears as a quiet chip where it happened, inside the sentence it landed in, and a turn that genuinely ran over another says so by naming the other speaker.
- Durations are gone from the transcript. A badge reading "0.2 s during Ivan" on every fragment was precision nobody acts on, and it made the page harder to read than no marking at all. The measurements are still there for anything that wants them; they are no longer shown.
captions.vttis now always produced, from the transcript itself. It used to exist only when an LLM endpoint was configured, as a by-product of cleanup.LLM_BASE_URL,OPENROUTER_API_KEYandLLM_MODELin the deploy environment now only pre-fill the LLM settings on the app's first start; after that the stored settings are what every build receives.- A non-administrator sees no template picker: the template registry is admin-only, so their insight runs whichever template the administrator configured, and the card says so rather than offering a control that would be refused. On a deployment with no AI endpoint the card does not appear at all — the existing "No AI endpoint is available" notice takes its place — and in a standalone static export, which has no operator to ask, neither appears.
docs/exapp-update-constraints.mdanswers the question it used to flag as unverified: an in-placeapp_api:app:updatedoes rewrite AppAPI's route allowlist, but only when the manifest it is handed carries a new<version>— with the same version it short-circuits on "already updated" and the allowlist is untouched. Routes therefore behave unlike deploy environment variables, which stay creation-time. The rule for release review is that a release changing<routes>must bump<version>, whichscripts/bump-exapp-version.shalready does. The measurement is written up with its evidence and is re-runnable asharness/bin/check-route-refresh.sh.- The privacy note now covers insights: a question sends the picked meetings' transcript and summary text to the configured LLM endpoint, the call uses the instance's API key — so the request reaches your provider attributable to the deployment, not to the person who asked — and the answer is written into that person's own Nextcloud Files, never beside the recordings.
- The subprocesses an insight runs no longer inherit the operator's own credentials. The AppAPI shared secret, the Talk recording and signaling secrets and the operator API token are removed from their environment: none of them is read by any
cassinicommand, and the AppAPI secret in particular is what lets a process act as any account on the instance. The step that only assembles the meetings loses the LLM credentials as well, since it never calls a model. - An insight is bounded in the three ways that can run away: how many meetings one may cover, how long one attempt may take before it is stopped, and how large the answer may be. Each limit is a refusal that says so, never a silently shortened document, and only two runs are performed at a time — the rest wait, and say they are queued.
- The in-Nextcloud viewer now reads its meeting list from the app's list endpoint rather than the published catalogue file, so a Nextcloud outage no longer empties the list: it keeps showing the meetings it last loaded and says what went wrong, instead of silently rendering as though the account had no recordings. Standalone exports and the static viewer are unaffected, and the viewer falls back to the catalogue file against an app that does not serve the endpoint.
- Transcription runs on the CPU when the host has no GPU. Operator builds
resolve their device per run — CUDA when the image carries the CUDA runtime and
an NVIDIA device is visible, CPU otherwise — instead of blocking every build on
a GPU-less install. Recordings already stuck inbuild/blockedrecover with
Rerun in Cassini Admin; no image or deploy-daemon change is needed. - Each image carries the model of the tier it runs by default, and any other
tier downloads once into a model cache on the persistent volume. The portable
image bakes the 0.6B int8 model, the GPU image bakes fp32, and both stay small.
A download survives a container recreate, and a host with no network egress
gets a clear error that names the missing model. Cassini Admin shows the size
of that download before the first build of the tier. cassini meetings listnow asks the app to filter by date and room, instead of downloading the whole catalogue and narrowing it locally. Against an app that does not offer that yet, it falls back to the previous behaviour, so an older CLI and a newer one both keep working.cassini meetings list,rooms,fetchandcontextnow report an unreachable recordings archive as a failure rather than as an empty result. Previously an outage was indistinguishable from "this account has no recordings" — or, when fetching by id, from "no recording you can read at that id".- Stop reasons and recording durations appear in run metadata.
The operator view formats raw reason codes into plain labels and displays the total recorded duration in sidebar cards and detail panes. cassini meetings contextfetches the recording catalog once per run instead of once per meeting, so every meeting in a bundle is resolved against a single view of what you are allowed to read.--keep-opusis refused when more than one meeting is asked for, and so is the same meeting id given twice. Both used to be silently meaningless; one file cannot hold several recordings, and the same transcript twice spends the context budget on one meeting while reading as coverage of two.- The publish pipeline's summary step and the
summariseinsight now run the same prompt file, not two copies of it. The summary is an insight like any other, so it names the template it runs; the bytes it sends are unchanged. - A prompt whose bytes are not compiled into the image is not listed and cannot be named — a template you can select is a template that will run. Two of the templates drawn in the design have no prompt authored yet and are therefore absent rather than shown as unavailable.
- Editing a shipped prompt now fails the lint check until its version is bumped and its hash re-pinned. Prompt files are markdown, and a markdown-only change used to run no tests at all while changing what the product sends to a model.
CASSINI_SUMMARY_DISABLEDno longer blockscassini insight run. It turns off the summary written when a meeting publishes; an insight is a document you asked for by name, and turning insights off is removing the endpoint.- Configuration lives inside the Operator surface again, behind a left nav, instead of in a Settings tab of its own. Two tabs remain — Browse and Operator — and Operator now opens on a nav with two groups: Console (Recordings) and Settings (AI providers, Publish pipeline, Insight templates). The panels themselves are unchanged; only where you reach them moved. Below 720px the nav is a dropdown, and each panel opens at its own top rather than wherever the previous one was scrolled to.
- A panel can be linked to directly:
#surface=operator&panel=endpointsopens AI providers. - The run console splits into a list and a detail pane based on how wide the console itself is, not how wide the browser window is. In Nextcloud the app sits beside Nextcloud's own navigation, so a window wide enough by the old measure could still squeeze the run detail into a couple of hundred pixels.
- Registering your first AI endpoint now switches meeting summaries on, pointed at it. An install that has just configured an endpoint and still publishes meetings without summaries has done the work and not got the feature. It fires once — on the save that takes a deployment from having no endpoint to having one — so an administrator who turns summarising off keeps it off, whatever they save afterwards. The AI providers page says so before you save, since it is a decision about what leaves your deployment.
- AI providers configures endpoints and nothing else. Adding one is a form rather than a hidden row of fields, a saved endpoint reports whether its model list actually came back (and says so plainly when it did not, since an endpoint with no
/modelsroute still answers completions), and the page says which endpoint an insight will actually reach. - Publish pipeline is now the whole pipeline in the order it runs: detected hardware, transcription quality, summarisation, and the device override last. Summarisation moved here from AI providers — it is a step between the call ending and the meeting being published, and it was only living under endpoints because its endpoint is.
- The summary step names an insight template from the registry instead of asking for a workflow id as free text, and its model field is a combobox that loads the endpoint's models when you open it rather than behind a "Load models" button. Typing a model the list does not have is still accepted: the list is the endpoint's, not a limit on what you may send.
- The "this needs an AI provider" state is now the same compact card wherever it appears — the summarisation step, Insight templates, and the Prepare panel — with the button that fixes it. Anyone who is not an administrator still gets the explanation instead, because that panel would refuse them.
- Create an insight in the Prepare panel is the template you pick and what it will ask, in one box: choosing a template shows the question it puts to the meetings and the shape of the document that comes back, or a question box where the template takes one. It opens on a real template rather than on a row reading "this deployment's default", which named nothing and read as a setting instead of a choice.
- The Prepare panel no longer lists previous insight runs. It was a job console in a panel whose subject is the meetings you just picked, and a second place insights were listed — free to disagree with the browse catalogue behind it. The card now shows only the run it starts, which is feedback for a button press; every insight, that one included, is a card in the browse list as soon as it exists.
- The publish pipeline's summary step opens on Meeting summary — the template it actually runs — instead of a "Default (the one Cassini ships)" row standing in front of it.
- Meetings / Insights moved out of the list header into the rooms rail, under "Show", beside the other narrowing of the same archive, with a clearer gap between the two lists.
- A meeting's header now carries its name, room, date, length and speakers, and stays put while the transcript scrolls under it. The insights a meeting was used in read directly under its summary rather than in a strip below the whole transcript, and an insight's own header carries its room, date and the model that answered.
- Insight cards in the browse list are filled rather than outlined, so they read as a different kind of thing at a glance rather than only by the rule down their edge.
- Upgraded
sherpa-onnxGo bindings and native runtime libraries to v1.13.7 across the recorder and container base images. - Transcription output changes for every meeting, not only for the listed terms. Hotwords are only read under modified beam search, so the balanced and best quality tiers now decode with beam search instead of greedy search. It is a different decoder and it decides differently on marginal audio: measured on real meeting audio it lost a sentence on one clip and gained words on another. Decode cost is 1.3x to 1.5x, still far faster than realtime (worst measured realtime factor 0.091, so an hour of meeting decodes in about five and a half minutes on CPU). Memory is unchanged.
- The fast quality tier is unaffected. It runs a CTC model, which sherpa-onnx cannot bias, so it keeps greedy search and records the vocabulary as not applied rather than ignoring it silently.
- The vocabulary previously steered the LLM cleanup prompt and explicitly did not affect the transcript. It now affects the transcript and nothing else.
- Decoder hints need a model bundle that ships
bpe.vocab. Bundles without one record the vocabulary as not applied, and say so, rather than decoding unbiased in silence. - Re-running a meeting no longer erases the tags and marks added to its recording after it was published: the rerun carries them into the recording it delivers. Marks made against audio the rerun changed are kept and flagged as no longer matching, never dropped. If marks are being written at the moment of the rerun, the publish fails and asks to be re-run rather than overwrite them.
cassini-operator backfill-searchrecognises an archived recording that has gained marks since it was delivered as the same recording as the local copy — it compares the audio, not the whole file — and keeps indexing it from the local transcript's own segments.- The UI-only branch preview job is temporarily disabled (D-739). Every meeting in the published archive uses the
exact-pcmaudio integrity policy retired on 2026-09-02, so the export can read none of them and the preview failed for every branch on every push, over data no branch can fix.docs/runbooks/repair-published-archive.mdis the repair procedure, and re-enabling the job is one line and part of closing D-739. Opt-in processing previews are unaffected: they build their meetings from raw recordings. - The endpoint's address stays with administrators. The insight document's provenance records the model and endpoint kind but no longer its base URL; an unnamed endpoint is shown as "Endpoint 1" rather than by its host; and a model list that cannot be fetched is reported to non-administrators without the URL.
- Registering your first endpoint switches summarising on exactly once in a deployment's life. Removing every endpoint and adding one back no longer switches it on again for an administrator who had turned it off.
- Removing an AI endpoint asks for confirmation first, naming the endpoint, that its stored key is destroyed, and which steps stop running on it.
- Finding words inside a meeting now counts the matches, steps between them, and marks where they are on the rail beside the transcript. Hiding the turns that do not match is now a switch, off by default, instead of what always happened.
- Generating an insight closes the Prepare panel and puts the new run at the top of the browse list, where it updates like every other insight.
- The selection is bounded to the operator's 20-meeting cap before a request is made, with a sentence saying how many to unpick.
- Cassini works out who can see recordings when the app is enabled, instead of refusing to record until it is told. A fresh install records as "everyone with a Nextcloud account", from the moment the
cassiniaccount exists. An install whoseCassiniTeam folder already holds recordings keeps "meeting participants", without asking and without moving anything, and recordings stranded in the other root are counted and offered a move. Cassini never widens an existing archive on its own, and "nobody has chosen yet" has stopped being a state an install can be in. A mode resolved this way reportsmode_source: "resolved_on_enable"; a mode an older version recorded on its own, or an interrupted switch left behind, is kept and no longer needs confirming. Where Cassini cannot read the recordings it already has, it records no mode, says so onGET /operator/status, and looks again the next time the app is enabled; recording still goes ahead. TheCASSINI_STORAGE_MODEdeploy option is unchanged and still takes precedence. - A problem is one sentence now. Where storage is genuinely broken, Cassini says it cannot save recordings right now, names the cause in words, and offers Try again and Show details; the step, the paths and the commands live inside that disclosure. Somebody who is not an administrator gets the same sentence and no buttons, and nobody is told that an administrator has to decide anything. The cause is written by the operator, one sentence per check, so
/statusand the app say the same thing about the same failure. A check that has not run yet, as after a container restart, is a neutral band rather than a warning, and the meeting list stays where it is. - The storage API reports the install as well as the archive.
GET /operator/storagecarriesfirst_run, answered once per install, andmigrationwith the phase and counts while a switch runs.GET /operator/setupcarries the mode, so the audience can be named for people who are not administrators.recordings_accesson/statusgains acausesentence, and/setupcarries the form of it that names no account, path or app. - The App Store description, the microsite and the documentation say who can see a recording before they say how it works, with the default first and the opt-in after it, in the same words the settings section uses.
- The warning about widening no longer overstates how permanent it is. Switching to "Everyone with a Nextcloud account" said the recordings it opens will not be re-limited by switching back, which is still true and no longer the whole story: they can be limited again from the same settings section.
- A public viewer is read-only by construction: the marks in a recording are drawn, and no control that would add or remove one is shown. Previously the viewer hid tags entirely when there was no operator to write them.
- Tags that no vocabulary knows — every tag in a recording read on its own — are dealt distinct colours from the palette instead of each hashing its own id, which collided often enough to show several tags in one colour.
- Rewrote the root README around the Nextcloud app: what you get, what leaves your server, the meeting file, requirements, install steps, and the CLI and agent skill.
- Replaced the documentation site's pages with a set about installing and running the app, and added
docs/cli.mdfor the CLI from a checkout. - Renaming or restyling a tag uses the existing visibility-scoped background rewrite flow. Only recordings the caller can read are updated; inaccessible recordings keep their saved appearance.
- Graceful degradation on unpatched sherpa-onnx runtime. Parakeet v3 speech
transcription no longer halts when running against an upstream or unpatched
libsherpa-onnx-c-apiruntime. Instead of failing recognizer initialization,
Cassini logs an informative warning, gracefully falls back to the standard
decode profile (defaultVADDecodePolicy()), and recordsreferenceFrontend: false
in provenance metadata. This keeps transcription available when the reference frontend
is absent. - Cassini images now build the newest stable, officially signed FFmpeg source release selected at image-build time. CPU and CUDA image smoke tests gate the media capabilities Cassini requires instead of pinning a release number.
- Release images now build with the FFmpeg version the current
mainimage was already built and gated against, instead of resolving the newest upstream release again at tag time. Main keeps tracking the newest stable release, so an upstream regression still surfaces there first — but a release is never the first place a given FFmpeg appears, rebuilding a tag reproduces the same binary, and rolling back a release no longer silently rolls back FFmpeg with it. - Playback is shown in neutral rather than the accent colour: the word being played is inverted, and the play button, the scrubber and the Follow switch match it. The switch is called Follow at every width, turning it back on takes you straight to the audio, and the "Auto-scroll paused" badge is gone.
- A meeting reads with less around it: its details sit on two lines under the title, Summary, Insights and Transcript share one heading, and the transcript's search counts its matches and steps between them inside the field. The meeting always leaves a strip of the list beside it to click back to.
- The meeting list, the rooms rail and the Browse and Operator tabs follow the Cassini design. Dates name the day of the week, and who can open recordings is stated beside Rooms rather than among the filters.
- The Operator's sidebar is a drawer on a phone, and its settings pages sit on the page rather than in nested cards. First-run setup asks who can see recordings as a choice, and its problems read as warnings with a way forward.
- Prepare warns only about what stops it: a selection over the limit, or a meeting published in the older format.
- Portable meetings identify their recording from canonical compressed Opus packets, making integrity checks independent of decoder implementation details.
- Portable packing now converges on FFmpeg's stable Ogg end-trim granule before embedding that compressed identity, including multi-speaker mixes whose first metadata remux normalizes a few trailing samples.
- Portable word transcripts no longer carry origin roles or source links.
The packer, inspector, viewer, and static exporter accept entries with no role
and ignore legacy origin metadata. Display transcripts still pair with their
source words; withdrawn cleanup entries are skipped. The reference schema and
format documentation also drop unused chapters and duplicate meeting hints. - Packed meetings use the published portable meeting format:
CASSINI_FORMAT=org.cassini.portable-meeting/1, manifest"version": 1, and a resolvableCASSINI_PAYLOAD_SCHEMAserved at https://format.gocassini.com/. Readers accept this contract directly and reject any other format. - Portable meeting producers and readers now implement only the published version 1 contract. Transcript bodies contain word items, the media digest comes from
integrity.opusAudioSha256, and unsupported shapes are not repaired or upgraded. - Add opt-in recording-first scheduling: defer builds/sealing/publication while
recorders are active, and yield running builds without spending resource retry
attempts or losing captured media. - Serialize recorder finalization across subprocesses under recording priority.
- Credit conservatively reclaimable cgroup file cache during memory admission,
fixing waits caused by clean cache rather than memory pressure. - Add explicit concurrent/recording-first/auto policies. Auto permits recording
overlap using live host/cgroup CPU and memory headroom, with a GPU-aware host
CPU budget. Existing deployments retain concurrent behavior by default. - Simplified Go helpers and workflow context handling; shortened repetitive implementation comments.
- Moved the removed Python transcriber's architecture notes into
docs/history/. - Linux amd64/arm64 CPU builds now use prebuilt Parakeet v3 frontend libraries
fromcodemyriad/sherpa-onnx-go-linux v1.13.7-cassini.4, with matching upstream
v1.13.7 Go bindings. Normal developer, Docker and CI builds no longer compile
sherpa-onnx. CUDA source builds consume a checksum-verified fork commit instead
of applying a local patch. Other platforms retain stock libraries and the
graceful fallback. Operator runtime diagnostics coalesce concurrent probes
and refresh cached results, including unavailable runtimes, after 30 seconds. - Tag edits appear immediately after saving, while recordings update in the background. Delayed recording updates remain visible and can be retried.
- Pending tag edits now survive operator restarts. Keep the annotation database on durable storage; rebuilding it from recordings cannot recover edits that have not yet reached those recordings.
- Repairing a recording with the same audio restores saved tags. Replacing its audio clears the old annotations.
- Recordings skipped during initial annotation import retry automatically and when listed or opened.
- Large valid annotation documents can update their recordings; permanent recorder rejections show a blocked status instead of retrying indefinitely.
- An older background status error no longer clears tags saved by a newer edit.
- Rebuilding tag/search rows restores pending edits from the durable annotation database, even while the archive is unavailable.
- Browsing the app prepares newly seeded recordings for tagging, imports wait for download capacity instead of being skipped, and bulk tagging automatically retries temporary preparation responses.
Removed
- Word-level playback highlighting is gone from the meeting view. The transcript lit one word at a time and invited you to click a word to seek, which claimed a precision our transcripts do not carry. A line is the unit now: click any line to play from there. The "Exact words" toggle and the word/mixed/passage timing badge go with it.
- The LLM readable-transcript cleanup step, which was shelved. Transcripts publish as transcribed, with speaker attribution and paragraph breaks but no model rewriting the words. Gone with it:
CASSINI_READABLE_STRICT_BATCHES, the--strict-readable-cleanupbuild flag, thecassini-readablecomponent, and thetranscript.readable.v1.jsonproducer. Meeting summaries are unaffected and remain the only LLM step in the pipeline. Files that already carry a readable transcript still open: the portable format keeps reading them. - The Model override field. It accepted only the three models the quality
tiers already select, so it duplicated the tier control with no
discoverability — and pinning an int8 model alongside CUDA produced a run that
fragmented back onto the host CPU while reporting a GPU. Quality and device are
now the whole of the policy. Amodel_overrideleft in an existing
settings.jsonis ignored. - The Insights step in AI providers. It configured "run insights somewhere other than the summary endpoint" while looking like the switch that turns insights on, which it never was. The field is still persisted and served, so
PUT operator/settings/llmcan still point insights at a larger model. - The Prepare panel no longer says that a meeting "does not record whether it has a summary". It is true of every meeting published before the catalog carried the field, it changes nothing about what the bundle contains, and it buried the one gap that does stop a bundle being assembled.
- Removed the top-level
ops/directory; moved AppAPI registration helpers and batch processing scripts toscripts/, relocated production deploy tooling tocodemyriad-systems, and purged stray out-of-scope host configuration. - The Setup tab and its wizard are gone. Cassini no longer opens on a question that blocks every recording until somebody answers it, and a link to
#surface=setupnow lands on Browse. - Removed an accidentally tracked harness executable; harness scripts continue to run its Go source.
- Removed obsolete diagnostic reproduction tests; the production setup paths retain regression coverage.
Fixed
- Manifest validation now rejects environment overrides whose names appear only outside environment declarations or inside XML comments, preventing AppAPI from silently dropping those values.
- Tagging a selection of meetings now uses one atomic database batch and displays all updated rows together. Safe retries reuse the batch request ID; recordings continue syncing individually across the archive workers.
- CI no longer fails when a third-party apt repository preinstalled on GitHub's runner image serves a bad package index. Every workflow install step now runs
scripts/ci-apt-update.sh, which drops apt sources that are not the Ubuntu archive before updating, so a vendor we never install from cannot take a build down. On 2026-09-09 a corrupt Google Chrome index turned nine jobs across four workflows red onmainwithapt-get updateexit 100; the previous remedy namedpackages.microsoft.comat three of the nine call sites, and this replaces it at all of them. - Restarting the Cassini container no longer stops it publishing until an administrator disables and re-enables the app. Cassini checked how Nextcloud is set up on the app's enabled edge, and that check was the only thing that could mark the storage usable — so a plain restart left it reporting an unknown state and refusing to publish, with nothing obviously wrong. It now re-checks at startup as well, for installations that have already recorded which storage model they use. A first install is unaffected: Nextcloud rejects the calls until registration finishes, so that case still waits for the enabled edge, which fires moments later.
cassini-operator backfill-searchnow indexes recordings this Cassini has no local copy of, by reading the published recording itself. Before, a meeting whose job record or working copy had been lost — which is every meeting published by an earlier installation, since the recordings outlive the app's own volume — was skipped entirely and silently absent from search. On the demo archive that was 25 of 30 meetings. Those are indexed at coarser granularity, and each result says which kind it is.cassini-operator backfill-searchnow verifies a local transcript against the checksum the archive itself records for the delivered recording, instead of a digest from the job database that could never disagree with the local copy. A meeting whose local transcript belongs to a re-run that never published is detected and indexed from the published recording instead, so search only ever cites words that are in a recording you can play.- A read failure during
backfill-search— a timeout, a busy Nextcloud — no longer removes a meeting's existing search entries. It is reported as failed and re-running is the fix; entries only change on evidence about the recording itself. backfill-searchno longer reads the job database at all, so it can run while the operator is publishing; the old database-is-locked failure is gone. Where the delivery recorded a checksum, a re-run now confirms an unchanged meeting with one metadata query instead of re-downloading the recording.- Speaker attribution now calibrates a participant's noise floor across all of their packet streams. A rotation or rejoin produces a fresh stream, and a short one carrying only ongoing speech had that speech as its own floor — so the real speaker measured as barely audible and a quiet track carrying only their bleed won the word by 20 dB.
- Words keep their attribution provenance through LLM readable cleanup, so summaries actually exclude probable crosstalk. Readable cleanup and summarisation normally share one configured model, so losing the flag there made the summary protection a no-op in exactly the ordinary path.
- Filtering no longer discards older segments that carry text but no word list.
- A participant's noise floor is now the quietest floor any of their streams established, rather than a percentile over their pooled frames. Pooling was duration-weighted, so a long speech-only rejoin swamped the short stream that had actually seen the microphone idle, and crosstalk beat the real speaker again.
- Readable cleanup maps each rewritten word to the single best-overlapping source word, so one contradicted word can no longer flag a legitimate neighbour and delete it from the summary.
- Portable meetings now carry the per-word attribution evidence:
cassini packcopiesattributionGapDbandlowConfidenceSpeakerfromtranscript.words.v1.jsoninto the portable transcript items (andcassini extractwrites them back out) instead of stripping them from the published.opus. - The synthetic meeting generator now slides a turn that starts before the same speaker's previous one has finished, instead of aborting the whole run. Aborting happened partway through, leaving earlier participants generated and later ones missing — which is how the committed showcase fixture ended up with real speech for only two of its six speakers.
- Transcription tests now build meetings with production's shape — late-joining tracks, DTX-sparse tracks, and rejoined streams sharing one participant id — instead of tidy tracks that all start together and are fully dense. The previous fixtures could not express the conditions that two thirds of production streams actually have.
- The meeting list and header now show the date the meeting was recorded, not the date its file was last processed. Reprocessing an archive no longer restamps every meeting with the rebuild day.
- Meeting list cards now show the meeting's start time alongside its date.
- The viewer now shows when two people spoke at once. Overlapping speech used to render as ordinary consecutive paragraphs with no signal at all, and because a turn is cut at every speaker change, a listener's "right" in the middle of someone's sentence split that sentence across two paragraphs with an unrelated turn wedged between them — so a continuous utterance read as two turns and an interruption. An interrupted turn is now drawn as one turn containing the interjection, each turn carries a badge naming who it overlapped and for how long, and playback highlights every turn that is sounding rather than only the latest one to start.
- Overlap is judged on the words that actually sound rather than on paragraph extents, and a word whose end ran past its own audio is pulled back before anything is drawn. Without that, meetings recorded before the timing fix would light up with overlap that never happened: across nine archived meetings their paragraphs intersect for 94.2 s while their words only sound together for 15.7 s. A meeting whose recording already measured where each word ends says so in its own file, and the viewer leaves its timings exactly as recorded — so a word somebody genuinely held for a second and a half stays that long.
- A word now ends where its speaker's own audio ends. Parakeet stamps a trailing punctuation mark at the next acoustic onset, and the token-to-word conversion took that mark's timestamp as the word's end — so a sentence-final word could run for seconds while its speaker was silent, and across whoever spoke during that pause, which the viewer then drew as an overlap that never happened. The mark still travels in the word's text; it no longer decides the word's extent. Instead the energy gate that already decides whether a word is real follows the speaker's own track forward from the last spoken token and stops at the last active 10 ms of it, bridging a silence of up to 200 ms as a stop closure inside the word. A word built entirely from punctuation carries no audio of its own and is never extended, and no word may pass the end the old rule gave it. Measured against unmodified
mainon a 36-minute three-participant meeting (5549 words, the production fp32 model), the longest word falls from 4.07 s to 1.44 s and cross-speaker overlap stamped over a silent track from 4.18 s to 1.69 s, against 1.37 s of audible speech given up across the whole meeting, two words of it losing 100 ms or more. Those are whole-release totals; the final refinement alone — dropping an 80 ms grace after the last active frame — accounts for 0.38 s of that speech and for no word losing 100 ms. - Playing a long meeting no longer pins a processor core. The viewer rebuilt the entire transcript's highlight sixty times a second and re-checked every word on screen in order to move one of them, so on the longest published meetings playback ran at about twenty frames a second, the page stuttered while scrolling, and the highlight visibly trailed the audio it was meant to track. The highlight is now worked out once per transcript and touched only when the playhead actually crosses a word, which took the worst meeting in the archive from using most of a core to using under a tenth of one, at a steady sixty frames a second. Clicking a word to seek costs about half what it did on the same meetings. The longer you listened the worse it got, too: playback accumulated duplicate timers until it was running forty-four clocks where one was needed, so a five-minute listen was heavier per second than a one-minute listen. Memory and timer count now stay flat — measured over a continuous five-minute listen on the largest meeting.
- A long answer is paragraphed again. Rebuilding a turn from the pieces the producer cut also undid the breaks it makes when somebody talks for a while, so one standup update rendered as a single 209-second block of text (the longest rows on that meeting were 209 s, 101 s and 97 s against a median of 8.7 s). The reading unit now follows those breaks where nothing came between two of the speaker's blocks, and overrides them only where somebody else's speech cut a sentence in half, which is the case this rebuilding exists for. Same meeting: longest row 58 s, median 10.4 s.
- A quick back-and-forth is no longer folded into one turn. Rebuilding a speaker's turn now needs the person who spoke in between to have actually overlapped or touched it: if they said their piece in clean air and stopped, both people kept their own turn, in the order they spoke. Without that, five real turns of a fast exchange collapsed into one row with an answer rendered before the question it answered.
- Playback follows the paragraph it is reading. It used to follow whichever producer fragment was sounding, so during two people talking over each other the page smooth-scrolled between the pieces of the same rebuilt paragraph every few hundred milliseconds.
- The "probably crosstalk" warning is judged once over everything a turn actually said, instead of once per fragment. One flagged fragment no longer condemns a whole genuine turn, and a short reaction that is entirely flagged keeps its warning now that it renders as a chip.
- The viewer now shows the meeting summary the recorder seals into the portable
.opus. The summary was already inside the file (thesummary.mdattachment thatcassini meetingsreads), but the web viewer and the in-Nextcloud app never displayed it, so a meeting with a summary opened as transcript-only. It renders as Markdown in the existing Summary panel; a file without one, or with a damaged attachment, still opens without a panel rather than failing. - Selecting Fast now works on a CPU host. The pre-build environment check
demanded transducer model files of every non-fp32 model, so the 110M CTC
model — which ships a singlemodel.int8.onnx— failed validation although it
was bundled and correct. - An int8 model can no longer be pinned alongside CUDA. Those graphs fragment
back onto the host CPU under the CUDA execution provider, so the combination
reported a GPU run while doing something else; it is now refused when saved
and when a build is admitted. - A stored
device_override=cpuis preserved instead of being erased on every
operator restart. Pinningcudaon a host without a usable GPU still blocks
the build with an actionable message rather than silently using the CPU. - Filter values no longer appear in
cassini meetingserror output. The request URL is printed when a call fails, and its query string was not being masked. - Routine recording stops no longer trigger an error banner in the operator view.
Successful runs with exit code 0 and normal stop reasons do not display the red error alert with internal runtime cancellation messages. The view displays an error alert only when a run or attempt actually fails. - Recordings from Nextcloud Talk display the call room token.
The operator view reads the Talk base URL and room token from the trigger request. Sidebar cards and run details now show the call token and a direct link to the room instead of generic placeholder text. - A meeting link's timestamp survives a trip through the Operator surface. The shell writes its own hash params — the selected run, and now the selected settings panel — after the viewer's
t=, and the viewer only readst=when it is the last thing in the URL. So opening a recording at a moment, stepping into Operator, and coming back left a link that still named the moment but no longer jumped to it. The shell now keepst=last, the way the viewing layer already does when it writes a link of its own. - Registering an AI provider is now the whole of the setup an insight needs. Before this, an administrator could save an endpoint and still be told "No AI endpoint is available" everywhere in the app, because an insight only resolved an endpoint through a pipeline step somebody had also switched on. An insight now resolves, in order: the endpoint its own step names, then the summary step's, then any registered provider — and the operator hands that resolved endpoint to the run, so what the settings page says and what the run receives cannot disagree. The rule that follows is stated in the AI providers panel and in
docs/privacy.md: every endpoint you register is one an insight may reach, and removing them all is how insights are switched off. Summarising stays a separate opt-in that runs on every recording. - An insight document no longer opens with a paragraph of YAML. The recorder writes its provenance — which meetings, which prompt version and hash, which model — as front matter at the top of the file, and markdown renders
---as a horizontal rule, so twenty quoted keys arrived on screen above the answer somebody asked for. The provenance is still in the file, and the panel shows the same facts in its header and its "This run" list. - Hardened AppAPI registration lifecycle handling in
scripts/lib-exapp-register.sh: stop immediately if unregister fails prior to re-registration, verify disabled status before re-enabling during enable cycling, reject arbitrary moving or non-release image tags by default, and requirejqwith valid manifest index parsing in preflight. - Restored word-level playback highlighting, hover and click-to-seek in the meeting viewer, including simultaneous speakers and inline interjections. Keyboard activation seeks to a focused word without also toggling playback; cleaned and untimed transcript text remains intact.
- A search across meetings no longer lets one talkative meeting fill the whole page of results and hide every other meeting that matched.
- A summary or insight the model stopped writing at its output limit is no longer recorded as a success. The run fails and says the answer was cut off at the model's output limit, so a half-written document is never published as a whole one.
- The AI endpoint picker now works on a plain compose deployment too: the
/ai/providersroutes were mounted only under the ExApp base path, so the Prepare panel's provider list 404ed there. - Two insight runs can no longer run at once on one request: a retry whose row had been failed by the stale-run sweep and retried again used to start beside the newer attempt.
- An insight stopped by an operator restart now says so and asks you to retry it, instead of claiming it took longer than an hour.
- A finished insight whose outcome could not be written down is retried a few times before giving up, and if it still fails the log names the delivered document so it can be found.
- A succeeded insight whose document you have since moved or deleted now says where it was, rather than "its document is empty".
- Meeting-context refusals are never cached by the app proxy, and a meeting list that could not be read answers "unavailable" rather than "not one of yours".
- The operator no longer logs a false "publish sink unavailable; using local" at startup. Recordings were always delivered to the configured destination; the message was wrong, and it pointed anyone reading the log at a storage problem that did not exist.
- An insight run always asks the chosen provider for that provider's model; it can no longer inherit the summary step's model on a different endpoint.
- When the AI endpoints or a model list cannot be read, the app now shows the operator's own error instead of a generic HTTP status. A provider base URL ending in
/chat/completionsor/modelsis refused with a hint to give the API root. - A non-admin's "No AI endpoint" card in the Prepare panel refreshes each time the panel opens, once an administrator has configured one.
- The AI providers form warns when an OpenRouter key is pasted without its
sk-or-v1-prefix, which used to save cleanly and then fail every run with a 401. - Finding more than one word inside a meeting now works. Typing a phrase used to return nothing at all.
- Ctrl+F (⌘F) now opens the transcript find instead of Nextcloud's own search popup.
- Narrowing the meeting list by room or tag now applies to search results too. Searching and then picking a tag used to leave meetings from outside the filter in the list, which looked as though the filter had stopped working.
- Changing the room or tag while a search is active now re-runs the search instead of leaving the previous answer on screen.
- Fixed selected tags being silently replaced by an existing case-insensitive label match. Meetings can retain multiple selected tag IDs, including legacy case variants, and the tag vocabulary now combines the same ID across recording namespaces. [D-773]
- A meeting's header no longer writes down facts it does not have: a recording with no room said "No room", and one with no date showed its own filename under a calendar icon.
- Applying an existing tag to another recording now copies its visible colour and icon into the new recording, including when tagging a selection. Existing local styles remain unchanged.
Standalone viewers preserve saved tag colours and explicitly cleared icons.
Annotation database upgrades preserve pending edits when moving from either
the main or portable-style schema.
The portable-file reader retains saved colour and icon fields, so embedded
recordings show their archived appearance instead of fallback grey tags. - Guest participants in Nextcloud Talk meetings are now attributed under the display names they entered in the Talk UI rather than synthetic placeholder identifiers (such as
participant-sXHFMabV). The recorder resolves decoupled Nextcloud room session IDs and actor IDs against active WebRTC signaling sessions, retains attendee display names delivered in presence updates, and upgrades early synthetic placeholder names when the participant's chosen name arrives. - Prevented FFmpeg 4.4 crashes when a participant audio track starts several minutes into a meeting.
- Prevented repeated native decoder failures from filling the ExApp container with multi-gigabyte core dumps.
- Kept readable truncated recordings buildable when FFprobe reports a non-fatal container warning alongside a valid packet timestamp.
- Keyboard focus moves into a meeting, an insight or Prepare when it opens and stays there: the page underneath can no longer be reached with Tab or read by a screen reader. Their close buttons are larger.
- Controls, borders, and hover and selected states stay visible in Nextcloud's high-contrast themes.
- The meeting and insight drawers stay readable in Nextcloud's high-contrast themes.
- Nothing in the list hides under the selection bar when the bar grows a second line, and an insights listing that failed reads as an error.
- Reduce missing speech in Parakeet v3 transcriptions by matching the model’s reference audio preprocessing. Preserve detected utterances with surrounding recorded audio instead of splitting them every ten seconds and appending synthetic silence. Retain beam decoding and vocabulary hints.
- Keep participant audio on the meeting clock after long mute gaps and codec pre-skip, including recordings with a nonzero container start time.
- Fix duplicated overlap words when adjacent decodes disagree only on sentence-final punctuation.
- Include the corrected native runtime in CPU/CUDA builds and the developer CLI.
- Add recorded-audio comparisons and resumable full-meeting GPU validation tools.
- Automatic participant-name hints no longer overwhelm uncertain audio. They use a capped decoder score of 0.5 while operator-entered vocabulary keeps its configured score, and each participant's own name is omitted from their track. This prevents short names such as “Silvio” from repeating hundreds of times while retaining useful spelling hints for other speakers. Published provenance records the participant-name policy and its effective score.
- A portable
.opuswhose transcript body cannot be read is no longer called a broken file. That transcript is unavailable, named in a warning, and the meeting, the speakers and the other transcripts still read.cassini inspectstill exits non-zero. - A repeated load-bearing tag now stops
cassini inspectfrom printing any part of the manifest. It used to declare the metadata invalid and then print the title, the meeting id, the speakers and every descriptor anyway. - Transcript bodies are verified against the manifest's
payloadRefrather than theCASSINI_TX_*tags that copy it. A corrupted body whose tag happened to agree with it was accepted, and a good body whose copied tag had drifted was rejected; a disagreement is now a warning. - The transcript a reader opens is the one
cassini inspectmarksdefault=yes, including in a file where no entry carries the flag and array order decides. words=is the number of items read out of the chunk set, not the count the body declares.- Model ids containing
_no longer produce a transcript id the packer rejects, which let a bundle build and then fail at packing. - The exported transcript JSON keeps one segment per speaker turn. It used to collapse a multi-speaker transcript into a single segment with no speaker at all.
cassini inspectexits non-zero on unusable manifest metadata instead of printing a summary and reporting success.- Portable
.opusfiles whose chunk sets carry base64url=padding now read. Producers write the encoding unpadded, but most base64url encoders pad by default, so a file written by another implementation was rejected outright as invalid metadata. - A transcript's chunk count and the default transcript now come from the manifest rather than from the
CASSINI_TX_*_CHUNK_COUNTandCASSINI_TRANSCRIPT_DEFAULTtags that mirror it. A file whose two layers had drifted lost a transcript that was entirely present, returned a silently truncated one, or served a different transcript than the manifest names; the disagreement is now reported and the file still reads. cassini inspectreports the word count it actually decoded out of a portable.opus, names any transcript whose body it could not read, and exits non-zero. It used to print the manifest's declaredwords=900andcassini=okfor a file whose transcript was unreachable.- A meeting's
words=is the default transcript's count rather than the sum across every transcript in the file. Alternative transcripts are further passes over the same speech, so a three-word meeting published with two passes was reported as six words. - Keep recording-setup edits responsive during background status refreshes and
prevent older responses from replacing newly saved configuration. - Corrected installation and development guides for CPU transcription, the unified Cassini app, recording access, and optional summary/insight data sharing.
- Fixed the app’s development proxy so the Operator section, settings and storage controls can reach a root-mounted operator API.
- Fixed readiness requests and proxy path boundaries, rejected fallback/login responses in Operator detection, and stopped storage setup when its refreshed plan no longer includes the selected mode.
- Replaced the homepage’s unsupported recording command with the documented CLI syntax.
- Fixed concurrent atomic file writes by giving each writer its own temporary file.
- Aligned architecture and API docs with the unified app and two-service Compose bundle, and corrected standalone publish examples.
- The operator now hands its Talk recording secret to the recorder it starts. Since D-447 the operator generates that secret when the administrator sets none, and the recorder never received it, so on such installs Talk showed a working recording backend and every recording failed with "talk auth mode hpb-internal requires CASSINI_TALK_RECORDING_SECRET to be set".
- Fix the embedded viewer showing no transcript for newly published portable
version 1 meetings whose transcript bodies live in separate OpusTags chunks. - Build: the ExApp image installs the web workspace from the root
package-lock.jsonwithnpm ciinstead of resolving it afresh from the registry on every build. On 2026-09-03 the fresh resolution tripped an npm 10.9 bug (Cannot read properties of null (reading 'edgesOut')) on trees that had built minutes earlier.
Security
- Search results never carry transcript text, and the search index cannot return the words it indexed. A search query is kept out of the operator's request log, where it would otherwise sit indefinitely.
- A
tagfilter on the meeting list is kept out of the operator's request log, as search queries are. - A search term or tag no longer appears in the CLI's error output when the connection to Nextcloud fails before it answers.
- Tags travel with a recording. Anyone you share a tagged recording with can see who tagged what, by Nextcloud user id.