Ce n'est pas une histoire de levée de fonds ni de pivot stratégique. C'est l'histoire d'un problème banal que j'ai vu se répéter trop souvent, et d'une série de décisions techniques que j'ai prises — parfois bien, parfois avec du recul.
Le problème que personne ne nomme
À un moment, j'observe une chargée RH qui passe vingt minutes à remplir un formulaire d'onboarding SIRH. Elle a son tableau Excel devant elle, elle copie "Martin", colle, copie "12/03/1987", colle, copie l'IBAN, colle. Pour un seul collaborateur. Elle en a douze à traiter ce matin. Et demain, il y aura les formulaires de mutuelle, de prévoyance, les accès informatiques...
Ce problème n'a pas de nom dans les outils existants. Zapier dit "on va connecter vos apps". Make dit "on va créer un workflow". Les RPA disent "on va coder un robot". Mais là, on parle d'une personne avec un Excel et un formulaire web sans API. Pas de webhook. Pas de connecteur. Un humain, un fichier, une URL.
La question que je me pose : est-ce qu'on peut automatiser le copier-coller sans supprimer l'humain de l'équation ?
Le premier prototype : un script Playwright
Le premier code que j'écris est un script Playwright. Playwright est la bibliothèque de Microsoft pour contrôler un navigateur Chromium de façon programmatique — le même navigateur qu'un humain utiliserait, pas un simple parser HTML. C'est la première décision clé : ne pas scraper le DOM statiquement, mais ouvrir une vraie fenêtre, avec le rendu JavaScript complet, les éléments dynamiques, les composants React ou Vue qui se génèrent côté client.
Le script de base tient en une cinquantaine de lignes :
- Ouvrir l'URL dans Chromium
- Lister les éléments interactifs visibles (
input,select,textarea) - Pour chaque élément, récupérer son label, son placeholder, son name, son id
- Mapper manuellement colonne Excel → sélecteur CSS
- Remplir via
page.fill(),page.selectOption(),page.check()
Ça marche. Sur le formulaire test. Pendant cinq minutes. Puis on essaie un autre formulaire.
La complexité cachée des formulaires web
La diversité des formulaires web est un gouffre. Ce qu'on imagine comme "un champ texte" cache en réalité des dizaines de variantes :
- Les selects en cascade : sélectionner "France" dans "Pays" fait apparaître "Île-de-France" dans "Région" qui fait apparaître "Paris" dans "Département". Chaque sélection est un événement asynchrone qui déclenche un rechargement partiel.
- Les date pickers custom : le champ
input[type="date"]natif est souvent remplacé par un composant JavaScript avec son propre calendrier.page.fill()n'y fait rien. - Les champs masqués : certains formulaires SIRH masquent les champs tant que le formulaire n'est pas dans un état précis (onglet actif, étape de wizard...).
- Les selects déguisés : des
<div>qui se comportent comme des<select>, avec ARIA roles et keyboard navigation, mais aucun<option>dans le DOM. - Les champs autocomplete : taper dans un champ déclenche une requête API, un dropdown s'affiche, il faut cliquer dessus.
Le form-inspector.js qui inspecte les formulaires est progressivement devenu plus subtil : il filtre les éléments visibles (pas de display:none, pas de visibility:hidden, pas de opacity:0), il déduit le label associé à un champ en remontant le DOM (label[for], aria-label, aria-labelledby, texte du parent direct), il score chaque champ détecté par sa probabilité d'être pertinent.
L'IA dans tout ça — où, pourquoi, et surtout comment
La question du mapping colonnes/champs est le moment où l'IA entre en jeu. Comment savoir que la colonne "NOM" doit aller dans le champ "last_name", et que "Date naissance" correspond à "Birthdate" sur un formulaire en anglais ? C'est du langage naturel, de la sémantique — un LLM est fait pour ça.
Mais il y a une règle que je pose dès le début, non négociable : l'IA ne voit jamais les valeurs réelles de vos données. Elle reçoit uniquement :
- La liste des noms de colonnes du fichier Excel (
["Prénom", "NOM", "Date_naissance", "IBAN", ...]) - La liste des champs détectés dans le formulaire (
["first_name", "last_name", "dob", ...]avec leurs labels)
Elle rend un JSON de mapping. C'est tout. Les 500 lignes de votre fichier Excel ne transitent jamais par un serveur d'IA.
L'architecture dual-LLM pour Excel
Pour les fichiers Excel complexes — tableaux non tabulaires, cellules fusionnées, titres en milieu de tableau, données réparties sur plusieurs onglets — un seul appel LLM ne suffit pas. J'ai conçu un pipeline en deux temps :
- Premier appel (
extractExcelData) : on donne au LLM le texte brut de l'Excel (quelques centaines de caractères, jamais les valeurs sensibles). Il identifie quelles lignes sont les en-têtes, où commence le tableau de données, quelles colonnes sont pertinentes. Il rend un objet structuré. - Deuxième appel (
suggestMapping) : avec la liste propre d'en-têtes extraite, on suggère le mapping vers les champs du formulaire.
Pour les fichiers simples (un tableau avec en-tête en ligne 1), le premier appel est quasi trivial. Pour un document RH complexe avec des sections, des titres, des sous-totaux, il fait la différence entre un mapping réussi et un désastre.
Rate limiting partagé
Les deux appels LLM partagent un même compteur de rate limiting (checkRateLimit() dans lib/llm.js). Par défaut : 20 requêtes par minute, par IP. Ce n'est pas une contrainte arbitraire — c'est la réalité des APIs LLM gratuites ou peu chères. Et ça force à ne pas multiplier les appels inutilement.
La décision architecturale la plus importante
Très tôt, je prends une décision que certains trouvent contraignante : DataToForm ne soumettra jamais un formulaire. Pas de paramètre optionnel "auto-submit", pas de mode sans surveillance. Par construction.
La fonction fillCurrentPage() dans generic-form-filler.js remplit les champs, puis s'arrête. Elle n'accepte pas de sélecteur de soumission. Si vous regardez l'API /api/fill-current-row, il n'y a pas de paramètre submit dans le schéma. Ce n'est pas un oubli — c'est délibéré.
Pourquoi ? Trois raisons :
- Responsabilité légale. Une déclaration URSSAF fausse, un IBAN erroné, un dossier de sinistre incomplet : les conséquences d'une soumission automatique peuvent être sérieuses. L'humain doit valider.
- La confiance se construit. Un outil qui ne soumet jamais sans votre accord est un outil que vous pouvez essayer sans anxiété. C'est aussi un argument commercial — mais c'est d'abord une conviction.
- Les agents IA font l'inverse. Browser Use, Skyvern, Operator soumettent automatiquement. Ce n'est pas qu'ils sont mal conçus — c'est leur cas d'usage. Le nôtre est différent : des données Excel en masse, avec relecture ligne par ligne.
La stack technique et pourquoi ces choix
Node.js ESM, pas de framework serveur lourd
Le backend est en Node.js avec les modules ES natifs (import/export plutôt que require). Express pour le routing HTTP. Multer pour les uploads de fichiers. Pas de framework "batteries included" — chaque dépendance est là pour une raison précise.
Playwright Chromium, pas Puppeteer
Playwright est plus robuste que Puppeteer pour la gestion des sélecteurs, les attentes d'éléments, et le support des iframes. Il gère nativement les waitForSelector avec timeout configurable, et son API pour les selects en cascade (page.selectOption() avec fallback click) est plus fiable.
Chromium headless sur le serveur, mais avec une option pour ouvrir une vraie fenêtre visible quand l'utilisateur doit se connecter lui-même. Ce détail — la fenêtre visible pour la connexion manuelle — est important : aucun identifiant ne transite par le serveur.
WebSocket pour le suivi temps réel
L'analyse d'un formulaire peut prendre 5 à 15 secondes. Le LLM peut prendre 3 à 8 secondes. On envoie des événements au client via WebSocket (ws) pendant tout le process : "Ouverture du navigateur", "Analyse des champs", "IA au travail"... C'est du feedback UX, mais aussi un signal que le système est vivant et progresse.
SheetJS pour Excel — et pourquoi deux appels LLM
SheetJS (xlsx) parse les fichiers Excel. Il est rapide, supporte .xlsx, .xls et .csv, et fonctionne en mémoire (le fichier n'est jamais écrit sur disque). Pour les structures tabulaires classiques, XLSX.utils.sheet_to_json() donne un tableau propre en une ligne. Pour les structures complexes, on extrait le texte brut avec sheet_to_csv() et on le donne au premier LLM pour qu'il reconstruise la structure.
Vanilla JS côté client — zéro framework, zéro bundler
Le frontend est en JavaScript natif. Pas de React, pas de Vue, pas de Vite, pas de Webpack. Un fichier app.js, un fichier style.css, un fichier index.html. Les modules ES natifs du navigateur (import dans les <script type="module">) remplacent les bundlers.
Ce choix simplifie radicalement le cycle de développement : modifier un fichier, rafraîchir le navigateur, voir le résultat. Pas de npm run build, pas de source maps à configurer, pas de hot module replacement à déboguer.
Le système d'i18n maison (i18n.js + fichiers JSON) gère trois langues sans dépendance externe. Chaque texte UI est derrière une clé (data-i18n="clé"), les JSON sont chargés à la demande selon la locale détectée.
Multilingue dès le départ
FR, EN, ES. Pas un afterthought — une contrainte de conception initiale. Chaque article de blog a ses propres slugs localisés avec hreflang injecté côté serveur. La sélection de langue stockée en cookie (dtf_locale), pas en localStorage, pour la compatibilité avec le rendu serveur.
Où en est-on
DataToForm est en bêta ouverte. La première ligne de chaque document est traitée gratuitement — c'est une contrainte FREE_ROW_LIMIT = 1 dans server.js, pas une limite côté client qu'on pourrait contourner.
Ce qui manque encore et qui est en cours :
- Les selects en cascade (la logique est en
select-utils.js, mais certains composants custom résistent) - Les formulaires multi-pages (wizard steps) — on remplit une page, on attend, on continue
- Google Sheets comme source de données en plus d'Excel
- La mémorisation du mapping entre deux sessions sur le même formulaire
Ce qui ne changera pas :
- L'IA ne verra jamais vos valeurs réelles
- Le formulaire ne sera jamais soumis automatiquement
- Le fichier ne sera jamais stocké sur le serveur
Si vous avez un cas d'usage particulier ou des retours sur un formulaire que DataToForm n'arrive pas à remplir correctement, la page Contact est là pour ça. L'outil s'améliore avec les cas réels.