summaryrefslogtreecommitdiff
path: root/SPEC.md
blob: 6bf3000a18a6da2474e263cb7593fb45dc04da91 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# Parley specification

## 1. Purpose

Parley is a local-first dictation tool for GNOME on Wayland. It turns microphone recordings into text with the existing Parakeet transcription runtime and inserts the result at the focused application's caret when the desktop permits it.

The normal interaction is:

1. Place the caret in an editable field.
2. Press a global shortcut or click the top-bar microphone.
3. Speak.
4. Press the shortcut or click again.
5. Parley transcribes locally.
6. Parley inserts through the configured insertion backend.
7. Parley also saves the transcript and copies it to the clipboard.

The supported target environment is deliberately narrow:

- fully updated Arch Linux
- GNOME Shell 50 on Wayland
- IBus 1.5.34
- PipeWire/WirePlumber with PulseAudio compatibility
- Python 3.14 with PyGObject
- The existing `transcribe-cli` binary and Parakeet Unified EN model

## 2. Goals

- Feel native on GNOME: top-bar status, global shortcut, notifications, and clear recording/transcribing states.
- Work in as many desktop applications as Wayland and application input-method support allow.
- Keep recording and transcription entirely local.
- Preserve every successful transcript even when insertion fails.
- Keep GNOME Shell responsive and isolated from model, audio, and subprocess failures.
- Provide useful operation before the Shell extension is complete.
- Keep insertion backends replaceable behind one interface.
- Continue to offer a standalone CLI frontend to the shared transcription core.

## 3. Non-goals and platform limits

- Parley cannot guarantee insertion into literally every application. Lock/login screens, games, custom-rendered controls, unsupported XWayland clients, and applications without input-method support may reject insertion.
- Parley will not bypass the lock screen or cross user/session boundaries.
- Continuous live captioning and partial streaming text are not required for v1.
- Flatpak packaging is not a v1 requirement. A Flatpak would require a separate portal-oriented audio and filesystem design.
- Cloud transcription is out of scope.
- The Shell extension will not load the model, capture audio, or run transcription.

## 4. Transcription pipeline

The terminal-independent Python core now:

- records the default input with FFmpeg's `pulse` input at mono 16 kHz PCM;
- invokes `transcribe-cli` as a cancellable child process;
- uses `parakeet-unified-en-0.6b-Q8_0.gguf` (approximately 698 MiB);
- parses JSONL batch output;
- persists UTF-8 transcripts and their WAV recordings together; and
- exposes the same core to the standalone terminal frontend and daemon.

Development can use runtime assets from the sibling `transcribe-parakeet` checkout. Installed assets default to `~/.local/bin/transcribe-cli` and `~/.local/share/parley/models/`. The current runtime binary links to CUDA 13 and Intel MKL libraries. Packaging must declare those host dependencies or later supply a more portable build.

## 5. Architecture

```text
GNOME Shell extension          toggle CLI / custom shortcut
          |                              |
          +------------- D-Bus ----------+
                         |
                         v
              Parley per-user daemon
              - state machine
              - audio recording
              - transcription
              - transcript history
              - clipboard/notifications
              - insertion policy
                         |
          +--------------+----------------+
          |              |                |
          v              v                v
        IBus       RemoteDesktop/       Clipboard
                    libei paste
```

### 5.1 Shared core

Terminal-independent Python modules own:

- recording lifecycle;
- transcription invocation and JSON parsing;
- transcript persistence;
- configuration;
- errors and cancellation.

The existing CLI behavior becomes one frontend to this core. No reusable core function may read raw terminal keys or require a TTY.

### 5.2 Per-user daemon

A long-running Python/GObject process owns mutable state and serializes operations. It runs as a systemd user service and is activated on demand where practical.

Only one recording/transcription operation may run at a time. Long-running work and subprocess waits must not block the GLib main loop.

Provisional D-Bus identity:

- bus name: `org.parley.Transcription1`
- object path: `/org/parley/Transcription1`
- interface: `org.parley.Transcription1`

The name can be finalized before public packaging.

Minimum methods:

- `StartRecording()`
- `StopAndTranscribe()`
- `Toggle()`
- `Cancel()`
- `CopyLastTranscript()`
- `InsertLastTranscript()`
- `OpenTranscriptFolder()`

Minimum read-only properties:

- `State`: `idle`, `recording`, `transcribing`, `inserting`, or `error`
- `LastTranscript`
- `LastTranscriptPath`
- `LastError`
- `InsertionMode`
- `AutoInsert`

Minimum signals:

