Add push-to-talk dictation design spec

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 09:29:49 +02:00
co-authored by Claude Sonnet 5.5
commit ad9f97d129
@@ -0,0 +1,82 @@
# Push-to-talk dictatie-app voor macOS — ontwerp
Datum: 2026-10-01 · Status: concept ter review · Werktitel: `Sst`
## Doel
Een systeembrede dictatie-app, vergelijkbaar met Wispr Flow. De gebruiker houdt **Fn** ingedrukt, spreekt, laat los, en de transcriptie verschijnt op de cursor in elke app. Alles draait lokaal op de Mac. Nederlands en Engels mogen door elkaar gebruikt worden.
## Buiten scope (v1)
- AI-cleanup (vulwoorden, zelfcorrecties, toon per app). Alleen de naad (`Cleaner`) wordt voorbereid.
- Streaming-transcriptie tijdens het praten.
- Mac App Store-distributie, accounts, sync, cloud.
- Branding. Die volgt als aparte stap na de werkende kern.
## Doelomgeving
Apple M4 Pro, 24 GB RAM, macOS 27, Xcode 27, Swift 6.4. Alleen Apple Silicon.
## Aannames
- Persoonlijk gebruik, niet-gesandboxed. De Mac App Store-sandbox verbiedt globale key-taps en gesimuleerd plakken.
- Niets verlaat de Mac. Audio wordt niet bewaard.
- Spraakmodel: Whisper `large-v3-turbo` via WhisperKit, met automatische taalherkenning.
## Architectuur
Een macOS-menubalk-app (SwiftUI `MenuBarExtra`). De logica staat in een Swift Package (testbaar met `swift test`). De Xcode-app is een dunne schil eromheen.
| Unit | Taak | Afhankelijk van |
|---|---|---|
| `HotkeyMonitor` | Detecteert Fn omlaag/omhoog via `CGEventTap`; stuurt `pressed`/`released` events | Input Monitoring |
| `AudioRecorder` | `AVAudioEngine` neemt op naar een 16 kHz mono Float-buffer; start bij `pressed`, stopt bij `released` | Microfoon |
| `Transcriber` (protocol) | `transcribe(samples) async throws -> String`; eerste implementatie `WhisperKitTranscriber` | WhisperKit |
| `Cleaner` (protocol) | `clean(text, context) async throws -> String`; v1 `PassthroughCleaner` | n.v.t. |
| `TextInserter` | Zet tekst op het klembord, simuleert ⌘V, herstelt het klembord | Accessibility |
| `DictationController` | Statemachine `idle → recording → transcribing → inserting → idle`; enige component dat de andere kent | bovenstaande |
| `OverlayPanel` | Floating `NSPanel` met pilletje: opname, golfvorm, bezig | SwiftUI |
| `MenuBarExtra` | Status, instellingen, model-download, rechten-check | n.v.t. |
Ontwerpprincipes: elke unit heeft één taak en een duidelijke interface. `Transcriber` en `Cleaner` zijn protocollen, zodat modellen (bijvoorbeeld Parakeet of een LLM) later verwisselbaar zijn. Tests gebruiken fakes voor de protocollen.
## Dataflow
1. Fn omlaag: opname start, overlay verschijnt.
2. Fn omhoog: opname stopt.
3. Samples gaan naar `Transcriber`, daarna naar `Cleaner`.
4. `TextInserter` voegt de tekst in; overlay verdwijnt.
Het model wordt bij het opstarten geladen en blijft warm in het geheugen.
## Foutafhandeling
- Opname korter dan ~0,3 s: negeren (per ongeluk aangetikt).
- Rechten ontbreken: menubalk toont wat ontbreekt, met knop naar de juiste Systeeminstellingen.
- Model nog niet gedownload: eerste start toont voortgang; dictatie is dan uitgeschakeld.
- Invoegen mislukt: tekst blijft op het klembord, overlay meldt dit.
- Stilte of lege transcriptie: niets invoegen.
- Transcriptiefout: overlay toont een korte foutmelding; de controller keert terug naar `idle`.
## Vereiste rechten
Microfoon, Accessibility (plakken simuleren), Input Monitoring (Fn-toets).
## Testen
- Unit-tests voor `DictationController` met fake `Transcriber`, `Cleaner`, `TextInserter` (toestandsovergangen, korte opnames, foutpaden).
- Unit-test voor het klembord-herstel in `TextInserter` (injecteerbare pasteboard).
- Handmatige rooktest van de hele keten in TextEdit, Notes en een browser: Nederlands, Engels en gemengd.
## Bouwvolgorde (skeleton eerst)
1. Package plus menubalk-app; hotkey logt alleen "recording…".
2. Opnemen naar een buffer.
3. WhisperKit transcriberen.
4. Tekst invoegen.
5. Overlay-pill.
6. Cleanup, instellingen en branding (aparte specs).
## Open punten
Geen voor het skeleton. De definitieve naam en branding worden in een latere stap bepaald.