@gotcos/glasses-server 6.91.1 → 6.92.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 +34 -1
- package/CHANGELOG.md +97 -0
- package/README.md +32 -1
- package/managed-runtime-contract.json +8 -1
- package/package.json +1 -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/prompt-edit.ts +84 -3
- package/server/lib/token-audit.ts +6 -1
- package/server/routes/dictation-polish-settings.ts +8 -1
- package/server/routes/prompt-drafts.ts +3 -2
- package/server/routes/prompt-edit.ts +47 -5
- package/server/scripts/dictation-parity-6920-mutation-gate.mjs +229 -0
- package/server/scripts/generate-dictation-fillers-fixture.ts +18 -0
package/.env.example
CHANGED
|
@@ -82,11 +82,44 @@ BIND_HOST=0.0.0.0
|
|
|
82
82
|
# COS_DICTATION_REGISTERS=1 makes it literal). On by default; 0 stops it applying
|
|
83
83
|
# everywhere (the capability then says enabled false; the rules are kept).
|
|
84
84
|
# COS_DICTATION_LEARNED_STYLE=0
|
|
85
|
-
# The meaning guard runs
|
|
85
|
+
# The meaning guard runs only on Sonnet with v4 (never on Haiku, Luna, other models,
|
|
86
86
|
# prompt v3 or meeting lines): it refuses a polish that lost a negation, number,
|
|
87
87
|
# weekday or month, or name you said (or added a negation or number), and pastes the
|
|
88
88
|
# unpolished text. 0 turns it off.
|
|
89
89
|
# COS_DICTATION_MEANING_GUARD=0
|
|
90
|
+
# Daily polish budgets, one per caller (local day): Mac dictation and G2 messages
|
|
91
|
+
# (default 300; the older shared variable is its fallback), G2 meeting lines (200),
|
|
92
|
+
# prompt drafts (200).
|
|
93
|
+
# COS_DICTATION_AUTOCLEAN_MAX_PER_DAY=300
|
|
94
|
+
# COS_DICTATION_AUTOCLEAN_MAX_PER_DAY_MESSAGE=300
|
|
95
|
+
# COS_DICTATION_AUTOCLEAN_MAX_PER_DAY_MEETING=200
|
|
96
|
+
# COS_DICTATION_AUTOCLEAN_MAX_PER_DAY_PROMPT_DRAFT=200
|
|
97
|
+
# The polish prompt for every model (v1 to v4); the alias above takes 3 or v3.
|
|
98
|
+
# COS_DICTATION_POLISH_PROMPT=v3
|
|
99
|
+
# The deterministic um/uh pass before the model (English text only since 6.92.0;
|
|
100
|
+
# never meeting lines). On by default; 0 sends the text to the model as heard.
|
|
101
|
+
# COS_DICTATION_FILLER_PASS=0
|
|
102
|
+
# "forward slash pricing" written "/pricing" before the model. On by default.
|
|
103
|
+
# COS_DICTATION_SPOKEN_PATHS=0
|
|
104
|
+
# Start the polish process at key press (Claude models). On by default.
|
|
105
|
+
# COS_DICTATION_PREWARM=0
|
|
106
|
+
# Skip the model when the text has nothing for it to fix. Off by default.
|
|
107
|
+
# COS_DICTATION_POLISH_SKIP_CLEAN=1
|
|
108
|
+
# 6.92.0: the polish breakers are keyed by model and caller group (Mac dictation, G2
|
|
109
|
+
# messages and prompt drafts; meeting lines on their own). A client that stops waiting
|
|
110
|
+
# never counts as a failure; this many such aborts in a row, each after 4 s or more of
|
|
111
|
+
# model time, pause that model's polish for _MS (default 4 and 300000, 5 minutes).
|
|
112
|
+
# COS_DICTATION_SLOW_BREAKER_ABORTS=4
|
|
113
|
+
# COS_DICTATION_SLOW_BREAKER_MS=300000
|
|
114
|
+
# 6.92.0: G2's prompt edit. On by default; 0 answers 503 "Edit is off". At most
|
|
115
|
+
# _DAILY_CAP edits a local day (default 200, its own counter), then 429 "Edit limit
|
|
116
|
+
# reached"; 3 failures in a row pause it 10 minutes (503 "Edit paused"). The model,
|
|
117
|
+
# effort and timeout default to sonnet, low and 45000 ms.
|
|
118
|
+
# COS_PROMPT_EDIT=0
|
|
119
|
+
# COS_PROMPT_EDIT_DAILY_CAP=200
|
|
120
|
+
# COS_PROMPT_EDIT_MODEL=sonnet
|
|
121
|
+
# COS_PROMPT_EDIT_EFFORT=low
|
|
122
|
+
# COS_PROMPT_EDIT_TIMEOUT_MS=45000
|
|
90
123
|
|
|
91
124
|
# Build 204+ uses server-owned durable query jobs by default. The server fsyncs an
|
|
92
125
|
# accepted prompt before returning 202, then keeps provider work running if the
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,100 @@
|
|
|
1
|
+
## 6.92.0 - Dictation parity, Wave A (server): breakers that tell an abort from a failure, bounded prompt edit, a filler pass that leaves other languages and paths alone
|
|
2
|
+
|
|
3
|
+
2026-10-25, plan r3 ("dictation parity", Wave A; pairs with COS Control 0.5.345). Zero `claude -p` calls: every
|
|
4
|
+
change is proven by execution tests, a fake CLI and mutation gates.
|
|
5
|
+
|
|
6
|
+
**What changes by default:**
|
|
7
|
+
- **Polish breakers** (`server/lib/dictation-polish-pipeline.ts`) are keyed by (model, bucket group): `dictation`
|
|
8
|
+
(Mac dictation, G2 messages, prompt drafts) and `meeting`. A G2 meeting that fails no longer turns off message polish.
|
|
9
|
+
- A client abort (Control stopped waiting, the request closed) is **neutral** to the 2-failure breaker: it neither
|
|
10
|
+
counts nor resets. Before, two slow Sonnet polishes past Control's wait could turn Sonnet polish off for 30 minutes.
|
|
11
|
+
- A new **slow breaker** per key: `COS_DICTATION_SLOW_BREAKER_ABORTS` (default 4) aborts in a row, each after at least
|
|
12
|
+
4 s of model time (stamped when the abort happens, not after the provider process is killed), with no success between, skip polish as `breaker_open` for
|
|
13
|
+
`COS_DICTATION_SLOW_BREAKER_MS` (default 300000, 5 minutes). An abort sooner than 4 s (a disconnect right after a
|
|
14
|
+
sidecar restart) does not count. Any success closes both breakers.
|
|
15
|
+
- Each abort writes a token-audit row with `polish_outcome: "aborted"`, codes only. `aborted` is audit-only: a
|
|
16
|
+
finalize answer's `polishOutcome` keeps its three values.
|
|
17
|
+
- `POST /api/dictation/polish/prewarm` asks the same keyed breaker as the finalize for its surface.
|
|
18
|
+
- **The filler pass** (`server/lib/dictation-fillers.ts`, on since 6.79.1) had removed German and Portuguese words
|
|
19
|
+
("um drei Uhr" became "drei Uhr", "Er kommt morgen." became "Kommt morgen.") and damaged paths ("src/um.txt" became
|
|
20
|
+
"src/.txt"). Now:
|
|
21
|
+
- **Token exemptions:** a filler is kept when the character right before it is one of `/ . @ _ : # $`, or the
|
|
22
|
+
character right after it is one of `/ @ _ # $`, or a `.` or `:` followed by a letter or digit. A sentence-final
|
|
23
|
+
"um." or "um:" still goes.
|
|
24
|
+
- **Language gate** (`stripFillersIfEnglish`, in the caller; `stripDisfluencies` stays the pure token rules) uses its
|
|
25
|
+
own function, `fillerTextLooksEnglish`. The meaning guard keeps 6.91.1's `looksEnglish` byte for byte, so "merci
|
|
26
|
+
Dana" and "gracias Marco" polished without the name are still refused. The filler gate: not English when
|
|
27
|
+
`looksEnglish` says so; under 12 words, English words (the function words, plus a short-reply list: yes, no, yeah,
|
|
28
|
+
yep, ok, okay, sure, thanks, thank, please, sorry, hi, hey, hello, good, great, fine, cool, done, right, maybe,
|
|
29
|
+
sounds, call, send, tell, ask, am, pm, weekdays, months but "may") must outnumber any foreign function word
|
|
30
|
+
(German, Portuguese, Spanish, French, Dutch; never an all-caps acronym), and an "um" or "er" said inline needs at
|
|
31
|
+
least one English word beside it. So "um yes", "er no", "um thanks Dana", "um Tuesday" and "um 5 pm works" lose
|
|
32
|
+
their filler as in 6.91.1, while "Comprei um carro", "Er kommt morgen." and "Wir treffen uns um 5 Uhr" keep it.
|
|
33
|
+
Known cost: an inline "um" beside only a name ("um Dana") is kept.
|
|
34
|
+
- **Canonical fixture** `server/lib/__fixtures__/dictation-fillers.json`, generated by the server's own functions
|
|
35
|
+
(`server/scripts/generate-dictation-fillers-fixture.ts`) from hand-written expectations: 140 rows, 108 scope `both`
|
|
36
|
+
(the token rules, which COS Control's port must match byte for byte; its 5 short-reply rows hold through both
|
|
37
|
+
repos' language gates too) and 32 scope `server` (the server's language gate). Its sha256,
|
|
38
|
+
`a531d0a99b4661dedcde5d81dbaa304eae73f916eb30e2efd7589e1e0a71de55`, is pinned in
|
|
39
|
+
`dictation-fillers-fixture.test.ts` and in Control's copy.
|
|
40
|
+
- **G2 prompt edit** (`POST /api/prompt-edit`) now answers how it stops. Its limits and settings are unchanged (24,000
|
|
41
|
+
character drafts, 2,000 character instructions, `COS_PROMPT_EDIT_MODEL`, `_EFFORT`, `_TIMEOUT_MS`: sonnet, low,
|
|
42
|
+
45000), and so is its environment, so a Claude CLI signed in with an API key keeps working.
|
|
43
|
+
- Runs through a new isolated runner, `server/lib/claude-text-runner.ts`: `--tools ''`, no MCP, hooks off in exactly
|
|
44
|
+
one `--settings`, `--permission-mode dontAsk`, no session file, a scratch working directory, a verified absolute
|
|
45
|
+
binary. `spawnClaudeText` stays for meeting summaries (prompt-edit.ts stays TOOL_CAPABLE in the key-lanes census;
|
|
46
|
+
the runner is TOOL_LESS).
|
|
47
|
+
- Switch `COS_PROMPT_EDIT` (read with `readSwitch`, on by default): off answers `503 Edit is off`.
|
|
48
|
+
- Cap `COS_PROMPT_EDIT_DAILY_CAP` (default 200 a local day) in its own counter file (`.prompt_edit_count.json`),
|
|
49
|
+
never the dictation polish counter: `429 Edit limit reached`.
|
|
50
|
+
- Breaker: 3 CLI failures in a row open it for 10 minutes (`503 Edit paused`); a client abort is neutral.
|
|
51
|
+
- A failure answers `500 Edit failed`; the CLI's stdout and stderr tails are gone from the answer and the log.
|
|
52
|
+
- Every refusal is `text/plain` and 24 characters or fewer, so G2 6.10.654 shows `Edit failed.` then, for example,
|
|
53
|
+
`API 429: Edit limit reached` (40 characters at most). A contract test runs 654's own `ApiHTTPError` and
|
|
54
|
+
`applyVoicePromptEdit`, copied verbatim into `server/lib/__fixtures__/g2-6.10.654-prompt-edit.json`, against the
|
|
55
|
+
real refusals: the footer fits and no undo entry is pushed.
|
|
56
|
+
- One audit row per edit that ran (`caller: prompt_edit`, `prompt_edit_outcome` ok, failed or aborted; sizes only).
|
|
57
|
+
- **`GET` and `PUT /api/dictation/polish/options`** add `meaningGuard` (false only with `COS_DICTATION_MEANING_GUARD=0`)
|
|
58
|
+
and `stylePairs` (the model and prompt pairs the guard, Your style and the register run on, as `/rules` computes
|
|
59
|
+
them), so Control 0.5.345's picker line tells the truth. Older servers omit both.
|
|
60
|
+
|
|
61
|
+
**Corrections and housekeeping:**
|
|
62
|
+
- The meaning guard runs **only on Sonnet with v4** (a Haiku forced to v4 gets none). The 6.91.0 wording "where v4
|
|
63
|
+
runs" is corrected in `.env.example` and the README.
|
|
64
|
+
- Eval harness limits: the custom-polish default `--max-calls` is 205 (its default run is 201 calls; it was 200, so a
|
|
65
|
+
default run refused itself) and the format eval's is 50 (it was 40, under the canary's 45). Case counts are pinned
|
|
66
|
+
in `server/scripts/dictation-eval-harness.test.ts`. Nothing was run.
|
|
67
|
+
- Managed runtime: `COS_DICTATION_SLOW_BREAKER_ABORTS`, `_MS`, `COS_PROMPT_EDIT`, `_DAILY_CAP`, `_MODEL`, `_EFFORT` and
|
|
68
|
+
`_TIMEOUT_MS` join `managed-runtime-contract.json` `optionalEnvironment`. `dictation-polish-budget.test.ts` now
|
|
69
|
+
checks all 20 dictation and prompt-edit keys there and in `.env.example`, including the five it missed
|
|
70
|
+
(`SPOKEN_PATHS`, `PROMPT_VERSION`, `LEARNED_STYLE`, `MEANING_GUARD`, `REGISTERS`).
|
|
71
|
+
- Test isolation: the polish goldens (`dictation-polish-prompt-v4.test.ts`), `prompt-drafts`, `dictation-polish-settings`,
|
|
72
|
+
the prompt-edit tests, `dictation-polish-breakers` and `transcript-cleanup` each get a private `COS_PROFILE_PATH` as
|
|
73
|
+
well as a private `COS_DATA_DIR`, restored after the file. In a 4-worker run `transcript-cleanup.test.ts` wrote the
|
|
74
|
+
vocabulary "Jewel360" into the run's shared profile while the goldens read it.
|
|
75
|
+
- The key-lanes off-identity recordings (`server/lib/__fixtures__/key-lanes/off-*.json`, recorded by hand, not run by
|
|
76
|
+
the suite) still show prompt edit's 6.91.1 argv; a re-recording will show the runner's.
|
|
77
|
+
|
|
78
|
+
**Gates:** mutation gates, each from a green baseline:
|
|
79
|
+
- new `server/scripts/dictation-parity-6920-mutation-gate.mjs`: **64 of 64**, each killed by its named test (baseline
|
|
80
|
+
360 tests in 13 files): abort-neutral, abort then error counts one, the slow threshold, open time and env read, the
|
|
81
|
+
4 s model-time rule and its boundary, the slow breaker consulted and reset, the aborted audit row, the bucket key
|
|
82
|
+
(meeting apart, prompt drafts with messages), prewarm keyed, prompt edit's switch and default, cap, cap boundary and
|
|
83
|
+
default, call counted, own counter, breaker, threshold, failure and success, abort neutral, text/plain refusal, no
|
|
84
|
+
CLI tails, the 24-character refusal, the runner (no tools, scratch directory, environment kept), the language gate
|
|
85
|
+
(wired in the filler step and the pipeline, its own function with the long-text ratio and the short-reply words,
|
|
86
|
+
foreign words, all-caps, inline um/er, a pause makes a hesitation, and the meaning guard keeping 6.91.1's check), each
|
|
87
|
+
token exemption character before and after, the dot and colon needing a letter or digit, both exemption checks
|
|
88
|
+
wired, the options `meaningGuard` and `stylePairs`, and both harness defaults;
|
|
89
|
+
- dictation polish 92 of 92 (`prewarm-breaker` retargeted to the keyed breaker), format 63 of 63, custom polish 46 of
|
|
90
|
+
46, voice style 183 of 183 (its anchors unchanged from 6.91.1);
|
|
91
|
+
- dictation STT 21 of 22: its two anchors in `stt-settings.ts` already matched twice at 6.91.1, so the gate stopped
|
|
92
|
+
before its first mutant; a leading newline now pins each. The survivor, `hq-parakeet-warms`
|
|
93
|
+
(`server/lib/stt-runtime.ts`), is in code this release does not touch and was hidden by that stop;
|
|
94
|
+
- full suite, 4 workers, scratch HOME, COS_DATA_DIR and COS_PROFILE_PATH: 9,180 passed, 14 skipped, 0 failed;
|
|
95
|
+
- `check-gate-anchors.mjs --base 96be0b8`: every anchor in a changed file matches once (the one pre-existing
|
|
96
|
+
`manifest-lock-version-drift` miss is unchanged); `tsc --noEmit` clean.
|
|
97
|
+
|
|
1
98
|
## 6.91.1 - The meaning guard reads clock times, so a time format you asked for is not refused
|
|
2
99
|
|
|
3
100
|
2026-10-25: canary C5 on the pair that ships (Sonnet, v4, instructions and the learned style) found the 6.91.0 guard
|
package/README.md
CHANGED
|
@@ -868,7 +868,7 @@ built-in polish rules, which keep precedence.
|
|
|
868
868
|
answer says `learnedStyle: true` and `learnedStyleHash` (8 hex), never the
|
|
869
869
|
rules.
|
|
870
870
|
- **Meaning guard.** After the model, for messages and prompt drafts, and only
|
|
871
|
-
|
|
871
|
+
on Sonnet with v4 (Haiku, Luna, any other model and prompt v3 get no
|
|
872
872
|
guard, so their answers are 6.90.0's): if a negation, number, weekday or
|
|
873
873
|
month, or name the speaker kept is missing from the answer (or a negation or
|
|
874
874
|
number appears that was never said), the pre-polish text goes out with
|
|
@@ -889,6 +889,37 @@ built-in polish rules, which keep precedence.
|
|
|
889
889
|
(`{protocol: 1, maxChars: 1500, maxRules: 20, enabled}`) and
|
|
890
890
|
`capabilities.dictationCleanup.polishOptions` (`{protocol: 1}`).
|
|
891
891
|
|
|
892
|
+
### Dictation reliability and prompt edit bounds (6.92.0)
|
|
893
|
+
|
|
894
|
+
- **Breakers keyed by model and caller group.** Mac dictation, G2 messages and
|
|
895
|
+
prompt drafts share one group; G2 meeting lines have their own, so a failing
|
|
896
|
+
meeting can no longer turn off message polish. 2 CLI failures in a row open
|
|
897
|
+
a model's breaker for 30 minutes, as before. A client that stops waiting (the
|
|
898
|
+
request closes) is not a failure. `COS_DICTATION_SLOW_BREAKER_ABORTS` (4)
|
|
899
|
+
such aborts in a row, each after 4 s or more of model time, pause that model
|
|
900
|
+
for `COS_DICTATION_SLOW_BREAKER_MS` (300000). Either one open answers
|
|
901
|
+
`polishSkipped: "breaker_open"`, and the prewarm route asks the same keyed
|
|
902
|
+
breaker as its surface. An abort writes an audit row with `polish_outcome`
|
|
903
|
+
`aborted` (codes only); the finalize answer never says `aborted`.
|
|
904
|
+
- **The filler pass** keeps a filler that is part of a path, handle or code
|
|
905
|
+
token (`src/um.txt`, `~/uh`, `@uh`, `um.py`, `um:8080`), and runs only when
|
|
906
|
+
the text is mostly English (`fillerTextLooksEnglish`; short replies such as
|
|
907
|
+
"um yes" count as English), so "um 5 Uhr", "um carro" and "Er kommt" go out
|
|
908
|
+
as said. The meaning guard keeps its 6.91.1 language check. The rules are pinned by `server/lib/__fixtures__/dictation-fillers.json`,
|
|
909
|
+
generated by the server's own functions; COS Control carries a byte-identical
|
|
910
|
+
copy and pins the same sha256.
|
|
911
|
+
- **`GET /api/dictation/polish/options`** adds `meaningGuard` (the
|
|
912
|
+
`COS_DICTATION_MEANING_GUARD` switch) and `stylePairs` (the pairs the guard,
|
|
913
|
+
Your style and the register run on).
|
|
914
|
+
- **G2 prompt edit** (`POST /api/prompt-edit`) runs through an isolated text
|
|
915
|
+
runner (no tools, MCP, hooks or session, a scratch directory) and now stops:
|
|
916
|
+
`COS_PROMPT_EDIT=0` answers `503 Edit is off`; `COS_PROMPT_EDIT_DAILY_CAP`
|
|
917
|
+
(default 200 a local day, its own counter) then `429 Edit limit reached`; 3
|
|
918
|
+
CLI failures in a row pause it 10 minutes (`503 Edit paused`). A failure is
|
|
919
|
+
`500 Edit failed`, never the CLI's output. Every refusal is `text/plain`.
|
|
920
|
+
`COS_PROMPT_EDIT_MODEL`, `_EFFORT` and `_TIMEOUT_MS` are unchanged
|
|
921
|
+
(sonnet, low, 45000).
|
|
922
|
+
|
|
892
923
|
### Parakeet (FluidAudio CoreML, offline)
|
|
893
924
|
|
|
894
925
|
`POST /api/stt-settings/parakeet/install` installs FluidAudio's `fluidaudiocli`
|
|
@@ -90,7 +90,14 @@
|
|
|
90
90
|
"COS_DICTATION_PROMPT_VERSION",
|
|
91
91
|
"COS_DICTATION_LEARNED_STYLE",
|
|
92
92
|
"COS_DICTATION_MEANING_GUARD",
|
|
93
|
-
"COS_DICTATION_REGISTERS"
|
|
93
|
+
"COS_DICTATION_REGISTERS",
|
|
94
|
+
"COS_DICTATION_SLOW_BREAKER_ABORTS",
|
|
95
|
+
"COS_DICTATION_SLOW_BREAKER_MS",
|
|
96
|
+
"COS_PROMPT_EDIT",
|
|
97
|
+
"COS_PROMPT_EDIT_DAILY_CAP",
|
|
98
|
+
"COS_PROMPT_EDIT_MODEL",
|
|
99
|
+
"COS_PROMPT_EDIT_EFFORT",
|
|
100
|
+
"COS_PROMPT_EDIT_TIMEOUT_MS"
|
|
94
101
|
],
|
|
95
102
|
"maintenance": {
|
|
96
103
|
"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.92.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": {
|
|
@@ -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
|
+
}
|