- `StateChanged(state)`
- `TranscriptReady(text, path)`
- `Error(code, message)`
- `InsertionFinished(backend, success, message)`

Exact D-Bus signatures and error names will be versioned in an interface XML file during implementation.

### 5.3 State machine

Normal flow:

```text
idle -> recording -> transcribing -> inserting -> idle
```

If automatic insertion is disabled or unavailable:

```text
idle -> recording -> transcribing -> idle
```

Rules:

- `Toggle` starts from `idle` and stops from `recording`.
- A toggle while transcribing or inserting must not start a second operation.
- `Cancel` during recording discards temporary audio.
- Cancellation during transcription should terminate the child process cleanly, escalating only when necessary.
- Errors transition through `error`, emit details, and leave the daemon recoverable without a restart.
- Temporary files are removed after completion or cancellation.
- A successful transcript is persisted before insertion is attempted.

### 5.4 systemd user service

The daemon is managed by a user unit. The unit must:

- restart only on genuine crashes, with rate limiting;
- inherit the graphical session environment needed for D-Bus, PipeWire, IBus, and portals;
- stop child recording/transcription processes on shutdown; and
- log structured, actionable messages to the user journal without logging microphone audio.

### 5.5 Thin clients

A small command-line client calls the D-Bus service. It supports at least:

```text
parleyctl toggle
parleyctl start
parleyctl stop
parleyctl cancel
parleyctl status
parleyctl copy
parleyctl insert
```

This client is used by the early GNOME custom keyboard shortcut and remains a fallback if the Shell extension is disabled by an upgrade.

## 6. Recording and transcription

### 6.1 Recording v1

Keep the proven FFmpeg path through `pipewire-pulse`:

- input: `pulse`, device `default`;
- mono;
- 16 kHz;
- signed 16-bit PCM WAV;
- configurable maximum duration;
- controlled shutdown that produces a valid WAV;
- clear handling for missing/default-device changes.

Direct PipeWire capture may be evaluated later but is not required merely for architectural purity.

### 6.2 Transcription v1

Invoke the existing binary with the current model and options unless testing justifies a change:

- quiet output;
- language `en`;
- no timestamps;
- 30 ms stream chunks;
- JSONL batch output.

Binary and model paths are configuration, not hard-coded repository assumptions. Development may point at the sibling prototype's assets. Distribution should not duplicate the model unnecessarily.

### 6.3 Persistence

- Save every successful transcript and its WAV recording before insertion.
- Group recordings by local date and save each pair as `HH-MM-SS.txt` and `HH-MM-SS.wav`.
- Store user data under an XDG-appropriate data directory by default; support a configurable transcript directory.
- Write UTF-8 text with a final newline.
- Keep `LastTranscript` and its path available over D-Bus.
- A history UI is optional for v1, but the storage layout must not prevent one.

### 6.4 Performance

The initial implementation may launch `transcribe-cli` once per utterance. Reloading the approximately 700 MiB model is expected to be the largest latency cost.

After correctness is established, evaluate a persistent worker or a library/server API that keeps the model loaded. The daemon and D-Bus contracts must not depend on one-shot subprocesses so this can change internally.

## 7. Insertion strategy

All insertion implementations conform to one daemon-side abstraction, conceptually:

```python
insert_text(text, context) -> InsertionResult
```

An `InsertionResult` identifies the backend, success/failure, and a user-safe explanation. Backend selection is configurable and must degrade predictably.

Regardless of selected backend, a successful transcript is saved and copied to the clipboard before automatic insertion is attempted.

### 7.1 Primary candidate: IBus `commit_text`

A minimal persistent passthrough IBus engine is implemented. When it owns the focused input context, it commits the transcript as semantic text. While transcription is running, it uses temporary IBus preedit text for an animated in-field processing indicator; the indicator is never inserted into the document. Initial end-to-end insertion and ordinary passthrough typing work across the applications recorded in the bake-off matrix; the full input-behavior matrix is still required.

Benefits:

- follows the desktop input-method path rather than simulating hardware;
- correct Unicode and multiline handling;
- no keyboard-layout translation;
- fewer focus races than clipboard plus a synthetic paste shortcut;
- no remote-control permission prompt.

The engine must be active for the focused input context. Parley therefore uses temporary activation for each dictation session: it remembers the current engine, selects Parley while recording and transcribing, commits semantic text, and restores the previous engine after insertion, cancellation, or failure. If GNOME changes input sources during the session, Parley records the latest choice, reacquires its engine, and restores that choice afterward. The committed transcript is Unicode text and does not depend on the active keyboard layout.

### 7.2 Safe fallback: clipboard only

