21 KiB
Sst als volwaardige macOS-app — ontwerp
Datum: 2026-10-01 · Status: concept ter review · Bouwt voort op 2026-10-01-push-to-talk-dictation-design.md (de engine) en de goedgekeurde tokens in docs/design/tokens.md (bron: Design/tokens.json, v0.1.0-draft)
Doel
Van menubalk-hulpprogramma naar een echte Mac-app: Dock-icoon, app-menu, een vrij schaalbaar hoofdvenster, een instellingenvenster (⌘,) en een dubbelklikbaar Sst.app dat in Programma's kan staan. Het menubalkitem blijft als secundaire status- en sneltoetsweergave. De interface volgt het goedgekeurde ontwerpsysteem: een jaren-80 veldrecorder, met uitsluitend waarden uit de tokens.
Wat de gebruiker krijgt
- Hoofdvenster: doorzoekbare transcriptiegeschiedenis met een kopieerknop per rij; een live VU-meter, teller en LED-balk tijdens het opnemen; start- en stopknop; het woordenboek.
- Instellingen (⌘,): sneltoets en model.
- Woordenboek: woorden en correctieparen, bewerkbaar in de app én als gewoon tekstbestand. Werkt op twee manieren: voorrang bij het model (bias) en een gegarandeerde correctieronde achteraf.
- Geschiedenis toont correcties: wanneer het woordenboek iets wijzigt, zie je wat er stond en wat het werd.
Buiten scope
- Overlay-pill tijdens het dicteren, AI-cleanup (taalmodel), streaming-transcriptie.
- Meerdere woordenboeken of profielen, per-app-instellingen, import of export van de geschiedenis, synchronisatie.
- Notarisatie, DMG, automatische updates, automatisch starten bij inloggen.
- Toggle-modus (eenmaal drukken start, nogmaals stopt). Alleen vasthouden.
- Definitieve naam en merk. "Sst" is de werktitel; de naam staat op één plek (
AppIdentity) zodat hernoemen goedkoop blijft. - Een app-icoon in volledige merkkwaliteit; wel een eerste icoon in het goedgekeurde ontwerp.
Beslissingen en aannames
- De engine blijft in gedrag identiek. Er komen vier kleine, additieve raakpunten bij (zie "Engine-raakpunten"). Bestaande tests blijven ongewijzigd groen.
- "Een kopie van elk document" is een kopieerknop per transcriptie (kopieert de uiteindelijke tekst).
- UI-taal is Nederlands, in één bestand met teksten (
Strings), zodat vertalen later kan. Systeemmenu's (Hide, Quit, enz.) volgen de taal van macOS. - Geschiedenis staat standaard aan en bewaart alleen tekst, nooit audio. Een schakelaar in Instellingen zet bewaren uit en een knop wist alles.
- Sluit je het hoofdvenster, dan draait de app door (Dock-icoon en menubalkitem blijven). Dicteren in andere apps blijft werken. ⌘Q sluit de app echt af. Klikken op het Dock-icoon opent het venster weer.
- Is Sst zelf de voorgrond-app (of start je de opname met de knop in het venster), dan wordt de tekst niet geplakt maar alleen in de geschiedenis bewaard. Anders zou het dictaat in het zoekveld van Sst terechtkomen.
- Ondertekening: met je Apple Development-certificaat, zodat macOS je rechten (Microfoon, Accessibility, Input Monitoring) na een herbouw niet vergeet. Het script zoekt zelf de eerste "Apple Development"-identiteit en valt anders terug op ad-hoc, met een duidelijke waarschuwing. Geen identiteit staat in de repo.
- Sneltoetsen: alleen modifier-toetsen, vasthouden. Keuze: Fn (standaard), Rechter Option, Rechter Command, Rechter Control, Linker Control.
- Afwerking (Aluminium of Graphite) volgt het uiterlijk van macOS.
Architectuur
Eén Swift Package, vier lagen, steeds in één richting afhankelijk:
Sst (app) → SstUI (tokens, componenten)
├────────→ SstData → SstCore (woordenboek, correcties, geschiedenis)
└────────→ SstWhisper → SstCore (de engine; dunne uitbreidingen)
| Module | Verantwoordelijkheid | Testbaar zonder UI |
|---|---|---|
SstCore / SstWhisper |
Engine (bestaat). Vier additieve raakpunten. | ja |
SstData |
DictionaryFile, CorrectionEngine, EntryWarnings, VocabularyBuilder, HistoryStore, HistorySearch, AppSettings. Deelt de typen AppliedCorrection en TextCorrecting met SstCore. |
ja |
SstUI |
Gegenereerde DesignTokens, componenten (toets, lampje, VU-meter, segmentteller, LED-balk, paneel, verzonken vlak, zeefdruk-label) |
gedeeltelijk |
Sst |
Scenes, menu's, AppModel, vensters, bundelscript |
handmatig |
Engine-raakpunten (allemaal additief, met standaardwaarden)
- Voorrang voor het model.
Transcriberkrijgttranscribe(_ samples: [Float], vocabulary: [String]) async throws -> String. De oude methode blijft bestaan als extensie met een lege lijst.WhisperKitTranscriberzet de woorden om inDecodingOptions.promptTokens: tekst" " + woorden.joined(", "), viatokenizer.encode(text:), gefilterd op tokens onderspecialTokens.specialTokenBegin(zo doet de WhisperKit-CLI het ook). - Niveau-uitvoer.
AudioRecorderkrijgtonLevel: ((Float) -> Void)?: RMS in dBFS per buffer van 1024 frames (~21 ms bij 48 kHz). De bestaande buffer van 4096 blijft ongewijzigd voor de opname zelf; de niveau-tap rekent op dezelfde audio. - Resultaat-hook en correctie.
DictationControllerkrijgt een optionelecorrector: TextCorrecting?enonResult: ((DictationResult) -> Void)?.DictationResultbevatrawText,finalText,corrections,duration,date. Zonder corrector en hook gedraagt de controller zich exact als nu. - Instelbare toets.
HotkeyMonitorkrijgt eenHotkeyBinding(keycode + vlagmasker) via een nieuweModifierKeyTracker.FnKeyTrackerblijft bestaan en wordt de Fn-variant. Het keycode-filter (nu 63) wordt de keycode van de gekozen binding.
App-shell
Scenes
Window("Sst", id: "main") — één hoofdvenster, geen documentvensters
Settings — ⌘,
MenuBarExtra — secundair
.commands { … } — app-menu en menu's
Zonder LSUIElement, NSPrincipalClass standaard. applicationShouldHandleReopen opent het hoofdvenster als alle vensters dicht zijn. Vensterafmetingen komen uit layout.window.*: minimaal 800×560, standaard 1080×700; macOS onthoudt de laatste grootte.
Menu's
| Menu | Inhoud |
|---|---|
| Sst | Over Sst, Instellingen… (⌘,), Verberg, Verberg anderen, Toon alles, Stop Sst (⌘Q) |
| Archief | Sluit venster (⌘W) |
| Bewerken | Standaard (Kopieer, Selecteer alles, Zoek…); Verwijder transcriptie (⌘⌫); Wis geschiedenis… |
| Opname | Start of stop opname (⌥⌘R); Transcripties (⌘1); Woordenboek (⌘2); Zoek (⌘F) |
| Venster / Help | Standaard |
Menubalkitem (secundair)
Statusicoon met SF Symbols (mic, mic.fill, waveform, exclamationmark.triangle), geen aangepast beeld. Menu: één statusregel, de gekozen sneltoets ("Houd RECHTER OPTION ingedrukt"), "Open Sst", "Instellingen…", "Stop Sst". Geen geschiedenis, geen meter.
Bundel en installatie
scripts/make-app.sh bouwt build/Sst.app met: CFBundleIdentifier (huidige local.sst.app blijft, zodat bestaande rechten blijven werken), versie, LSApplicationCategoryType productivity, CFBundleIconFile, NSMicrophoneUsageDescription in het Nederlands, zonder LSUIElement. Het script ondertekent met de gevonden identiteit. --install kopieert naar /Applications (alleen door de gebruiker uit te voeren). Het icoon komt uit een script dat een SwiftUI-weergave met de tokens naar een .icns rendert (Design/make_icon).
Rechten en eerste start
Een blok "CONTROLES" onderaan de zijbalk toont vier lampjes: Microfoon, Accessibility, Input Monitoring, Model. Elk heeft een knop naar de juiste Systeeminstellingen. Rechten worden opnieuw gelezen zodra de app actief wordt en elke 2 s zolang het blok zichtbaar is, zodat de weergave niet meer verouderd raakt. Tot alle vier branden staat de REC-toets uit met een duidelijke reden.
Hoofdvenster
Indeling (afmetingen en kleuren uitsluitend uit tokens):
┌─ zijbalk 232 ─┬─ deck (vast, bovenaan) ───────────────────────┐
│ TRANSCRIPTIES │ [VU-meter] [00:12.4] [LED-balk] ● REC STOP │
│ WOORDENBOEK ├────────────────────────────────────────────────┤
│ │ zoekveld │
│ CONTROLES │ lijst (papier) of woordenboek │
└───────────────┴────────────────────────────────────────────────┘
Deck
- VU-meter: 0 VU = −20 dBFS.
vu = rmsDbfs + 20, schaal −20…+3. De wijzer volgtonLevelmet de veer uitmotion.needle.*(respons 0,30 s, demping 0,80; met "Verminder beweging" demping 1,0). Piek-LED 1000 ms. Getekend metCanvas, ververst met 60 Hz viaTimelineViewalleen wanneer het venster zichtbaar is én er wordt opgenomen; daarna valt de wijzer terug en de timer stopt (geen CPU in rust). - Teller: segmentcijfers (
segment.*) opsurface.display, ghost-segmenten voor gedoofde delen. Toont de opnametijd; in rust de duur van de laatste opname. - LED-balk: 12 segmenten (8 groen, 3 amber, 1 rood) op hetzelfde signaal.
- REC / STOP: REC start, STOP beëindigt; de REC-toets blijft ingedrukt zolang er wordt opgenomen, met brandend lampje. Een sessie vanuit het venster slaat alleen op (zie beslissing 6).
- Fout: een amber lampje met een korte regel; wordt gewist bij de volgende opname.
Transcripties
- Gesorteerd nieuw → oud, gegroepeerd per dag (zeefdruk-kop). Rij: tijd (
type.counterSmall), tekst (type.reading), kopieerknop, minimale hoogtesize.row.height. - Zoeken (⌘F): live, niet hoofdlettergevoelig en diakriet-ongevoelig, op zowel de uiteindelijke als de oorspronkelijke tekst; woorden hoeven niet aaneen te staan (alle zoekwoorden moeten voorkomen).
- Correcties zichtbaar: vervangen stukken krijgen de amber markering (
opacity.mark); onder de rij staat "2 CORRECTIES · cloud code → Claude Code". Geselecteerd toont de rij de oorspronkelijke tekst naast de uiteindelijke. - Kopieerknop kopieert de uiteindelijke tekst; rechtsklik: kopieer origineel, verwijder. Lege staat: korte zeefdruk-tekst, geen illustratie.
Woordenboek
Tabel op papier-oppervlak met kolommen GEHOORD → SCHRIJF. Een regel zonder pijl is een term (alleen SCHRIJF). Toevoegen met +, bewerken met dubbelklik, verwijderen met ⌫, ongedaan maken met ⌘Z, zoeken met het zoekveld. Een knop "Open bestand" toont het bestand in de standaardeditor. Twee kleine schakelaars: "Voorrang" en "Correctie" (beide standaard aan), zodat je kunt zien wat elk mechanisme doet. Een amber lampje op de rij toont een waarschuwing (zie hieronder).
Instellingen
Venster layout.settings.width breed, drie tabbladen als zeefdruk-toetsen.
- Sneltoets: de gekozen toets wordt vastgelegd door erop te drukken (de app is dan actief en luistert met een lokale event-monitor). Alleen de toegestane modifiers uit beslissing 8; andere toetsen worden afgewezen met uitleg. Bij Fn staat een hint over de Globe-instelling van macOS.
- Model: vaste lijst van varianten (op dit moment:
large-v3-v20240930_turbostandaard, de 632 MB-variant ensmall; exacte namen worden bij de bouw tegen WhisperKit gecontroleerd). Per model: status (aanwezig, gekozen, niet gedownload), download- of verwijderknop en voortgang als LED-balk. Wisselen laadt het nieuwe model en zet dicteren uit tot het klaar is; mislukt het laden, dan blijft het vorige model actief met een duidelijke melding. Toont de opslaglocatie met "Toon in Finder". - Privacy: schakelaar "Transcripties bewaren" en knop "Wis geschiedenis…" (met bevestiging).
Woordenboek — het ontwerp in detail
Bestand
~/Library/Application Support/Sst/dictionary.txt, UTF-8, één invoer per regel:
# Opmerkingen beginnen met #
Anthropic
Vercel
Supabase
cloud code => Claude Code
- Een regel zonder
=>is een term. Een regel met=>is een correctiepaar (links gehoord, rechts schrijven). Spaties rond=>en aan de uiteinden worden genegeerd. - Opmerkingen, lege regels en de volgorde blijven bij opslaan behouden: de app leest naar een lijst van regels (invoer, opmerking, leeg) en schrijft die letterlijk terug. Een onleesbare regel blijft staan en krijgt een waarschuwing, ze wordt nooit stilletjes weggegooid.
- De app schrijft atomisch en let op wijzigingen door een externe editor (bestandsmonitor, 200 ms debounce) en herlaadt dan. Verdwijnt de invoer die je aan het bewerken was, dan sluit de editor met een melding.
Mechanisme 1 — voorrang bij het model
VocabularyBuilder maakt een korte lijst: eerst de termen (bestandsvolgorde), dan de unieke rechterkanten van de correctieparen. Maximaal 24 woorden en 64 tokens; past er meer niet, dan vallen de laatste woorden af. Doorgegeven als promptTokens (zie raakpunt 1).
Tegen verzinnen op stilte:
- Is het RMS van de opname lager dan 0,015 (het stiltefilter van 0,005 blijft erboven), dan wordt geen prompt meegegeven.
- Bestaat het resultaat (genormaliseerd: kleine letters, zonder leestekens) uitsluitend uit de eerste k termen van de prompt met k ≥ 2, dan wordt het verworpen: dat is een echo van de prompt, geen spraak. Eén term alleen ("Anthropic") is wel geldige spraak.
- WhisperKit zet de snelle prefill-cache uit zodra een prompt is gezet; de snelheid wordt bij de bouw gemeten met en zonder prompt en in het plan vastgelegd. Doel: onder 1,5 s voor 8 s spraak.
Mechanisme 2 — correctieronde (het gegarandeerde pad)
CorrectionEngine loopt na de transcriptie over de tekst:
- Hele woorden, niet hoofdlettergevoelig, één doorgang van links naar rechts. De langste overeenkomst wint (bij gelijke lengte de eerste in het bestand). Vervangen tekst wordt niet opnieuw gescand, dus A→B en B→C vormen nooit een ketting.
- Aaneengeplakte en gekoppelde vormen: de bronzin wordt gesplitst op spaties en koppeltekens; de delen worden verbonden met
[spatie, koppelteken]*. "cloud code" herkent dus "cloud code", "cloudcode", "Cloud-Code" en "CLOUD CODE". Een bron van één deel (geen splitsing) herkent alleen zichzelf, hoofdletter-onafhankelijk. - Woordgrenzen zijn Unicode-bewust: de overeenkomst mag niet direct aan een letter of cijfer grenzen. Daarom raakt "cloud code" nooit "Cloudflare", "cloud codex" of het losse woord "cloud".
- Termen (invoer zonder pijl) krijgen een normaliserende regel: dezelfde letters, maar met de gewenste schrijfwijze (hoofdletters en spatiëring). "claude code", "claudecode" en "Claude-Code" worden "Claude Code". Uitzondering: is de term in kleine letters een gewoon woord (bijvoorbeeld "Reef" of "Slack"), dan krijgt hij alleen voorrang bij het model en géén automatische hoofdletterregel; de rij laat dat zien.
- De vervanging is letterlijk (geen sjablonen);
$en\blijven staan. - De engine geeft per vervanging terug: gehoord (zoals het in de tekst stond), geschreven, de regel, en de plaats in de uiteindelijke tekst.
Waarschuwingen bij invoer die veel voorkomt
EntryWarnings bepaalt bij toevoegen en bewerken, met een injecteerbare WordKnowledge. Standaard: de Engelse woordenlijst van NaturalLanguage (alleen gewone woorden; "anthropic" en "vercel" staan er niet in, "cloud" en "reef" wel), de Nederlandse spellingcontrole van macOS, en een meegeleverde lijst van de meest voorkomende functiewoorden in beide talen (circa 170):
| Niveau | Wanneer |
|---|---|
| Waarschuwing | De bron is één woord dat in nl of en bestaat ("cloud" → wordt overal vervangen) |
| Waarschuwing | De bron is korter dan 3 letters |
| Waarschuwing | Een deel van een meerdelige bron is een veelvoorkomend functiewoord ("in de") |
| Waarschuwing | De aaneengeplakte vorm is zelf een bestaand woord ("some thing" → "something") |
| Let op | Alle delen zijn gewone woorden: de correctie werkt alleen op de hele combinatie (jouw voorbeeld "cloud code") |
| Fout | Dubbele bron (de eerste telt) of een lege kant |
Een waarschuwing blokkeert het opslaan niet, maar toont een amber lampje op de rij en de reden in de editor.
Zichtbaar in de geschiedenis
Elke opname bewaart rawText, finalText en corrections. De rij toont de markeringen en de samenvatting. Is er geen correctie gedaan, dan staat er niets extra. Zo zie je direct of het woordenboek iets doet.
Geschiedenis
HistoryStore bewaart ~/Library/Application Support/Sst/history.json:
{ "version": 1, "entries": [
{ "id": "UUID", "date": "ISO-8601", "duration": 8.4,
"rawText": "…", "finalText": "…",
"corrections": [ { "heard": "cloud code", "wrote": "Claude Code", "rule": "cloud code", "start": 12, "length": 11 } ] } ] }
Atomisch schrijven, met korte debounce; geheugen-eerst. Meer dan 10 000 regels is geen doel van deze versie. Onleesbaar bestand: hernoemen naar history.corrupt-<datum>.json en vers beginnen, met een melding. Staat bewaren uit, dan wordt niets geschreven en toont het hoofdvenster een regel dat de geschiedenis uit staat.
Ontwerpsysteem in code
Design/build_tokens.py krijgt een Swift-uitvoer (--swift) die Sources/SstUI/DesignTokens.swift genereert uit Design/tokens.json:
- Kleuren als dynamische
NSColor/Colormet aluminium- en graphite-waarden, schaduwen als lagenlijsten, afmetingen, typografie en beweging als constanten. - Componenten gebruiken uitsluitend deze constanten. Een test zoekt in
Sources/SstUIenSources/Sstnaar losse hex-kleuren en vaste puntafmetingen buiten de gegenereerde file en faalt als ze er zijn. - Een test controleert dat het gegenereerde bestand overeenkomt met een verse generatie (geen drift) en dat de contrastcontrole slaagt.
- Componenten:
KeyButtonStyle(rust, ingedrukt, REC),LampView,PanelBackground,WellBackground,SeamDivider,SilkLabel,VUMeterView,SegmentCounterView,LEDBarView,PaperList. De geborstelde textuur wordt één keer gegenereerd en gecachet.
Toegankelijkheid
- Alle bediening bereikbaar met het toetsenbord; zichtbare focusring (
border.focus,line.focus). - VoiceOver: toetsen en lampjes hebben labels ("Opname starten", "Microfoon: toegestaan"); de VU-meter meldt zijn waarde ("niveau min 12 VU") met 2 Hz, niet met 60 Hz.
- "Verminder beweging" wordt gerespecteerd (zie
motion.reducedMotion). - Alle tekstparen halen ≥ 4,5:1 (gecontroleerd door het tokenscript).
Foutafhandeling
| Situatie | Gedrag |
|---|---|
| Model niet gedownload / laden mislukt | Controle-lampje uit, REC uitgeschakeld, melding met knop "Opnieuw proberen"; vorige model blijft bij wisselen actief |
| Geen netwerk bij eerste download | Duidelijke melding; geen herhaalde verzoeken op de achtergrond |
| Rechten ontbreken | Lampje en knop in CONTROLES; hotkey-opnames geven een korte fout |
| Plakken mislukt | Tekst blijft op het klembord en staat wel in de geschiedenis |
| Woordenboekbestand ontbreekt | Wordt aangemaakt met een korte kop; leeg is geldig |
| Woordenboekregel onleesbaar | Regel blijft behouden, krijgt een waarschuwing |
| Geschiedenisbestand beschadigd | Hernoemen, vers beginnen, melding |
Testen
- Unit (zonder UI):
DictionaryFile(ronde parse → schrijf met opmerkingen en volgorde),CorrectionEngine(spatie/koppelteken/aaneengeplakt, "Cloudflare" en "cloud codex" blijven onaangeroerd, langste eerst, geen ketens, hoofdletters, Unicode,$/\letterlijk),EntryWarningsmet een valseWordKnowledge,VocabularyBuilder(limieten, volgorde), stilte-bias-regels,HistoryStore(lezen, schrijven, beschadigd),HistorySearch(diakrieten, meerdere woorden), VU-afbeelding (dBFS → VU → hoek),ModifierKeyTrackermet meerdere bindingen, en de engine-raakpunten met nep-onderdelen. Het bestaande 13-testen-pakket blijft groen. - Tokens: parity- en contrasttest (zie "Ontwerpsysteem in code").
- Handmatig (met de gebruiker, zoals bij het skeleton): Dock-icoon en app-menu; venster vrij schalen; ⌘, ; sluiten en weer openen via het Dock; dicteren in TextEdit, browser en terwijl Sst achter staat; dicteren terwijl Sst voorop staat (alleen opslaan); woordenboek: term, correctiepaar, "cloud-code"-variaties, "Cloudflare" blijft; waarschuwing bij "cloud"; bestand in een editor wijzigen en zien herladen; model wisselen; rechten intrekken en terugzien; VoiceOver kort; "Verminder beweging".
Bouwvolgorde
Elke fase levert op zichzelf testbare software en krijgt een eigen plan:
- Engine-raakpunten (SstCore/SstWhisper) met tests.
- SstData: woordenboek, correcties, waarschuwingen, voorrang, geschiedenis, zoeken, met tests.
- SstUI: tokens genereren in Swift, componenten, parity- en contrasttest, een klein ontwikkelvenster om componenten te bekijken.
- App-shell: scenes, menu's, menubalkitem, Instellingen (sneltoets en model), bundelscript, ondertekening, icoon.
- Hoofdvenster: deck met meter, geschiedenis, woordenboekweergave, bedrading met
AppModel; handmatige rooktest.
Open punten (graag bevestigen)
- De vier engine-raakpunten, in het bijzonder dat
Transcribereen woordenlijst accepteert. - "Kopie van elk document" als kopieerknop per transcriptie.
- Nederlandse UI-teksten met macOS-systeemmenu's in de taal van het systeem.
- De vaste lijst toegestane sneltoetsen en alleen vasthouden.
- Dat Sst niet plakt wanneer Sst zelf voorop staat of de opname via het venster start.
- De keuze om geschiedenis standaard aan te zetten.