@gotcos/glasses-server 6.71.13 → 6.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -238,12 +238,11 @@ BIND_HOST=0.0.0.0
238
238
  # COS_MEETING_PROGRESSIVE_HQ=1
239
239
  # COS_MEETING_PROGRESSIVE_HQ_THREADS=2 # optional lower override; cannot raise the tier cap
240
240
 
241
- # Experimental HQ timing and Meeting Ask evidence. Both default OFF. The native
242
- # timestamp candidate uses CLI JSON plus its own VAD mapping; live JSON and DTW
243
- # retain their crash protections. Ask additionally requires progressive HQ.
244
- # This path FAILED the 30-second live refinement gate on the test machine;
245
- # human-corrected fact accuracy is also pending. Do not enable for public use.
246
- # COS_BATCH_HQ_TIMESTAMPS=1
241
+ # HQ timing and Meeting Ask evidence. Since 6.72.0 HQ timestamps follow the HQ
242
+ # setting (on for "large-v3"); COS_BATCH_HQ_TIMESTAMPS is an override (see the
243
+ # Speech-to-text section below). Meeting Ask HQ evidence stays default OFF and
244
+ # additionally needs COS_BATCH_HQ_TIMESTAMPS=1 and progressive HQ: it FAILED the
245
+ # 30-second live refinement gate on the test machine. Do not enable for public use.
247
246
  # COS_MEETING_ASK_HQ_EVIDENCE=1
248
247
 
249
248
  # Meeting-wide Fact check (6.71.13). Default ON: the first Fact check in a
@@ -367,3 +366,14 @@ COS_CONTINUE_FULL_PERMISSIONS=1
367
366
  # Unset uses Homebrew then PATH. An invalid explicit path fails closed.
368
367
  # COS_WHISPER_CLI_BIN=/absolute/path/to/whisper-cli
369
368
  # COS_WHISPER_SERVER_BIN=/absolute/path/to/whisper-server