Copy the transcript and notify the user to press Ctrl+V. This requires one user action but does not synthesize input and should always remain available.

### 7.3 Compatibility fallback: RemoteDesktop portal plus libei

Use the XDG Desktop Portal RemoteDesktop API to request keyboard control and libei/EIS to synthesize a paste shortcut after placing the transcript on the clipboard.

Requirements:

- request only the capability needed;
- support GNOME restore tokens and persist them securely when returned;
- handle missing, expired, denied, or revoked permission without losing text;
- clearly expose when a remote-control session is active;
- do not assume persistence is guaranteed on every portal backend or forever;
- recreate the session when required; and
- fall back to clipboard-only on any failure.

This avoids root/uinput configuration and is preferred over `ydotool` for synthetic paste. It still has focus races, depends on the target accepting Ctrl+V, and cannot insert arbitrary Unicode directly—it pastes clipboard contents.

### 7.4 Last resort: `ydotool`

An opt-in debugging/compatibility backend may synthesize Ctrl+V through `ydotool`. It is never the default because it requires access to `uinput`, may involve a privileged helper or device rules, weakens the Wayland security boundary, and retains synthetic-input focus races.

### 7.5 Backend policy

The insertion bake-off determines the shipped default. Intended preference order is:

1. IBus, if reliable;
2. clipboard only as the universally safe fallback;
3. RemoteDesktop/libei as an explicitly enabled compatibility mode;
4. `ydotool` as an explicitly enabled last resort.

The UI may offer `ibus`, `clipboard`, `portal-paste`, and `ydotool-paste` modes. An `auto` mode may use IBus and degrade to clipboard, but it must not unexpectedly open a remote-control permission dialog or start synthetic input without prior user choice.

## 8. GNOME integration

### 8.1 Early global shortcut

Before the extension exists, install/document a GNOME custom keyboard shortcut that runs `parleyctl toggle`. This provides an end-to-end workflow early:

```text
hotkey -> record -> hotkey -> transcribe -> save/copy -> manual paste
```

Installation must not overwrite an existing conflicting shortcut without confirmation. The shortcut command remains supported after the extension ships.

### 8.2 GNOME Shell extension

A GNOME Shell 50 extension provides:

- a top-bar microphone indicator;
- idle, recording, transcribing, inserting, and error presentation;
- click to start/stop;
- cancel action;
- copy and insert-last actions;
- open-transcript-folder action;
- automatic insertion toggle;
- insertion mode display/selection or a settings link;
- notifications; and
- an extension-managed global shortcut.

The extension is a D-Bus client only. It must use documented GNOME Shell extension APIs wherever available, declare compatible Shell versions explicitly, and tolerate daemon unavailability.

The extension shortcut replaces or disables the development custom shortcut to prevent duplicate activation, while `parleyctl` remains usable as a recovery path.

## 9. Notifications and clipboard

- Copy every successful transcript to the clipboard.
- Desktop notifications are disabled by default and can be explicitly enabled.
- When enabled, notify on transcription completion when no visible UI feedback is available.
- When enabled, notify when insertion falls back to manual paste.
- Surface concise errors while retaining details in the journal.
- Avoid repeated noisy notifications when the top-bar state already makes progress obvious.
- Use a registered desktop application identity for production notifications; `notify-send` is acceptable during the prototype phase.

## 10. Configuration

Daemon configuration uses the optional TOML file `~/.config/parley/config.toml` with system-oriented defaults. Parley does not use application-specific environment variables. GNOME Shell-only preferences may use GSettings when schemas are introduced.

Planned settings:

- microphone/device;
- model and runtime paths;
- language;
- transcript directory;
- maximum recording duration;
- automatic insertion enabled;
- insertion backend;
- notification and sound preferences;
- shortcut;
- optional punctuation/formatting behavior.

Secrets and portal restore tokens must not be exposed over D-Bus or written to logs. Restore-token storage should follow portal guidance; if it is not itself sensitive, it must still be treated as user-private state.

## 11. Security and privacy

- Audio and text remain local unless the user explicitly copies or pastes them into another service/application.
- Audio from successful transcriptions is retained beside its transcript. Temporary audio is deleted after cancellation or error.
- The recording state must always be visibly indicated.
- Do not log transcript contents by default.
- Portal control is opt-in and revocable.
- `ydotool` support is opt-in and documents its elevated input capability.
- D-Bus methods are available only on the user's session bus; no system-wide service is needed.

## 12. Delivery phases

### Phase 1 — Refactor (complete)

- Create the terminal-independent core.
- Implement start/stop/cancel recording operations.
- Extract transcription and persistence.
- Preserve the current CLI as a frontend.
- Add unit tests around state-independent parsing, paths, and errors.

