@gotcos/glasses-server 6.71.14 → 6.73.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,35 @@
1
+ ## [6.73.0] - 2026-10-13 - Prompts and dictation follow the speech-to-text settings
2
+
3
+ Paired clients: none required. Every phone, including the public 618, already accepts the new dictation text (it never checks a model on dictation). COS Control and the phone can read the new `dictation` block in `GET /api/stt-settings` to replace "Dictation is unchanged" in their copy.
4
+
5
+ - Dictation follows the settings (`docs/dictation-stt-contract.md`). The live provisional words (`/api/prompt-drafts/:id/peek`), the closed-chunk HUD text and the Fast mode final follow the Preview model. The final text with Fast mode off, and the speculative HQ warm, follow the HQ model. `/api/transcribe` (Quick VOICE, browser recordings) follows the same split. With the defaults (preview `turbo`, HQ `large-v3`) every dictation decode is the 6.72.0 Whisper decode, unchanged.
6
+ - Fast mode now means "the fast engine you picked", the Preview model. On a standard Mac the Whisper choice is the Turbo live worker; on a Max-tier Mac it is the live Large-v3 worker, as before. A `large-v3` or `nemotron-560` Preview choice keeps dictation on Whisper and says why.
7
+ - Parakeet Ultra runs for a prompt only when it is installed, warm, and the breaker is closed. When it is cold, failed, busy, or the audio is not 16 kHz WAV (the browser recorder posts WebM), the 6.72.0 Whisper engine decodes the same audio in the same request, so the user never sees a blank or an error because of Parakeet. The engine is chosen when each decode starts and never changes during it. A finalize takes one settings snapshot for all its chunks, and a chunk decoded under a different selection is decoded again instead of reused (cached records now carry `sttKey`; records from before 6.73.0 count as the 6.72.0 defaults).
8
+ - Parakeet dictation runs on a new interactive lane: normal priority, never queued behind a meeting batch, at most two at once. The live preview uses the existing one-at-a-time preview lane. Dictation Parakeet failures count on the same breaker as the meeting preview; a busy lane does not. Choosing a Parakeet HQ model now also warms Parakeet (never while a recording is live).
9
+ - Second opinion and resolver never run on dictation. The quiet gate never drops dictation audio: on clean spoken one-to-three-word commands the Silero rule (under 0.5 s of speech) would have dropped 18 of 30. The front end applies to Parakeet dictation decodes; Whisper dictation keeps raw audio as before (highpass changed the first word of Large-v3 HQ on 24 of 149 real prompt chunks).
10
+ - Additive response fields: `engine` (`whisper`, `parakeet-ultra`, or `mixed` on a finalize) and `engineReason` on `/api/transcribe`, finalize and retry; `engine` on `prompt_transcript` events. `backend` reads `parakeet-ultra` for a Parakeet decode. `actualQuality` keeps its three values, so a Parakeet HQ prompt is HQ, not degraded.
11
+ - `GET /api/stt-settings` adds `dictation` (`preview`, `fast`, `final`, each with `engine`, `follows`, `reason`, `reasonCode`; `frontEnd`, `quietGate: "none"`, `secondOpinion: "off"`, `note`) and `appliesTo.dictation`. Every 6.72.0 key keeps its value. The Parakeet preview option note now says dictation uses it on every phone.
12
+ - `COS_DICTATION_FOLLOWS_STT=0` holds every dictation decode on its 6.72.0 Whisper engine.
13
+ - Measured on this Mac (M3 Ultra, private engines on spare ports; full tables in the contract): on 152 real prompt chunks of 2 to 15 s, Parakeet Ultra decodes at p50 233 ms, p95 419 ms (plus the highpass front end, p50 58 ms), the live Large-v3 worker at 316 / 467 ms, the Turbo live worker at 195 / 241 ms, and whisper-cli Large-v3 HQ at 1,544 / 1,844 ms. On a synthetic set with known text, Parakeet Ultra matched 69% of name targets clean and 63% in G2 babble, against 98% and 86% for Large-v3 HQ; numbers with units and negations were level (negations 100% for every engine). Parakeet is clearly weaker on names for prompts.
14
+ - No language model is called by any transcription path. No new downloads.
15
+ - Tests: new suites for the dictation mapping, the Parakeet dictation runner, the plan in `transcribeAudioBuffer`, prompt-draft reuse across selections and the Parakeet live preview, the interactive lane and the HQ warm-up. Mutation gate: `node server/scripts/dictation-stt-mutation-gate.mjs` (repository only), green baseline first (100 tests in 8 files), each target matched exactly once: 22 of 22 killed, each by a named test.
16
+
17
+ ## [6.72.0] - 2026-10-13 - Speech-to-text switcher, front-end fixes, Parakeet Ultra as an option
18
+
19
+ 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`.
20
+
21
+ - 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)".
22
+ - 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.
23
+ - 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.
24
+ - 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.
25
+ - 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.
26
+ - 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.
27
+ - 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.
28
+ - 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`).
29
+ - 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.
30
+ - 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.
31
+ - 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.
32
+
1
33
  ## [6.71.14] - 2026-10-13 - A finished capture no longer reads as a live recording
