Mochi : un petit vous qui prend vos notes
Une app macOS qui transforme ce que vous dites en notes dans votre coffre Obsidian. La reconnaissance et la séparation des voix tournent sur le Mac : aucun audio n'en sort, et aucune minute ne coûte quoi que ce soit.

- 0
- octet d'audio envoyé
- 3
- niveaux de reconnaissance en local
- 4
- voix séparées par appel
Le contexte
Mochi est une app macOS qui se pose au bord de votre écran et note ce que vous dites. Appuyez sur Option et Espace, parlez, et une note atterrit dans votre coffre Obsidian avant que vous ayez fini votre pensée.
Elle vous ressemble. Vous envoyez une photo pendant la configuration et Mochi en dessine une caricature : grosse tête, grands yeux, membres courts. Ce personnage est l’interface. Il dort pendant que vous travaillez et se réveille quand vous parlez.
La reconnaissance et la séparation des voix tournent sur le Mac. Aucun audio n’est envoyé, et aucune minute de transcription ne coûte quoi que ce soit.
Le problème
Une note vocale est facile à enregistrer et ne se réécoute presque jamais. L’enregistrement est la fin du processus plutôt que son début, alors il reste dans un dossier avec un horodatage pour nom et personne ne le rouvre.
Les outils de dictée règlent la moitié du problème en produisant du texte, puis vous le rendent au lieu de le déposer là où vos notes vivent vraiment. Les outils de transcription de réunions règlent une autre moitié en envoyant tout l’appel sur le serveur de quelqu’un, ce qui est un échange correct pour une équipe commerciale et un mauvais échange pour une conversation privée.
Les deux laissent le même trou. Ce que je voulais n’était pas une transcription. C’était que la phrase dite à voix haute soit déjà dans le coffre, dans le dossier où j’irais regarder, sans que j’aie décidé d’aller l’y mettre.
Mon rôle
Tout. Nuits, c’est une personne.
L’app Swift, le pipeline de reconnaissance, l’écriture dans le coffre, le backend Worker, le site, la marque, la mascotte, les prix, les pages légales et la position sur la vie privée. Il n’y a personne à qui refiler l’arbitrage inconfortable, ce qui se voit surtout aux endroits où la réponse honnête coûte une fonctionnalité.
L’approche
J’ai décidé de l’ordre des écritures avant tout le reste, et ça a tranché tous les débats qui ont suivi. La transcription mot pour mot part dans le coffre en premier, toujours, avant qu’un modèle de langage soit appelé. La version mise au propre est ajoutée ensuite à la même note, en second bloc.
Cet ordre, c’est le produit. Si le réseau est coupé, si la clé est mauvaise, si le fournisseur passe un mauvais après-midi, vous avez quand même vos mots. Une panne de modèle dégrade la note ; elle ne peut jamais la perdre. Tout le reste découle de cette écriture placée en premier. La reconnaissance doit être locale, sinon la première écriture dépend du réseau. Le modèle doit être le vôtre, parce que la seconde écriture est optionnelle et doit être facturée à qui l’a choisie.
La mascotte relève de la même décision, au niveau du ressenti. Une icône dans la barre de menus est un utilitaire, et son absence passerait inaperçue. Un petit personnage qui vous ressemble et qui réagit en un dixième de seconde après le raccourci, ça vous manquerait. Ça valait la peine de construire un système d’animation pour ça.
La construction
Swift natif. SwiftUI pour les panneaux, AppKit pour la surcouche, parce qu’une fenêtre qui flotte au-dessus de tout, ignore les clics à la demande et se comporte correctement à travers les Spaces et plusieurs écrans, ce n’est pas ce que SwiftUI a envie de vous donner. Le personnage est composé de sprites vectoriels animés par keyframes et ressorts, pas par un moteur de jeu.
La reconnaissance, c’est Parakeet via FluidAudio, exporté en CoreML en trois niveaux : 227,5 Mo pour l’anglais, 335,7 Mo pour 25 langues, et 483,1 Mo pour les accents et les pièces bruyantes. Ces chiffres sont mesurés, pas mémorisés. Un script lit l’arborescence du dépôt de modèles, ne compte que les fichiers exacts que le SDK audio télécharge, et génère un fichier Swift que les écrans d’onboarding lisent. Personne ne peut taper une taille à la main dans l’interface. Une version antérieure de la spec portait des estimations à peu près deux fois plus flatteuses que la réalité, sur chaque ligne, ce qui est exactement le sens dans lequel les estimations dérivent, et le script existe pour que ça n’arrive pas deux fois.
Le plus important que j’aie trouvé est un bug de silence, et il a changé l’architecture. Une fenêtre de décodage qui se termine à l’intérieur d’un silence renvoie une chaîne vide, sans erreur et avec une confiance élevée. Douze secondes de parole se transcrivent correctement. Les mêmes douze secondes suivies de trois secondes de silence se transcrivent en rien du tout. Pour un produit dont toute la promesse est le compte rendu mot pour mot, un échec qui ressemble à un succès est le pire disponible. Donc le silence n’atteint jamais le moteur de reconnaissance. Silero VAD découpe d’abord l’enregistrement en régions de parole, chaque région est transcrite séparément, et les plus courtes sont élargies avec l’audio environnant plutôt que rembourrées de zéros, parce que le silence ajouté est précisément le déclencheur. Le modèle de détection de voix est toujours installé et jamais optionnel : c’est une décision de justesse, pas de performance.
Le mode réunion enregistre l’audio système et le micro sur deux pistes séparées, pour que votre propre voix ne soit jamais quelque chose que le logiciel doit deviner. La diarisation en flux sépare jusqu’à quatre voix en direct. Reconnaître la même personne d’une réunion à l’autre demande un second moteur, ce que j’ai mis trop longtemps à accepter : un diariseur en flux ne garde aucun embedding entre les sessions, donc la voix numéro un d’aujourd’hui et la voix numéro un de la semaine prochaine n’ont rien à voir, et aucun réglage n’y change quoi que ce soit. Une seconde passe tourne désormais après l’appel. Elle prend huit à quinze secondes des plus longues prises de parole de chaque voix, en extrait un embedding de 256 dimensions, et le compare aux profils stockés sur le Mac par similarité cosinus. Le seuil est volontairement placé du côté prudent, parce qu’une correspondance manquée coûte un renommage alors qu’une fausse correspondance met le nom d’un collègue sur les mots d’un inconnu. L’attribution est décidée sur toutes les voix à la fois, si bien que dans un appel à deux le premier locuteur ne peut pas réclamer un profil que le second a mieux matché.
L’écriture dans le coffre est la seule partie avec une vraie suite de tests : 29 cas sur la construction du markdown et le comportement sur disque. Elle écrit de façon atomique, ajoute au lieu de remplacer, et ne réécrit jamais une note que vous avez modifiée, parce qu’un coffre synchronisé par iCloud punit ces trois erreurs par des fichiers de conflit.
Les modèles de langage sont les vôtres. Un seul registre, une seule forme de requête compatible OpenAI, des surcharges par fournisseur là où la forme diffère, sur OpenRouter, Anthropic, Gemini, DeepSeek et d’autres. Vous collez une clé, elle va dans le Trousseau, et elle ne touche jamais mon backend. L’inférence n’est donc jamais un coût pour l’entreprise, et c’est ce qui permet à un seul forfait à 12 $ d’inclure une transcription illimitée sans astérisque.
La génération du personnage est la seule chose qui me coûte de l’argent par utilisateur, et c’est devenue l’ingénierie la plus intéressante du projet. Elle se fait en deux étapes chez un seul fournisseur, via OpenRouter. L’étape une transforme votre photo en quatre personnages candidats, à environ treize centimes pièce, et vous en choisissez un. L’étape deux dessine les neuf états graphiques de ce personnage en vrai SVG, à environ huit centimes pièce. Le choix parmi quatre est une dépense assumée : trois de ces dessins sont jetés, et l’alternative serait d’obliger quelqu’un à accepter la première chose qu’un modèle a dessinée de son propre visage.
Le modèle vectoriel n’expose aucun paramètre de transparence, ce qui aurait dû impliquer de payer en plus un service de détourage. À la place, chaque appel force un aplat de couleur connue, et l’unique rectangle plein cadre est retiré du SVG renvoyé. Le retrait ne considère que les enfants directs de la racine, donc un rectangle placé dans un masque de découpe n’est jamais pris pour le fond. Un seul fournisseur, aucun service de détourage, une vraie transparence.
Ce point d’API m’a appris l’essentiel de ce que je sais sur la vérification par le résultat plutôt que par le code de statut. Les modèles d’image sont absents de la liste principale et n’apparaissent que sur une liste séparée. Un paramètre de format documenté dans le schéma de requête ne sélectionne rien, parce que la sortie vectorielle se choisit en prenant un modèle vectoriel et jamais en renseignant le champ. Et les clés de fournisseur non reconnues sont silencieusement ignorées plutôt que rejetées, donc un nom de champ mal orthographié renvoie une réponse parfaitement réussie avec aucun des contrôles appliqués. Le fournisseur est explicitement épinglé sur l’appel qui porte la photo, et c’est de la conformité plutôt que de l’hygiène de routage : le fournisseur d’images est celui qui figure dans la liste des sous-traitants, et un basculement silencieux enverrait la photo du visage de quelqu’un à une entreprise qui n’y figure pas.
Clerk gère la connexion via son flux hébergé pour macOS. Polar est marchand de référence, donc la TVA mondiale est le problème de quelqu’un d’autre, et il émet la clé de licence avec une limite d’activations qui vous laisse changer de Mac sans ticket de support. Sparkle livre les mises à jour, et ne redémarre jamais pendant un enregistrement. Le site est en Astro 7 sur Cloudflare Workers, avec l’API et D1 à côté.
Il n’y a ni rapport de crash, ni analytics, ni télémétrie dans l’app. C’est une décision et pas un oubli, et c’est celle que je m’attends à regretter sur le plan opérationnel et d’aucune autre façon. Mochi passe ses journées avec un micro ouvert, un enregistrement de réunion en cours et le texte de ce qui vient d’être dit. Un rapporteur de crash à l’intérieur de ce processus envoie des piles d’appels, des noms de threads et, un mauvais jour, ce qui traînait dans un tampon au moment de la mort du processus. On ne peut pas promettre que les enregistrements ne quittent jamais le Mac tout en livrant un agent dont le métier est de m’envoyer des données depuis un processus qui avait un enregistrement ouvert. Ce que ça achète, c’est une phrase qui n’a besoin d’aucun astérisque, ni sur le site, ni dans l’app, ni dans la politique de confidentialité. Ce que ça coûte, c’est que rien n’arrive tout seul : un bug n’est jamais plus visible que le rapport que quelqu’un choisit d’envoyer. La compensation est un bouton qui copie dans le presse-papiers les quinze dernières minutes du journal de l’app, sa version et la build de macOS, et rien d’autre. Vous le lisez, puis vous décidez.
Le résultat
Pas encore livré. Le site est en ligne sur mochiformac.com et collecte une liste d’attente, et la page de téléchargement dit franchement qu’il n’y a pas de build à vous donner aujourd’hui et que je ne vais pas inventer une date.
Le chemin de la capture jusqu’au coffre fonctionne de bout en bout, et c’est la chose que j’ai tenu à terminer avant de toucher à l’authentification, à la facturation ou à la génération du personnage.
Ce que je ferais autrement
J’ai écrit les tailles des modèles dans la spécification de mémoire et chacune était fausse d’à peu près un facteur deux, toujours en ma faveur. Maintenant un script les mesure et l’interface ne peut afficher que ce que le script a écrit. La version générale de cette leçon m’a coûté cher : tout nombre qu’un utilisateur va lire devrait être généré, jamais tapé.
J’ai aussi construit le diariseur en flux d’abord, en supposant que reconnaître une voix qui revient serait un problème de réglage par-dessus. C’est un autre problème qui demande un autre modèle, et j’ai perdu de vraies semaines à essayer de tirer des embeddings de quelque chose qui n’en a aucun à donner.




