OpenDerja_TN — un schéma est la seule chose qu'on ne peut pas reprendre
Douze millions de personnes écrivent la derja tunisienne chaque jour — dans les textos, les commentaires, les légendes — et il n'existe presque aucune trace correcte, continue et sous licence ouverte sur laquelle on puisse réellement bâtir un outil. Il y a eu des efforts de standardisation. Il y a quelques corpus. Il y a d'autres plateformes de crowdsourcing pour les dialectes arabes. Mais pas tout ça ensemble, maintenu continuellement, comme un seul service ouvert avec un traducteur qui marche par-dessus.
Alors OpenDerja_TN est ça : des contributeurs écrivent, corrigent et confirment de la derja de leur propre région ; il en sort un jeu de données citable, un lexique, et un traducteur en direct construit directement sur le corpus. C'est un projet non lucratif dès la première ligne de code — pas une ambiance, une décision structurelle.
C'est encore le jeu des montagnes du Kef, si tu as lu le chapitre deux. Quelque chose qui est à nous, dont personne ne s'occupe correctement, alors. L'échelle est juste très différente maintenant.
La seule décision qu'on ne peut vraiment pas défaire
L'essentiel de l'architecture ici est mon plan réutilisé. Ce qui est vraiment nouveau, c'est le modèle de données, et presque chaque choix difficile vient d'une seule observation :
Un dialecte est un continuum, et un schéma qui force une réponse unique détruit une information qui ne pourra jamais être récupérée.
Le code peut être réécrit en un week-end. Des mois d'annotation communautaire, non. Chaque endroit où le schéma force une valeur là où la réalité en a plusieurs est de l'information détruite au moment de l'écriture, et aucune passe de modélisation ultérieure ne la récupère. Donc :
La région est un ensemble, pas une valeur. Un mot peut être nord-ouest et nord en même temps. Il peut être pan-tunisien. Il peut être nord-ouest à une époque et général à une autre. Stocke une région par enregistrement — ce qui est la forme par défaut d'une colonne de base de données et rend chaque requête plus simple — et tu as fait un choix faux irrécupérable sur chaque ligne. Alors la région est un ensemble, et un champ séparé enregistre si l'élément est régional tout court, parce que « quelles régions » et « est-ce même régional » sont deux questions différentes et les fusionner rend les deux sans réponse.
Les annotations s'attachent à des plages de caractères. Toujours. Même celles qui s'appliquent à une phrase entière — son registre, son époque — sont écrites comme une plage couvrant le texte complet, plutôt que comme une plage nulle signifiant « le tout ». Une seule forme pour chaque annotation signifie un seul mécanisme de consensus, un seul chemin de requête, un seul ensemble d'invariants. Autorise le nul et chaque consommateur des données gère deux cas pour toujours.
Les tokens sont des annotations, pas une hiérarchie. Parce que la standardisation réécrit le texte, et une annotation ancrée à un offset peut migrer avec une réécriture, tandis qu'une hiérarchie de tokens parallèle dérive silencieusement hors de synchro.
Le consensus est clé sur la plage complète — segment, type, offset de début, offset de fin — pas juste segment et type. Cle-le sur segment-et-type seuls et deux annotations sur des parties différentes de la même phrase s'écrasent l'une l'autre, sans erreur, sans moyen de le remarquer après coup.
La clé d'idempotence de la tâche porte un emplacement obligatoire. Les clés d'idempotence existent pour arrêter le travail en double. Mais une clé trop grossière ne crée pas de doublons — elle fusionne du travail censé se faire en parallèle. Un segment qui a besoin de trois confirmations indépendantes est trois tâches distinctes par conception ; clé-les sur juste segment-et-type et ces trois deviennent une, et le corpus a discrètement un tiers de la confirmation qu'il annonce avoir. L'emplacement est obligatoire plutôt qu'optionnel parce qu'un champ optionnel avec un défaut sensé est un champ que quelqu'un oublie, et l'échec ici est silencieux.
Le partage train/dev/test est fixé à l'insertion et jamais, au grand jamais, changé. C'est la seule chose qui rend une affirmation de précision honnête. Une ligne déplacée entre partitions après coup invalide chaque nombre calculé depuis.
La sortie machine n'est jamais un fait. Tout ce qu'un modèle produit entre comme une suggestion — attribuée à un utilisateur système, marquée, exigeant une confirmation humaine avant de pouvoir toucher un champ canonique. Dans un corpus que les gens vont citer, la différence entre « un Tunisien a dit ça » et « un modèle a deviné ça » est toute la valeur du jeu de données, alors cette frontière est structurelle, pas une convention.
De la retenue, écrite pour que personne ne la défasse gentiment
Il y a trois plans de langage dans l'architecture : TypeScript pour le plan de contrôle, Rust pour un futur plan de données, Python pour un futur plan d'intelligence. Seul celui en TypeScript a du code.
La tentation avec un découpage comme ça, c'est de commencer à écrire le Rust tout de suite, parce que c'est la partie amusante. La règle qui m'arrête : le plan de contrôle reste TypeScript en permanence, et une étape ne passe à Rust que quand elle est mesurée comme un goulet d'étranglement à l'échelle réelle du corpus — pas quand il semble que ça irait plus vite. Les modules Rust existent comme des jointures vides, pré-échafaudées, pour que le déplacement éventuel soit peu coûteux. Qu'ils soient encore vides est le but.
Et j'ai écrit ça dans le propre registre du projet — explicitement, « ces modules vides ne sont pas un trou à combler, n'y touchez pas tant que les scrapers n'existent pas de bout en bout et n'ont pas été benchmarkés » — parce qu'une absence délibérée ressemble exactement à un travail inachevé, et un moi futur (ou un contributeur futur) trouvera ces fichiers vides et se mettra gentiment à les remplir. Une présence délibérée s'explique d'elle-même. Une absence délibérée a besoin de documentation.
La suite de tests qui a rétréci au lieu d'échouer
Une restructuration en monorepo a déplacé la plupart des chemins du projet. Après, la campagne de tests était au vert.
Elle était au vert parce que 13 des 57 suites de tests ne pouvaient plus résoudre leurs imports et avaient simplement cessé de tourner. Parmi les mortes, il y avait un spec de 400 lignes couvrant le moteur de tâches — la pièce de logique métier la plus importante de toute la base de code — qui ne s'exécutait pas, silencieusement. Rien n'a échoué. La ligne de résumé avait l'air en bonne santé, juste plus courte. Et personne ne lit un décompte de suites.
Corriger la résolution de modules a fait passer la campagne de trompeusement-verte à 59 suites et 302 tests qui passent vraiment.
Une suite qui ne peut pas résoudre ses imports et une suite qui n'existe pas sont indiscernables dans un résumé. Après tout déplacement structurel, le nombre qui vaut la peine d'être vérifié est le décompte, pas la couleur.
Pourquoi le non-lucratif est une décision structurelle
Les phases un et deux de ce projet sont cadrées pour que, s'il s'arrête après la phase deux, il ait quand même produit quelque chose qui n'existait pas avant : un jeu de données de derja publié, citable, sous licence ouverte, étiqueté par région, plus un outil public qu'un Tunisien non technique peut réellement utiliser. Tout ce qui vient après est du bonus, pas une exigence pour que le travail ait valu la peine.
Pour un projet solo sans financement et sans échéance, définir un point d'arrêt qui compte quand même comme un succès est autant une décision de conception qu'une décision de planification. C'est la décision qui rend la chose survivable. Je l'ai appris à la dure sur d'autres projets où « fait » était toujours juste derrière la colline suivante.