Technique

Comment Dreamalizing fait tourner un modèle de langage sans cloud

Un rêve est ce qu'il y a de plus personnel qu'une personne puisse écrire. C'est pourquoi le texte du rêve dans Dreamalizing n'est jamais envoyé à un service d'IA externe. Cet article décrit comment cela fonctionne techniquement, les choix qui en ont découlé et ce qui a mal tourné en cours de route.

Il est écrit pour les développeurs et pour tous ceux qui veulent vérifier si la promesse de confidentialité est plus qu'une simple phrase sur la page d'accueil.

La promesse, en une phrase

Votre texte de rêve est traité par notre propre modèle de langage sur des serveurs situés au sein de l'Union européenne. Vos données sont stockées de manière cryptée et votre texte de rêve n'est pas envoyé à des services d'IA externes.

Cette phrase se trouve dans l'application, dans les magasins et sur ce site. Tout ce qui suit existe pour la concrétiser, même si quelqu'un fait une faute de frappe dans un fichier de configuration.

La chaîne, du téléphone au modèle

L'application (React Native, Expo) communique avec une API (NestJS, PostgreSQL) sur un serveur loué dans l'UE. Ce serveur ne possède pas de modèle de langage. Il accède, via un tunnel WireGuard, à une passerelle sur du matériel propre, et cette passerelle communique avec le modèle.

Le modèle est Gemma 4 avec 26 milliards de paramètres dans une configuration mixture-of-experts, dont environ 4 milliards sont actifs par token, en quantification 8-bits. Un seul modèle fait tout : les questions, l'analyse et l'extraction structurée. La fenêtre de contexte est fixée à 32 768 tokens, car l'analyse la plus lourde a utilisé jusqu'à présent 13 671 tokens et la sortie peut aller jusqu'à 8 192 tokens.

Jusqu'en août, deux modèles tournaient : un plus lourd pour les questions et l'analyse, un plus léger pour l'extraction. Cela s'est avéré plus coûteux que ce que cela rapportait. Les deux modèles se disputaient la mémoire, ce qui entraînait le chargement d'un modèle trois fois par rêve, soit 7 à 10 secondes à chaque fois. Le modèle plus lourd effectue l'extraction avec 3,7 secondes de retard, mais économise 27 secondes de temps de chargement.

Local-only comme surveillance, pas comme intention

L'API possède une couche de routage qui peut envoyer vers plusieurs fournisseurs. C'est pratique lors du développement et dangereux en production. C'est pourquoi il existe trois verrous.

Premièrement, un flag strict dans le runtime. S'il est activé, l'API refuse tout appel contenant du contenu de rêve vers un fournisseur non local. Une soupape de sécurité existe, mais elle est liée à une date d'expiration maximale de quatorze jours et expire automatiquement ; une date invalide ou trop lointaine est ignorée et la surveillance reste fermée.

Deuxièmement, la passerelle. Chaque ID de modèle demandé par l'API commence par le préfixe local-. La passerelle lit cela comme étant local-only et refuse de livrer une telle requête à un fournisseur cloud, même si une chaîne de repli pointe vers celui-ci.

Troisièmement, les tests. Un test impose que l'API ne supprime jamais le préfixe, un autre impose que la configuration du modèle des environnements de test et de production soit identique. Le premier test existe parce que l'API supprimait bien le préfixe jusqu'à la fin du mois d'août. De ce fait, le repli de la passerelle aurait pu théoriquement envoyer du texte de rêve vers le cloud, avec un statut 200 et sans signal. L'application n'avait pas encore été lancée à l'époque et n'avait pas d'utilisateurs actifs en dehors de nous-mêmes. Nous avons fermé le chemin et ajouté le test, afin que cela ne puisse plus se produire, même de manière inaperçue.

Le temps d'attente est un choix de conception

Un modèle local sur une seule machine est plus lent qu'une API cloud. Les questions arrivent après 20 à 40 secondes, l'analyse après 30 à 60. Nous ne le cachons pas ; l'application indique ce qui se passe et combien de temps cela prendra environ.

L'ordre est conçu en conséquence. D'abord, les questions sont envoyées à l'application, basées sur le texte brut du rêve. Ensuite, en parallèle, un deuxième appel construit les données structurées dont l'analyse aura besoin plus tard. Entre-temps, le rêveur répond aux questions et ne remarque rien de ce deuxième appel.

Sur la machine tourne un limiteur qui n'autorise qu'un seul appel à la fois par utilisateur et qui ne laisse jamais passer les appels optionnels avant les appels obligatoires. Nous avons appris ce dernier point suite à un incident en production : un appel optionnel a évincé l'appel obligatoire, et le rêveur n'a pas attendu.

Stockage et hors ligne

Le texte du rêve et les réponses sont stockés de manière cryptée par utilisateur avec AES-256-GCM et une clé propre à chaque utilisateur, qui est elle-même sous une clé principale. Sur le téléphone, la file d'attente des rêves non encore envoyés se trouve dans une base de données SQLCipher cryptée avec une clé stockée dans le stockage sécurisé de l'appareil.

Chaque action d'écriture porte un ID d'opération stable. Si la connexion est coupée après l'envoi mais avant la réponse, une répétition produira le même rêve, et non un second. Le registre de ces ID ne contient jamais de texte de rêve, seulement le type d'action, un hash et le résultat.

Ce que cela coûte

Une seule machine constitue un plafond. En cas d'utilisateurs simultanés, les appels s'accumulent et le temps d'attente augmente. C'est le prix à payer pour ne pas utiliser le cloud, et la raison pour laquelle nous avons commencé aux Pays-Bas et non dans le monde entier.

Le second prix est que le modèle est moins performant que les plus grands modèles cloud. C'est pourquoi nous mesurons toujours les modifications de prompt sur le modèle local, jamais sur un modèle cloud ; ce qui fonctionne sur un modèle plus grand ne fonctionne pas automatiquement ici.

La suite

Les questions sur l'architecture sont les bienvenues. Ce qui est écrit ici est l'état des lieux de septembre 2026 ; si quelque chose change, cet article changera également.