@gotcos/glasses-server 6.91.1 → 6.93.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 +64 -2
- package/CHANGELOG.md +154 -29
- package/README.md +90 -1
- package/docs/active-query-jobs.md +1 -1
- package/docs/test-plan-6.71.7.md +1 -1
- package/managed-runtime-contract.json +10 -1
- package/package.json +2 -1
- package/server/lib/ask-session-id.ts +2 -1
- package/server/lib/claude-circuit.ts +8 -4
- package/server/lib/claude-text-runner.ts +113 -0
- package/server/lib/dictation-fillers-fixture-cases.ts +214 -0
- package/server/lib/dictation-fillers.ts +30 -0
- package/server/lib/dictation-meaning-guard.ts +47 -0
- package/server/lib/dictation-polish-pipeline.ts +90 -12
- package/server/lib/embedding-eviction.ts +60 -2
- package/server/lib/fuzzy-correct.ts +1 -1
- package/server/lib/hallucination-filter.ts +1 -1
- package/server/lib/imported-library-rows.ts +4 -2
- package/server/lib/live-cos-ask.ts +3 -2
- package/server/lib/mac-live-voices.ts +57 -0
- package/server/lib/mac-recording-asr.ts +16 -8
- package/server/lib/mac-speaker-projection.ts +211 -0
- package/server/lib/mac-stream-diarizer.ts +185 -0
- package/server/lib/mac-voice-assignment.ts +361 -0
- package/server/lib/mac-voice-labels.ts +70 -8
- package/server/lib/mac-window-replay.ts +271 -0
- package/server/lib/mac-window-voices.ts +390 -0
- package/server/lib/meeting-actions.ts +1 -1
- package/server/lib/meeting-ask-owner.ts +4 -4
- package/server/lib/meeting-ask-transcript-sources.ts +41 -3
- package/server/lib/meeting-audio-archive.ts +4 -0
- package/server/lib/meeting-batch-transcribe.ts +7 -1
- package/server/lib/meeting-context-keys.ts +6 -3
- package/server/lib/meeting-corrections.ts +4 -4
- package/server/lib/meeting-fact-check.ts +3 -3
- package/server/lib/meeting-parse.ts +1 -1
- package/server/lib/meeting-relabel-enrolment.ts +8 -8
- package/server/lib/meeting-relabel.ts +3 -3
- package/server/lib/meeting-scribe-content.ts +1 -1
- package/server/lib/meeting-speaker-review.ts +8 -8
- package/server/lib/meeting-store.ts +19 -9
- package/server/lib/nemotron-final.ts +114 -9
- package/server/lib/nemotron-live.ts +58 -6
- package/server/lib/phone-recording.ts +114 -0
- package/server/lib/prompt-edit.ts +84 -3
- package/server/lib/prompt-tail-guard.ts +1 -1
- package/server/lib/speaker-embeddings.ts +154 -28
- package/server/lib/speaker-name.ts +1 -1
- package/server/lib/stranded-session-actions.ts +7 -2
- package/server/lib/token-audit.ts +6 -1
- package/server/lib/training-audio-provenance.ts +3 -2
- package/server/lib/transcript-cleanup-context.ts +3 -3
- package/server/lib/transcript-cleanup-model.ts +1 -1
- package/server/lib/transcript-cleanup-spans.ts +10 -10
- package/server/lib/transcript-cleanup-text.ts +4 -4
- package/server/lib/transcript-cleanup.ts +1 -1
- package/server/lib/unsaved-audio-quarantine.ts +10 -1
- package/server/lib/voice-directory.ts +1 -1
- package/server/lib/voice-evidence-guard.ts +2 -1
- package/server/lib/voice-profile-store.ts +11 -5
- package/server/lib/whisper-local.ts +1 -1
- package/server/routes/dictation-polish-settings.ts +8 -1
- package/server/routes/health.ts +8 -0
- package/server/routes/meeting.ts +189 -19
- package/server/routes/meetings.ts +9 -5
- package/server/routes/prompt-drafts.ts +3 -2
- package/server/routes/prompt-edit.ts +47 -5
- package/server/routes/transcribe-stream.ts +140 -15
- package/server/routes/voice.ts +12 -4
- package/server/scripts/dictation-parity-6920-mutation-gate.mjs +229 -0
- package/server/scripts/generate-dictation-fillers-fixture.ts +18 -0
- package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +225 -0
- package/server/scripts/mac-window-voices-6930-mutation-gate.mjs +193 -0
package/README.md
CHANGED
|
@@ -654,6 +654,64 @@ the warm-up and the final passes. `/api/health` is public, so this carries codes
|
|
|
654
654
|
and counts only, never a speaker name, path or error text; those stay in
|
|
655
655
|
`server.log`. A Nemotron fallback never degrades `readiness.status`.
|
|
656
656
|
|
|
657
|
+
### Live Mac voices (6.92.1, 6.93.0)
|
|
658
|
+
|
|
659
|
+
Live `Voice N` is **on by default** (6.93.0) and has one switch,
|
|
660
|
+
`COS_MAC_LIVE_VOICES`: unset or `window` is the default, `anchors` is the older path and
|
|
661
|
+
`off` (also `0`, `false`, `no`) gives every live chunk as one `Ext` speaker until it is
|
|
662
|
+
saved, as in 6.92.0. A `Voice N` is anonymous: never a name, never enrolled and never
|
|
663
|
+
trained on its own. Renaming one is text only unless you assign it to an existing voice
|
|
664
|
+
profile (see below).
|
|
665
|
+
|
|
666
|
+
- **`COS_MAC_LIVE_VOICES=anchors`** (6.92.1 path, opt-in): a chunk reads `Voice N` after two
|
|
667
|
+
independent clean observations agree. On natural audio this rarely fires.
|
|
668
|
+
- **`COS_MAC_LIVE_VOICES=window`** or unset (6.93.0 path, the default, replaces the anchors): each chunk is
|
|
669
|
+
diarized inside the last five durable chunks (30 s) with the same pinned CLI, with a
|
|
670
|
+
timeout sized from the window. A carried voice is used only when the chunk diarized
|
|
671
|
+
alone agrees, which catches a merge of two voices speaking in the same chunk but not
|
|
672
|
+
yet a clean hand-over to a new voice at a chunk boundary. A timeout or refused window
|
|
673
|
+
falls back to the plain per-chunk run, and two of the last three window timeouts stop the window for
|
|
674
|
+
that capture. A voice silent for longer than the window comes back unidentified.
|
|
675
|
+
- **`COS_MAC_VOICE_LABELS`** (on by default) governs the after-save pass; `0` also
|
|
676
|
+
turns both live modes off. Setting it to `1` is the default, not an opt-in.
|
|
677
|
+
|
|
678
|
+
Known limit: a clean hand-over between two people exactly at a chunk boundary can give
|
|
679
|
+
the new speaker the previous number. Set `COS_MAC_LIVE_VOICES=off` in
|
|
680
|
+
`~/.cos-glasses/.env` and restart when nothing is recording to turn it off.
|
|
681
|
+
|
|
682
|
+
**Assigning a Mac voice to a voice profile (6.93.0).** `POST /api/meeting/:sessionId/relabel`
|
|
683
|
+
with `assignVoiceProfile: true` adds a Mac recording `Voice N`'s audio to an already
|
|
684
|
+
enrolled person (never the wearer's own profile: `409 owner_profile`). A Mac recording banks
|
|
685
|
+
no embeddings, so the server extracts them from the retained chunk audio (7 days), keeps only
|
|
686
|
+
that voice, and applies the same coherence and diversity gates as a glasses rename. A dry run
|
|
687
|
+
(`dryRun: true`) reports the person, the most samples it would add, how many units were
|
|
688
|
+
embedded and thrown out as a different voice, and the similarity to the stored profile, and
|
|
689
|
+
writes nothing. A confirm is refused with `409` when the voice sounds unlike the stored
|
|
690
|
+
profile (`below_floor`, under 0.55), the label holds more than one voice (`mixed_voice`) or
|
|
691
|
+
there is too little audio (`too_little_audio`), unless the request carries `force: true`; a
|
|
692
|
+
confirm that would add nothing is refused whole (`422 nothing_to_enrol`). Samples are tagged
|
|
693
|
+
`mac-assign:<sessionId>`: assisted, capped at 10 per profile, never evicting any other kind of
|
|
694
|
+
sample, retracted by "Not in this meeting". It is refused (`409 capture_active`) while any
|
|
695
|
+
meeting is being recorded, and it yields to the event loop while it reads audio. Without the
|
|
696
|
+
flag a Mac rename is text only. `GET /api/health` reports `capabilities.macLiveVoices.mode`
|
|
697
|
+
(`off`, `anchors` or `window`) and, for `window`, counts only.
|
|
698
|
+
|
|
699
|
+
The saved meeting is relabeled by the whole-meeting pass. Live numbers are kept;
|
|
700
|
+
voices the live path did not prove are numbered above them at save.
|
|
701
|
+
|
|
702
|
+
### iPhone microphone meetings (6.93.0, off by default)
|
|
703
|
+
|
|
704
|
+
With `COS_IPHONE_MICROPHONE_MEETINGS=1`, HQ set to Parakeet Ultra and Parakeet
|
|
705
|
+
installed, `GET /api/health` reports `capabilities.phoneMicrophoneMeetings.enabled`,
|
|
706
|
+
and COS Glasses 6.10.655 can record a meeting from the iPhone microphone without
|
|
707
|
+
the glasses connected. Chunks upload through `POST /api/transcribe-stream` with
|
|
708
|
+
`sourceType=iphone_microphone&speakerPolicy=anonymous`, transcribe with Parakeet
|
|
709
|
+
Ultra and the profile dictionary (no Whisper or cloud fallback), and save like any
|
|
710
|
+
meeting. Every line is `Ext`: no wearer label, no voice matching, no training.
|
|
711
|
+
This is the microphone only, not iOS system audio, so it cannot record Zoom,
|
|
712
|
+
YouTube or call audio playing on the phone. COS Control must be 0.5.351 or newer
|
|
713
|
+
before this is switched on. Protocol: `docs/iphone-microphone-protocol.md`.
|
|
714
|
+
|
|
657
715
|
## Speech-to-text settings (6.72.0)
|
|
658
716
|
|
|
659
717
|
One settings object chooses which engine previews live meeting speech, which
|
|
@@ -868,7 +926,7 @@ built-in polish rules, which keep precedence.
|
|
|
868
926
|
answer says `learnedStyle: true` and `learnedStyleHash` (8 hex), never the
|
|
869
927
|
rules.
|
|
870
928
|
- **Meaning guard.** After the model, for messages and prompt drafts, and only
|
|
871
|
-
|
|
929
|
+
on Sonnet with v4 (Haiku, Luna, any other model and prompt v3 get no
|
|
872
930
|
guard, so their answers are 6.90.0's): if a negation, number, weekday or
|
|
873
931
|
month, or name the speaker kept is missing from the answer (or a negation or
|
|
874
932
|
number appears that was never said), the pre-polish text goes out with
|
|
@@ -889,6 +947,37 @@ built-in polish rules, which keep precedence.
|
|
|
889
947
|
(`{protocol: 1, maxChars: 1500, maxRules: 20, enabled}`) and
|
|
890
948
|
`capabilities.dictationCleanup.polishOptions` (`{protocol: 1}`).
|
|
891
949
|
|
|
950
|
+
### Dictation reliability and prompt edit bounds (6.92.0)
|
|
951
|
+
|
|
952
|
+
- **Breakers keyed by model and caller group.** Mac dictation, G2 messages and
|
|
953
|
+
prompt drafts share one group; G2 meeting lines have their own, so a failing
|
|
954
|
+
meeting can no longer turn off message polish. 2 CLI failures in a row open
|
|
955
|
+
a model's breaker for 30 minutes, as before. A client that stops waiting (the
|
|
956
|
+
request closes) is not a failure. `COS_DICTATION_SLOW_BREAKER_ABORTS` (4)
|
|
957
|
+
such aborts in a row, each after 4 s or more of model time, pause that model
|
|
958
|
+
for `COS_DICTATION_SLOW_BREAKER_MS` (300000). Either one open answers
|
|
959
|
+
`polishSkipped: "breaker_open"`, and the prewarm route asks the same keyed
|
|
960
|
+
breaker as its surface. An abort writes an audit row with `polish_outcome`
|
|
961
|
+
`aborted` (codes only); the finalize answer never says `aborted`.
|
|
962
|
+
- **The filler pass** keeps a filler that is part of a path, handle or code
|
|
963
|
+
token (`src/um.txt`, `~/uh`, `@uh`, `um.py`, `um:8080`), and runs only when
|
|
964
|
+
the text is mostly English (`fillerTextLooksEnglish`; short replies such as
|
|
965
|
+
"um yes" count as English), so "um 5 Uhr", "um carro" and "Er kommt" go out
|
|
966
|
+
as said. The meaning guard keeps its 6.91.1 language check. The rules are pinned by `server/lib/__fixtures__/dictation-fillers.json`,
|
|
967
|
+
generated by the server's own functions; COS Control carries a byte-identical
|
|
968
|
+
copy and pins the same sha256.
|
|
969
|
+
- **`GET /api/dictation/polish/options`** adds `meaningGuard` (the
|
|
970
|
+
`COS_DICTATION_MEANING_GUARD` switch) and `stylePairs` (the pairs the guard,
|
|
971
|
+
Your style and the register run on).
|
|
972
|
+
- **G2 prompt edit** (`POST /api/prompt-edit`) runs through an isolated text
|
|
973
|
+
runner (no tools, MCP, hooks or session, a scratch directory) and now stops:
|
|
974
|
+
`COS_PROMPT_EDIT=0` answers `503 Edit is off`; `COS_PROMPT_EDIT_DAILY_CAP`
|
|
975
|
+
(default 200 a local day, its own counter) then `429 Edit limit reached`; 3
|
|
976
|
+
CLI failures in a row pause it 10 minutes (`503 Edit paused`). A failure is
|
|
977
|
+
`500 Edit failed`, never the CLI's output. Every refusal is `text/plain`.
|
|
978
|
+
`COS_PROMPT_EDIT_MODEL`, `_EFFORT` and `_TIMEOUT_MS` are unchanged
|
|
979
|
+
(sonnet, low, 45000).
|
|
980
|
+
|
|
892
981
|
### Parakeet (FluidAudio CoreML, offline)
|
|
893
982
|
|
|
894
983
|
`POST /api/stt-settings/parakeet/install` installs FluidAudio's `fluidaudiocli`
|
|
@@ -43,7 +43,7 @@ Values are illustrative.
|
|
|
43
43
|
"model": "opus",
|
|
44
44
|
"resolvedModel": "claude-opus-4-8",
|
|
45
45
|
"messageNo": 1287,
|
|
46
|
-
"title": "What did
|
|
46
|
+
"title": "What did Gordon say about the October webinar numbers?"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"jobId": "9b2a7d40-1c3e-4f58-8e2d-6a0c4b9e7f21",
|
package/docs/test-plan-6.71.7.md
CHANGED
|
@@ -9,7 +9,7 @@ Candidate only. Server base is public 6.71.5; 6.71.6 is reserved by the separate
|
|
|
9
9
|
- New broker tests compare 144 approval/unstamped admission and tick cases to the actual 6.71.5 implementation. Stamped questions cover connected/no-client, active/idle/unknown desk, 90-second visibility latch, timeout ceilings, canceled hooks, session boundaries/Interrupt, child events, background requests, parallel questions and first-answer races. A desk completion may add either string or array answers to its input without changing which question it settles.
|
|
10
10
|
- Eleven isolated guard mutations are killed by their named assertions after a green baseline. Original source bytes are restored and the release checkout is never mutated.
|
|
11
11
|
- Companion status probe measures 20 updates per Ask/Transcript case at 390x844, 320x568 and 390x420, with long content scrolled above the bottom. Composer position, reading height, scroll position and status height must stay within 0.5 px. The private old-CSS control must fail. Standard/full WebKit-strict tests, release typechecks, manufacturer contracts, startup timer quarantine and package asset hashes must pass on final bundles.
|
|
12
|
-
- Receipts/logs:
|
|
12
|
+
- Receipts/logs: the release owner's local `glasses-question-answering-2026-10-11` evidence directory (not bundled). Local automated evidence does not prove stability inside the physical Even host.
|
|
13
13
|
|
|
14
14
|
## Phone status canary (629 on the current server)
|
|
15
15
|
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"COS_LIVE_CUES_V2_MAC",
|
|
39
39
|
"COS_TAILSCALE_NUDGE",
|
|
40
40
|
"COS_MAC_VOICE_LABELS",
|
|
41
|
+
"COS_MAC_LIVE_VOICES",
|
|
42
|
+
"COS_IPHONE_MICROPHONE_MEETINGS",
|
|
41
43
|
"COS_LIVE_CUES_JEV_DAILY_TOKENS",
|
|
42
44
|
"COS_LIVE_CUES_SUBQ_DAILY_TOKENS",
|
|
43
45
|
"COS_DECISION_PROVIDER_CUES",
|
|
@@ -90,7 +92,14 @@
|
|
|
90
92
|
"COS_DICTATION_PROMPT_VERSION",
|
|
91
93
|
"COS_DICTATION_LEARNED_STYLE",
|
|
92
94
|
"COS_DICTATION_MEANING_GUARD",
|
|
93
|
-
"COS_DICTATION_REGISTERS"
|
|
95
|
+
"COS_DICTATION_REGISTERS",
|
|
96
|
+
"COS_DICTATION_SLOW_BREAKER_ABORTS",
|
|
97
|
+
"COS_DICTATION_SLOW_BREAKER_MS",
|
|
98
|
+
"COS_PROMPT_EDIT",
|
|
99
|
+
"COS_PROMPT_EDIT_DAILY_CAP",
|
|
100
|
+
"COS_PROMPT_EDIT_MODEL",
|
|
101
|
+
"COS_PROMPT_EDIT_EFFORT",
|
|
102
|
+
"COS_PROMPT_EDIT_TIMEOUT_MS"
|
|
94
103
|
],
|
|
95
104
|
"maintenance": {
|
|
96
105
|
"scope": "cross_boot",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.93.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": {
|
|
@@ -163,6 +163,7 @@
|
|
|
163
163
|
"node": ">=20.11"
|
|
164
164
|
},
|
|
165
165
|
"dependencies": {
|
|
166
|
+
"body-parser": "^2.4.0",
|
|
166
167
|
"cors": "^2.8.6",
|
|
167
168
|
"express": "^5.2.1",
|
|
168
169
|
"tsx": "^4.21.0"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Which recordings may open live Ask, its history and Work evidence. 6.73.8: a Mac capture
|
|
2
2
|
// (mac_<uuid>, lib/mac-recording.ts MAC_CAPTURE_ID) uses the same Ask as a G2 meeting; before this
|
|
3
3
|
// every Mac request and saved-question read failed with invalid_session_id.
|
|
4
|
+
// Future iPhone microphone captures share this store under their own phone_ IDs.
|
|
4
5
|
export const MAC_ASK_SESSION = /^mac_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
5
|
-
export const ASK_SESSION = /^(?:meeting_[A-Za-z0-9_-]{1,89}|mac_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/
|
|
6
|
+
export const ASK_SESSION = /^(?:meeting_[A-Za-z0-9_-]{1,89}|(?:mac_|phone_(?:[0-9]{13}_)?)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/
|
|
@@ -15,16 +15,18 @@ export interface ClaudeBreaker {
|
|
|
15
15
|
|
|
16
16
|
export function createBreaker(opts: {
|
|
17
17
|
label: string
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/** 6.92.0: a function is read at each check, so an environment value applies after a restart. */
|
|
19
|
+
maxFailures?: number | (() => number)
|
|
20
|
+
cooldownMs?: number | (() => number)
|
|
20
21
|
}): ClaudeBreaker {
|
|
21
|
-
const
|
|
22
|
-
const cooldownMs = opts.cooldownMs ?? 30 * 60 * 1000 // 30 minutes
|
|
22
|
+
const read = (value: number | (() => number) | undefined, fallback: number) => typeof value === 'function' ? value() : value ?? fallback
|
|
23
23
|
let consecutiveFailures = 0
|
|
24
24
|
let openedAt = 0
|
|
25
25
|
|
|
26
26
|
return {
|
|
27
27
|
isOpen(): boolean {
|
|
28
|
+
const maxFailures = read(opts.maxFailures, 2)
|
|
29
|
+
const cooldownMs = read(opts.cooldownMs, 30 * 60 * 1000) // 30 minutes
|
|
28
30
|
if (consecutiveFailures < maxFailures) return false
|
|
29
31
|
const elapsed = Date.now() - openedAt
|
|
30
32
|
if (elapsed >= cooldownMs) {
|
|
@@ -35,6 +37,8 @@ export function createBreaker(opts: {
|
|
|
35
37
|
return true
|
|
36
38
|
},
|
|
37
39
|
recordFailure(): void {
|
|
40
|
+
const maxFailures = read(opts.maxFailures, 2)
|
|
41
|
+
const cooldownMs = read(opts.cooldownMs, 30 * 60 * 1000)
|
|
38
42
|
consecutiveFailures++
|
|
39
43
|
if (consecutiveFailures >= maxFailures) {
|
|
40
44
|
openedAt = Date.now()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 6.92.0 (plan r3, A-W0b): a small isolated `claude -p` text runner, for callers that only turn text
|
|
3
|
+
* into text (G2's prompt edit today). It replaces spawnClaudeText for those callers; spawnClaudeText
|
|
4
|
+
* stays in prompt-edit.ts for meeting summaries.
|
|
5
|
+
*
|
|
6
|
+
* Isolation, the dictation polish caller's (dictation-clean.ts claudePolishArgs) minus its env strip:
|
|
7
|
+
* - no tools (`--tools ''`), so the key-secrets deny layer has nothing to guard (key-lanes census:
|
|
8
|
+
* TOOL_LESS_CLAUDE); no MCP servers (`--strict-mcp-config` with an empty config); no hooks, in
|
|
9
|
+
* exactly ONE `--settings` layer; `--permission-mode dontAsk`; no session file;
|
|
10
|
+
* - `--model` is always passed, so it can never silently become Opus;
|
|
11
|
+
* - a fresh scratch working directory, never the server's own;
|
|
12
|
+
* - a verified absolute binary (provider-binary.ts), never a bare PATH lookup.
|
|
13
|
+
* The environment is kept as spawnClaudeText kept it (only CLAUDECODE is removed and PATH repaired),
|
|
14
|
+
* so a person who signs the Claude CLI in with an API key keeps working.
|
|
15
|
+
*
|
|
16
|
+
* Errors carry a code and never the CLI's stdout or stderr: those can hold the draft or login data.
|
|
17
|
+
*/
|
|
18
|
+
import { spawn } from 'node:child_process'
|
|
19
|
+
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
|
|
20
|
+
import { tmpdir } from 'node:os'
|
|
21
|
+
import { dirname, join } from 'node:path'
|
|
22
|
+
import { resolveProviderBinary } from './provider-binary.js'
|
|
23
|
+
import { terminateProviderProcess } from './provider-process-lifecycle.js'
|
|
24
|
+
|
|
25
|
+
export type ClaudeTextErrorCode = 'unavailable' | 'aborted' | 'timeout' | 'failed' | 'empty' | 'too_large' | 'input'
|
|
26
|
+
|
|
27
|
+
export class ClaudeTextError extends Error {
|
|
28
|
+
constructor(readonly code: ClaudeTextErrorCode, label: string) {
|
|
29
|
+
super(`${label} ${code}`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ClaudeTextRunOptions {
|
|
34
|
+
model: string
|
|
35
|
+
effort: string
|
|
36
|
+
timeoutMs: number
|
|
37
|
+
systemPrompt: string
|
|
38
|
+
/** Prefix for the error message ("Prompt edit"). */
|
|
39
|
+
label: string
|
|
40
|
+
signal?: AbortSignal
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The argv: one `--settings`, no tools, no MCP, no hooks, no session. */
|
|
44
|
+
export function claudeTextArgs(model: string, effort: string, systemPrompt: string): string[] {
|
|
45
|
+
return [
|
|
46
|
+
'-p', '--model', model, '--effort', effort, '--output-format', 'text',
|
|
47
|
+
'--no-session-persistence', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}',
|
|
48
|
+
'--tools', '', '--settings', '{"disableAllHooks":true}', '--permission-mode', 'dontAsk',
|
|
49
|
+
'--system-prompt', systemPrompt,
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** spawnClaudeText's environment: everything but CLAUDECODE (anti-recursion), with the binary's dir on PATH. */
|
|
54
|
+
export function claudeTextEnv(binary: string, env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
55
|
+
const out = { ...env }
|
|
56
|
+
delete out.CLAUDECODE
|
|
57
|
+
out.PATH = [dirname(binary), '/opt/homebrew/bin', out.PATH || ''].join(':')
|
|
58
|
+
out.NO_OPEN_BROWSER = '1'
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const MAX_OUTPUT_CHARS = 256_000
|
|
63
|
+
|
|
64
|
+
/** Run the prompt on stdin; resolve the trimmed text answer, or reject with a ClaudeTextError. */
|
|
65
|
+
export async function runClaudeText(prompt: string, opts: ClaudeTextRunOptions): Promise<string> {
|
|
66
|
+
if (opts.signal?.aborted) throw new ClaudeTextError('aborted', opts.label)
|
|
67
|
+
const binary = resolveProviderBinary('claude')
|
|
68
|
+
if (!binary.ok) throw new ClaudeTextError('unavailable', opts.label)
|
|
69
|
+
const timeoutMs = Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : 45_000
|
|
70
|
+
const scratch = mkdtempSync(join(tmpdir(), 'cos-claude-text-'))
|
|
71
|
+
try {
|
|
72
|
+
const workspace = join(scratch, 'workspace')
|
|
73
|
+
mkdirSync(workspace, { mode: 0o700 })
|
|
74
|
+
return await new Promise<string>((resolve, reject) => {
|
|
75
|
+
const proc = spawn(binary.path, claudeTextArgs(opts.model, opts.effort, opts.systemPrompt), {
|
|
76
|
+
stdio: ['pipe', 'pipe', 'pipe'], env: claudeTextEnv(binary.path), cwd: workspace, detached: true,
|
|
77
|
+
})
|
|
78
|
+
let stdout = ''
|
|
79
|
+
let settled = false
|
|
80
|
+
const finish = (error?: ClaudeTextError, output?: string, terminate = false) => {
|
|
81
|
+
if (settled) return
|
|
82
|
+
settled = true
|
|
83
|
+
clearTimeout(timer)
|
|
84
|
+
opts.signal?.removeEventListener('abort', abort)
|
|
85
|
+
const complete = () => error ? reject(error) : resolve(output!)
|
|
86
|
+
if (terminate) void terminateProviderProcess(proc).then(complete, complete)
|
|
87
|
+
else complete()
|
|
88
|
+
}
|
|
89
|
+
const abort = () => finish(new ClaudeTextError('aborted', opts.label), undefined, true)
|
|
90
|
+
const timer = setTimeout(() => finish(new ClaudeTextError('timeout', opts.label), undefined, true), timeoutMs)
|
|
91
|
+
proc.stdout.on('data', (chunk: Buffer) => {
|
|
92
|
+
stdout += chunk.toString()
|
|
93
|
+
if (stdout.length > MAX_OUTPUT_CHARS) finish(new ClaudeTextError('too_large', opts.label), undefined, true)
|
|
94
|
+
})
|
|
95
|
+
// Never read or log stderr: it can carry login or draft data.
|
|
96
|
+
proc.stderr.resume()
|
|
97
|
+
proc.on('error', () => finish(new ClaudeTextError('unavailable', opts.label), undefined, true))
|
|
98
|
+
proc.on('close', (code) => {
|
|
99
|
+
if (settled) return
|
|
100
|
+
if (code !== 0) return finish(new ClaudeTextError('failed', opts.label))
|
|
101
|
+
const output = stdout.trim()
|
|
102
|
+
if (!output) return finish(new ClaudeTextError('empty', opts.label))
|
|
103
|
+
finish(undefined, output)
|
|
104
|
+
})
|
|
105
|
+
proc.stdin.on('error', () => finish(new ClaudeTextError('input', opts.label), undefined, true))
|
|
106
|
+
opts.signal?.addEventListener('abort', abort, { once: true })
|
|
107
|
+
if (opts.signal?.aborted) return abort()
|
|
108
|
+
try { proc.stdin.end(prompt) } catch { finish(new ClaudeTextError('input', opts.label), undefined, true) }
|
|
109
|
+
})
|
|
110
|
+
} finally {
|
|
111
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 6.92.0 (plan r3, A-W10): the canonical filler fixture's cases, one source of truth for the server's
|
|
3
|
+
* filler pass and COS Control's port. `want` is written by hand, so the generated fixture is checked
|
|
4
|
+
* against an expectation, never only against the function that produced it.
|
|
5
|
+
*
|
|
6
|
+
* scope "both" the token rules alone (stripDisfluencies). Control's port must match 100%. The
|
|
7
|
+
* "language" rows among them (short English replies) also hold through the
|
|
8
|
+
* server's language gate, and must through Control's.
|
|
9
|
+
* scope "server" the server's outbound filler step (stripFillersIfEnglish: the language gate,
|
|
10
|
+
* then the token rules). The gate is the server's own detector, so these rows are
|
|
11
|
+
* not a parity target; Control gates with its own detector.
|
|
12
|
+
*
|
|
13
|
+
* Synthetic, fictional text only. Regenerate the fixture with
|
|
14
|
+
* npx tsx server/scripts/generate-dictation-fillers-fixture.ts
|
|
15
|
+
* and update the sha256 pinned in dictation-fillers-fixture.test.ts (and in COS Control's copy).
|
|
16
|
+
*/
|
|
17
|
+
import { stripDisfluencies, stripFillersIfEnglish } from './dictation-fillers.js'
|
|
18
|
+
|
|
19
|
+
export type FillerFixtureScope = 'both' | 'server' | 'client'
|
|
20
|
+
export interface FillerFixtureCase { id: string; scope: FillerFixtureScope; group: 'english' | 'exemption' | 'language'; input: string; want: string }
|
|
21
|
+
export interface FillerFixtureRow { id: string; scope: FillerFixtureScope; group: FillerFixtureCase['group']; input: string; expected: string }
|
|
22
|
+
|
|
23
|
+
/** The 6.79.1 filler-pass tables (dictation-fillers.test.ts), as [input, want]. */
|
|
24
|
+
const ENGLISH: Array<[string, string]> = [
|
|
25
|
+
['We need uh the deck by Friday', 'We need the deck by Friday'],
|
|
26
|
+
['Send the um the update', 'Send the the update'],
|
|
27
|
+
['ship it er tomorrow', 'ship it tomorrow'],
|
|
28
|
+
['ship it erm tomorrow', 'ship it tomorrow'],
|
|
29
|
+
['Book the room uhm at noon', 'Book the room at noon'],
|
|
30
|
+
['So, um, we should ship', 'So, we should ship'],
|
|
31
|
+
['The plan, uh, is solid', 'The plan, is solid'],
|
|
32
|
+
['Um, I think we are ready', 'I think we are ready'],
|
|
33
|
+
['Uh, can you check the build', 'Can you check the build'],
|
|
34
|
+
['um, so we go', 'so we go'],
|
|
35
|
+
['Okay. Um, let us start', 'Okay. Let us start'],
|
|
36
|
+
['Done? Uh, maybe not', 'Done? Maybe not'],
|
|
37
|
+
['Hmm. Let me think about it.', 'Let me think about it.'],
|
|
38
|
+
['Okay. Um. Let us go', 'Okay. Let us go'],
|
|
39
|
+
['Mhm, sure thing', 'Sure thing'],
|
|
40
|
+
['I think, uh.', 'I think.'],
|
|
41
|
+
['Is that right, hmm?', 'Is that right?'],
|
|
42
|
+
['Let us ship it, um', 'Let us ship it'],
|
|
43
|
+
['Let us ship it um.', 'Let us ship it.'],
|
|
44
|
+
['Call (uh) Dana', 'Call () Dana'],
|
|
45
|
+
['It was um... great', 'It was great'],
|
|
46
|
+
['It was um… great', 'It was great'],
|
|
47
|
+
['well — um — we could', 'well — we could'],
|
|
48
|
+
['Um... I forgot', 'I forgot'],
|
|
49
|
+
['um um the deck', 'the deck'],
|
|
50
|
+
['Uh, um, so the plan', 'So the plan'],
|
|
51
|
+
['Ummm, yes', 'Yes'],
|
|
52
|
+
['the hmmm thing', 'the thing'],
|
|
53
|
+
['it is ahh fine', 'it is fine'],
|
|
54
|
+
['Ahh, I see', 'I see'],
|
|
55
|
+
['Mm, that works', 'That works'],
|
|
56
|
+
['that uhh works', 'that works'],
|
|
57
|
+
['He said "um, no"', 'He said "no"'],
|
|
58
|
+
['First line\num, second line', 'First line\nsecond line'],
|
|
59
|
+
['First line\nUm, second line', 'First line\nSecond line'],
|
|
60
|
+
['Ask, um, Ah Kow to join', 'Ask, Ah Kow to join'],
|
|
61
|
+
['uh uh the deck', 'the deck'],
|
|
62
|
+
['uh hmm let me think', 'let me think'],
|
|
63
|
+
['Uh hmm, the plan', 'The plan'],
|
|
64
|
+
['Hmm. Let me check ... and Ah Kim\'s notes', 'Let me check ... and Ah Kim\'s notes'],
|
|
65
|
+
['at 5 p.m. uh we meet', 'at 5 p.m. we meet'],
|
|
66
|
+
['e.g. um the deck', 'e.g. the deck'],
|
|
67
|
+
['We met Mr. uh Jones', 'We met Mr. Jones'],
|
|
68
|
+
['I spoke to the U.S. uh team', 'I spoke to the U.S. team'],
|
|
69
|
+
['Smith Jr. um signed', 'Smith Jr. signed'],
|
|
70
|
+
['I think, Um, we should', 'I think, we should'],
|
|
71
|
+
['Wait... Uh, maybe', 'Wait... maybe'],
|
|
72
|
+
['Wait. Uh, maybe', 'Wait. Maybe'],
|
|
73
|
+
['um,, so we go', 'so we go'],
|
|
74
|
+
['Bring an umbrella and keep going ahead', 'Bring an umbrella and keep going ahead'],
|
|
75
|
+
['To err is human', 'To err is human'],
|
|
76
|
+
['The hummingbird hummed', 'The hummingbird hummed'],
|
|
77
|
+
['Erma and Ahmed will join', 'Erma and Ahmed will join'],
|
|
78
|
+
['Check the umami and the uhaul', 'Check the umami and the uhaul'],
|
|
79
|
+
['Uh-oh, the build broke', 'Uh-oh, the build broke'],
|
|
80
|
+
['She said uh-huh and mm-hmm', 'She said uh-huh and mm-hmm'],
|
|
81
|
+
['UM beat ER in the final, AH was close', 'UM beat ER in the final, AH was close'],
|
|
82
|
+
['HMM is the model name', 'HMM is the model name'],
|
|
83
|
+
['Use a 5 mm bolt and a 10mm spacer', 'Use a 5 mm bolt and a 10mm spacer'],
|
|
84
|
+
['Ask Ah Kow to join the call', 'Ask Ah Kow to join the call'],
|
|
85
|
+
['Loop in Er Lin and Hm Tran', 'Loop in Er Lin and Hm Tran'],
|
|
86
|
+
['The note from Mm Patel is attached', 'The note from Mm Patel is attached'],
|
|
87
|
+
['We met Mr. Ah and Ms. Er yesterday', 'We met Mr. Ah and Ms. Er yesterday'],
|
|
88
|
+
['Dr. Er said the scan is clear', 'Dr. Er said the scan is clear'],
|
|
89
|
+
['Dr. Er said', 'Dr. Er said'],
|
|
90
|
+
['Dr. Er said yes', 'Dr. Er said yes'],
|
|
91
|
+
['Ask Jr. Ah and Sr. Um', 'Ask Jr. Ah and Sr. Um'],
|
|
92
|
+
['Ship it to the U.S. Um, team first', 'Ship it to the U.S. Um, team first'],
|
|
93
|
+
['Dr. Er, the surgeon, called', 'Dr. Er, the surgeon, called'],
|
|
94
|
+
['Meet with J. Ah tomorrow', 'Meet with J. Ah tomorrow'],
|
|
95
|
+
['Ask Mrs. Um and Prof. Hm about it', 'Ask Mrs. Um and Prof. Hm about it'],
|
|
96
|
+
['"um" is a filler word', '"um" is a filler word'],
|
|
97
|
+
['Say “uh” less often', 'Say “uh” less often'],
|
|
98
|
+
['He said uh huh and left', 'He said uh huh and left'],
|
|
99
|
+
['uh oh, the build broke', 'uh oh, the build broke'],
|
|
100
|
+
['She went mm hmm and nodded', 'She went mm hmm and nodded'],
|
|
101
|
+
['Plain text with no hesitation.', 'Plain text with no hesitation.'],
|
|
102
|
+
[', and a leading comma stays when nothing was removed', ', and a leading comma stays when nothing was removed'],
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
/** 6.92.0 token exemptions and quote rules: scope "both". A filler that is part of a path, handle or code token is kept. */
|
|
106
|
+
const EXEMPTION: Array<[string, string]> = [
|
|
107
|
+
['open src/um.txt now', 'open src/um.txt now'],
|
|
108
|
+
['cd ~/uh and list it', 'cd ~/uh and list it'],
|
|
109
|
+
['ping @uh about the deck', 'ping @uh about the deck'],
|
|
110
|
+
['the tag #um is trending', 'the tag #um is trending'],
|
|
111
|
+
['echo $um in the shell', 'echo $um in the shell'],
|
|
112
|
+
['set the my_um field', 'set the my_um field'],
|
|
113
|
+
['rename it to .um before lunch', 'rename it to .um before lunch'],
|
|
114
|
+
['connect to host:um today', 'connect to host:um today'],
|
|
115
|
+
['run um.py again', 'run um.py again'],
|
|
116
|
+
['open port um:8080 first', 'open port um:8080 first'],
|
|
117
|
+
['the um/bin folder', 'the um/bin folder'],
|
|
118
|
+
['mail um@example.com later', 'mail um@example.com later'],
|
|
119
|
+
['the um_id column', 'the um_id column'],
|
|
120
|
+
['issue um#12 is open', 'issue um#12 is open'],
|
|
121
|
+
['the um$ variable', 'the um$ variable'],
|
|
122
|
+
['pass the -um flag', 'pass the -um flag'],
|
|
123
|
+
// A sentence-final period or colon is not part of a token.
|
|
124
|
+
['Let us ship it um.', 'Let us ship it.'],
|
|
125
|
+
['The reason is um: we ran out', 'The reason is: we ran out'],
|
|
126
|
+
['src/um.txt and um, the notes', 'src/um.txt and the notes'],
|
|
127
|
+
// Meaning, places and quotes.
|
|
128
|
+
['Uh-oh, the build broke', 'Uh-oh, the build broke'],
|
|
129
|
+
['We flew to Umbria in May', 'We flew to Umbria in May'],
|
|
130
|
+
['uh huh, sounds good', 'uh huh, sounds good'],
|
|
131
|
+
['she said "um" twice', 'she said "um" twice'],
|
|
132
|
+
['He said "um, no"', 'He said "no"'],
|
|
133
|
+
['Erm, the uhaul is here', 'The uhaul is here'],
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
/** 6.92.0 language gate: scope "server". Not English goes out as said; English still loses its fillers. */
|
|
137
|
+
const LANGUAGE: Array<[string, string]> = [
|
|
138
|
+
['Wir treffen uns um 5 Uhr', 'Wir treffen uns um 5 Uhr'],
|
|
139
|
+
['um drei Uhr', 'um drei Uhr'],
|
|
140
|
+
['Er kommt morgen.', 'Er kommt morgen.'],
|
|
141
|
+
['Er ist heute nicht da', 'Er ist heute nicht da'],
|
|
142
|
+
['Ich komme um 9 Uhr', 'Ich komme um 9 Uhr'],
|
|
143
|
+
['Er hat um 8 Uhr angerufen', 'Er hat um 8 Uhr angerufen'],
|
|
144
|
+
['Comprei um carro novo', 'Comprei um carro novo'],
|
|
145
|
+
['um carro', 'um carro'],
|
|
146
|
+
['Ela tem um gato', 'Ela tem um gato'],
|
|
147
|
+
['Vamos sair um pouco', 'Vamos sair um pouco'],
|
|
148
|
+
['Isso custa um real', 'Isso custa um real'],
|
|
149
|
+
['Han er hjemme nu', 'Han er hjemme nu'],
|
|
150
|
+
['Det er fint', 'Det er fint'],
|
|
151
|
+
['Er is een probleem', 'Er is een probleem'],
|
|
152
|
+
['Er komt morgen', 'Er komt morgen'],
|
|
153
|
+
['ship it er tomorrow', 'ship it tomorrow'],
|
|
154
|
+
['We need um the deck by Friday', 'We need the deck by Friday'],
|
|
155
|
+
['Um, sure thing', 'Sure thing'],
|
|
156
|
+
['er, the plan works', 'the plan works'],
|
|
157
|
+
['Ask Marco um about the pricing', 'Ask Marco about the pricing'],
|
|
158
|
+
['um the MIT report', 'the MIT report'],
|
|
159
|
+
['Send the deck to Dana, um, por favor', 'Send the deck to Dana, por favor'],
|
|
160
|
+
// A time said in English ("pm") is English evidence.
|
|
161
|
+
['um 5 pm works', '5 pm works'],
|
|
162
|
+
// 6.92.0 QA: short English replies and commands lose their filler, as in 6.91.1.
|
|
163
|
+
['um okay', 'okay'],
|
|
164
|
+
['um sure', 'sure'],
|
|
165
|
+
['um yeah', 'yeah'],
|
|
166
|
+
['um no', 'no'],
|
|
167
|
+
['um call Dana', 'call Dana'],
|
|
168
|
+
['um Tuesday', 'Tuesday'],
|
|
169
|
+
['ok um send', 'ok send'],
|
|
170
|
+
['Um, thanks', 'Thanks'],
|
|
171
|
+
// The known cost: an inline um with no English word beside it is kept.
|
|
172
|
+
['um Dana', 'um Dana'],
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 6.92.0 QA: short English replies, scope "both". COS Control's port must give the same answer
|
|
177
|
+
* through its own gate and token rules; the server's gate and token rules both give it (pinned).
|
|
178
|
+
*/
|
|
179
|
+
const SHORT_REPLIES: Array<[string, string]> = [
|
|
180
|
+
['um yes', 'yes'],
|
|
181
|
+
['Um yes.', 'Yes.'],
|
|
182
|
+
['er no', 'no'],
|
|
183
|
+
['um thanks Dana', 'thanks Dana'],
|
|
184
|
+
['um sounds good', 'sounds good'],
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
const pad = (n: number) => String(n).padStart(3, '0')
|
|
188
|
+
export const FILLER_FIXTURE_CASES: FillerFixtureCase[] = [
|
|
189
|
+
...ENGLISH.map(([input, want], i) => ({ id: `en${pad(i + 1)}`, scope: 'both' as const, group: 'english' as const, input, want })),
|
|
190
|
+
...EXEMPTION.map(([input, want], i) => ({ id: `ex${pad(i + 1)}`, scope: 'both' as const, group: 'exemption' as const, input, want })),
|
|
191
|
+
...LANGUAGE.map(([input, want], i) => ({ id: `la${pad(i + 1)}`, scope: 'server' as const, group: 'language' as const, input, want })),
|
|
192
|
+
...SHORT_REPLIES.map(([input, want], i) => ({ id: `sr${pad(i + 1)}`, scope: 'both' as const, group: 'language' as const, input, want })),
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
/** The server function a row's scope names. */
|
|
196
|
+
export function fillerFixtureOutput(scope: FillerFixtureScope, input: string): string {
|
|
197
|
+
return scope === 'server' ? stripFillersIfEnglish(input) : stripDisfluencies(input)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The fixture file's exact text: generated by the server functions, never by hand. */
|
|
201
|
+
export function renderFillerFixture(): string {
|
|
202
|
+
const rows: FillerFixtureRow[] = FILLER_FIXTURE_CASES.map(c => ({ id: c.id, scope: c.scope, group: c.group, input: c.input, expected: fillerFixtureOutput(c.scope, c.input) }))
|
|
203
|
+
return JSON.stringify({
|
|
204
|
+
schema: 1,
|
|
205
|
+
generator: 'server/scripts/generate-dictation-fillers-fixture.ts',
|
|
206
|
+
tokens: ['um+', 'uhm', 'uh+', 'er', 'erm', 'ah+', 'hm+', 'mm+', 'mhm'],
|
|
207
|
+
scopes: {
|
|
208
|
+
both: 'stripDisfluencies: the token rules alone; a port must match every row',
|
|
209
|
+
server: 'stripFillersIfEnglish: the server language gate, then the token rules',
|
|
210
|
+
client: 'reserved for client-only rows; the server generates none',
|
|
211
|
+
},
|
|
212
|
+
rows,
|
|
213
|
+
}, null, 2) + '\n'
|
|
214
|
+
}
|
|
@@ -20,9 +20,22 @@
|
|
|
20
20
|
* still loses "uh"), and any quoted mention ('"um"').
|
|
21
21
|
* - "uh huh", "uh oh" and "mm hmm" written without a hyphen: answers, not hesitations.
|
|
22
22
|
* - "like", "you know", "I mean": filler only sometimes, so the model decides, not a regex.
|
|
23
|
+
* - 6.92.0: a token that touches a path, handle or code character is part of that token, not a
|
|
24
|
+
* hesitation: "src/um.txt", "~/uh", "@uh", "#um", "$um", "my_um", "um.py", "um:8080", "um/bin".
|
|
25
|
+
* Kept when the character right before it is one of / . @ _ : # $ (a leading - is already
|
|
26
|
+
* outside the token pattern), or the character right after it is one of / @ _ # $, or a . or :
|
|
27
|
+
* followed by a letter or digit. A sentence-final "um." or "um:" (a space or the end after it)
|
|
28
|
+
* still goes.
|
|
23
29
|
* A filler inside a longer quote ("um, no") is removed like any other.
|
|
24
30
|
* If nothing but fillers was said, the original text is returned unchanged.
|
|
31
|
+
*
|
|
32
|
+
* 6.92.0: the language gate lives in the caller, not here (`stripFillersIfEnglish`): "um" and "er"
|
|
33
|
+
* are words in German, Portuguese, Danish, Norwegian and Dutch ("um 5 Uhr", "um carro", "Er kommt
|
|
34
|
+
* morgen"). This function stays the pure token rules that the canonical fixture
|
|
35
|
+
* (__fixtures__/dictation-fillers.json, scope "both") pins for COS Control's port.
|
|
25
36
|
*/
|
|
37
|
+
import { fillerTextLooksEnglish } from './dictation-meaning-guard.js'
|
|
38
|
+
|
|
26
39
|
|
|
27
40
|
const FILLER = /(?<![\p{L}\p{N}'’-])(um+|uhm|uh+|erm?|ah+|hm+|mm+|mhm)(?![\p{L}\p{N}'’-])/giu
|
|
28
41
|
/** After a filler: a pause mark (comma, ellipsis, dash) with its spacing. */
|
|
@@ -38,6 +51,10 @@ const QUOTE_OPEN = /["“”'‘’]$/
|
|
|
38
51
|
const QUOTE_CLOSE = /^["“”'‘’]/
|
|
39
52
|
/** A pause mark right after the token: a Titlecase filler set off this way is a hesitation. */
|
|
40
53
|
const PAUSE_AFTER = /^[ \t]*(?:,|\.{2,}|…)/
|
|
54
|
+
/** 6.92.0: a path, handle or code character right before the token ("~/uh", "@uh", "#um", ".um"). */
|
|
55
|
+
const TOUCH_BEFORE = /[/.@_:#$]$/
|
|
56
|
+
/** 6.92.0: right after it: / @ _ # $ ("um/bin", "um_id"), or . or : with a letter or digit next ("um.py", "um:8080"). */
|
|
57
|
+
const TOUCH_AFTER = /^(?:[/@_#$]|[.:][\p{L}\p{N}])/u
|
|
41
58
|
|
|
42
59
|
function afterAbbreviation(before: string): boolean {
|
|
43
60
|
const trimmed = before.replace(/[ \t]+$/, '')
|
|
@@ -73,6 +90,9 @@ export function stripDisfluencies(text: string): string {
|
|
|
73
90
|
const atStart = sentenceStart(out + before)
|
|
74
91
|
const keep = (token.length > 1 && token === token.toUpperCase())
|
|
75
92
|
|| (/^mm+$/i.test(token) && /\d[ \t]*$/.test(out + before))
|
|
93
|
+
// 6.92.0: part of a path, handle or code token, never a hesitation.
|
|
94
|
+
|| TOUCH_BEFORE.test(out + before)
|
|
95
|
+
|| TOUCH_AFTER.test(after)
|
|
76
96
|
// A capital away from a sentence start is a name ("Ask Ah Kow"), unless a pause mark
|
|
77
97
|
// sets it off as a hesitation ("I think, Um, we should").
|
|
78
98
|
|| (!atStart && /^[A-Z]/.test(token) && !PAUSE_AFTER.test(after))
|
|
@@ -114,3 +134,13 @@ export function stripDisfluencies(text: string): string {
|
|
|
114
134
|
const result = (out + rest).replace(/^[ \t]*[,;][ \t]*/, '')
|
|
115
135
|
return result.trim() === '' ? text : result
|
|
116
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 6.92.0: the server's outbound filler step. The token rules (stripDisfluencies) run only when the
|
|
140
|
+
* text is mostly English (dictation-meaning-guard.ts fillerTextLooksEnglish), so "Wir treffen uns um 5 Uhr",
|
|
141
|
+
* "Comprei um carro" and "Er kommt morgen." go out as said. COS Control gates its own port with
|
|
142
|
+
* its own language detector; that is why the fixture tags these rows scope "server".
|
|
143
|
+
*/
|
|
144
|
+
export function stripFillersIfEnglish(text: string): string {
|
|
145
|
+
return fillerTextLooksEnglish(text) ? stripDisfluencies(text) : text
|
|
146
|
+
}
|