2
34
 
3
35
  - 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`.
package/README.md CHANGED
@@ -148,7 +148,7 @@ range is the exact Tailscale/CGNAT allocation (`100.64.0.0/10`), not all of
148
148
  `COS_OPENAI_WHISPER_FALLBACK=1` opt-in and a configured key; a key alone never
149
149
  uploads audio.
150
150
  - HQ prompt dictation is requested by default; the phone's **Fast mode** switch
151
- opts into turbo. Server 6.16.0 reports whether full local large-v3 actually
151
+ opts into the fast engine (the STT Preview model since 6.73.0). Server 6.16.0 reports whether full local large-v3 actually
152
152
  ran, and compatible companions alert once if an HQ request used Fast or
153
153
  Cloud instead of silently claiming HQ.
154
154
  - Local-first spoken reply playback through Kokoro on Apple silicon. The first
@@ -643,6 +643,190 @@ the warm-up and the final passes. `/api/health` is public, so this carries codes
643
643
  and counts only, never a speaker name, path or error text; those stay in
644
644
  `server.log`. A Nemotron fallback never degrades `readiness.status`.
645
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
+ "dictation": {"preview": {"engine": "whisper", "follows": "preview", "reason": null, "reasonCode": null},
683
+ "fast": {"engine": "whisper", "follows": "preview", "reason": null, "reasonCode": null},
684
+ "final": {"engine": "whisper", "follows": "hq", "reason": null, "reasonCode": null},
685
+ "frontEnd": "raw", "quietGate": "none", "secondOpinion": "off", "note": "..."},
686
+ "appliesTo": {"preview": "next live chunk", "hq": "next saved meeting", "dictation": "next dictation chunk"}
687
+ }
688
+ ```
689
+
690
+ | Setting | Ids (default first) | Applies from |
691
+ | --- | --- | --- |
692
+ | `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 |
693
+ | `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 |
694
+ | `secondOpinion` | `off`, `parakeet-ultra`, `large-v3` (for a Parakeet HQ) | next saved meeting |
695
+ | `resolver` | `false`, `true` | next saved meeting |
696
+ | `frontEnd` | `highpass` (80 Hz highpass only), `full` (highpass, afftdn, loudnorm) | next live chunk and next saved meeting |
697
+ | `quietGate` | `silero` (under 0.5 s of detected speech), `rms` (RMS below 150) | next live chunk |
698
+
699
+ - Stored atomically in `<data>/stt-settings.json` and read on every use, so
700
+ nothing needs a restart. An HQ job takes its plan when the meeting is saved
701
+ (or when an orphan recovery is accepted) and never switches engines mid-job.
702
+ An unreadable file runs the defaults and is logged once.
703
+ - An unknown key or id, a wrong type, an unavailable choice, or a second opinion
704
+ on the same engine as HQ answers `400 {"error": "..."}` in plain words.
705
+ Setting HQ onto the second opinion's engine moves the second opinion to the
706
+ other engine (or off) instead of failing.
707
+ - `effective` is what will actually run. A stored choice that is no longer
708
+ possible falls back: the preview to Turbo, HQ to Large-v3, the second opinion
709
+ to off. `reason` (a plain sentence) and `reasonCode` say why.
710
+ - `lastJob` is the last HQ job's outcome: windows decoded and lost, primary and
711
+ timestamp fallbacks, second-opinion failures, checkpoints reused.
712
+ - To run Parakeet as the HQ engine, send `PUT {"hq":"parakeet-ultra"}`; with
713
+ `{"secondOpinion":"large-v3","resolver":true}` Large-v3 also hears every window
714
+ and the resolver may take profile vocabulary terms from it (a Parakeet primary
715
+ never gets gap fill). See the evaluation for what each measured.
716
+ - `COS_BATCH_HQ_TIMESTAMPS` still overrides HQ timestamps: `1` forces them on,
717
+ `0` off. `COS_WHISPER_MEETING_PREVIEW=0` still turns the meeting preview off.
718
+ - On the meeting preview lane (`POST /api/transcribe-stream/preview`) a client
719
+ gets a non-Turbo preview only if it lists that model in the `previewModels`
720
+ query parameter (comma-separated; COS Glasses 6.10.639 sends it); every other
721
+ client gets Whisper Turbo, so a phone that only accepts Turbo never sees
722
+ another model. Playback audio filters are unchanged. The Whisper previews keep
723
+ raw audio; the Parakeet preview hears the selected front end.
724
+ - Since 6.73.0 the settings drive dictation too; see "Dictation" below.
725
+ - The quiet gate decides the prompt and the front end of a live chunk. The
726
+ silence-hallucination text drops (URL-only, repeated thank-you, a quiet
727
+ vocabulary echo) stay on the pre-6.72 RMS test, because a chunk holding one
728
+ short real name can be Silero-quiet.
729
+
730
+ ### Dictation (6.73.0)
731
+
732
+ Prompts and dictation (Record Message, hold-to-talk, edit instructions, Quick
733
+ VOICE) follow the same settings. Contract and measurements:
734
+ `docs/dictation-stt-contract.md`.
735
+
736
+ | Dictation decode | Follows | Whisper choice (default) | `parakeet` / `parakeet-ultra` |
737
+ | --- | --- | --- | --- |
738
+ | Live provisional words (`/api/prompt-drafts/:id/peek`) | Preview model | the 6.72.0 preview sidecar | Parakeet Ultra |
739
+ | Closed-chunk HUD text and the Fast mode final | Preview model | the live worker (Turbo, or Large-v3 on a Max-tier Mac) | Parakeet Ultra |
740
+ | Final text with Fast mode off, and the HQ warm | HQ model | whisper-cli Large-v3 | Parakeet Ultra |
741
+
742
+ - Fast mode means "the fast engine you picked": the Preview model. With Fast
743
+ mode off the final text comes from the HQ model.
744
+ - Parakeet runs for a prompt only when installed, warm, and with the breaker
745
+ closed. Otherwise, and when a Parakeet decode fails, is busy, or the audio is
746
+ not 16 kHz WAV (the browser recorder posts WebM), the 6.72.0 Whisper engine
747
+ decodes that audio and the response says why (`engineReason`). Selecting a
748
+ Parakeet HQ model also warms Parakeet. Dictation Parakeet failures count on
749
+ the same breaker as the meeting preview.
750
+ - The engine is chosen when each decode starts. A finalize takes one snapshot
751
+ for all its chunks, and a chunk decoded under another selection is decoded
752
+ again rather than reused.
753
+ - Second opinion and resolver never run on dictation. The quiet gate never
754
+ drops dictation audio. The front end applies to Parakeet dictation decodes;
755
+ Whisper dictation decodes keep raw audio, as before.
756
+ - Responses gain `engine` (`whisper`, `parakeet-ultra`, or `mixed` on a
757
+ finalize) and `engineReason`; `prompt_transcript` events gain `engine`.
758
+ `actualQuality` keeps its values, so a Parakeet HQ prompt is not "degraded".
759
+ No phone needs a new request field: the phone never checks a model on
760
+ dictation text.
761
+ - `COS_DICTATION_FOLLOWS_STT=0` holds every dictation decode on its 6.72.0
762
+ Whisper engine.
763
+
764
+ ### Parakeet (FluidAudio CoreML, offline)
765
+
766
+ `POST /api/stt-settings/parakeet/install` installs FluidAudio's `fluidaudiocli`
767
+ 0.17.4 and Parakeet Ultra (CoreML, `FluidInference/parakeet-ultra-coreml` at
768
+ revision `95eaa59a`, 632 MB), pinned by SHA-256 for every file
769
+ (`server/lib/parakeet-manifest.ts`), under `<data>/stt/parakeet/current`.
770
+ - A local copy whose hash matches is copied instead of downloaded. Looked for,
771
+ in order: the body's `localSource`, `COS_PARAKEET_LOCAL_SOURCES`, the drop-in
772
+ folder `<data>/stt/parakeet-sources` (link or copy a folder holding
773
+ `parakeet-ultra/` there and an install started with `{}` uses it), and
774
+ FluidAudio's own cache. A named `localSource` that lacks a file fails with a
775
+ plain error unless `allowDownload` is true; otherwise missing files come from
776
+ the pinned revision. One mismatched byte fails the install with nothing
777
+ installed. A download that delivers no bytes for 60 s is abandoned.
778
+ `progress.source` says how many files were copied and downloaded, and from
779
+ which folders.
780
+ - FluidAudio publishes no CLI binary, and the CLI is pinned to one build
781
+ (SHA-256 `47141546...`, the build speaker separation uses at
782
+ `~/.cos-glasses/bin/fluidaudiocli`). On a Mac without that build `installable`
783
+ is false and the note says why, so no client offers an install that cannot
784
+ succeed. Apple silicon only.
785
+ - 409 while a recording is live. A crash between the two renames of a
786
+ reinstall leaves the old copy aside; the next start puts it back.
787
+
788
+ Every decode runs under `sandbox-exec`: network denied, and writes allowed only
789
+ to the run's temp directory, the private CoreML cache HOME and the per-user
790
+ temp directory FluidAudio buffers long audio in. Reads stay allowed (system
791
+ frameworks); that is a stated limit. `--local-model-dir` (FluidAudio's path that
792
+ never downloads), a key-free environment, and the CLI's full hash checked once
793
+ per server process. No sandbox, no run. Nothing here calls a language model.
794
+
795
+ - HQ: runs are queued one at a time at lower CPU priority, the first with a
796
+ 180 s deadline (CoreML may compile) and later ones 30 s. Two Parakeet failures
797
+ in a row stop Parakeet for the rest of that job. A Parakeet HQ window that
798
+ fails, or returns no words over at least 0.5 s of detected speech, is decoded
799
+ by Large-v3 instead. A failed second opinion leaves the primary exactly as
800
+ heard. Second opinions run only in a saved meeting's HQ job (and orphan
801
+ recovery), never as part of live capture.
802
+ - Preview: one decode at a time, a 0.9 s deadline, then Turbo only while the
803
+ phone's 1.8 s budget allows (otherwise a quiet 204). Three misses in a row
804
+ open a breaker (Turbo for 1 minute, doubling to 10). The warm-up (an 8 s clip;
805
+ warm when it decodes within 1 s) is deferred while a recording is live and
806
+ retried at most every 2 minutes after a failure.
807
+
808
+ ### The resolver and the second opinion
809
+
810
+ With a second opinion planned, the second engine hears the same front-end audio
811
+ as the primary. A progressive-HQ checkpoint made under the same settings is
812
+ reused as the primary, so only the second opinion is decoded. The two
813
+ hypotheses are aligned word by word by edit distance over normalized words
814
+ (the resolver code has a time-skew pairing limit, not exposed and off: it
815
+ measured worse). The resolver then takes the second opinion only for:
816
+ - words a Whisper primary has nothing for (gap fill), and only inside a span
817
+ the primary's own VAD marked as speech, with the second opinion's mean word
818
+ confidence at least 0.5;
819
+ - a profile vocabulary term the primary missed, inside a short disagreement
820
+ (at most 4 words, the primary's side at most 2 longer).
821
+
822
+ It never changes a region whose two sides differ in a negation, a number or a
823
+ unit, whatever its length. Both raw hypotheses and every disagreement (with
824
+ what was actually kept) are saved with the meeting, additively:
825
+ `batchSegments[].stt` and a meeting-level `stt` in the sidecar; an orphan
826
+ recovery adds `stt` and `sttSegments`. Older clients ignore them. The
827
+ evaluation and the defaults' evidence:
828
+ [docs/stt-672-evaluation.md](docs/stt-672-evaluation.md).
829
+
646
830
  ## Final dictation polish (6.61.5)
647
831
 
648
832
  With glasses app 6.10.616, **Settings → Voice → Polish model** offers **Haiku 5.5**
@@ -667,8 +851,9 @@ and empty workspace. Live preview and canonical meeting transcription are unchan
667
851
  ## HQ dictation
668
852
 
669
853
  Prompt dictation defaults to HQ. The phone owns the preference: **Fast mode
670
- OFF** requests HQ, and **Fast mode ON** requests turbo. The Mac performs all
671
- decoding; the phone does not run Whisper.
854
+ OFF** requests HQ, and **Fast mode ON** requests the fast engine (the live
855
+ worker, or Parakeet Ultra when it is the Preview model; see "Dictation (6.73.0)").
856
+ The Mac performs all decoding; the phone does not run Whisper.
672
857
 
673
858
  For the recommended **Balanced** setup, run:
674
859
 
@@ -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.14",
3
+ "version": "6.73.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": {
@@ -52,6 +52,8 @@
52
52
  "!server/scripts/work-search-mutation-gate.mjs",
53
53
  "!server/scripts/fact-check-mutation-gate.mjs",
54
54
  "!server/scripts/stale-session-mutation-gate.mjs",
55
+ "!server/scripts/stt-mutation-gate.mjs",
56
+ "!server/scripts/dictation-stt-mutation-gate.mjs",
55
57
  "!server/scripts/pairing-mutation-gate.mjs",
56
58
  "!server/scripts/away-hold-mutation-gate.mjs",
57
59
  "!server/scripts/nemotron-mutation-gate.mjs",
@@ -85,6 +87,7 @@
85
87
  "bin/hooks/cos-cursor-observer.cjs",
86
88
  "docs/provider-parity.md",
87
89
  "docs/asr-canaries.md",
90
+ "docs/stt-672-evaluation.md",
88
91
  "docs/pairing-contract.md",
89
92
  "docs/pairing-fixture.json",
90
93
  "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 {