@gotcos/glasses-server 6.91.0 → 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 +131 -2
- package/README.md +36 -2
- 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 +162 -12
- 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,126 @@
|
|
|
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
|
+
|
|
98
|
+
## 6.91.1 - The meaning guard reads clock times, so a time format you asked for is not refused
|
|
99
|
+
|
|
100
|
+
2026-10-25: canary C5 on the pair that ships (Sonnet, v4, instructions and the learned style) found the 6.91.0 guard
|
|
101
|
+
refusing answers that followed the person's own instruction "write times in the form 10:30 am": "at ten" written
|
|
102
|
+
"10:00 am" counted "am" and "00" as added numbers, and the unpolished text went out. 6.90.0 pasted that formatting, so
|
|
103
|
+
this was a step backward for any Sonnet user with a time-format instruction. Fixed before npm latest moves. Zero
|
|
104
|
+
`claude -p` calls.
|
|
105
|
+
|
|
106
|
+
- **Clock times** (`server/lib/dictation-meaning-guard.ts`): every time, spoken or written, in the dictation and in the
|
|
107
|
+
answer, is read as minutes since midnight: "ten", "ten thirty", "half past ten", "quarter to five", "ten o'clock",
|
|
108
|
+
"ten oh five", 10, 10:00, 10:30, am, pm, a.m., p.m., "22:30" and "twenty two thirty". Without am or pm an hour from
|
|
109
|
+
1 to 12 reads both ways, so "ten thirty pm" written "22:30" and "ten thirty" written "22:30" are kept. am, pm and a
|
|
110
|
+
trailing ":00" are format only and never count as added numbers: "at ten" written "at 10:00 am", "ten thirty
|
|
111
|
+
Central" written "10:30 am Central" and "meet at 9" written "meet at 9:00" are kept. A real change still trips:
|
|
112
|
+
"ten thirty" written "11:30", "7 am" written "7 pm", "3:00 pm" written "3", and a time the speaker never said. A year
|
|
113
|
+
that reads like a time ("nineteen fifty" as "1950") still matches its plain form.
|
|
114
|
+
- **C5 re-checked offline:** the 30 stored C5 answers (synthetic) run through the 6.91.1 guard: 0 of 15 stacked and 0
|
|
115
|
+
of 14 plain refused (6.91.0 refused b02 and b09), so what would go out is 11/11 backtracks on both arms. Every other
|
|
116
|
+
stored synthetic output: still 0 false trips in 356. Route test: on Sonnet v4 with the time-format instruction saved,
|
|
117
|
+
b02 and b09 go out polished, and a changed time is still refused.
|
|
118
|
+
- **Eval:** `STACKED_INSTRUCTIONS` is 985 characters (it was 1,033, over the 1,000 cap, so the reader refused it and
|
|
119
|
+
the 6.91.0 Haiku stacked arm ran without instructions), and the eval now stops if the reader would refuse the set.
|
|
120
|
+
- **Known limit, kept:** the largest possible stack (985-character instructions and all 13 learned-style rules) costs
|
|
121
|
+
Sonnet v4 +4.3% at p50 and **+46.7% at p90** (9,260 vs 6,311 ms, C5 2026-10-25, n=15). No user has that today: the
|
|
122
|
+
learned style starts empty and grows a rule at a time. Prompts are unchanged for it.
|
|
123
|
+
|
|
1
124
|
## 6.91.0 - Dictation that learns your voice: prompt v4 on Sonnet, Your style, the meaning guard, an opt-in literal register
|
|
2
125
|
|
|
3
126
|
2026-10-24 22:45: extend AI polish into "a Grammarly but further for voice dictation", remove ungrammatical spoken
|
|
@@ -133,8 +256,14 @@ builds the capture, the classifier and Your style.
|
|
|
133
256
|
(+3.2%); Sonnet p50 3,024 vs 2,949 (+2.5%), p90 4,460 vs 3,997 (+11.6%). Stacked arm (Haiku, v4 + 1,033-character
|
|
134
257
|
instructions + the largest learned style, 767 characters): p50 3,147 vs 2,794 (+12.6%), p90 3,646 vs 3,520 (+3.6%),
|
|
135
258
|
backtracks 11/11. C4 cold, Sonnet: v4 p50 4,346 vs v3 4,713. The absolute Haiku bar is measured after install.
|
|
136
|
-
**Pending before npm latest
|
|
137
|
-
|
|
259
|
+
**Pending before npm latest, run 2026-10-25: DISPROVEN (p90).** The stacked arm on the pair that ships, Sonnet with
|
|
260
|
+
v4, 985-character instructions and the largest learned style (13 rules, 767 characters), against plain Sonnet v4, on
|
|
261
|
+
the 15-case backtrack and new-preserve slice, interleaved, warm (29 of 30 warm; 30 calls, 1 plain call timed out at
|
|
262
|
+
20 s): p50 4,968 vs 4,762 ms (+4.3%), p90 9,260 vs 6,311 ms (+46.7%, over the +25% bar; 4 of 15 stacked calls took
|
|
263
|
+
7.9 to 18.0 s, 1 of 14 plain). Backtracks 11/11 in the model's answers on both arms; new preserve 4/4 stacked, 3/3
|
|
264
|
+
plain. What would go out: 9/11 stacked, because the meaning guard refused two answers that followed the instruction
|
|
265
|
+
"write times in the form 10:30 am" ("at ten" as "10:00 am" adds "am" and "00"). The Haiku stacked number above ran
|
|
266
|
+
without its instructions: that set is 1,033 characters, over the 1,000 cap, so the reader refused it.
|
|
138
267
|
- **C6: PROVEN** (route and golden tests). Every new route is 401 without the token; an old client body works; a skip
|
|
139
268
|
path sends no `polishOutcome`; a port of the 0.5.343 helper and `choose` logs a guarded answer as
|
|
140
269
|
`fillers_only:failed` or `skipped:failed`, never applied; all four switches off, and Haiku outside a terminal, give the
|
package/README.md
CHANGED
|
@@ -868,14 +868,17 @@ 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
|
|
875
875
|
`polishOutcome: "guarded"`, `guard: "<kind>"` and `polishSkipped: "failed"`.
|
|
876
876
|
A self-correction may drop what it replaced. "2 million", "25th",
|
|
877
877
|
"dont", "half past ten" and a numbered list you asked for read as what you
|
|
878
|
-
said.
|
|
878
|
+
said. 6.91.1: clock times compare as minutes since midnight, so "at ten"
|
|
879
|
+
written "10:00 am" and "ten thirty pm" written "22:30" are kept (am, pm and
|
|
880
|
+
":00" are format only), while "ten thirty" written "11:30" trips. The log
|
|
881
|
+
line carries counts only. `COS_DICTATION_MEANING_GUARD=0` turns
|
|
879
882
|
it off.
|
|
880
883
|
- **Finalize answers** add, all optional: `polishOutcome` (`polished`,
|
|
881
884
|
`unchanged` or `guarded`, about the model step; an empty model answer is a
|
|
@@ -886,6 +889,37 @@ built-in polish rules, which keep precedence.
|
|
|
886
889
|
(`{protocol: 1, maxChars: 1500, maxRules: 20, enabled}`) and
|
|
887
890
|
`capabilities.dictationCleanup.polishOptions` (`{protocol: 1}`).
|
|
888
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
|
+
|
|
889
923
|
### Parakeet (FluidAudio CoreML, offline)
|
|
890
924
|
|
|
891
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
|
+
}
|