Een AI-app zonder server, zonder account, en niets verlaat de telefoon
Een zaterdag in de gezinsagenda: „Piscine 10h avec Tom“. Diezelfde avond krijgt het kind een Frans dictee over het zwembaduitje met Tom, geschreven op zijn niveau, dat de congruentieregel oefent die het vorige week fout had.
Niets van die zaterdag verlaat de telefoon. Niet het agenda-item, niet de voornaam van het kind, niet zijn fouten. Er is geen account aan te maken, geen server te bellen, geen statistiek. Zet het toestel in vliegtuigmodus en de app doet precies wat hij een minuut eerder deed.
De app heet Dictée Ludique. Ik heb hem geschreven, hij staat in de App Store, en hij is het duidelijkste antwoord dat ik heb op een vraag die klanten me nu bijna elke week stellen: kunnen we het AI-deel doen zonder de gegevens van onze gebruikers ergens heen te sturen?
Op Apple-hardware is het antwoord ja, en het bestaat vandaag. De rest hiervan gaat over wat daarvoor nodig was, wat het me kostte, en wat iOS 27 in september verandert.
Waarom niet gewoon een chatbot vragen
Dat is de voor de hand liggende vraag, en de hele app is eruit ontstaan.
Ik begon zoals iedereen. Ik vroeg een chatbot een dictee te schrijven voor een kind van negen, en hij schreef er een. Zelfs een behoorlijke.
Toen deed ik het de week erna opnieuw en zag ik wat ik echt had. Een chatbot geeft je een tekst. Hij geeft je geen systeem. Hij weet niet dat dit kind de congruentie van het voltooid deelwoord in november beheerste en die in januari kwijt was, dat homofonen nog geen enkele keer zijn getoetst, of dat de laatste drie teksten allemaal over dezelfde vakantie gingen. Ik droeg dat allemaal zelf en typte het opnieuw in elke prompt. Het schrijven was gratis. De boekhouding was het hele werk.
Wat ik wilde was de machinerie rond het schrijven. Vijftien spellingvaardigheden uit het leerplan, per kind bijgehouden, elk op beheerst, in ontwikkeling, wankel of nog niet gezien. Een correctiestap na elk dictee die die toestanden verzet en de ouder ongeveer vijftien seconden kost. Daarna een volgende tekst die eerst mikt op wat wankel is en daarna op wat nog niet langskwam, met lengte en dichtheid die het laatste resultaat volgen, en zonder één instelling die iemand moet configureren.
Het moest ook als een spel voelen, dat is de andere helft van de naam. Geen punten en badges. Er is bewust nergens in de app een totaalscore, want een gemiddelde over vijftien vaardigheden betekent niets en zou het enige cijfer zijn dat een kind onthoudt. Het spel is dat het kind drie kaarten krijgt en er één kiest, en dat de tekst over zijn eigen zaterdag gaat in plaats van over die van een vreemde. Een kind schrijft anders wanneer het dictee over zijn verjaardag gaat en niet over de kat van de buren.
De generatie bleek het kleine deel. De lus eromheen is de app, en die heeft alles hieronder bepaald.
Waarom de cloud nooit ter sprake kwam
Er is een regelgevende reden, en er is de reden die ik echt had.
De regelgevende is gewoon. Dit is een kinder-app die in Europa wordt verkocht, dus de vragen over gegevensbescherming komen met de eerste ouder die het privacyscherm opent, niet na een investeringsronde. Niets verzamelen betekent niets aan te geven, niets te lekken en niets uit te leggen.
De reden die het echt besliste: de app leest een gezinsagenda. Dat is het meest intieme bestand op een telefoon. Er staan ziekenhuisafspraken in, overdrachten bij co-ouderschap, sollicitatiegesprekken, de week waarin iemand het huis uit ging. Ik ging daar geen leiding vanaf bouwen naar een datacenter dat niet van mij is, en geen enkel privacybeleid dat ik had kunnen schrijven zou me daar rustig bij hebben gemaakt.
De beperking kwam dus eerst, en de architectuur moest ermee leven.
De agenda wordt gefilterd voordat het model hem ziet
Dit is het deel waarvan iedereen aanneemt dat de AI het doet. Dat doet hij niet.
Een ruwe titel van een afspraak bereikt het model nooit. Wat het model bereikt is een canonieke naamwoordgroep uit een gesloten lijst van drieënzeventig herkende vormen. „Piscine 10h avec Tom“ komt eruit als „la sortie piscine“. „Anniv de Tom“ komt eruit als „l’anniversaire de Tom“, en die voornaam, gevangen door een bewust smal patroon, is de enige vrije tekst in de hele keten.
Er wordt vergeleken op woordgrenzen, wat pietluttig klinkt tot je ziet wat het voorkomt. „ski“ mag niet aanslaan op „Kandinsky“. „colo“, de Franse afkorting voor een zomerkamp, mag niet aanslaan op „coloscopie“. De langste vorm wint, dus „sortie scolaire“ verslaat het kale „sortie“.
Een tweede lijst van tweeënzestig woorden diskwalificeert de hele afspraak meteen: ziekenhuis, begrafenis, scheiding, werkloosheid, gevangenis, en de rest van de registers waar een gezinsagenda vol mee staat. „Anniversaire de mamie à l’hôpital“ is geen bruikbare verjaardag, ook al staat het woord verjaardag er gewoon.
Wat de lijst niet herkent houdt helemaal geen naam. Het komt op het ouderscherm als een suggestie zonder etiket, en het kan niet worden gebruikt tot de ouder er zelf een naam voor typt. Eenmaal geweigerd wordt het nooit meer aangeboden.
Het leesvenster is met opzet asymmetrisch: twee jaar terug voor eenmalige afspraken, twee weken vooruit, en maar één week terug voor alles wat zich herhaalt. Een kind herinnert zich een reis jarenlang. Een judoles van achttien maanden geleden is ruis.
Dit alles is gewone Swift. Het is deterministisch, het wordt gedekt door tests, en het is het deel dat het eigenlijke privacywerk doet. Het model is de laatste stap, niet de eerste.
Het model schrijft de tekst en verder niets
De generatie draait op het model van Apple op het toestel via het Foundation Models framework, dat met iOS 26 kwam. De uitvoer is gestuurd: een @Generable-structuur met een korte titel en een array van zinnen, afgetopt op twaalf.
Zinnen als array in plaats van één blok proza, want het model houdt een doellengte veel beter vast wanneer het de zinnen zelf telt.
Aan het model wordt nooit gevraagd waar de spellingvallen zitten. Dat is het werk van een deterministische analyse met eigen tests. Een model dat zijn eigen uitvoer annoteert zit er af en toe naast zonder dat iemand het merkt, en dat is erger dan er luidruchtig naast zitten.
Het ziet ook geen sjabloon. De met de hand geschreven bibliotheek moet elk kind bedienen, dus draagt zij plaatshouders en een congruentiemechaniek. Hier kent de app de voornaam en het grammaticale geslacht al, dus vraagt zij om „Léa est partie“ in plaats van „{ENFANT} est parti{E_ENFANT}“. Een hele klasse fouten verdwijnt.
Geen gegenereerde tekst krijgt een vrijbrief
Elke gegenereerde tekst doorloopt dezelfde validatie in zeven stappen als de met de hand geschreven bibliotheek: woordenschat afgestemd op het schoolniveau, toegestane werkwoordstijden, zinslengte, verboden registers, de vaardigheden die het kind moet oefenen, zijn eigen voornaam, en het agendamoment dat echt genoemd wordt.
Een gegenereerde tekst heeft geen voorrecht boven een tekst die een mens heeft nagelezen. Hij heeft er zelfs iets minder, omdat hij een plafond nodig had dat de bibliotheek nooit nodig had.
Wanneer vijf pogingen mislukken, serveert de app een tekst uit de bibliotheek en zegt niets. Het kind komt nooit te weten dat het model een slechte minuut had.
Wat het model op het toestel me echt kostte
Dit deel staat in geen enkele WWDC-sessie, dus hier zijn de cijfers uit de logs van mijn eigen toestel.
- Eén generatie liep 48,9 seconden weg voordat zij stierf op „Exceeded model context window size“. De vier andere pogingen, met identieke instructies, kwamen terug in 2 tot 3,5 seconden. De invoer was nooit het probleem. De uitvoer hield niet op. Daar komt het plafond van twaalf zinnen vandaan.
- De eerste versie haalt geregeld een derde van de gevraagde lengte. Gemeten in één sessie: 29 woorden, dan 184, dan 34, bij een instructie die om 55 tot 75 vroeg. De temperatuur verhogen schudt alleen het toeval op. Het model in helder Frans vertellen wat het net miste, werkt wel. De herhaallus stuurt daarom nu een zin terug die het verschil uitlegt, in plaats van alleen aan de temperatuur te draaien.
- Het model produceerde teksten die elke controle doorstonden en voor de lezer toch verkeerd waren. Elf zinnen voor een kind van negen, het aantal woorden binnen bereik, elke zin onder het maximum, en het kind krijgt een telegram. Een mens had elk sjabloon in de bibliotheek geschreven, dus daar bestond dat gebrek niet. Het model doet het met opvallende regelmaat, vandaar het plafond op het aantal zinnen.
- De beveiligingen van Apple slaan aan zonder te zeggen waarom. De fout luidt „Detected content likely to be unsafe“ en verder niets. Vijf herhalingen bij oplopende temperaturen met dezelfde invoer mislukken vijf keer op dezelfde manier. Wat de app nu doet is dingen op volgorde weghalen: eerst het agendamoment, omdat dat de enige inhoud is die de app niet zelf schreef, daarna de inhoudsregel in de instructies. De trede die deblokkeert ís de diagnose, en die gaat als zodanig het log in.
- EventKit kostte me een middag, om redenen die niets met AI te maken hadden. Een EKEventStore die vóór de toestemming wordt aangemaakt houdt een lege cache voor de duur van het proces, dus de agendabevraging levert nul afspraken op, ook nadat de ouder op Sta toe heeft getikt. En authorizationStatus(for:) loopt achter op het verlenen: op iPad kwam de aanvraag terug met true terwijl de status, dertien milliseconden later gelezen, nog notDetermined zei. Beide zien er precies uit als een rechtenbug en geen van beide is er een.
Nog één beperking heeft de app meer gevormd dan alle andere. Vijf pogingen is het plafond wanneer een kind naar een scherm „we schrijven je dictee“ kijkt. De app genereert daarom vooruit. Een dictee hardop voorlezen duurt minuten, en de generator werkt in dat venster, afgetopt op tien ronden per sessie, gekoppeld aan het leesscherm en afgebroken bij het sluiten. Niets ongevalideerds komt ooit in de cache. Het koopt tijd, geen soepelheid.
Wat iOS 27 verandert
Het grootste deel van de lijst hierboven wordt opgelost door de versie die in september uitkomt, wat vreemd is om te schrijven over een framework dat één jaar oud is.
- Het model op het toestel is opnieuw gebouwd, met betere logica, beter aanroepen van gereedschappen, en beveiligingen die volgens Apple minder valse alarmen geven. Dat raakt direct mijn duurste faalwijze. Beter opvolgen van instructies zou ook het aantal herhalingen moeten verlagen, en dat is het verschil tussen vooruit genereren en op verzoek genereren.
- response.usage meldt nu de tokens voor invoer, uitvoer, cache en redeneren, model.contextSize toont het venster, en tokenCount(for:) telt een prompt vóór het versturen. Mijn uitschieter van 48,9 seconden wordt daarmee iets dat ik zie aankomen in plaats van iets dat ik achteraf in een log terugvind.
- Het model op het toestel accepteert afbeeldingen, en het framework levert een OCRTool op het toestel, gedragen door Vision. Daar denk ik steeds aan. De beschrijving in de App Store belooft vandaag dat de app de bladzijde niet fotografeert, omdat handschriftherkenning fouten verzint die het kind nooit maakte, en een spelling-app die een fout hallucineert is erger dan geen app. Privacy was daar nooit de enige reden om nee te zeggen. Maar het volledig op het toestel kunnen proberen, zonder enige upload, is nieuw.
- Het LanguageModel-protocol laat elk model een sessie dragen. De app had die vorm al: één loket beslist tijdens de uitvoering of het framework op dit systeem bestaat, en daardoor bedient één binary zowel macOS 15 zonder Apple Intelligence als macOS 26 mét. Die indirectie hoort nu bij het besturingssysteem in plaats van bij mij.
- DynamicProfile laat één sessie midden in een gesprek van instructies en gereedschappen wisselen. Een dictee schrijven en een dictee corrigeren zijn in deze app twee verschillende taken, en vandaag twee verschillende sessies.
Het gratis cloudmodel van Apple, standaard uitgeschakeld
Dezelfde versie maakt het heel makkelijk om het toestel te verlaten.
PrivateCloudComputeLanguageModel geeft een contextvenster van 32.000 tokens en een instelling voor redeneren, zonder API-sleutel, zonder account en zonder cloudrekening voor kleine ontwikkelaars onder een drempel van twee miljoen eerste downloads. Anthropic en Google publiceren Swift-pakketten die op hetzelfde sessietype aansluiten. Deze app omzetten is ongeveer één regel, en de tekst zou langer terugkomen, beter gestructureerd, en veel vaker al bij de eerste poging geldig.
Er is een echt argument om het te nemen, en het is niet het argument dat ik aan het begin verwachtte. Het model op het toestel schrijft een goed dictee voor een kind van negen. Het wordt mager wanneer degene die vraagt een volwassene is die Frans leert en een langere tekst wil met bijzinnen en een bredere woordenschat, of een ouder die ambitieuzer wil dan het niveauschema voorstelt. Die lezers bestaan, ze gebruiken de app al, en het kleinste model op de telefoon schrijft niet echt voor hen.
In iOS 27 zal de app het daarom als instelling aanbieden, en die instelling wordt uitgeschakeld geleverd. Installeer de app, verander niets, en de belofte die hij vandaag doet blijft staan: er verlaat niets het toestel, en de vliegtuigmodus bewijst het in vier seconden.
Wat die optie überhaupt aanbiedbaar maakt is precies het werk hierboven. Ook met de schakelaar aan steekt een ruwe agendatitel nog steeds nooit het netwerk over. De witte lijst heeft hem allang teruggebracht tot een canonieke naamwoordgroep, dus wat reist is „la sortie piscine“ en een voornaam, nooit „RDV Dr Meunier, résultats“. Het filter is gebouwd om een kind tegen het model te beschermen. Het blijkt het gezin ook tegen het netwerk te beschermen.
Het privacyscherm groeit met één zin, en dat is het deel dat ik goed wil doen. Eén regel die zegt dat er niets van het toestel af gaat, wordt er twee: er gaat niets van het toestel af, tenzij u rijkere teksten aanzet, en hier staat precies wat er dan naar buiten gaat. De privécloud van Apple is een goede belofte en ik geloof hem. Het blijft een belofte, en een belofte hoort achter een schakelaar die een ouder bewust heeft omgezet, niet in een standaardinstelling die zij nooit hebben gelezen.
Het toestel blijft ook om een minder verheven reden de standaard. Een dictee in de trein, in de auto, in een vakantiehuis met slechte wifi, is precies het moment waarop een gezin naar deze app grijpt. Wat de instelling ook zegt, het moet blijven werken zonder enig netwerk, dus de motor op het toestel gaat nergens heen.
Als u dit in uw eigen app nodig hebt
Als u naar een app kijkt waarbij de gegevens het toestel niet mogen verlaten, en iemand heeft u verteld dat dit AI uitsluit, dan is dat advies een jaar oud. Zorg, onderwijs, HR, juridisch, alles waar kinderen bij betrokken zijn, alles waarbij een klantcontract het land noemt waar de gegevens moeten staan: de weg via het toestel bestaat en hij werkt nu.
De kanttekeningen, die u beter van mij hoort dan in maand twee ontdekt:
- Het model op het toestel is klein. Het schrijft een goede korte tekst. Het zal zich niet door een lang document heen redeneren. Pas de functie aan op het model in plaats van andersom.
- U gaat een validatielaag schrijven. Behandel het model als een snelle schrijver zonder oordeel en zet er deterministische code omheen. In die code woont het product eigenlijk.
- U hebt een terugval nodig voor het toestel dat helemaal geen Apple Intelligence heeft, en die moet onzichtbaar zijn. Hier staat een met de hand geschreven bibliotheek achter het model, en een kind kan niet zeggen welke van de twee de tekst van vandaag schreef.
- Toestel en cloud zijn geen keuze tussen twee producten. Zet het filteren vóór het model in plaats van erin, dan kunt u de cloud later als instelling aanbieden zonder één woord van uw privacyscherm te herschrijven.
- Niets hiervan is webtechnologie. Het is EventKit, een privacymanifest, gestuurde generatie, en gedrag van het platform dat zich alleen op echte hardware laat zien, één reden te meer waarom native een cross-platform wrapper verslaat wanneer het moeilijke deel het besturingssysteem is.
Het geheel, inclusief agendafilter, kostte dagen en geen maanden. Het grootste deel van die tijd ging naar de validatieketen en de faalpaden, niet naar de prompt.
Nodig: een app die niets naar buiten kan sturen?
Native iOS en Android, gebouwd rond AI op het toestel, EventKit en de platform-API’s die een webview niet bereikt. Een werkende app op uw eigen toestel aan het eind van dag één, vanaf 500 € excl. btw.
Mijn project bespreken