Tous les articles

Une app avec de l'IA, sans serveur, sans compte, et rien qui quitte le téléphone

· 10 min de lecture

Un samedi dans l'agenda familial : « Piscine 10h avec Tom ». Le soir même, l'app propose à l'enfant une dictée sur la sortie piscine avec Tom, écrite à son niveau, qui travaille la règle d'accord qu'il a ratée la semaine dernière.

Rien de ce samedi ne quitte le téléphone. Ni l'événement d'agenda, ni le prénom de l'enfant, ni ses fautes. Il n'y a pas de compte à créer, pas de serveur à appeler, pas de statistiques. Mettez l'appareil en mode avion : l'app se comporte exactement comme une minute plus tôt.

L'app s'appelle Dictée Ludique. Je l'ai écrite, elle est sur l'App Store, et c'est la réponse la plus claire que j'aie à une question que les clients me posent maintenant presque chaque semaine : peut-on faire la partie IA sans envoyer les données des utilisateurs où que ce soit ?

Sur du matériel Apple, oui, et c'est disponible aujourd'hui. La suite raconte ce qu'il a fallu, ce que ça m'a coûté, et ce que change iOS 27 en septembre.

Pourquoi ne pas simplement demander à un chatbot

C'est la question évidente, et c'est celle dont l'app est née.

J'ai commencé comme tout le monde. J'ai demandé à un chatbot d'écrire une dictée pour un enfant de neuf ans, et il en a écrit une. Correcte, même.

Puis j'ai recommencé la semaine suivante, et j'ai vu ce que j'avais vraiment. Un chatbot vous donne un texte. Il ne vous donne pas un système. Il ne sait pas que cet enfant tenait l'accord du participe passé en novembre et l'avait perdu en janvier, que les homophones n'ont jamais été testés une seule fois, ou que les trois derniers textes parlaient tous des mêmes vacances. C'était moi qui portais tout ça, et qui le retapais dans chaque prompt. L'écriture était gratuite. La comptabilité, c'était tout le travail.

Ce que je voulais, c'était la mécanique autour de l'écriture. Quinze compétences d'orthographe du programme scolaire, suivies enfant par enfant, chacune posée sur acquis, en cours, fragile ou pas encore vu. Une étape de correction après chaque dictée qui fait bouger ces états, et qui prend une quinzaine de secondes au parent. Puis un texte suivant qui vise d'abord ce qui est fragile, ensuite ce qui n'est pas encore venu, avec la longueur et la densité qui suivent le dernier résultat, et aucun réglage à faire pour personne.

Il fallait aussi que ce soit un jeu, ce qui est l'autre moitié du nom. Pas des points et des badges. Il n'y a volontairement aucun score global dans l'app, parce qu'une moyenne sur quinze compétences ne veut rien dire et serait le seul chiffre qu'un enfant retiendrait. Le jeu, c'est que l'enfant reçoit trois cartes et en choisit une, et que le texte parle de son samedi à lui plutôt que de celui d'un inconnu. Un enfant n'écrit pas pareil quand la dictée parle de son anniversaire plutôt que du chat du voisin.

La génération s'est révélée être la petite partie. C'est la boucle autour qui fait l'app, et c'est elle qui a décidé de tout ce qui suit.

Pourquoi le cloud n'a jamais été une option

Il y a une raison réglementaire, et il y a celle que j'avais vraiment.

La réglementaire est ordinaire. C'est une app pour enfants vendue en Europe, donc les questions de protection des données arrivent avec le premier parent qui ouvre l'écran de confidentialité, pas après une levée de fonds. Ne rien collecter, c'est n'avoir rien à déclarer, rien à faire fuiter et rien à expliquer.

Celle qui a vraiment tranché : l'app lit un agenda familial. C'est le fichier le plus intime d'un téléphone. Il contient des rendez-vous d'hôpital, des passages de garde, des entretiens d'embauche, la semaine où quelqu'un est parti de la maison. Je n'allais pas construire un tuyau de là vers un datacenter qui ne m'appartient pas, et aucune politique de confidentialité que j'aurais pu écrire ne m'aurait mis à l'aise avec ça.

La contrainte est donc venue en premier, et l'architecture a dû faire avec.

L'agenda est filtré avant que le modèle le voie

C'est la partie que tout le monde croit confiée à l'IA. Elle ne l'est pas.

