En AI-app utan server, utan konto, och inget lämnar telefonen
En lördag i familjekalendern: ”Piscine 10h avec Tom”. Samma kväll får barnet en fransk diktamen om simhallsutflykten med Tom, skriven på dess nivå, som tränar den kongruensregel det hade fel på förra veckan.
Inget från den lördagen lämnar telefonen. Inte kalenderposten, inte barnets förnamn, inte dess fel. Det finns inget konto att skapa, ingen server att ringa, ingen statistik. Sätt enheten i flygplansläge, så beter sig appen precis som en minut tidigare.
Appen heter Dictée Ludique. Jag har skrivit den, den finns i App Store, och den är det tydligaste svar jag har på en fråga som kunder nu ställer nästan varje vecka: kan vi göra AI-delen utan att skicka våra användares data någonstans?
På Apple-hårdvara är svaret ja, och det finns i dag. Resten här handlar om vad det krävdes, vad det kostade mig, och vad iOS 27 ändrar i september.
Varför inte bara fråga en chattbot
Det är den självklara frågan, och hela appen föddes ur den.
Jag började som vem som helst. Jag bad en chattbot skriva en diktamen åt ett nioårigt barn, och den skrev en. Till och med en anständig.
Sedan gjorde jag om det veckan därpå och såg vad jag faktiskt hade. En chattbot ger dig en text. Den ger dig inget system. Den vet inte att det här barnet satt säkert på participkongruens i november och hade tappat det i januari, att homofoner aldrig testats en enda gång, eller att de tre senaste texterna alla handlade om samma semester. Det var jag som bar allt det och skrev in det på nytt i varje prompt. Skrivandet var gratis. Bokföringen var hela arbetet.
Det jag ville ha var maskineriet runt skrivandet. Femton stavningsfärdigheter ur läroplanen, följda barn för barn, var och en satt på behärskad, på gång, skör eller inte sedd än. Ett rättningssteg efter varje diktamen som flyttar de tillstånden och tar föräldern ungefär femton sekunder. Sedan en nästa text som först siktar på det sköra och därefter på det som ännu inte dykt upp, med längd och täthet som följer förra resultatet, och utan en enda inställning som någon behöver konfigurera.
Det skulle också kännas som ett spel, vilket är den andra halvan av namnet. Inte poäng och märken. Det finns medvetet ingen totalpoäng någonstans i appen, för ett genomsnitt över femton färdigheter betyder ingenting och skulle vara den enda siffra ett barn minns. Spelet är att barnet får tre kort och väljer ett, och att texten handlar om dess egen lördag i stället för en främlings. Ett barn skriver annorlunda när diktamen handlar om dess födelsedag i stället för grannens katt.
Genereringen visade sig vara den lilla delen. Slingan runt den är appen, och den har avgjort allt nedanför.
Varför molnet aldrig var aktuellt
Det finns ett regulatoriskt skäl, och så finns det skälet jag faktiskt hade.
Det regulatoriska är vardagligt. Det här är en barnapp som säljs i Europa, så dataskyddsfrågorna kommer med den första förälder som öppnar integritetsskärmen, inte efter en finansieringsrunda. Att samla in ingenting betyder inget att deklarera, inget att läcka och inget att förklara.
Det som verkligen avgjorde: appen läser en familjekalender. Det är den mest privata filen i en telefon. Där står sjukhusbesök, överlämningar vid delad vårdnad, anställningsintervjuer, veckan då någon flyttade ut. Jag tänkte inte bygga ett rör därifrån till ett datacenter jag inte äger, och ingen integritetspolicy jag hade kunnat skriva hade gjort mig lugn med det.
Begränsningen kom alltså först, och arkitekturen fick leva med den.
Kalendern filtreras innan modellen ser den
Det här är den del som alla antar att AI:n sköter. Det gör den inte.
En rå händelsetitel når aldrig modellen. Det som når den är en kanonisk nominalfras från en sluten lista med sjuttiotre igenkända former. ”Piscine 10h avec Tom” kommer ut som ”la sortie piscine”. ”Anniv de Tom” kommer ut som ”l’anniversaire de Tom”, och det förnamnet, fångat av ett medvetet smalt mönster, är den enda fria texten i hela kedjan.
Jämförelsen sker vid ordgränser, vilket låter petigt tills man ser vad det förhindrar. ”ski” får inte slå till på ”Kandinsky”. ”colo”, den franska förkortningen för ett sommarläger, får inte slå till på ”coloscopie”. Den längsta formen vinner, så ”sortie scolaire” slår det nakna ”sortie”.
En andra lista på sextiotvå ord diskvalificerar hela händelsen direkt: sjukhus, begravning, skilsmässa, arbetslöshet, fängelse, och resten av de register en familjekalender är full av. ”Anniversaire de mamie à l’hôpital” är ingen användbar födelsedag, även om ordet födelsedag står precis där.
Det listan inte känner igen behåller inget namn alls. Det landar på förälderskärmen som ett förslag utan etikett, och det kan inte användas förrän föräldern själv skriver in ett namn för det. Nekat en gång föreslås det aldrig igen.
Läsfönstret är asymmetriskt med flit: två år bakåt för engångshändelser, två veckor framåt, och bara en vecka bakåt för allt som upprepas. Ett barn minns en resa i åratal. En judolektion från arton månader sedan är brus.
Allt detta är vanlig Swift. Det är deterministiskt, det täcks av tester, och det är den del som gör det egentliga integritetsarbetet. Modellen är sista steget, inte det första.
Modellen skriver texten och inget annat
Genereringen körs på Apples modell i enheten via Foundation Models-ramverket, som kom med iOS 26. Utdata är styrd: en @Generable-struktur med en kort titel och en array av meningar, med tak på tolv.
Meningar som array i stället för ett block prosa, för modellen håller en mållängd betydligt bättre när den räknar meningarna själv.
Modellen får aldrig frågan var stavfällorna ligger. Det är arbetet för en deterministisk analys med egna tester. En modell som annoterar sin egen utdata har fel då och då utan att någon märker det, och det är värre än att ha fel högljutt.
Den ser inte heller någon mall. Det handskrivna biblioteket måste tjäna vilket barn som helst, så det bär platshållare och en kongruensmekanik. Här känner appen redan förnamnet och dess grammatiska genus, så den ber om ”Léa est partie” i stället för ”{ENFANT} est parti{E_ENFANT}”. En hel klass av fel försvinner.
Ingen genererad text får fribiljett
Varje genererad text går genom samma validering i sju steg som det handskrivna biblioteket: ordförråd avstämt mot årskursen, tillåtna tempus, meningslängd, förbjudna register, de färdigheter barnet ska träna, dess eget förnamn, och det kalenderögonblick som faktiskt nämns.
En genererad text har inget företräde framför en text en människa har granskat. Den har till och med lite mindre, eftersom den krävde ett tak som biblioteket aldrig behövde.
När fem försök misslyckas serverar appen en text ur biblioteket och säger ingenting. Barnet får aldrig veta att modellen hade en dålig minut.
Vad modellen i enheten faktiskt kostade mig
Den delen finns i ingen WWDC-session, så här är siffrorna ur loggarna på min egen enhet.
- En generering skenade i 48,9 sekunder innan den dog på ”Exceeded model context window size”. De fyra andra försöken, med identiska instruktioner, kom tillbaka på 2 till 3,5 sekunder. Indata var aldrig problemet. Utdata ville inte sluta. Därifrån kommer taket på tolv meningar.
- Första utkastet landar regelbundet på en tredjedel av begärd längd. Uppmätt under en session: 29 ord, sedan 184, sedan 34, mot en instruktion som bad om 55 till 75. Att höja temperaturen skakar bara om slumpen. Att säga till modellen på klar franska vad den just missade fungerar. Omtagningsslingan skickar därför nu tillbaka en mening som förklarar avvikelsen, i stället för att bara vrida på temperaturen.
- Modellen producerade texter som klarade varje kontroll och ändå var fel för läsaren. Elva meningar till ett nioårigt barn, ordantalet inom intervallet, varje mening under maxgränsen, och barnet får ett telegram. En människa hade skrivit varje mall i biblioteket, så det felet fanns aldrig där. Modellen gör det med anmärkningsvärd uthållighet, därav taket på antalet meningar.
- Apples skyddsräcken slår till utan att säga varför. Felet lyder ”Detected content likely to be unsafe” och inget mer. Fem omtagningar vid stigande temperaturer med samma indata misslyckas fem gånger på samma sätt. Det appen gör nu är att ta bort saker i tur och ordning: först kalenderögonblicket, eftersom det är det enda innehåll appen inte skrivit själv, sedan innehållsregeln i instruktionerna. Steget som låser upp är diagnosen, och det går in i loggen som en sådan.
- EventKit kostade mig en eftermiddag av skäl som inte hade med AI att göra. En EKEventStore som skapas före behörigheten behåller en tom cache under hela processens livstid, så kalenderläsningen ger noll händelser även efter att föräldern tryckt Tillåt. Och authorizationStatus(for:) släpar efter beviljandet: på iPad kom förfrågan tillbaka som true medan statusen, läst tretton millisekunder senare, fortfarande sa notDetermined. Båda ser precis ut som en behörighetsbugg och ingen av dem är det.
En sista begränsning har format appen mer än alla andra. Fem försök är taket när ett barn tittar på en skärm som säger ”vi skriver din diktamen”. Appen genererar därför i förväg. Att läsa en diktamen högt tar minuter, och generatorn arbetar i det fönstret, med tak på tio rundor per session, knuten till lässkärmen och avbruten när den stängs. Inget ovaliderat hamnar någonsin i cachen. Det köper tid, inte mildhet.
Vad iOS 27 ändrar
Merparten av listan ovan åtgärdas av versionen som släpps i september, vilket är märkligt att skriva om ett ramverk som är ett år gammalt.
- Modellen i enheten är ombyggd, med bättre logik, bättre verktygsanrop och skyddsräcken som enligt Apple ger färre falsklarm. Det träffar direkt mitt dyraste felläge. Bättre följsamhet mot instruktioner borde också sänka antalet omtagningar, och det är skillnaden mellan att generera i förväg och att generera på begäran.
- response.usage rapporterar nu tokens för indata, utdata, cache och resonemang, model.contextSize visar fönstret, och tokenCount(for:) räknar en prompt före sändning. Min skena på 48,9 sekunder blir därmed något jag kan se komma i stället för något jag hittar i en logg efteråt.
- Modellen i enheten tar emot bilder, och ramverket levererar ett OCRTool i enheten, buret av Vision. Det är det jag ständigt återkommer till. Beskrivningen i App Store lovar i dag att appen inte fotograferar sidan, eftersom handskriftstolkning hittar på fel barnet aldrig gjort, och en stavningsapp som hallucinerar ett fel är sämre än ingen app. Integriteten var aldrig det enda skälet att säga nej där. Men att kunna pröva det helt i enheten, utan någon uppladdning, är nytt.
- LanguageModel-protokollet låter vilken modell som helst bära en session. Appen hade redan den formen: en enda lucka avgör under körning om ramverket finns på det här systemet, och därför betjänar en enda binär både macOS 15 utan Apple Intelligence och macOS 26 med. Den omvägen tillhör nu operativsystemet i stället för mig.
- DynamicProfile låter en enda session byta instruktioner och verktyg mitt i samtalet. Att skriva en diktamen och att rätta en är två olika uppgifter i den här appen, och i dag två olika sessioner.
Apples gratis molnmodell, avstängd som standard
Samma version gör det mycket lätt att lämna enheten.
PrivateCloudComputeLanguageModel ger ett kontextfönster på 32 000 tokens och en inställning för resonemang, utan API-nyckel, utan konto och utan molnräkning för små utvecklare under en gräns på två miljoner förstagångsnedladdningar. Anthropic och Google publicerar Swift-paket som kopplas till samma sessionstyp. Att växla över den här appen är ungefär en rad, och texten skulle komma tillbaka längre, bättre byggd och betydligt oftare godkänd redan vid första försöket.
Det finns ett verkligt argument för att ta emot, och det är inte det jag väntade mig från början. Modellen i enheten skriver en bra diktamen åt ett nioårigt barn. Den blir tunn när den som frågar är en vuxen som lär sig franska och vill ha en längre text med bisatser och bredare ordförråd, eller en förälder som vill ha något mer ambitiöst än nivåtabellen föreslår. De läsarna finns, de använder redan appen, och den minsta modellen i telefonen skriver inte riktigt för dem.
I iOS 27 kommer appen därför att erbjuda det som en inställning, och den inställningen levereras avstängd. Installera appen, ändra ingenting, och löftet den ger i dag håller fortfarande: inget lämnar enheten, och flygplansläget bevisar det på fyra sekunder.
Det som över huvud taget gör alternativet möjligt att erbjuda är just arbetet längre upp. Även med reglaget på korsar en rå kalendertitel fortfarande aldrig nätverket. Vitlistan har för länge sedan reducerat den till en kanonisk nominalfras, så det som färdas är ”la sortie piscine” och ett förnamn, aldrig ”RDV Dr Meunier, résultats”. Filtret byggdes för att skydda ett barn från modellen. Det visar sig skydda familjen från nätverket också.
Integritetsskärmen växer med en mening, och det är den del jag vill få rätt. En rad som säger att inget lämnar enheten blir två: inget lämnar enheten, om du inte slår på rikare texter, och här står exakt vad som går ut då. Apples privata moln är ett bra löfte och jag tror på det. Det förblir ett löfte, och ett löfte hör hemma bakom ett reglage en förälder medvetet slagit på, inte i en standardinställning de aldrig läst.
Enheten förblir standard även av ett mindre glamoröst skäl. En diktamen på tåget, i bilen, i ett hyrt hus med dåligt wifi, är precis det tillfälle då en familj tar fram den här appen. Vad inställningen än säger måste det fortsätta fungera helt utan nätverk, så motorn i enheten går ingenstans.
Om du behöver det här i din egen app
Om du tittar på en app där data inte får lämna enheten, och någon har sagt att det utesluter AI, är det rådet ett år gammalt. Vård, utbildning, HR, juridik, allt som rör barn, allt där ett kundavtal namnger landet data måste ligga i: vägen via enheten finns och den fungerar nu.
Förbehållen, som du hellre hör från mig än upptäcker i månad två:
- Modellen i enheten är liten. Den skriver en bra kort text. Den resonerar sig inte genom ett långt dokument. Anpassa funktionen till modellen i stället för tvärtom.
- Du kommer att skriva ett valideringslager. Behandla modellen som en snabb skribent utan omdöme och lägg deterministisk kod runt den. Det är i den koden produkten faktiskt bor.
- Du behöver en reserv för enheten som inte har Apple Intelligence alls, och den måste vara osynlig. Här står ett handskrivet bibliotek bakom modellen, och ett barn kan inte säga vilken av de två som skrev dagens text.
- Enhet och moln är inget val mellan två produkter. Lägg filtreringen framför modellen i stället för inuti den, så kan du erbjuda molnet som en inställning senare utan att skriva om ett enda ord i din integritetsskärm.
- Inget av detta är webbteknik. Det är EventKit, ett integritetsmanifest, styrd generering och plattformsbeteende som bara visar sig på riktig hårdvara, ett skäl till varför native slår en cross-platform-wrapper när den svåra delen är operativsystemet.
Alltihop, kalenderfiltret inräknat, tog dagar och inte månader. Merparten av tiden gick till valideringskedjan och felvägarna, inte till prompten.
Behöver du en app som inte kan skicka något utåt?
Native iOS och Android, byggt kring AI i enheten, EventKit och de plattforms-API:er en webview inte når. En fungerande app på din egen enhet innan dag ett är slut, från 500 € exkl. moms.
Prata om mitt projekt