Exit criterion: CLI behavior works through the shared core without core TTY dependencies.

### Phase 2 — Daemon, D-Bus, and early hotkey (complete for development)

- Implement the GLib daemon and state machine.
- Add the systemd user unit.
- Add `parleyctl`.
- Save and copy successful transcripts.
- Add prototype notifications.
- Configure a GNOME custom shortcut for `parleyctl toggle`.

Exit criterion: hotkey-driven recording/transcription reliably ends with a saved transcript on the clipboard.

### Phase 3 — Insertion bake-off (in progress)

Build two deliberately small prototypes:

- IBus `commit_text`, testing persistent passthrough and temporary activation;
- RemoteDesktop portal/libei paste, including permission denial and restore-token reuse.

Test both against:

- GNOME Text Editor or another GTK 4 editor;
- Firefox;
- Chromium;
- LibreOffice;
- GNOME Console/Terminal;
- VS Code or another Electron application;
- a Qt application;
- representative XWayland applications;
- multiple keyboard layouts if available.

For IBus passthrough, explicitly test ordinary typing, modifiers, application shortcuts, compose/dead keys, layout switching, and interaction with any real IM engines. For temporary activation, test caret/focus retention and engine restoration.

For portal paste, test first authorization, restored sessions, logout/login, permission revocation, focus changes, multiline/Unicode clipboard content, and applications that remap or reject Ctrl+V.

Exit criterion: record results, choose the default backend, and document unsupported applications. If IBus passthrough or activation is flaky, use clipboard as the safe default and retain portal paste as opt-in.

### Phase 4 — GNOME Shell extension (baseline implemented)

- Implement the top-bar indicator and menu.
- Synchronize state over D-Bus.
- Add notifications and the extension shortcut.
- Keep `parleyctl` and the custom-shortcut setup available as recovery tools.

Exit criterion: all daemon states and failures are represented without blocking or destabilizing GNOME Shell.

### Phase 5 — Production insertion backends

- Turn successful prototypes into selectable backends.
- Implement deterministic fallback behavior.
- Ensure every failed insertion leaves text saved and copied.

### Phase 6 — UX, performance, and packaging

- Evaluate a persistent loaded model.
- Add microphone selection and maximum duration UI.
- Add optional push-to-talk behavior where reliable key-release handling is available.
- Add punctuation/formatting commands.
- Package the daemon, D-Bus interface, systemd unit, desktop metadata, GSettings schema, IBus component, CLI, and extension.
- Document CUDA/MKL requirements or provide a portable runtime build.

## 13. Testing requirements

### Automated

- state-machine transitions and invalid calls;
- cancellation and child-process cleanup;
- JSONL parsing, including malformed/no-result/error rows;
- dated transcript and WAV pair persistence;
- D-Bus methods, properties, signals, and typed errors;
- insertion fallback selection;
- configuration migration/defaults;
- no transcript loss on insertion failure.

Runtime and desktop dependencies should be abstracted so core tests do not require a microphone, GNOME Shell, IBus, or the 698 MiB model.

### Manual integration

- repeated start/stop/cancel cycles;
- microphone removal and unavailable input;
- daemon restart and Shell extension restart;
- GNOME logout/login;
- GPU/runtime failures;
- long recordings and maximum-duration cutoff;
- the Phase 3 application matrix;
- focus changes while transcribing;
- clipboard ownership after daemon exit/restart;
- extension disabled after a GNOME upgrade.

## 14. Acceptance criteria for the first useful release

- A keyboard shortcut starts and stops recording.
- Recording and transcription do not block GNOME Shell.
- Every successful transcript is saved and copied.
- The selected insertion backend inserts in the supported application matrix or reports a clear fallback.
- Cancellation and errors leave the service ready for another recording.
- A top-bar indicator accurately shows recording and processing states.
- The tool works without network access.
- Disabling or breaking the Shell extension does not remove CLI/hotkey access to the daemon.

## 15. Open decisions

These must be resolved by prototypes or packaging work rather than assumption:

- whether the promising IBus persistent passthrough results remain reliable across the full test matrix;
- exact compatibility of IBus and portal paste across the target application's real versions;
- GNOME portal restore-token lifetime and user experience across login sessions;
- final reverse-DNS application/D-Bus identifier;
- persistent model API/process design;
- portable versus host-specific CUDA/MKL distribution;
- direct PipeWire capture versus keeping FFmpeg/PulseAudio compatibility;
- whether push-to-talk can use stable public GNOME APIs on Shell 50.