Un titre d'événement brut n'atteint jamais le modèle. Ce qui l'atteint, c'est un groupe nominal canonique produit par une liste fermée de soixante-treize formes reconnues. « Piscine 10h avec Tom » ressort en « la sortie piscine ». « Anniv de Tom » ressort en « l'anniversaire de Tom », et ce prénom, capturé par un motif volontairement étroit, est le seul texte libre de toute la chaîne.

La comparaison se fait à la frontière de mot, ce qui a l'air pointilleux jusqu'à ce qu'on voie ce que ça évite. « ski » ne doit pas s'allumer sur « Kandinsky ». « colo » ne doit pas s'allumer sur « coloscopie ». La forme la plus longue gagne, donc « sortie scolaire » prime sur « sortie ».

Une deuxième liste de soixante-deux mots disqualifie l'événement entier à vue : hôpital, enterrement, divorce, chômage, prison, et le reste des registres dont un agenda familial est plein. « Anniversaire de mamie à l'hôpital » n'est pas un anniversaire exploitable, même si le mot anniversaire est bien là.

Ce que la liste ne reconnaît pas ne garde aucun nom. Ça arrive sur l'écran du parent comme une suggestion sans étiquette, et ça ne peut pas servir tant que le parent ne lui a pas donné un nom lui-même. Refusé une fois, ce n'est jamais reproposé.

La fenêtre de lecture est asymétrique exprès : deux ans en arrière pour les événements uniques, deux semaines devant, et une seule semaine en arrière pour tout ce qui se répète. Un enfant se souvient d'un voyage pendant des années. Un cours de judo d'il y a dix-huit mois, c'est du bruit.

Tout cela, c'est du Swift ordinaire. C'est déterministe, c'est couvert par des tests, et c'est la partie qui fait le vrai travail de confidentialité. Le modèle est la dernière étape, pas la première.

Le modèle écrit le texte, et rien d'autre

La génération tourne sur le modèle embarqué d'Apple via le framework Foundation Models, arrivé avec iOS 26. La sortie est guidée : une structure @Generable qui porte un titre court et un tableau de phrases, plafonné à douze.

Des phrases en tableau plutôt qu'un bloc de prose, parce que le modèle tient bien mieux une longueur visée quand il compte lui-même ses phrases.

On ne demande jamais au modèle où sont les pièges d'orthographe. C'est le travail d'un analyseur déterministe qui a ses propres tests. Un modèle qui annote sa propre sortie se trompe de temps en temps sans que personne le voie, ce qui est pire que de se tromper bruyamment.

Il ne voit pas non plus de canevas. La bibliothèque écrite à la main doit servir n'importe quel enfant, donc elle porte des emplacements et une mécanique d'accords. Ici l'app connaît déjà le prénom et son genre grammatical, donc elle demande « Léa est partie » au lieu de « {ENFANT} est parti{E_ENFANT} ». Toute une classe d'erreurs disparaît.

Aucun texte généré n'a de passe-droit

Chaque texte généré passe la même validation en sept étapes que la bibliothèque écrite à la main : vocabulaire calibré au niveau scolaire, temps autorisés, longueur des phrases, registres interdits, les compétences que l'enfant doit travailler, son propre prénom, et le moment d'agenda réellement évoqué.

Un texte généré n'a aucun privilège sur un texte relu par un humain. Il en a même un peu moins, parce qu'il a fallu lui ajouter un plafond dont la bibliothèque n'a jamais eu besoin.

Quand cinq tentatives échouent, l'app sert un texte de la bibliothèque et ne dit rien. L'enfant n'apprend jamais que le modèle a eu une mauvaise minute.

Ce que l'embarqué m'a vraiment coûté

