Mochi: ett litet du som för dina anteckningar
En macOS-app som gör det du säger till anteckningar i ditt Obsidian-valv. Igenkänning och talarseparation körs på Macen, så ingen ljudfil lämnar den och ingen minut kostar något.

- 0
- byte ljud uppladdat
- 3
- igenkänningsnivåer på enheten
- 4
- talare separerade per samtal
Sammanhanget
Mochi är en macOS-app som sätter sig vid kanten av skärmen och skriver ner det du säger. Tryck Alt och mellanslag, prata, och en anteckning landar i ditt Obsidian-valv innan du har hunnit tänka färdigt.
Den ser ut som du. Du laddar upp ett foto under installationen och Mochi ritar en liten karikatyr av dig: stort huvud, stora ögon, korta armar och ben. Den figuren är gränssnittet. Den sover medan du jobbar och vaknar när du pratar.
Igenkänning och talarseparation körs på Macen. Inget ljud laddas upp, och ingen transkriberad minut kostar något.
Problemet
En röstanteckning är lätt att spela in och blir nästan aldrig läst. Inspelningen är slutet på processen snarare än början, så den ligger kvar i en mapp med en tidsstämpel som namn och ingen öppnar den igen.
Dikteringsverktyg löser hälften av det genom att producera text, och räcker sedan texten till dig i stället för till platsen där dina anteckningar faktiskt bor. Verktyg för mötestranskribering löser en annan hälft genom att ladda upp hela samtalet till någons server, vilket är ett rimligt byte för ett säljteam och ett dåligt byte för ett privat samtal.
Båda lämnar samma hål. Det jag ville ha var inte en transkription. Det var att meningen jag sa högt redan skulle ligga i valvet, i mappen jag hade tittat i, utan att jag hade bestämt mig för att gå och lägga den där.
Min roll
Allt. Nuits är en person.
Swift-appen, igenkänningskedjan, valvskrivningen, Worker-backenden, webbplatsen, varumärket, maskoten, prissättningen, de juridiska sidorna och integritetslinjen. Det finns ingen att lämna över den obekväma avvägningen till, vilket framför allt syns på de ställen där det ärliga svaret kostar en funktion.
Angreppssättet
Jag bestämde skrivordningen före allt annat, och den avgjorde varenda diskussion som kom sedan. Den ordagranna transkriptionen går ner i valvet först, alltid, innan någon språkmodell över huvud taget anropas. Den städade versionen läggs till i samma anteckning efteråt, som ett andra block.
Den ordningen är produkten. Om nätet ligger nere, om nyckeln är fel, om leverantören har en dålig eftermiddag, har du fortfarande dina ord. Ett modellfel försämrar anteckningen; det kan aldrig förlora den. Allt annat följer av att den skrivningen kommer först. Igenkänningen måste vara lokal, eftersom en molnigenkännare gör den första skrivningen beroende av nätet. Modellen måste vara din, eftersom den andra skrivningen är valfri och ska faktureras den som valde den.
Maskoten är samma beslut fast på känslonivå. En ikon i menyraden är ett verktyg, och ingen skulle märka att den försvann. En liten figur som ser ut som du och reagerar inom en tiondels sekund efter tangenttryckningen är något du skulle sakna. Det var värt att bygga ett animationssystem för.
Bygget
Native Swift. SwiftUI för panelerna, AppKit för överlägget, eftersom ett fönster som svävar ovanpå allt, ignorerar klick på begäran och beter sig korrekt över Spaces och flera skärmar inte är något SwiftUI vill ge dig. Figuren är komponerad av vektorsprites med keyframe- och fjäderanimation, inte en spelmotor.
Igenkänningen är Parakeet via FluidAudio, exporterad till CoreML i tre nivåer: 227,5 MB för engelska, 335,7 MB för 25 språk, och 483,1 MB för brytningar och stökiga rum. De siffrorna är uppmätta, inte ihågkomna. Ett skript läser modellarkivets filträd, räknar bara de exakta filer som ljud-SDK:t faktiskt hämtar, och genererar en Swift-fil som korten i onboardingen läser ifrån. Ingen kan skriva in en storlek för hand i gränssnittet. Ett tidigare utkast av specifikationen bar uppskattningar som var ungefär dubbelt så fördelaktiga som sanningen, på varenda rad, vilket är exakt den riktning uppskattningar glider åt, och skriptet finns för att det inte ska kunna hända igen.
Det viktigaste jag hittade var en tystnadsbugg, och den ändrade arkitekturen. Ett avkodningsfönster som slutar inne i en tystnad returnerar en tom sträng, utan fel och med hög rapporterad tillförlitlighet. Tolv sekunder tal transkriberas korrekt. Samma tolv sekunder med tre sekunder tystnad på slutet transkriberas till ingenting alls. För en produkt vars hela löfte är den ordagranna uppteckningen är ett fel som ser ut som en framgång det värsta som finns. Alltså når tystnad aldrig igenkännaren. Silero VAD delar först upp inspelningen i talregioner, varje region transkriberas för sig, och korta breddas med omgivande ljud i stället för att fyllas ut med nollor, eftersom påhängd tystnad är precis det som utlöser felet. Modellen för talaktivitet är alltid installerad och aldrig valfri, och det är ett korrekthetsbeslut snarare än ett prestandabeslut.
Mötesläget spelar in systemljud och mikrofon på separata spår, så att din egen röst aldrig är något programmet måste gissa sig till. Strömmande diarisering separerar upp till fyra talare live. Att känna igen samma person mellan två olika möten kräver en helt annan motor, vilket tog mig för lång tid att acceptera: en strömmande diariserare sparar inga embeddingar mellan sessioner, så talare ett i dag och talare ett nästa vecka är obesläktade röster, och ingen finjustering ändrar det. Nu körs ett andra pass efter samtalet. Det tar åtta till femton sekunder ur varje rösts längsta inlägg, extraherar en 256-dimensionell embedding och matchar den mot profilerna som ligger på Macen med cosinuslikhet. Tröskeln ligger medvetet på den försiktiga sidan, eftersom en missad matchning kostar en omdöpning medan en falsk matchning sätter en kollegas namn på en främlings ord. Tilldelningen avgörs för alla röster på en gång, så i ett samtal mellan två personer kan den första talaren inte lägga beslag på en profil som den andra matchade bättre.
Valvskrivningen är den enda delen med en riktig testsvit: 29 fall över markdown-byggandet och beteendet på disk. Den skriver atomärt, lägger till i stället för att ersätta, och skriver aldrig om en anteckning du har redigerat, eftersom ett valv som synkas av iCloud straffar alla tre misstagen med konfliktfiler.
Språkmodellerna är dina. Ett register, en OpenAI-kompatibel begäransform, leverantörsspecifika undantag där formen skiljer sig, över OpenRouter, Anthropic, Gemini, DeepSeek och andra. Du klistrar in en nyckel, den hamnar i nyckelringen, och den rör aldrig min backend. Inferens är därmed aldrig en kostnad för verksamheten, och det är det som låter ett enda paket på 12 dollar innehålla obegränsad transkribering utan asterisk.
Figurgenereringen är det enda som kostar mig pengar per användare, och den blev projektets mest intressanta ingenjörsarbete. Den går i två steg mot en enda leverantör via OpenRouter. Steg ett gör ditt foto till fyra kandidatfigurer för ungefär tretton cent styck, och du väljer en. Steg två ritar den figurens nio tillståndsbilder som äkta SVG för ungefär åtta cent styck. Valet mellan fyra är en medveten kostnad: tre av teckningarna kastas, och alternativet är att tvinga någon att acceptera det första en modell ritade av deras eget ansikte.
Vektormodellen erbjuder ingen parameter för transparens, vilket borde ha inneburit att också betala en tjänst för bakgrundsborttagning. I stället tvingar varje anrop fram en plan känd bakgrundsfärg, och den enda rektangeln som täcker hela ytan tas bort ur den returnerade SVG:n efteråt. Borttagningen tittar bara på rotens direkta barn, så en rektangel inuti en klippbana förväxlas aldrig med bakgrunden. En leverantör, ingen urklippstjänst, äkta transparens.
Den slutpunkten lärde mig det mesta jag vet om att verifiera mot resultatet i stället för mot statuskoden. Bildmodellerna saknas helt i huvudlistan och dyker bara upp i en separat. En formatparameter som är dokumenterad i begäransschemat väljer ingenting, eftersom vektorutdata väljs genom att ta en vektormodell och aldrig genom att fylla i fältet. Och okända leverantörsnycklar tappas tyst i stället för att avvisas, så ett felstavat fältnamn ger ett fullständigt lyckat svar utan att en enda kontroll har tillämpats. Leverantören är explicit fastnålad på det anrop som bär fotot, och det är regelefterlevnad snarare än routinghygien: bildleverantören är den som står i underbiträdeslistan, och en tyst reservväg skulle skicka ett foto av någons ansikte till ett företag som inte står där.
Clerk sköter inloggningen via sitt hostade macOS-flöde. Polar är merchant of record, så global moms är någon annans jobb, och det utfärdar licensnyckeln med en aktiveringsgräns som låter dig byta Mac utan supportärende. Sparkle levererar uppdateringar, och startar aldrig om mitt i en inspelning. Webbplatsen är Astro 7 på Cloudflare Workers, med API:t och D1 bredvid.
Det finns ingen kraschrapportering, ingen analys och ingen telemetri i appen. Det är ett beslut och inte en glömska, och det är det beslut jag räknar med att ångra operativt och inte på något annat sätt. Mochi tillbringar sina dagar med en öppen mikrofon, en pågående mötesinspelning och texten till det som just sades. En kraschrapportör inuti den processen laddar upp anropsstackar, trådnamn och, en dålig dag, det som råkade ligga i en buffert när processen dog. Man kan inte lova att inspelningar aldrig lämnar Macen och samtidigt leverera en agent vars uppgift är att skicka mig data från en process som hade en inspelning öppen. Det jag får tillbaka är en mening som inte behöver någon asterisk någonstans på webbplatsen, i appen eller i integritetspolicyn. Det den kostar är att ingenting kommer in av sig självt: en bugg är aldrig mer synlig än den rapport någon väljer att skicka. Motvikten är en knapp som kopierar de senaste femton minuterna av appens egen logg, dess version och macOS-bygget till urklipp, och rör inget annat. Du läser det, sedan bestämmer du.
Utfallet
Inte lanserad. Webbplatsen är uppe på mochiformac.com och samlar en väntelista, och nedladdningssidan säger rakt ut att det inte finns något bygge att ge dig i dag och att jag inte tänker gissa ett datum.
Vägen från inspelning till valv fungerar hela vägen, vilket var det jag insisterade på att göra klart innan jag rörde inloggning, betalning eller figurgenerering.
Vad jag skulle göra annorlunda
Jag skrev in modellstorlekarna i specifikationen ur minnet och varenda en var fel med ungefär en faktor två, alltid till min fördel. Nu mäter ett skript dem och gränssnittet kan bara visa det skriptet skrev. Den allmänna versionen av den läxan blev dyr: varje siffra en användare kommer att läsa borde genereras, aldrig skrivas för hand.
Jag byggde också den strömmande diariseraren först och antog att igenkänning av en återkommande röst var ett finjusteringsproblem ovanpå den. Det är ett annat problem som kräver en annan modell, och jag förlorade riktiga veckor på att försöka få ut embeddingar ur något som inte har några.




