@sfgartland/better-pi-voice 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Severin Gartland
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,404 @@
1
- # Temporary Holding Version
1
+ # @sfgartland/better-pi-voice
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Streaming voice input for pi. Hold a key, talk, and the words land in the editor —
4
+ in the right spelling, including the jargon and filenames generic speech
5
+ recognition gets wrong.
6
+
7
+ Two interchangeable backends behind one pipeline:
8
+
9
+ | | Backend | Biasing | Needs |
10
+ |---|---|---|---|
11
+ | Cloud | **Deepgram `nova-3`** streaming | `keyterm` prompting (native) | a Deepgram API key |
12
+ | Local | **Parakeet** via `transcribe-cpp` | post-hoc correction pass only | a Parakeet GGUF |
13
+
14
+ ## Why this exists
15
+
16
+ Generic dictation is tuned for English prose, not for a terminal. It writes
17
+ "sub agent" and "work tree", guesses at filenames, turns `tsconfig` into "T's
18
+ config", and has no idea what your project is even called. That is not mostly a
19
+ model-quality problem — it is a missing-context problem, and it is fixable: tell
20
+ the recogniser the words the project actually uses. Cloud dictation is also a
21
+ black box: when the socket drops mid-sentence, most tools lose the sentence.
22
+ Local dictation usually trades accuracy for privacy and gives up on both.
23
+
24
+ `better-pi-voice` attacks all three: vocabulary biasing mined from the repo and
25
+ session, a recovery layer that will not drop a sentence, and a real local option
26
+ behind the same pipeline.
27
+
28
+ Claude Code's dictation is unusually good, and it isn't the model. Reverse-engineering
29
+ it (`claude-code@2.1.295`) turned up three things worth stealing:
30
+
31
+ 1. **Vocabulary biasing from the repository.** Before every session it pushes the
32
+ project name, the paths already in play, and a hardcoded list of developer jargon
33
+ (`subagent`, `worktree`, `OAuth`, `gRPC`…) to the recogniser. This is the single
34
+ biggest accuracy lever and it costs nothing.
35
+ 2. **Server-side endpointing** at 16 kHz mono, so finals land promptly instead of
36
+ waiting for you to press stop.
37
+ 3. **A recovery layer that never loses a sentence** — one retry before the first
38
+ transcript, replaying buffered audio when a stream silently drops, salvaging a
39
+ partial transcript when the socket dies, promoting an unreported interim to a
40
+ final, and a circuit breaker so a broken microphone fails loudly.
41
+
42
+ All three are reproduced in this extension. The design document with the full extraction notes is
43
+ at `docs/superpowers/specs/2026-10-15-pi-voice-input-design.md`.
44
+
45
+ ## What it actually buys you
46
+
47
+ A fixed sentence containing five terms, transcribed twice through the live Deepgram
48
+ API — once with no keyterms, once with the repository's terms:
49
+
50
+ ```
51
+ without keyterms … so the sub agent can use the Worktree T's config, then check
52
+ the Marginalia OAuth webhook.
53
+ with keyterms … so the subagent can use the worktree tsconfig, then check the
54
+ marginalia OAuth webhook.
55
+ ```
56
+
57
+ `tsconfig` → "T's config" is the failure mode in miniature: correct English, wrong
58
+ words. Two of the five terms came back wrong without biasing and all five were right
59
+ with it.
60
+
61
+ ## How it compares to the other pi voice extensions
62
+
63
+ Three voice extensions are in play, and they optimise for different things. This
64
+ one is for **accurate dictation of code vocabulary**, from the cloud or locally:
65
+
66
+ | | [`@earendil-works/pi-voice`](https://github.com/earendil-works/pi-voice) | [`pi-voice-stt`](https://github.com/cgarrot/pi-voice-stt) | **better-pi-voice** |
67
+ |---|---|---|---|
68
+ | Recognition | local only (`transcribe-cpp`) | several cloud providers + OpenAI-compatible | **Deepgram `nova-3` streaming** or local Parakeet |
69
+ | Repo-aware vocabulary biasing | no | no | **yes** — keyterms mined from the repo and session, native on Deepgram |
70
+ | Never-lose-a-sentence recovery | no | no | **yes** — retry, silent-drop replay, partial salvage, circuit breaker |
71
+ | Capture | bundled `pvrecorder` | `ffmpeg` (or a Mac bridge) | `arecord`, falling back to SoX `rec` |
72
+ | Transcribe an audio file | yes (`transcribe_file`) | yes (`transcribe_audio`) | no — this extension is for live dictation |
73
+ | Guided setup | `/voice-settings` onboarding, model download | config file / environment | `/voice setup` wizard, model download |
74
+ | Best for | a fully local, offline dictation tool | choosing among many cloud providers | dictating code accurately, with a private fallback |
75
+
76
+ All three are MIT and coexist as pi packages, so there is no need to pick
77
+ ideologically: use `pi-voice` if you want everything strictly local, `pi-voice-stt`
78
+ if you want a particular provider, and this one if the words that come back are the
79
+ thing you care about.
80
+
81
+ ## Install
82
+
83
+ ```bash
84
+ pi install /path/to/better-pi-voice # from a local checkout
85
+ pi install npm:@sfgartland/better-pi-voice # once published
86
+ ```
87
+
88
+ Then add a Deepgram key. The easiest way is `/voice key`:
89
+
90
+ - `/voice key <path-to-dotenv>` — use a key from an existing dotenv file (it checks one is
91
+ actually in there first), then switches to Deepgram.
92
+ - `/voice key` — paste the key at the prompt; it is written to `~/.pi/agent/better-pi-voice.key`
93
+ with `0600` permissions and referenced from the settings, so the secret never lands in
94
+ `better-pi-voice.json` (which is the file people sync into dotfiles repos).
95
+ - `/voice key --inline` — store the key in `better-pi-voice.json` itself. Explicit opt-in:
96
+ only do this if that file is not tracked by git.
97
+
98
+ You can also export it, which overrides everything the settings point at:
99
+
100
+ ```bash
101
+ DEEPGRAM_API_KEY=... pi
102
+ ```
103
+
104
+ or set `deepgram.apiKeyFile` to any dotenv by hand. Precedence is: environment variable,
105
+ then `deepgram.apiKey` (inline), then `deepgram.apiKeyFile`.
106
+
107
+ ## Guided setup
108
+
109
+ Run `/voice setup`, or just press the shortcut on a fresh install and it walks you
110
+ through it. The wizard asks, in order: how voice should run (Deepgram cloud or local
111
+ Parakeet), how to supply the key or which model to use, whether to hold or tap, and
112
+ which shortcut. Then it saves, and ends with an **overview** pane: the settings it
113
+ wrote, how to use them (hold or press the shortcut, where the text goes), and how to
114
+ change any of it later with `/voice setup`. Enter closes it; Esc goes back and
115
+ re-runs the wizard. There is no live microphone test to sit through — try the
116
+ shortcut yourself when you are ready.
117
+
118
+ For the key, if `DEEPGRAM_API_KEY` is already in your environment the wizard offers to
119
+ just use it; otherwise you can paste a key (kept `0600`, never in settings) or point at
120
+ a dotenv file. For the local backend, the model menu lists the known GGUFs best-first
121
+ with their download size — **selecting one downloads it** (checksum-verified, with a
122
+ progress line) — then falls back to any GGUF already on disk or a path you give.
123
+
124
+ If the model you chose can stream, the wizard then asks how it should run and spells out
125
+ the tradeoff: **Batch** transcribes in short slices while you talk (works with every
126
+ model, and faster on most machines) versus **Streaming**, which decodes continuously for
127
+ lower delay but needs a fast CPU to keep up in real time. A batch-only model skips the
128
+ question and says why. See [Batch mode](#batch-mode-the-default) below for the detail.
129
+
130
+ ## Use
131
+
132
+ The default binding is `ctrl+alt+z`; **hold it while you speak and release**. The
133
+ status line names the active backend and model (`● deepgram nova-3: listening`,
134
+ `transcribing with parakeet parakeet-unified-en-0.6b…`) and keeps showing it while
135
+ idle, so it is always obvious which one is listening. An audio-level meter appears
136
+ above the editor while recording.
137
+
138
+ | Mode | Start | Stop | Safety |
139
+ |---|---|---|---|
140
+ | `hold` (default) | press | release | falls back if the terminal never reports release |
141
+ | `tap` | press | press again | 15 s of silence, 120 s hard cap |
142
+
143
+ A hard cap applies in both modes so a stuck key cannot leave the microphone open.
144
+
145
+ Commands:
146
+
147
+ - `/voice` — backend, model, mode, shortcut, keyterm count, whether the key is visible, state
148
+ - `/voice setup` — guided setup (backend, key/model, mode, shortcut, then an overview of the result)
149
+ - `/voice backend` — pick a backend from a menu
150
+ - `/voice backend deepgram|parakeet` — switch directly, then reload
151
+ - `/voice model` — pick a local GGUF already in the Hugging Face cache, then reload
152
+ - `/voice model /path/to/model.gguf` — use a specific model file
153
+ - `/voice key <path-to-dotenv>` — use a key from a dotenv file, then switch to Deepgram
154
+ - `/voice key` — paste a key; stored `0600` in `~/.pi/agent/better-pi-voice.key`
155
+ - `/voice key --inline` — store the key in the settings file (explicit opt-in)
156
+ - `/voice set <key> <value>` — set `backend`, `model`, `mode`, `language`, `correction`,
157
+ `submitOnStop`, `chunkSeconds`, `silenceLevel`, `shortcut`, `apiKeyEnv` or `apiKeyFile`, then reload
158
+ - `/voice-keyterms` — list custom terms
159
+ - `/voice-keyterms add <term>` / `remove <term>`
160
+
161
+ The backend is **Deepgram by default**. Switching rewrites `better-pi-voice.json`
162
+ (fully expanded, so it stops looking like the minimal file you hand-wrote) and then
163
+ reloads the extension so the change takes effect immediately. You can also just edit
164
+ `"backend"` in the settings file and restart.
165
+
166
+ On a first run with no settings file, voice configures itself: Deepgram when an API
167
+ key is already reachable, otherwise the best local model already on disk. It then tells
168
+ you which backend it chose and which key to hold. If neither is available it writes
169
+ nothing and says what to set.
170
+
171
+ ## Keyterms
172
+
173
+ Terms are gathered from four places, most specific first:
174
+
175
+ 1. `keyterms.user` — your own list
176
+ 2. a built-in seed list of developer jargon
177
+ 3. the project directory name and the git repository name
178
+ 4. files touched by `read`/`write`/`edit` tool calls and paths mentioned in your messages
179
+
180
+ They are normalised (camelCase split, separators split, 3–20 characters, printable
181
+ ASCII, deduplicated) and capped well inside Deepgram's documented 100-term / 500-token
182
+ limit. A comma-joined keyterm list is accepted by Deepgram and silently boosts
183
+ *nothing*, so terms are sent as repeated query parameters — there is a test for that.
184
+
185
+ ## The local backend, honestly
186
+
187
+ **Parakeet cannot be vocabulary-biased.** `transcribe-cpp` 0.4.0 does have a
188
+ `vocabulary` input, but only for models that advertise `supports("vocabulary")`, and
189
+ the Parakeet GGUFs do not — loading `parakeet-tdt_ctc-110m`, the recommended model,
190
+ returns `false` for both `vocabulary` and `context_prompt`. So locally you get live
191
+ interims *or* native biasing, not both.
192
+
193
+ This extension takes interims, and makes up the difference with a deliberately dumb
194
+ deterministic repair pass that runs on finals only. It does two things:
195
+
196
+ 1. **Rejoin identifiers the recogniser split.** `tsconfig` comes back as "TS config".
197
+ Two adjacent tokens are joined only when their concatenation is *exactly* a keyterm,
198
+ they are separated by exactly one space, and the shorter one is an abbreviation
199
+ (uppercase, or at most two characters). "TS config" qualifies; "back end" and
200
+ "work tree" deliberately do not, because in the next sentence those are two words.
201
+ 2. **Repair a single misheard token**, within one edit of a keyterm for words up to 7
202
+ characters or two edits for longer ones, and never for tokens under 5 characters.
203
+ Distance is Damerau, so an adjacent swap counts once — that is how ASR mangles a
204
+ technical term most often (`subagent` → `subagnet`).
205
+
206
+ On the real output of the local backend for the test sentence, that turns
207
+ `the Worktree TS config` into `the Worktree tsconfig`. It does **not** rescue
208
+ `DRAM backend` from `deepgram backend` — too many edits, and below the length floor.
209
+
210
+ ```jsonc
211
+ "correction": "safe" // "off" | "safe" | "report"
212
+ ```
213
+
214
+ `"report"` changes nothing and logs what it *would* change — use it to find out
215
+ whether the pass is earning its keep before trusting it.
216
+
217
+ If you want true local biasing instead, a whisper-family model with `initialPrompt`
218
+ plus a batch re-pass at finalize is the route; this extension does not implement it.
219
+
220
+ **It is also the slow part.** How fast it runs depends on the machine; the numbers below
221
+ come from one reference machine — an 8-core x86_64 laptop using the bundled
222
+ `linux-x64-cpu-vulkan` build on the CPU path (its GPU was not usable there):
223
+
224
+ | | measured |
225
+ |---|---|
226
+ | first model load (698 MB, cold page cache) | ~2.7 s |
227
+ | warm reload (file already cached) | ~0.4 s |
228
+ | **streaming decode** | **~0.9× real-time — only just keeps up** |
229
+ | **batch** transcribe of the same audio | **3.5× real-time** |
230
+ | first interim out of the stream | ~0.6 s |
231
+
232
+ So live streaming works, but without headroom: the decoder consumes audio about as
233
+ fast as you speak it, and text drifts further behind over a long utterance. The batch
234
+ path is nearly four times faster, which is why a record-then-transcribe mode is worth
235
+ considering (see below). Treat the local backend as the private/offline option rather
236
+ than the fast one until it is running on usable compute.
237
+
238
+ ### Which local models can stream
239
+
240
+ `transcribe-cpp` ships 27 models; only these four can do live dictation at all:
241
+
242
+ | model | size | languages |
243
+ |---|---|---|
244
+ | `parakeet-unified-en-0.6b` | 731 MB | English |
245
+ | `multitalker-parakeet-streaming-0.6b-v1` | 734 MB | English |
246
+ | `nemotron-3.5-asr-streaming-0.6b` | 751 MB | 32 languages |
247
+ | `Voxtral-Mini-4B-Realtime-2602` | 4.7 GB | 13 languages |
248
+
249
+ There is **no streaming model lighter than parakeet-0.6b**. The genuinely small ones —
250
+ `medasr` (128 MB), `parakeet-tdt_ctc-110m` (135 MB), `canary-180m-flash` (218 MB),
251
+ `SenseVoiceSmall` (253 MB), `whisper-small` (270 MB) — are all batch-only, so using one
252
+ means giving up live text and transcribing after you release the key. With batch running
253
+ at 3.5× real-time that is a reasonable trade, and the pipeline already retains the whole
254
+ utterance, but it is not implemented yet.
255
+
256
+ For non-English dictation, `nemotron-3.5-asr-streaming-0.6b` is the only streaming option
257
+ that is not parakeet, at the same size.
258
+
259
+ ### Batch mode (the default)
260
+
261
+ `parakeet.mode` chooses how the local backend works, and **`batch` is the default**
262
+ because it is typically several times faster than streaming, and because it is the only
263
+ option for the small models, none of which can stream.
264
+
265
+ In batch mode the audio is transcribed in pieces while you are still speaking, so only
266
+ the tail is left when you release the key:
267
+
268
+ - `parakeet.chunkSeconds` (default 30) is a **target, not a limit**, and only its lower
269
+ 70% matters: the audio is cut at the **first real pause** at or after that point and
270
+ transcribed in the background. A pause arriving later than the target still triggers a
271
+ cut, and unbroken speech simply grows a longer chunk
272
+ - `parakeet.chunkSeconds: 0` disables chunking and transcribes once, at the end
273
+ - a chunk is only cut where the signal is quiet enough to be a pause. The threshold is
274
+ **derived from each recording** (`parakeet.silenceLevel: "auto"`): the noise floor is the
275
+ second-lowest level in the buffer, speech is the 75th percentile, and a boundary counts
276
+ as a pause when it falls in the lowest 30% of that range. The floor is a near-minimum
277
+ rather than a low percentile because pauses are brief — two silent chunks in forty leave
278
+ a percentile reporting pure speech, while the minimum still finds them. A fixed number
279
+ cannot span rooms — of two real recordings, one never dropped below 0.05
280
+ (music playing) and the other paused around 0.3. Pin a number to override.
281
+ - when the level barely varies there is no pause structure to find, so nothing is cut and
282
+ the whole utterance is transcribed in one piece. That is the safe answer for unbroken
283
+ speech and for steady background noise.
284
+ - **the default is calibrated for a model that tolerates a chunk starting mid-word**,
285
+ which the recommended `parakeet-tdt_ctc-110m` does. It matters: `canary-180m-flash`
286
+ returns an *empty* transcript for any clip that starts mid-utterance — in testing, at
287
+ offsets 4, 6, 8 and 10 s into a 24 s clip it produced 0 characters every time, while
288
+ parakeet-tdt_ctc-110m produced 292, 253, 215 and 192. If you use canary, set
289
+ `silenceLevel` to 0.05 so cuts only happen at true silence.
290
+
291
+ That last rule is not fussiness. Cutting mid-utterance loses words outright: canary
292
+ returns an **empty transcript** for any clip that begins mid-sentence, so a chunk cut at
293
+ merely the *quietest* moment silently discarded everything in it. Requiring real silence
294
+ costs tail latency on unbroken speech instead of costing speech.
295
+
296
+ ### Keeping the model loaded
297
+
298
+ The model is **released after every recording by default**, because a warm reload is only
299
+ about 0.4 s and holding ~700 MB between recordings is not a good trade on a laptop. Set
300
+ `parakeet.idleUnloadMs` to keep it resident instead — useful for larger models, where a
301
+ reload costs real time. When residency is on, the model is released by whichever of these
302
+ happens first:
303
+
304
+ - the idle window expires (`parakeet.idleUnloadMs`)
305
+ - the machine gets busy — one-minute load average per CPU reaching **0.8**
306
+ (`parakeet.loadShedRatio`), checked once a minute
307
+
308
+ A new recording cancels a pending release, and session shutdown releases immediately.
309
+ Note that the load-shedding half only matters while residency is on: at the default of 0
310
+ the memory is already back after each recording, so there is nothing to shed.
311
+
312
+ ## Settings
313
+
314
+ `~/.pi/agent/better-pi-voice.json`. Every field is optional; anything missing or
315
+ malformed falls back to a default and warns rather than breaking the microphone.
316
+
317
+ ```jsonc
318
+ {
319
+ "backend": "deepgram", // "deepgram" | "parakeet"
320
+ "mode": "hold", // "hold" | "tap"
321
+ "shortcut": "ctrl+alt+z",
322
+ "language": "en",
323
+ "deepgram": {
324
+ "apiKeyEnv": "DEEPGRAM_API_KEY",
325
+ "apiKey": null, // inline key, only written by /voice key --inline
326
+ "apiKeyFile": null, // dotenv file, used when the env var is unset
327
+ "model": "nova-3",
328
+ "smartFormat": true,
329
+ "endpointingMs": 300, // silence before an utterance is finalised
330
+ "utteranceEndMs": 1000
331
+ },
332
+ "parakeet": {
333
+ "modelPath": null, // null = read pi-voice's config if present
334
+ "mode": "batch", // "batch" (faster, chunked) | "streaming"
335
+ "chunkSeconds": 30, // cut at a pause near this length; 0 = only at the end
336
+ "silenceLevel": "auto", // "auto" derives the pause threshold per recording; a number pins it
337
+ "idleUnloadMs": 0, // 0 = release after each recording (see above)
338
+ "loadShedRatio": 0.8 // with residency on, release early once load per CPU reaches this
339
+ },
340
+ "keyterms": {
341
+ "enabled": true,
342
+ "seed": true,
343
+ "mineSessionFiles": true,
344
+ "maxTerms": 60,
345
+ "user": []
346
+ },
347
+ "correction": "safe",
348
+ "submitOnStop": false // false = leave the text in the editor
349
+ }
350
+ ```
351
+
352
+ ## When something goes wrong
353
+
354
+ Failures are reported with a cause rather than "check your network":
355
+
356
+ - **The microphone starts first.** Capture begins as soon as the key goes down, and
357
+ audio is buffered and then sent the instant the backend is ready — so the first
358
+ syllable is not lost while a model loads or a websocket connects. The microphone is
359
+ also probed at session start, so a missing device is reported then rather than when you
360
+ first hold the key.
361
+ - **Connection failures are diagnosed.** When the websocket fails, the extension makes
362
+ a small authenticated REST call to Deepgram and reports which it was: `Deepgram
363
+ rejected the API key`, `could not reach Deepgram (getaddrinfo ENOTFOUND ...)`, or
364
+ `Deepgram returned HTTP 503`. The websocket close code alone cannot distinguish those.
365
+ - **Errors carry their detail.** Whatever the socket's error event holds is included
366
+ in the message instead of a bare "connection error".
367
+ - **Speech is never lost.** One retry before the first transcript, replay of the
368
+ retained audio when a stream silently drops, partial transcripts salvaged, unreported
369
+ interims promoted to finals, and a circuit breaker after three fast failures.
370
+
371
+ ## Privacy
372
+
373
+ **Cloud mode sends your microphone audio to Deepgram.** It is billed per minute of
374
+ audio, it transcribes on their servers, and it is the default because it is the more
375
+ accurate of the two. Local mode sends nothing anywhere; use it for anything sensitive.
376
+ Buffered audio is held in memory only, is never written to disk, and is discarded when
377
+ the session ends.
378
+
379
+ ## Requirements
380
+
381
+ - Linux with `arecord` (alsa-utils), or SoX's `rec` as a fallback. macOS/Windows use
382
+ `rec` and are a degraded path — untested by us.
383
+ - Hold-to-talk needs Kitty keyboard protocol flag 2 for key *release*. It is verified
384
+ working on Kitty. Everywhere else it falls back to tap-to-toggle and says so once.
385
+ - The local backend needs `transcribe-cpp` and a Parakeet GGUF.
386
+
387
+ ## Development
388
+
389
+ ```bash
390
+ bun test # no network or microphone required
391
+ bunx tsc --noEmit
392
+ bunx biome check .
393
+
394
+ # live check against Deepgram, with and without keyterms
395
+ DEEPGRAM_API_KEY=... bun scripts/deepgram-smoke.ts speech.pcm
396
+ DEEPGRAM_API_KEY=... bun scripts/deepgram-smoke.ts speech.pcm subagent,worktree,tsconfig
397
+
398
+ # live check of the local backend (loads the GGUF, so it is slow)
399
+ bun scripts/parakeet-smoke.ts speech.pcm
400
+ ```
401
+
402
+ The pipeline is tested without a microphone, a network, or a model: capture, the
403
+ backend, and time are all injected seams, so every recovery rule has a test that runs
404
+ in milliseconds.