Cette partie n'est dans aucune session de la WWDC, alors voici les chiffres relevés dans les journaux de mon propre appareil.

  • Une génération s'est emballée pendant 48,9 secondes avant de mourir sur « Exceeded model context window size ». Les quatre autres tentatives, à consignes identiques, sont revenues en 2 à 3,5 secondes. L'entrée n'a jamais été le problème. C'est la sortie qui ne s'arrêtait plus. Le plafond de douze phrases vient de là.
  • Le premier jet fait régulièrement le tiers de la longueur demandée. Relevé sur une session : 29 mots, puis 184, puis 34, pour une consigne qui en demandait 55 à 75. Monter la température ne fait qu'agiter le hasard. Dire au modèle, en français clair, ce qu'il vient de rater, ça marche. La boucle de reprise renvoie donc maintenant une phrase qui explique l'écart, au lieu de seulement pousser la température.
  • Le modèle a produit des textes qui passaient tous les contrôles et restaient mauvais pour le lecteur. Onze phrases pour un enfant de neuf ans, nombre de mots dans la fourchette, chaque phrase sous le maximum, et l'enfant reçoit un télégramme. Un humain avait écrit chaque canevas de la bibliothèque, donc ce défaut n'y existait pas. Le modèle le produit avec une belle constance, d'où le plafond sur le nombre de phrases.
  • Les garde-fous d'Apple se déclenchent sans dire pourquoi. L'erreur est « Detected content likely to be unsafe », et rien d'autre. Cinq reprises à températures croissantes avec les mêmes entrées échouent cinq fois de la même façon. Ce que l'app fait maintenant, c'est retirer les choses dans l'ordre : le moment d'agenda d'abord, puisque c'est le seul contenu que l'app n'a pas écrit elle-même, puis la règle de contenu dans les consignes. La marche qui débloque est le diagnostic, et elle part dans le journal comme tel.
  • EventKit m'a coûté un après-midi pour des raisons qui n'ont rien à voir avec l'IA. Un EKEventStore créé avant l'autorisation garde un cache vide pour toute la durée du processus, donc la lecture de l'agenda rend zéro événement même après que le parent a touché Autoriser. Et authorizationStatus(for:) est en retard sur l'accord : sur iPad, la demande est revenue à true alors que le statut, relu treize millisecondes plus tard, disait encore notDetermined. Les deux ressemblent exactement à un bug de permission, et aucun des deux n'en est un.

Une dernière contrainte a façonné l'app plus que toutes les autres. Cinq tentatives, c'est le plafond quand un enfant regarde un écran « on écrit ta dictée ». L'app génère donc à l'avance. Lire une dictée à voix haute prend des minutes, et le générateur travaille pendant cette fenêtre, plafonné à dix tours par session, rattaché à l'écran de lecture et annulé à sa fermeture. Rien de non validé n'entre jamais dans le cache. Ça achète du temps, pas de l'indulgence.

Ce que change iOS 27

L'essentiel de la liste ci-dessus est traité par la version qui sort en septembre, ce qui est étrange à écrire d'un framework qui a un an.

  • Le modèle embarqué a été reconstruit, avec une meilleure logique, un meilleur appel d'outils, et des garde-fous qui, selon Apple, produisent moins de faux positifs. Ça vise directement mon mode d'échec le plus cher. Un meilleur suivi des consignes devrait aussi réduire le nombre de reprises, ce qui fait la différence entre générer à l'avance et générer à la demande.
  • response.usage rapporte maintenant les jetons d'entrée, de sortie, de cache et de raisonnement, model.contextSize expose la fenêtre, et tokenCount(for:) compte un prompt avant l'envoi. Mon emballement de 48,9 secondes devient quelque chose que je peux voir venir, au lieu de le retrouver dans un journal après coup.
  • Le modèle embarqué accepte des images, et le framework livre un OCRTool embarqué adossé à Vision. C'est celui auquel je repense sans arrêt. La fiche App Store promet aujourd'hui que l'app ne photographie pas la page, parce que la reconnaissance d'écriture invente des fautes que l'enfant n'a jamais faites, et une app d'orthographe qui hallucine une faute est pire que pas d'app du tout. La confidentialité n'a jamais été la seule raison de dire non là-dessus. Mais pouvoir l'essayer entièrement sur l'appareil, sans aucun envoi, c'est nouveau.
  • Le protocole LanguageModel permet à n'importe quel modèle d'alimenter une session. L'app avait déjà cette forme : un guichet unique décide à l'exécution si le framework existe sur ce système, ce qui permet à un seul binaire de servir macOS 15 sans Apple Intelligence et macOS 26 avec. Cette indirection appartient maintenant au système plutôt qu'à moi.
  • DynamicProfile permet à une seule session de changer de consignes et d'outils en cours de route. Écrire une dictée et la corriger sont deux métiers différents dans cette app, et aujourd'hui deux sessions différentes.

Le modèle cloud gratuit d'Apple, désactivé par défaut

La même version rend très facile d'arrêter d'être embarqué.

PrivateCloudComputeLanguageModel offre une fenêtre de 32 000 jetons et un réglage de raisonnement, sans clé d'API, sans compte, et sans facture cloud pour les petits éditeurs sous un seuil de deux millions de premiers téléchargements. Anthropic et Google publient des paquets Swift qui se branchent sur le même type de session. Basculer cette app tient à peu près en une ligne, et le texte reviendrait plus long, mieux construit, et bien plus souvent bon dès la première tentative.