369
+
370
+ # ── Speech-to-text settings (6.72.0) ────────────────────────────────────────
371
+ # The engines, front end and quiet gate are chosen in <data>/stt-settings.json
372
+ # through GET/PUT /api/stt-settings (COS Control 0.5.289, COS Glasses 6.10.639).
373
+ # These variables only move files or force a mode.
374
+ # COS_BATCH_HQ_TIMESTAMPS=1 # 1 forces HQ timestamps on, 0 forces the legacy decoder, unset follows the HQ setting
375
+ # COS_PARAKEET_DIR=~/.cos-glasses/data/stt/parakeet # where Parakeet installs (CLI copy, models, CoreML cache)
376
+ # COS_PARAKEET_LOCAL_SOURCES=/path/a:/path/b # folders holding verified model copies to install from (hash-checked)
377
+ # Drop-in alternative: link a folder holding parakeet-ultra/ to
378
+ # <data>/stt/parakeet-sources and an install started with {} uses it, no download.
379
+
package/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## [6.72.0] - 2026-10-13 - Speech-to-text switcher, front-end fixes, Parakeet Ultra as an option
2
+
3
+ Paired clients: COS Control 0.5.289 (Settings, Transcription) and COS Glasses 6.10.639 (Settings, Voice). Older clients show no switcher, but the new defaults below apply to their meetings too; change them with the routes or `<data>/stt-settings.json`.
4
+
5
+ - New settings route, token-protected: `GET /api/stt-settings`, `PUT /api/stt-settings` (partial body), `POST /api/stt-settings/parakeet/install` and `DELETE` on the same path to cancel. Stored atomically in `<data>/stt-settings.json` and read on every use, so nothing needs a restart: the preview, front end and quiet gate change from the next live chunk; HQ, second opinion and resolver from the next saved meeting (the plan is taken when the meeting is saved, or when an orphan recovery is accepted). An unreadable file runs the defaults and is logged once. Unknown or unavailable choices answer 400 in plain words. Clients hide the switcher on a 404. GET gives every slot its `effective` engine with a plain `reason` and a `reasonCode`, the last HQ job's outcome (`lastJob`: windows lost, primary and timestamp fallbacks, second-opinion failures, checkpoints reused), and Parakeet install progress and preview breaker state. README, "Speech-to-text settings (6.72.0)".
6
+ - Defaults: preview `turbo`, HQ `large-v3`, second opinion `off`, resolver off, front end `highpass`, quiet gate `silero`. A stored choice that is no longer possible falls back: the preview to Turbo, HQ to Large-v3, the second opinion to off.
7
+ - HQ `large-v3` decodes with timestamps and maps every token through Whisper's own VAD segment table (the 6.71.x `COS_BATCH_HQ_TIMESTAMPS` path, now the default; the variable still overrides). `large-v3-legacy` keeps the pre-6.72 decoder. A timestamp decode that fails is retried with the legacy decoder, so a timing problem never costs a window its text; `stt.fallbackReason` records it. The progressive-HQ checkpoint identity includes the timing mode and the front end.
8
+ - ASR front end: `highpass` (80 Hz highpass only) replaces highpass, afftdn and loudnorm for live chunks and HQ windows; `full` restores the old chain. Playback audio, dictation and prompt HQ are unchanged. The Whisper meeting previews keep raw audio; the Parakeet preview hears the selected front end.
9
+ - Quiet gate `silero`: a chunk with under 0.5 s of speech from a dedicated in-process Silero (probability 0.8, minimum speech 0.25 s, fed in 512-sample windows, about 17 ms a chunk) gets no prompt and no enhancement; `rms` restores RMS below 150. Bake-off canary 5 on the 10,091 chunks still on disk: fires on 80.8% of Silero-negative chunks and 0.91% of speech chunks (RMS 150: 9.7%); chosen on that table, and neither half of the sessions passes alone (67.8% and 91.8%). The silence-hallucination text drops stay on the RMS test, because a chunk with one short real name can be Silero-quiet.
10
+ - Parakeet Ultra (FluidAudio CoreML, Neural Engine, offline) is installable and selectable as the HQ engine, as the HQ second opinion and as the live preview. Parakeet TDT v2 was evaluated and removed: it gave no benefit Ultra cannot. The install copies or downloads pinned artifacts and checks every file's SHA-256: fluidaudiocli 0.17.4 (`47141546`, never downloaded: FluidAudio ships no binary, so the CLI must be the build speaker separation uses) and Parakeet Ultra `95eaa59a`, 654 MB in all. A verified local copy is used first: the body's `localSource` (a named source that lacks a file fails plainly unless `allowDownload` is true), `COS_PARAKEET_LOCAL_SOURCES`, the drop-in folder `<data>/stt/parakeet-sources`, or FluidAudio's cache. A download with no data for 60 s is abandoned; `progress.source` says what came from where. `installable` is false on a Mac without the pinned CLI build, and 409 while a recording is live. A crash in the middle of a reinstall is undone at the next start.
11
+ - Every Parakeet run is under `sandbox-exec` with network denied and writes allowed only to the run's directory, the private CoreML cache and the per-user temp directory (FluidAudio buffers audio over about 15 s there), with `--local-model-dir`, a key-free environment and the CLI's full hash checked once per process. Clips under 1 s are padded (FluidAudio returns nothing for 0.1 s). HQ runs are queued one at a time (180 s first deadline, then 30 s); two failures in a row stop Parakeet for the rest of the job; a failed or empty-over-speech Parakeet HQ window is decoded by Large-v3; when Whisper fails both ways and Parakeet is the planned second opinion, Parakeet becomes that window's primary. Parakeet children are stopped on shutdown. No language model is ever called.
12
+ - Second opinion and resolver (HQ only, algorithmic, off by default): the second engine hears the same front-end audio; a progressive checkpoint made under the same settings is reused as the primary so only the second opinion is decoded; the two hypotheses are aligned word by word. The resolver takes the second opinion only for words a Whisper primary has nothing for, inside a span the primary's own VAD marked as speech and with mean word confidence at least 0.5, and for a profile vocabulary term the primary missed inside a short disagreement. It never changes a region that differs in a negation, a number or a unit, whatever its length. Both raw hypotheses and every disagreement, with what was kept, are saved with the meeting (`batchSegments[].stt`, sidecar `stt`; orphan recovery adds `stt` and `sttSegments`).
13
+ - Evidence (silver, stratum B only, shipped code at production 30 s windows; `docs/stt-672-evaluation.md`): the resolver with Ultra moved WER -1.6% (CI -4.7 to +1.1) against Large-v3 alone and with v2 -4.2%, so neither passed the 10% adoption rule and the second opinion ships off. Parakeet Ultra as the HQ primary cut WER 13.24% to 11.76% on SILVER and 19.21% to 12.69% on SILVER-E, with entity recall up and no collapsed windows, but its SILVER interval reaches +2.6%, so it is offered, not the default. In the 10/13 fixtures no resolver configuration changed a negation or a unit. On CPU (the default HQ device) Large-v3 HQ alone runs over the 0.25 compute budget (1.19x real time on a loaded Mac); on Metal the default is 0.17.
14
+ - Preview options: `turbo` (default) and `parakeet` (Parakeet Ultra, experimental). `large-v3` and `nemotron-560` are listed as unavailable: a Large-v3 preview would share the live worker that writes the saved text, and FluidAudio runs Nemotron only as a whole-file batch. A client gets a Parakeet preview only if it lists `parakeet-ultra` in the `previewModels` query parameter (COS Glasses 6.10.639 does); every other client gets Turbo, so phones before 6.10.639 never see a model they reject. Parakeet gets 0.9 s, then Turbo only while the phone's 1.8 s budget allows; three misses open a breaker (Turbo for 1 minute, doubling to 10). The warm-up is deferred while a recording is live. On the shipped path the Parakeet Ultra preview passed the live-lane rule against Turbo (WER 15.71% to 14.04%, relative -10.7%, CI -18.4 to -1.4; entity recall unchanged; 1 of 40 clean non-speech units with text for both; request p95 0.68 s on a loaded Mac); Turbo stays the default and Parakeet is selectable. The preview setting drives only the meeting preview lane, not dictation.
15
+ - Tests: new suites for the settings route, Parakeet install, runs and the OS-enforced sandbox, the resolver, the live gate (with a synthetic speech fixture and the real Silero model), the HQ plan, fallbacks, checkpoint reuse and production call sites, and the preview dispatch. A real POST through the chunk route proves the silence text drops stay on RMS when Silero calls a loud chunk quiet. Mutation gate: `node server/scripts/stt-mutation-gate.mjs` (repository only), green baseline first (101 tests in 9 files), each target matched exactly once: 71 of 71 killed.
16
+
17
+ ## [6.71.14] - 2026-10-13 - A finished capture no longer reads as a live recording
18
+
19
+ - Root cause. Boot recovery of persisted meeting sessions (`active-sessions/<id>.json`) and of closed-session tombstones ran only if the maintenance gate was already open when the module loaded. A managed update boots behind a closed gate, so recovery was skipped for the life of the process. A persisted session was then neither adopted nor tombstoned: the minute sweep quarantined its audio, auto-recovery receipted it, and its session file stayed on disk. On 2026-10-12 that left a receipted two-chunk capture listed by COS Control as "Live now, No audio for 1054m" for 17 hours, while the status route answered `missing`.
20
+ - Recovery now runs once, when the gate opens. It runs the boot orphan-audio sweep and quarantine purge with it, never overwrites a session already live in memory, and does not quarantine a live session's audio (`isLive` also checks the session map). The minute sweep waits until recovery has run.
21
+ - Boot recovery moves aside (reason `terminal_capture`), rather than adopts, an active-session file that is the leftover of a capture whose quarantined audio was already decoded: recovered into a meeting or receipted no_speech. A capture that is only quarantined, never decoded, is adopted as before, which covers a crash between a chunk's first audio write and its first session write.
22
+ - No new chunk refusal. The phone treats any `410 session_closed` as "already saved" and deletes its local copy, so the server does not refuse a chunk because its capture is quarantined, recovered or receipted; such a chunk starts its own session and keeps the audio. The existing 4-hour closed-session 410 is unchanged, and because tombstones are now restored after a managed boot it applies there too, as it already did after an ordinary restart. Boot recovery adopts a session that resumed under a receipted id: a file counts as a leftover only when the quarantine is no older than its last activity and it has no chunk audio of its own, so a meeting is never split across a restart.
23
+ - New minute-sweep step, in its own try beside the orphaned-audio sweep: an `active-sessions/*.json` whose session is not live and has been idle at least 4 hours (`LOCAL_FIRST_MEETING_IDLE_RETENTION_MS`) is moved to `data/stale-active-session-files/<id>.<ms>.json`. A `.reason.json` (reason `not_live_idle_expired`) is written atomically first; if it cannot be written nothing moves. Idle time uses the earlier of the file's `lastActivityAt` and its mtime, so a future-dated value cannot keep a dead file. Moved files are removed 7 days after the move time in their name, never by mtime. A live session's file is never touched. A failed move logs once per session on stderr and leaves no reason file behind; a purge that removes files logs the count.
24
+ - `GET /api/meeting/sessions/:sessionId/status`: `state` keeps its 6.71.13 values and precedence (`saved`, `active`, `closed`, `missing`). Two additive fields: `outcome` (`receipted`: decoded, no speech; `recovered`: decoded and saved under a file the store no longer finds by session id; `quarantined`: audio held, not decoded yet) and `disposition` (`kind`, `reason`, `quarantinedAt`, `quarantineReason`, `receiptedAt`, `chunkFiles`, `savedFilename`, `recovering`, `audioRetainedUntil`). Both are null when `state` is `saved` or `active`, and when the quarantine holds nothing for the id. They cover every quarantine dir for the id: any undecoded one means `quarantined`. Older clients ignore both.
25
+ - Until recovery has run, the status for an id with no saved meeting answers `503 {"reason": "recovery_pending", "retryable": true}` with `Retry-After: 5`, instead of `missing`, and logs that once. Phone status callers treat a thrown non-2xx as defer-and-retry (the uploader's finalize defers; origin-loss recovery backs off). A saved meeting is answered normally.
26
+ - For COS Control: a gone file whose status is `closed` or `missing` with `outcome: "receipted"` can be shown as "No speech recorded", `recovered` as the saved row lookup, and `quarantined` as "Recovering capture". Control 0.5.287 ignores `outcome`.
27
+ - Tests no longer read the live maintenance gate under ~/Library: `vitest.config.ts` points `COS_MAINTENANCE_GATE_PATH` into the isolated data home.
28
+ - Mutation gate: `node server/scripts/stale-session-mutation-gate.mjs` (40 mutants over recovery, the boot sweep, the minute sweep and purge, the status route and the disposition reader; green baseline first).
29
+
1
30
  ## [6.71.13] - 2026-10-13 - Meeting-wide Fact check and follow-ups (candidate)
2
31
 
3
32
  - Fact check (the CTA, or a typed bare "Fact check") checks every checkable public claim in the meeting the first time, at most five, each in its own "Claim" row with a verdict (Supported, Mostly supported, Mixed, Contradicted, Unverified) and citations. Each later Fact check scans only what was said after the previous completed check. The span is on the durable exchange as `factCheckRange {fromMs, throughMs, full, claims}` in meeting-elapsed ms; `capabilities.factCheckScope: 1` advertises it. Older clients ignore both; transport stays `action=ask`. A named claim ("Fact check the flight to KC") checks that claim only.
package/README.md CHANGED
@@ -359,7 +359,10 @@ the false fences 6.53.1 left, and never releases a fence when `ps` cannot answer
359
359
  Meetings: a closed meeting's record ages out after **4 hours**, after which its status
360
360
  reads `missing` and the phone stops asking to save it. Saving a known meeting that has no
361
361
  transcript answers **404 `nothing_to_save`** (with its state); only an unknown id answers
362
- `session_not_found`.
362
+ `session_not_found`. From 6.71.14 a `missing` or `closed` capture whose audio reached the
363
+ unsaved-audio quarantine also carries an `outcome` (`receipted`, `recovered` or
364
+ `quarantined`) and a `disposition`, and for an id with no saved meeting the status answers **503 `recovery_pending`**
365
+ until boot recovery has run (see [Finished captures never read as live](#finished-captures-never-read-as-live-67114)).
363
366
 
364
367
  ## The summary behind the prompt box (6.54.0)
365
368
 
@@ -640,6 +643,153 @@ the warm-up and the final passes. `/api/health` is public, so this carries codes
640
643
  and counts only, never a speaker name, path or error text; those stay in
641
644
  `server.log`. A Nemotron fallback never degrades `readiness.status`.
642
645
 
646
+ ## Speech-to-text settings (6.72.0)
647
+
648
+ One settings object chooses which engine previews live meeting speech, which
649
+ engine writes the saved meeting (HQ), whether a second engine gives HQ a second
650
+ opinion, whether the resolver may use it, the ASR front end and the quiet gate.
651
+ Paired clients: COS Control 0.5.289 (Settings, Transcription) and COS Glasses
652
+ 6.10.639 (Settings, Voice). Both hide the card when `GET /api/stt-settings`
653
+ answers 404 (an older server). Older clients show no switcher at all, but the
654
+ server's defaults below still apply to their meetings; change them with the
655
+ routes, or by editing `<data>/stt-settings.json`. The routes are token-protected
656
+ like every `/api` route.
657
+
658
+ ```
659
+ GET /api/stt-settings
660
+ PUT /api/stt-settings partial body, e.g. {"hq":"parakeet-ultra"} or {"frontEnd":"full"}
661
+ POST /api/stt-settings/parakeet/install 202 {started:true, ...GET}; poll GET for progress
662
+ body {} | {"localSource":"/abs/path"} | {"localSource":"/abs/path","allowDownload":true}
663
+ DELETE /api/stt-settings/parakeet/install cancel a running install; nothing partial is kept
664
+ ```
665
+
666
+ GET returns (abridged):
667
+
668
+ ```json
669
+ {
670
+ "preview": {"selected": "turbo", "effective": "turbo", "reason": null, "reasonCode": null,
671
+ "models": ["large-v3-turbo", "parakeet-ultra"], "clientParam": "previewModels", "requiresApp": "6.10.639",
672
+ "note": "...", "options": [{"id": "turbo", "label": "Whisper Turbo", "available": true, "experimental": false, "note": "..."}]},
673
+ "hq": {"selected": "large-v3", "effective": "large-v3", "reason": null, "reasonCode": null, "options": ["..."]},
674
+ "secondOpinion": {"selected": "off", "effective": "off", "reason": null, "reasonCode": null, "options": ["..."]},
675
+ "resolver": {"enabled": false, "effective": false, "reason": null, "reasonCode": null},
676
+ "frontEnd": {"selected": "highpass", "options": ["..."]},
677
+ "quietGate": {"selected": "silero", "options": ["..."]},
678
+ "parakeet": {"installed": false, "installable": true, "version": "Parakeet Ultra 95eaa59a, CLI 0.17.4", "bytes": 0,
679
+ "progress": {"state": "idle", "bytesDone": 0, "bytesTotal": 654356591, "error": null},
680
+ "warm": {"preview": "cold"}, "preview": {"breaker": {"open": false, "retryAt": null, "misses": 0}, "counters": {}}, "note": "..."},
681
+ "lastJob": {"at": "...", "meeting": "...", "outcome": "batch", "summary": {"windows": 12, "primaryFallbacks": 0, "timestampFallbacks": 0, "secondOpinionFailures": 0, "windowsLost": 0}},
682
+ "appliesTo": {"preview": "next live chunk", "hq": "next saved meeting"}
683
+ }
684
+ ```
685
+
686
+ | Setting | Ids (default first) | Applies from |
687
+ | --- | --- | --- |
688
+ | `preview` | `turbo`; `parakeet` (Parakeet Ultra, experimental, once installed); `large-v3` and `nemotron-560` are listed with `available: false` (a Large-v3 preview would share the live worker that writes the saved text; FluidAudio 0.17.4 runs Nemotron only as a whole-file batch) | next live chunk |
689
+ | `hq` | `large-v3` (timestamps on, mapped through Whisper's own VAD segments), `large-v3-legacy` (the pre-6.72 decoder), `parakeet-ultra` (experimental) | next saved meeting |
690
+ | `secondOpinion` | `off`, `parakeet-ultra`, `large-v3` (for a Parakeet HQ) | next saved meeting |
691
+ | `resolver` | `false`, `true` | next saved meeting |
692
+ | `frontEnd` | `highpass` (80 Hz highpass only), `full` (highpass, afftdn, loudnorm) | next live chunk and next saved meeting |
693
+ | `quietGate` | `silero` (under 0.5 s of detected speech), `rms` (RMS below 150) | next live chunk |
694
+
695
+ - Stored atomically in `<data>/stt-settings.json` and read on every use, so
696
+ nothing needs a restart. An HQ job takes its plan when the meeting is saved
697
+ (or when an orphan recovery is accepted) and never switches engines mid-job.
698
+ An unreadable file runs the defaults and is logged once.
699
+ - An unknown key or id, a wrong type, an unavailable choice, or a second opinion
700
+ on the same engine as HQ answers `400 {"error": "..."}` in plain words.
701
+ Setting HQ onto the second opinion's engine moves the second opinion to the
702
+ other engine (or off) instead of failing.
703
+ - `effective` is what will actually run. A stored choice that is no longer
704
+ possible falls back: the preview to Turbo, HQ to Large-v3, the second opinion
705
+ to off. `reason` (a plain sentence) and `reasonCode` say why.
706
+ - `lastJob` is the last HQ job's outcome: windows decoded and lost, primary and
707
+ timestamp fallbacks, second-opinion failures, checkpoints reused.
708
+ - To run Parakeet as the HQ engine, send `PUT {"hq":"parakeet-ultra"}`; with
709
+ `{"secondOpinion":"large-v3","resolver":true}` Large-v3 also hears every window
710
+ and the resolver may take profile vocabulary terms from it (a Parakeet primary
711
+ never gets gap fill). See the evaluation for what each measured.
712
+ - `COS_BATCH_HQ_TIMESTAMPS` still overrides HQ timestamps: `1` forces them on,
713
+ `0` off. `COS_WHISPER_MEETING_PREVIEW=0` still turns the meeting preview off.
714
+ - The `preview` setting drives only the meeting preview lane
715
+ (`POST /api/transcribe-stream/preview`). A client gets a non-Turbo preview
716
+ only if it lists that model in the `previewModels` query parameter
717
+ (comma-separated; COS Glasses 6.10.639 sends it); every other client gets
718
+ Whisper Turbo, so a phone that only accepts Turbo never sees another model.
719
+ Dictation's provisional words, prompt HQ and Fast mode are unchanged, and so
720
+ are playback audio filters. The Whisper previews keep raw audio; the Parakeet
721
+ preview hears the selected front end.
722
+ - The quiet gate decides the prompt and the front end of a live chunk. The
723
+ silence-hallucination text drops (URL-only, repeated thank-you, a quiet
724
+ vocabulary echo) stay on the pre-6.72 RMS test, because a chunk holding one
725
+ short real name can be Silero-quiet.
726
+
727
+ ### Parakeet (FluidAudio CoreML, offline)
728
+
729
+ `POST /api/stt-settings/parakeet/install` installs FluidAudio's `fluidaudiocli`
730
+ 0.17.4 and Parakeet Ultra (CoreML, `FluidInference/parakeet-ultra-coreml` at
731
+ revision `95eaa59a`, 632 MB), pinned by SHA-256 for every file
732
+ (`server/lib/parakeet-manifest.ts`), under `<data>/stt/parakeet/current`.
733
+ - A local copy whose hash matches is copied instead of downloaded. Looked for,
734
+ in order: the body's `localSource`, `COS_PARAKEET_LOCAL_SOURCES`, the drop-in
735
+ folder `<data>/stt/parakeet-sources` (link or copy a folder holding
736
+ `parakeet-ultra/` there and an install started with `{}` uses it), and
737
+ FluidAudio's own cache. A named `localSource` that lacks a file fails with a
738
+ plain error unless `allowDownload` is true; otherwise missing files come from
739
+ the pinned revision. One mismatched byte fails the install with nothing
740
+ installed. A download that delivers no bytes for 60 s is abandoned.
741
+ `progress.source` says how many files were copied and downloaded, and from
742
+ which folders.
743
+ - FluidAudio publishes no CLI binary, and the CLI is pinned to one build
744
+ (SHA-256 `47141546...`, the build speaker separation uses at
745
+ `~/.cos-glasses/bin/fluidaudiocli`). On a Mac without that build `installable`
746
+ is false and the note says why, so no client offers an install that cannot
747
+ succeed. Apple silicon only.
748
+ - 409 while a recording is live. A crash between the two renames of a
749
+ reinstall leaves the old copy aside; the next start puts it back.
750
+
751
+ Every decode runs under `sandbox-exec`: network denied, and writes allowed only
752
+ to the run's temp directory, the private CoreML cache HOME and the per-user
753
+ temp directory FluidAudio buffers long audio in. Reads stay allowed (system
754
+ frameworks); that is a stated limit. `--local-model-dir` (FluidAudio's path that
755
+ never downloads), a key-free environment, and the CLI's full hash checked once
756
+ per server process. No sandbox, no run. Nothing here calls a language model.
757
+
758
+ - HQ: runs are queued one at a time at lower CPU priority, the first with a
759
+ 180 s deadline (CoreML may compile) and later ones 30 s. Two Parakeet failures
760
+ in a row stop Parakeet for the rest of that job. A Parakeet HQ window that
761
+ fails, or returns no words over at least 0.5 s of detected speech, is decoded
762
+ by Large-v3 instead. A failed second opinion leaves the primary exactly as
763
+ heard. Second opinions run only in a saved meeting's HQ job (and orphan
764
+ recovery), never as part of live capture.
765
+ - Preview: one decode at a time, a 0.9 s deadline, then Turbo only while the
766
+ phone's 1.8 s budget allows (otherwise a quiet 204). Three misses in a row
767
+ open a breaker (Turbo for 1 minute, doubling to 10). The warm-up (an 8 s clip;
768
+ warm when it decodes within 1 s) is deferred while a recording is live and
769
+ retried at most every 2 minutes after a failure.
770
+
771
+ ### The resolver and the second opinion
772
+
773
+ With a second opinion planned, the second engine hears the same front-end audio
774
+ as the primary. A progressive-HQ checkpoint made under the same settings is
775
+ reused as the primary, so only the second opinion is decoded. The two
776
+ hypotheses are aligned word by word by edit distance over normalized words
777
+ (the resolver code has a time-skew pairing limit, not exposed and off: it
778
+ measured worse). The resolver then takes the second opinion only for:
779
+ - words a Whisper primary has nothing for (gap fill), and only inside a span
780
+ the primary's own VAD marked as speech, with the second opinion's mean word
781
+ confidence at least 0.5;
782
+ - a profile vocabulary term the primary missed, inside a short disagreement
783
+ (at most 4 words, the primary's side at most 2 longer).
784
+
785
+ It never changes a region whose two sides differ in a negation, a number or a
786
+ unit, whatever its length. Both raw hypotheses and every disagreement (with
787
+ what was actually kept) are saved with the meeting, additively:
788
+ `batchSegments[].stt` and a meeting-level `stt` in the sidecar; an orphan
789
+ recovery adds `stt` and `sttSegments`. Older clients ignore them. The
790
+ evaluation and the defaults' evidence:
791
+ [docs/stt-672-evaluation.md](docs/stt-672-evaluation.md).
792
+
643
793
  ## Final dictation polish (6.61.5)
644
794
 
645
795
  With glasses app 6.10.616, **Settings → Voice → Polish model** offers **Haiku 5.5**
@@ -987,3 +1137,12 @@ Release receipt (2026-10-12): 6.71.12 is published to npm `latest`; registry int
987
1137
  ### Meeting-wide Fact check (6.71.13 candidate)
988
1138
 
989
1139
  Fact check checks every checkable public claim in the meeting the first time (at most five, one `Claim` row each), then only what was said after the previous completed check. A completed check records `factCheckRange {fromMs, throughMs, full, claims}` on the exchange in meeting-elapsed milliseconds; `capabilities.factCheckScope: 1` advertises it. Follow-ups such as "check it now" continue the pending check. A claim with an implicit starting point uses the profile `home_city` (for example `"home_city": "Austin, TX"` in `.cos-profile.json`), else the phone's weather area from the last 24 hours, and says so. `COS_MEETING_FACT_CHECK_SCOPE=0` returns Fact check to the 6.71.12 latest-claim path; `COS_MEETING_ASK_DOSE_GUARD=0` turns off the fixed answer to medication-dose questions.
1140
+
1141
+ ### Finished captures never read as live (6.71.14)
1142
+
1143
+ A server that boots behind a closed maintenance gate (every managed update) now runs meeting-session recovery once, when the gate opens. Until 6.71.13 that recovery was skipped for the life of the process, so a persisted `active-sessions/<id>.json` could outlive its capture and COS Control showed it as a live recording. Recovery never overwrites a session already live in memory, and the minute sweep waits for it.
1144
+
1145
+ - `GET /api/meeting/sessions/:id/status` keeps `state` to `saved`, `active`, `closed` and `missing`, with the same precedence as before. When the quarantine holds the capture it adds `outcome` (`receipted`: decoded, no speech; `recovered`: decoded and saved; `quarantined`: held, not decoded yet) and `disposition` (`kind`, `reason`, `quarantinedAt`, `quarantineReason`, `receiptedAt`, `chunkFiles`, `savedFilename`, `recovering`, `audioRetainedUntil`). Both are null for a saved or live session.
1146
+ - Until recovery has run, a status for an id with no saved meeting answers `503 {"reason": "recovery_pending", "retryable": true}` with `Retry-After: 5`, instead of a `missing` that would tell the phone the Mac lost the meeting.
1147
+ - No new chunk refusal: a chunk for a quarantined or receipted capture starts its own session, because the phone treats a 410 as "already saved" and deletes its copy. The existing 4-hour closed-session 410 still applies, now after a managed boot too. A session that resumed under a receipted id is adopted at the next boot, never split off.
1148
+ - Each minute, an `active-sessions/*.json` whose session is not live and has been idle for 4 hours (the earlier of its `lastActivityAt` and its file time) is moved to `data/stale-active-session-files/`, with a `.reason.json` written first. Moved files are removed 7 days after the move. Boot recovery also moves aside, rather than adopts, a file whose capture was already recovered or receipted.
@@ -0,0 +1,113 @@
1
+ # STT switcher evaluation (server 6.72.0, 2026-10-13)
2
+
3
+ Everything here is SILVER: the references are Fireflies transcripts of laptop-speaker calls (stratum B) from the 2026-10-10 bake-off corpus. No gold set exists. Numbers only; transcripts and audio stay in the private bake-off corpus outside this repository. Statistics are the bake-off's: paired blockwise bootstrap by meeting, 4,000 resamples, 95% percentile intervals. Gates are the bake-off's `canaries/gates.py` (adoption rule: relative WER gain at least 10% with the CI excluding zero; entity recall point at least -1 pt and CI lower bound above -5 pts; entity precision at least 85%; CLEAN stratum E not higher than the baseline; compute in budget). Scripts: `canaries/stt-672/` (not in the npm package).
4
+
5
+ **Which numbers are shipped-path.** Every HQ arm in section 1 and the Parakeet preview arm in section 4 ran through the shipped code: `transcribeSegments` at production 30 s windows (`segmentTranscriptChunks` over the stored chunks), the job plan, `asrFrontEnd` (highpass), the real Large-v3 and Parakeet decoders, the resolver, corrections, `stripOverlap` and speaker mapping; the preview through `transcribeMeetingPreview` and the preview route's own hallucination cleaning. One approximation: decodes were cached by input audio, so in a resolver arm Large-v3's 250-character prompt context came from the Large-v3-alone run. The Whisper Turbo preview baseline is the saved bake-off L4 arm (SILVER) and a private Turbo server run (SILVER-E, stratum E), both with the route's cleaning applied afterwards. Section 6 lists the earlier comparisons that used saved bake-off text; they are superseded.
6
+
7
+ ## Defaults chosen, and the rule applied
8
+
9
+ | Setting | Default | Evidence |
10
+ | --- | --- | --- |
11
+ | HQ | `large-v3` (timestamps on, VAD remap) | Item 1; the primary every arm is compared with. |
12
+ | Front end | `highpass` | Item 2. |
13
+ | Quiet gate | `silero` | Item 3, canary 5 (section 3). |
14
+ | Second opinion | `off` | Neither resolver configuration passed the rule (section 1). |
15
+ | Resolver | `false` | Same. It is available and records disagreements whenever a second opinion is chosen. |
16
+ | Preview | `turbo` | Parakeet Ultra passes the live-lane rule on the shipped path (-10.7% WER, CI -18.4 to -1.4) and is selectable; Turbo stays the default (section 4). |
17
+
18
+ Miles's rule (14:14): Ultra everywhere if it passes; v2 only with a proven benefit Ultra cannot give. **Branch 3 applied**: Resolver(Large-v3 + Ultra) fails, Resolver(Large-v3 + v2) fails, so the second opinion defaults off, Parakeet v2 is removed from the product and the install (464 MB never downloaded), and Ultra stays installable for the preview and the HQ primary.
19
+
20
+ ## 1. HQ on the shipped path (SILVER 39 windows / 13 meetings; SILVER-E 110 windows; 271 pooled entity mentions)
21
+
22
+ Baseline: Large-v3 alone through the same code, SILVER WER 13.24% [11.0, 15.7] (S/D/I 4.2 / 6.6 / 2.5), SILVER-E 19.21% [14.3, 26.6], pooled entity recall 81.9%, precision 89.5%.
23
+
24
+ | Arm | SILVER WER % [CI] | S / D / I | rel. vs Large-v3 [CI] | SILVER-E WER % | SILVER-E rel. [CI] | Entity recall diff [CI] | Precision | CLEAN E | Collapsed (SILVER-E) | Gates |
25
+ |---|---|---|---|---|---|---|---|---|---|---|
26
+ | Large-v3 alone | 13.24 | 4.2 / 6.6 / 2.5 | | 19.21 | | | 89.5 | 0/40 | 6 | |
27
+ | Resolver: Large-v3 + Ultra | 13.03 [10.7, 15.6] | 4.4 / 4.8 / 3.8 | -1.6% [-4.7, +1.1] | 17.86 | -7.0% [-8.5, -4.2] | +0.7 [0.0, +1.5] | 89.6 | 0/40 | 4 | fail: gain, CI |
28
+ | Resolver: Large-v3 + v2 | 12.69 [10.5, 15.0] | 4.3 / 5.3 / 3.0 | -4.2% [-6.5, -1.9] | 17.95 | -6.6% [-8.6, -5.0] | +0.7 [0.0, +1.7] | 88.9 | 0/40 | 4 | fail: gain |
29
+ | Parakeet Ultra alone (HQ) | 11.76 [9.6, 14.1] | 4.5 / 2.4 / 4.9 | -11.2% [-23.7, +2.6] | 12.69 | -33.9% [-46.7, -24.5] | +2.6 [-6.5, +8.6] | 94.2 | 0/40 | 0 | fail: CI, entity CI |
30
+ | Ultra primary + Large-v3 second opinion, resolver on | 11.75 [9.5, 14.1] | 4.5 / 2.4 / 4.9 | -11.3% [-23.7, +2.4] | 12.64 | -34.2% [-46.7, -24.7] | +4.4 [-4.7, +9.5] | 93.6 | 0/40 | 0 | fail: CI only |
31
+
32
+ - Paired, v2 minus Ultra as the resolver's second opinion: SILVER -0.34 pts [-0.68, -0.02], SILVER-E +0.09 pts [-0.53, +0.50]. v2 is a little better on SILVER but itself fails the 10% bar, so under the rule it does not stay.
33
+ - What the resolver did on SILVER with Ultra: 467 gap fills taken; refused 112 runs outside the primary VAD's speech, 16 under the 0.5 confidence floor, 20 that would add or change a negation, number or unit. These QA-round restrictions (no gap fill outside speech, a confidence floor, protection for every region) are why the gain fell from the earlier saved-text estimate (section 6).
34
+ - REAL stratum E (faint far-field speech the VAD misses, 30 units): any output 37% for Large-v3 and both resolver arms (no change), 43% for Parakeet Ultra as primary. CLEAN (tone, music, 40 units) is 0 for every arm; the VAD-off positive control still outputs on 97.5%, so the fixture can fail.
35
+ - Compute (decode wall clock over audio, this run): Large-v3 on Metal 0.170, Parakeet Ultra 0.018 (Neural Engine), the pair 0.188, inside 0.25. **On CPU, the production default device, the 0.25 budget is not met by Large-v3 alone**: 8 windows measured 1.19x real time on a machine at load 28 to 32 (pre-existing for Large-v3 HQ; Parakeet adds 0.018). Miles's Mac runs `COS_BATCH_HQ_METAL=1`.
36
+
37
+ ### Parakeet as the primary HQ (Miles: "It sounds like Parakeet should be the approach we use")
38
+
39
+ Evidence only; the default primary stays Large-v3. On the shipped path Ultra as the primary cut SILVER WER from 13.24% to 11.76% and SILVER-E from 19.21% to 12.69% (no collapsed windows against 6), with entity recall up 2.6 to 4.4 pts and precision 93.6 to 94.2%. It fails the rule only because the SILVER interval reaches +2.4 to +2.6% (13 meetings). With Large-v3 as its second opinion and the resolver on, only the WER interval fails. That configuration costs a full Large-v3 pass and is selectable: `PUT /api/stt-settings {"hq":"parakeet-ultra","secondOpinion":"large-v3","resolver":true}`. In the fixtures (section 2) Ultra also kept the negation and the unit far more often. Still needed before a default switch: human gold across strata A to D, more than 271 silver entity mentions, and Miles's explicit decision (it reverses the 2026-08-11 invariant).
40
+
41
+ ## 2. Negation and units fixtures (10/13 diagnostic, 24 overlapping windows, shipped path)
42
+
43
+ Each window (12 to 24 s, two to four chunks) ran through `transcribeSegments` exactly like a saved meeting. The provisional reference (BBC captions) says "born lacking" (4 windows) and "around 18 months" (4 windows).
44
+
45
+ | Arm | "lacking" / "having" | "months" / "minutes" | Negation or unit changed vs its own primary |
46
+ |---|---|---|---|
47
+ | Large-v3 alone | 1 / 0 | 0 / 4 | |
48
+ | Resolver Large-v3 + Ultra | 1 / 0 | 0 / 4 | 0 of 24 |
49
+ | Resolver Large-v3 + v2 | 1 / 0 | 0 / 4 | 0 of 24 |
50
+ | Parakeet Ultra alone | 4 / 0 | 3 / 1 | |
51
+ | Ultra + Large-v3 second opinion | 4 / 0 | 3 / 1 | 0 of 24 |
52
+
53
+ The resolver never changed a negation or a unit. It also cannot repair the primary's own errors, by design.
54
+
55
+ ## 3. Quiet gate (bake-off canary 5)
56
+
57
+ Labels: the bake-off's Silero v5.1.2 over each concatenated session; under 0.5 s of speech in a chunk is "Silero-negative". 10,091 of the 14,622 chunk WAVs are still on disk (32 of 44 sessions); 931 negative, 9,160 speech. The shipped gate is a dedicated in-process sherpa Silero (probability 0.8, minimum speech 0.25 s) fed in 512-sample windows, 17 ms p50 per 6 s chunk.
58
+
59
+ | Gate | Fires on Silero-negative [CI] | Fires on speech [CI] | Canary 5 |
60
+ |---|---|---|---|
61
+ | RMS below 150 (before 6.72) | 9.7% [7.9, 11.7] | 0.00% | fail (must catch) |
62
+ | sherpa Silero, p 0.5, quiet under 0.5 s | 75.5% | 0.47% | fail |
63
+ | **sherpa Silero, p 0.8, min speech 0.25 s (shipped)** | **80.8% [78.1, 83.2]** | **0.91% [0.73, 1.12]** | **pass (pooled)** |
64
+ | ggml Silero v5.1.2 per chunk (whisper-server's own model) | 72.9% | 0.40% | fail |
65
+
66
+ - The shipped settings were chosen on this table (8 detector settings by 3 thresholds), so the pass is not out of sample. By alternate session halves: 67.8% / 0.87% and 91.8% / 0.94%; no per-chunk setting passed both. Even whisper-server's own model fails per chunk.
67
+ - The gate decides only the prompt and the front end. The silence-hallucination text drops stay on RMS below 150, because a chunk with one short real name can be Silero-quiet.
68
+ - Found here, not fixed in this release: `trimSilence` (the prompt tail guard) hands sherpa the whole chunk in one call, which keeps only the last speech segment (a 6 s speech chunk read 0.32 s, windowed 5.03 s).
69
+
70
+ ## 4. Live preview: Parakeet Ultra vs Whisper Turbo
71
+
72
+ Live lane: every stored 6 s chunk of SILVER (39 windows, 1,170 chunks), SILVER-E (110 windows) and stratum E. Parakeet Ultra ran through `transcribeMeetingPreview` (the client listing `parakeet-ultra` in `previewModels`, the highpass front end, the 0.9 s Parakeet deadline, corrections) and then the preview route's cleaning (`stripInlineHallucinationsOneShot`, `isFullHallucination`); in this process the Turbo fallback had no sidecar, so 7 of 2,243 requests that missed the budget count as empty previews. Baseline: Whisper Turbo on raw audio (today's preview) with the same route cleaning and corrections applied to its saved text: the bake-off L4 arm on SILVER, a private Turbo server (the preview sidecar's exact arguments, never the user's 8177) on SILVER-E and stratum E. Pooled entity recall: 271 mentions.
73
+
74
+ | Preview arm | SILVER WER % [CI] | S / D / I | rel. vs Turbo [CI] | SILVER-E WER | Entity recall (271) diff [CI] | Precision | CLEAN E | REAL E | Request p50 / p95 / max s | Emission delay p50 / p95 s (est.) | Gates |
75
+ |---|---|---|---|---|---|---|---|---|---|---|---|
76
+ | Whisper Turbo (default) | 15.71 [13.3, 18.3] | 5.3 / 7.6 / 2.8 | | 17.31 | 81.9 | 94.5 | 1/40 | 10/30 | 0.33 / 0.47 / 0.77 | 3.49 / 6.16 | |
77
+ | **Parakeet Ultra (the `parakeet` option)** | **14.04 [11.5, 16.8]** | 5.5 / 3.6 / 5.0 | **-10.7% [-18.4, -1.4]** | 14.74 | 0.0 [-4.7, +6.9] | 94.9 | 1/40 | 17/30 | 0.42 / 0.68 / 1.25 | 3.61 / 6.30 | **PASS** |
78
+
79
+ - On the shipped path Parakeet Ultra passes every live-lane gate against Turbo. Turbo stays the default as decided (14:14 rule, point 4): the gain's interval only just excludes zero, a Parakeet preview needs the install, a warm model and COS Glasses 6.10.639, and Parakeet writes more text on REAL non-speech units (17 of 30 against 10), which holds faint far-field speech, so that is not proof of hallucination. Miles can select it and compare live.
80
+ - Request time is wall clock through the shipped path (spawn, highpass ffmpeg, decode, cleaning) on a Mac at load 25 to 36 with other work running; every arm stays inside the 1.0 s p95 live budget. fluidaudiocli has no server mode, so each preview is one short process; the compiled CoreML bundle is what stays warm.
81
+ - Emission delay is an estimate, the same method for both arms: for each word, the chunk's end minus the word's start (Parakeet v2 word starts from the first pass as the timing reference) plus the arm's request time. The 6 s chunk dominates it; real preview latency depends on how often the phone sends partial phrases.
82
+ - Earlier first-pass numbers (Parakeet decoded outside `transcribeMeetingPreview`, no corrections or route cleaning, 1.5 s deadline): Ultra -12.7% [-19.1, -4.5], 0/40 CLEAN; v2 -4.2% [-10.3, +5.1] with 3/40 CLEAN outputs, which is why the preview uses Ultra and v2 is gone. Highpass made Turbo slightly worse (+1.7% [-0.8, +3.7], 2/40 CLEAN), so the Whisper previews keep raw audio.
83
+
84
+ Negation and units, per 6 s chunk (the 20 chunks behind the 10/13 windows, each decoded alone): Turbo wrote "lacking" once and "minutes" once; Parakeet Ultra wrote "having" once and "months" once. Cut at 6 s Parakeet flips the negation it keeps on the 12 to 24 s windows. The preview is cosmetic (saved text never comes from it).
85
+
86
+ Contention (bounded, 30 chunks, while a real recording was live, all private instances): a private Large-v3 whisper-server with the live commit arguments, the shipped Nemotron diarizer run (`runNemotronCli`, Neural Engine, nice 10) and the Parakeet Ultra preview, each alone and then all three per chunk at once.
87
+
88
+ | Component | Alone p50 / p95 ms | All three at once p50 / p95 ms | Live plus diarizer only p95 ms |
89
+ |---|---|---|---|
90
+ | Live Large-v3 chunk | 567 / 711 | 603 / 732 | 743 |
91
+ | Diarizer (1.5 s budget) | 149 / 162 | 150 / 158 | 159 |
92
+ | Parakeet Ultra preview | 323 / 340 | 329 / 341 | |
93
+
94
+ No failures in any arm, and the user's server showed no new diarizer timeouts or Whisper failures across that run. A 30-chunk sample is not a thermal or multi-hour test, and it did not include a first CoreML compile next to the diarizer (the warm-up is now deferred while a recording is live instead).
95
+
96
+ ## 5. Install and offline proof
97
+
98
+ - Pinned manifest `server/lib/parakeet-manifest.ts`: fluidaudiocli 0.17.4 SHA-256 `4714154623b59c40404563828cb0fbeb4da6878e90f582ad776c1c2cd061d35a` (22,039,856 bytes) and Parakeet Ultra `FluidInference/parakeet-ultra-coreml@95eaa59a39d4394f047a4dc5cce480388a60d1b6` (21 files, 632,316,735 bytes); install total 654,356,591 bytes. Per-file hashes come from the bake-off download manifest; the bake-off copy matches all 21 files.
99
+ - The shipped installer installed from local copies into an isolated data dir with its network fetch replaced by a throwing function: verified and installed with no fetch.
100
+ - The sandbox profile is enforced by the OS, tested in `parakeet.test.ts`: under the profile, `curl` to a local HTTP server fails while the same request works unsandboxed, and a write outside the run directory, the CoreML cache and the per-user temp directory fails. FluidAudio buffers audio over about 15 s in the per-user temp directory; without that allowance every 30 s HQ window failed (found by this evaluation).
101
+ - Clips under about 0.3 s make FluidAudio exit 0 with no output (a meeting's last chunk); they are padded to 1 s.
102
+
103
+ ## 6. Superseded comparisons (saved bake-off text, not the shipped path)
104
+
105
+ The first pass compared saved bake-off outputs: Parakeet run over whole 3-minute windows in the bake-off's own sandbox without highpass, and Large-v3 from the bake-off H0ts arm or a timed re-decode composed outside `transcribeSegments`. On that basis Resolver(Large-v3 + Ultra) looked like -11.7% [-20.1, -6.1] against H0ts and v2 like -11.1% [-19.7, -2.8] against the re-decode. Those numbers depended on how the windows were composed and on a resolver without the speech-span, confidence and protection rules; the shipped path does not reproduce them, and the defaults follow section 1.
106
+
107
+ ## 7. Live impact of this evaluation
108
+
109
+ Most of it ran while Miles was recording. All decodes used private processes; the user's 8177/8178 servers were never called. The live-impact guard (the user's public `/api/health`) saw the diarizer fall back to the voiceprint on 3 chunks during the heaviest stretch (Metal HQ decode plus a Parakeet preview run on a machine at load 30 to 36); the runs then paused and paced themselves, and no Whisper failure was seen.
110
+
111
+ ## 8. Not established
112
+
113
+ Gold references (all of this is silver, stratum B only); entity coverage beyond 271 mentions; HQ compute on an idle CPU; Parakeet on Intel Macs (unsupported); cold-start preview behaviour after a reboot beyond the warm-up rule; a route-level test of orphan recovery's plan (pinned by a shared helper and its tests).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.71.13",
3
+ "version": "6.72.0",
4
4
  "description": "COS Glasses: self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,6 +51,8 @@
51
51
  "!server/scripts/work-progress-mutation-gate.mjs",
52
52
  "!server/scripts/work-search-mutation-gate.mjs",
53
53
  "!server/scripts/fact-check-mutation-gate.mjs",
54
+ "!server/scripts/stale-session-mutation-gate.mjs",
55
+ "!server/scripts/stt-mutation-gate.mjs",
54
56
  "!server/scripts/pairing-mutation-gate.mjs",
55
57
  "!server/scripts/away-hold-mutation-gate.mjs",
56
58
  "!server/scripts/nemotron-mutation-gate.mjs",
@@ -84,6 +86,7 @@
84
86
  "bin/hooks/cos-cursor-observer.cjs",
85
87
  "docs/provider-parity.md",
86
88
  "docs/asr-canaries.md",
89
+ "docs/stt-672-evaluation.md",
87
90
  "docs/pairing-contract.md",
88
91
  "docs/pairing-fixture.json",
89
92
  "bin/whisper-runtime.cjs",
package/server/index.ts CHANGED
@@ -71,7 +71,7 @@ import { ForkJobLedger, fileForkJobPersistence } from './lib/fork-job-ledger.js'
71
71
  import { nativeHead, realNativeHeadDeps } from './lib/native-head.js'
72
72
  import { threadOccupancy, holderActivity } from './lib/thread-occupancy.js'
73
73
  import { displayRouter } from './routes/display.js'
74
- import { transcribeStreamRouter } from './routes/transcribe-stream.js'
74
+ import { transcribeStreamRouter, getActiveTranscriptionSessionCount } from './routes/transcribe-stream.js'
75
75
  import { meetingRouter, resumeMeetingFinalizationJobs } from './routes/meeting.js'
76
76
  import { meetingsRouter } from './routes/meetings.js'
77
77
  import { openaiCompatRouter } from './routes/openai-compat.js'
@@ -102,6 +102,9 @@ import { recoverInterruptedNaming } from './lib/held-naming-batches.js'
102
102
  import { voiceRouter } from './routes/voice.js'
103
103
  import { glossaryRouter } from './routes/glossary.js'
104
104
  import { homeAreaRouter } from './routes/home-area.js'
105
+ import { createSttSettingsRouter } from './routes/stt-settings.js'
106
+ import { setRecordingLiveProbe, startPreviewWarmLoop, stopPreviewWarmLoop } from './lib/stt-runtime.js'
107
+ import { findPinnedCli, killParakeetChildren, sweepParakeetStaging, sweepStaleParakeetTmp } from './lib/parakeet.js'
105
108
  import { handoffsRouter } from './routes/handoffs.js'
106
109
  import { recoveryRouter } from './routes/recovery.js'
107
110
  import { promptEditRouter } from './routes/prompt-edit.js'
@@ -1101,6 +1104,7 @@ recoverInterruptedNaming()
1101
1104
  app.use('/api', voiceRouter)
1102
1105
  app.use('/api', glossaryRouter)
1103
1106
  app.use('/api', homeAreaRouter)
1107
+ app.use('/api', createSttSettingsRouter({ recordingLive: () => getActiveTranscriptionSessionCount() > 0 }))
1104
1108
  app.use('/api', handoffsRouter)
1105
1109
  app.use('/api', recoveryRouter)
1106
1110
  app.use('/api', promptEditRouter)
@@ -1175,6 +1179,9 @@ async function gracefulShutdown(): Promise<void> {
1175
1179
  try {
1176
1180
  const nemotron = killNemotronChildren()
1177
1181
  if (nemotron.killed || nemotron.removed) console.log(`[diarizer] shutdown: killed ${nemotron.killed} Nemotron run(s), removed ${nemotron.removed} temp dir(s)`)
1182
+ stopPreviewWarmLoop()
1183
+ const parakeet = killParakeetChildren()
1184
+ if (parakeet.killed || parakeet.removed) console.log(`[stt] shutdown: killed ${parakeet.killed} Parakeet run(s), removed ${parakeet.removed} temp dir(s)`)
1178
1185
  } catch { /* best effort */ }
1179
1186
  stopCodexModelCatalogRefresh()
1180
1187
  stopWhisperServer()
@@ -1335,6 +1342,16 @@ listenRequiredServers(listeners).then(() => {
1335
1342
  // 6.61.0: warm Nemotron in the background (the first Neural Engine compile
1336
1343
  // takes 15-30 s). Chunks use the voiceprint until it is warm; never awaited.
1337
1344
  startNemotronWarmup()
1345
+ // 6.72.0: leftovers of an interrupted Parakeet install or run, then the preview warm-up if selected.
1346
+ try {
1347
+ const swept = sweepParakeetStaging()
1348
+ if (swept.restored) console.log('[stt] Restored the Parakeet install a crash left mid-swap')
1349
+ sweepStaleParakeetTmp()
1350
+ } catch { /* best effort */ }
1351
+ setRecordingLiveProbe(() => getActiveTranscriptionSessionCount() > 0)
1352
+ // Hash the pinned FluidAudio CLI once at boot so the first Settings GET already knows `installable`.
1353
+ void findPinnedCli().catch(() => null)
1354
+ startPreviewWarmLoop()
1338
1355
  }
1339
1356
 
1340
1357
  // Durable recovery starts with the admitted runtime by default. A literal
@@ -1,4 +1,5 @@
1
1
  import type { WhisperWord } from './whisper-local.js'
2
+ import type { Disagreement } from './stt-resolver.js'
2
3
 
3
4
  export interface BatchSegment {
4
5
  startChunkIdx: number
@@ -8,11 +9,55 @@ export interface BatchSegment {
8
9
  speakers: string[]
9
10
  }
10
11
 
12
+ /** One hypothesis as an engine heard it, before any resolution (6.72.0). */
13
+ export interface SttHypothesis {
14
+ engine: string
15
+ text: string
16
+ /** Spoken words, seconds from the start of the window. Absent for an untimed decode. */
17
+ words?: Array<{ word: string; start: number; end: number; probability?: number }>
18
+ error?: string
19
+ }
20
+
21
+ /** Additive per-window record (6.72.0). Older clients ignore it. */
22
+ export interface SegmentStt {
23
+ engine: string
24
+ requestedEngine: string
25
+ fallbackReason?: string
26
+ reusedCheckpoint?: boolean
27
+ frontEnd: string
28
+ timestamps: boolean
29
+ primary: SttHypothesis
30
+ secondOpinion?: SttHypothesis
31
+ resolver: { version: string; enabled: boolean; applied: boolean; wordsFromSecond: number; skipped?: string }
32
+ disagreements: Disagreement[]
33
+ }
34
+
11
35
  export interface BatchResult {
12
36
  segment: BatchSegment
13
37
  text: string
14
38
  words: WhisperWord[]
15
39
  speakerWords: Array<{ word: string; start: number; end: number; speaker: string; similarity: number }>
40
+ /** Uncorrected spoken words and the decode's own VAD speech spans (6.72.0), so a
41
+ * progressive checkpoint can be reused as a resolver primary. */
42
+ spokenWords?: WhisperWord[]
43
+ speechSpans?: Array<[number, number]>
44
+ stt?: SegmentStt
45
+ }
46
+
47
+ /** Meeting-level summary of the STT plan an HQ job ran with (6.72.0). */
48
+ export interface BatchSttSummary {
49
+ plan: { engine: string; secondOpinion: string; resolver: boolean; frontEnd: string; timestamps: boolean }
50
+ resolverVersion: string
51
+ windows: number
52
+ /** Windows planned but not in the result (a decode failed both ways). */
53
+ windowsLost: number
54
+ disagreements: number
55
+ wordsFromSecond: number
56
+ secondOpinionFailures: number
57
+ primaryFallbacks: number
58
+ timestampFallbacks: number
59
+ checkpointsReused: number
60
+ resolverSkipped: number
16
61
  }
17
62
 
18
63
  export interface BatchTranscription {
@@ -20,6 +65,7 @@ export interface BatchTranscription {
20
65
  batchTranscript?: string
21
66
  batchSegments?: BatchResult[]
22
67
  qualityReport?: BatchQualityReport
68
+ stt?: BatchSttSummary
23
69
  }
24
70
 
25
71
  export interface BatchTranscriptSelection {
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import { durableAtomicWriteFileSync } from './atomic-fs.js'
3
- import type { BatchTranscription } from './batch-transcript-quality.js'
3
+ import type { BatchResult, BatchSttSummary, BatchTranscription } from './batch-transcript-quality.js'
4
4
 
5
5
  /** Replace only the final canonical transcript section. Missing markers fail closed. */
6
6
  export function replaceMeetingTranscriptAtomic(meetingPath: string, transcript: string): boolean {
@@ -38,10 +38,15 @@ export function persistBatchDecisionSidecar(
38
38
  text: result.text,
39
39
  words: result.words,
40
40
  speakerWords: result.speakerWords,
41
+ // 6.72.0, additive: both raw hypotheses and every disagreement.
42
+ ...(result.stt ? { stt: result.stt } : {}),
41
43
  }))
44
+ if (batchResult.stt) sidecar.stt = batchResult.stt
45
+ else delete sidecar.stt
42
46
  } else {
43
47
  delete sidecar.batchTranscript
44
48
  delete sidecar.batchSegments
49
+ delete sidecar.stt
45
50
  }
46
51
 
47
52
  durableAtomicWriteFileSync(sidecarPath, JSON.stringify(sidecar, null, 2), { mode: 0o600 })
@@ -52,3 +57,20 @@ export function persistBatchDecisionSidecar(
52
57
  export function canDeletePendingBatchAudio(batchApplied: boolean, metadataPersisted: boolean): boolean {
53
58
  return batchApplied && metadataPersisted
54
59
  }
60
+
61
+ /** Orphan recovery (6.72.0): keep both hypotheses and the disagreements beside
62
+ * the recovered text, additively. Recovery never sets batchApplied. */
63
+ export function persistSttSidecar(sidecarPath: string, results: readonly BatchResult[], summary: BatchSttSummary): boolean {
64
+ if (!existsSync(sidecarPath)) return false
65
+ const parsed = JSON.parse(readFileSync(sidecarPath, 'utf8')) as unknown
66
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false
67
+ const sidecar = parsed as Record<string, unknown>
68
+ sidecar.stt = summary
69
+ sidecar.sttSegments = results.filter(r => r.stt).map(r => ({
70
+ startChunkIdx: r.segment.startChunkIdx,
71
+ endChunkIdx: r.segment.endChunkIdx,
72
+ stt: r.stt,
73
+ }))
74
+ durableAtomicWriteFileSync(sidecarPath, JSON.stringify(sidecar, null, 2), { mode: 0o600 })
75
+ return true
76
+ }