Il y a un vrai argument pour le prendre, et ce n'est pas celui que j'attendais au départ. Le modèle embarqué écrit une bonne dictée pour un enfant de neuf ans. Il devient maigre quand la personne qui demande est un adulte qui apprend le français et qui veut un texte plus long, avec des subordonnées et un vocabulaire plus large, ou un parent qui vise plus ambitieux que ce que la grille de niveau propose. Ces lecteurs existent, ils utilisent déjà l'app, et le plus petit modèle du téléphone n'écrit pas vraiment pour eux.

Donc en iOS 27, l'app le proposera en réglage, et ce réglage sortira désactivé. Installez l'app, ne touchez à rien, et la promesse qu'elle tient aujourd'hui reste vraie : rien ne quitte l'appareil, et le mode avion le prouve en quatre secondes.

Ce qui rend cette option offrable, c'est justement le travail décrit plus haut. Même interrupteur allumé, un titre d'agenda brut ne traverse toujours pas le réseau. La liste blanche l'a déjà réduit à un groupe nominal canonique, donc ce qui voyage c'est « la sortie piscine » et un prénom, jamais « RDV Dr Meunier, résultats ». Le filtre a été construit pour protéger un enfant du modèle. Il se trouve qu'il protège aussi la famille du réseau.

L'écran de confidentialité gagne une phrase, et c'est la partie que je veux réussir. Une ligne qui dit que rien ne quitte l'appareil en devient deux : rien ne quitte l'appareil, sauf si vous activez les textes plus riches, et voici exactement ce qui sort quand vous le faites. Le cloud privé d'Apple est une bonne promesse et j'y crois. Ça reste une promesse, et une promesse a sa place derrière un interrupteur qu'un parent a choisi d'actionner, pas dans un réglage par défaut qu'il n'a jamais lu.

L'embarqué reste le défaut pour une raison moins noble aussi. Une dictée dans un train, dans une voiture, dans une location au wifi douteux, c'est exactement le moment où une famille ouvre cette app. Quoi que dise le réglage, il faut que ça continue de marcher sans réseau du tout, donc le moteur embarqué ne va nulle part.

Si vous en avez besoin dans votre propre app

Si vous regardez une app où les données ne peuvent pas quitter l'appareil, et qu'on vous a dit que ça excluait l'IA, ce conseil a un an de retard. Santé, éducation, RH, juridique, tout ce qui touche à des enfants, tout ce dont un contrat client nomme le pays où les données doivent rester : la voie embarquée existe et elle fonctionne maintenant.

Les réserves, que vous feriez mieux d'entendre de moi plutôt que de les découvrir au deuxième mois :

  • Le modèle embarqué est petit. Il écrit bien un texte court. Il ne raisonnera pas sur un long document. Ajustez la fonctionnalité au modèle plutôt que l'inverse.
  • Vous allez écrire une couche de validation. Traitez le modèle comme un rédacteur rapide et sans jugement, et entourez-le de code déterministe. C'est dans ce code que vit réellement le produit.
  • Il vous faut un repli pour l'appareil qui n'a pas Apple Intelligence du tout, et il doit être invisible. Ici une bibliothèque écrite à la main se tient derrière le modèle, et un enfant ne peut pas dire lequel des deux a écrit le texte du jour.
  • Embarqué et cloud ne sont pas un choix entre deux produits. Mettez le filtrage devant le modèle plutôt que dedans, et vous pourrez proposer le cloud en réglage plus tard sans réécrire une ligne de votre écran de confidentialité.
  • Rien de tout cela n'est du web. C'est EventKit, un manifeste de confidentialité, de la génération guidée, et des comportements de plateforme qui ne se montrent que sur du vrai matériel, ce qui est une raison de plus pour que le natif batte un wrapper multiplateforme quand la partie difficile est le système d'exploitation.

L'ensemble, filtre d'agenda compris, a pris des jours et non des mois. L'essentiel de ce temps est parti dans la chaîne de validation et les chemins d'échec, pas dans le prompt.

Besoin d'une app qui ne peut rien envoyer dehors ?

iOS et Android natifs, construits autour de l'IA embarquée, d'EventKit et des API de plateforme qu'un webview n'atteint pas. Une app fonctionnelle sur votre propre appareil dès la fin du premier jour, à partir de 500 € HT.

Discuter de mon projet