ENFRKOantony langlois
projets

Vocab Catcher

visiterautomatisationPythonTelegramGeminiObsidian

Bot Telegram qui transforme les mots que vous venez de rencontrer en flashcards Obsidian · enrichissement Gemini en mode JSON · un simple message texte devient une carte de répétition espacée en quelques secondes

Le problème à résoudre

Les apprenants en langues comme moi utilisent souvent des flashcards pour apprendre et retenir de nouveaux mots. Une flashcard a une face avec la signification en anglais (ou dans n'importe quelle langue) et une face dans la langue étudiée. Dans mon cas, le coréen.

Mais ce processus est très fastidieux et peut prendre beaucoup de temps. Et pendant que j'apprenais le coréen, je me suis rendu compte que dans la vie de tous les jours, quand un ami m'apprend un nouveau mot ou quand je croise un nouveau mot dans la rue par exemple, je n'ai pas le temps de créer une flashcard pour chaque mot sur le moment ni d'écrire une définition détaillée, alors je finis par les oublier 5 minutes plus tard, ce qui est un gâchis de connaissances énorme et cumulatif.

Comment je l'ai résolu

J'ai résolu ce problème en offrant un moyen de déposer rapidement de nouveaux mots, qui se charge ensuite tout seul de créer automatiquement des flashcards dans Obsidian. Ce qui permet d'apprendre de nouveaux mots sans effort au fil de la journée et de pouvoir les réviser une fois rentré, pour les mémoriser sur le long terme.

Un bot Telegram, auquel vous pouvez envoyer un mot accompagné en option d'une brève signification de ce mot, puis un script Python installé sur un VPS se charge de transformer ce mot en flashcard, en utilisant Gemini pour obtenir une signification plus détaillée et précise.

Il crée 2 flashcards, une avec simplement la définition du mot, et une avec une phrase contenant un blanc afin de vous entraîner à utiliser ce mot dans de vraies phrases. Les flashcards Obsidian sont simplement des fichiers markdown.

Une fois la flashcard créée, le bot Telegram nous répond en confirmant, et avec la signification du mot, pour être sûr que nous avons bien saisi le bon sens.

Les flashcards sont ensuite poussées vers un dépôt GitHub et, grâce au plugin Obsidian Git, Obsidian desktop et mobile peuvent récupérer les cartes depuis ce dépôt afin qu'elles soient ajoutées en temps réel à vos flashcards.

Architecture

phone
Client · capture
Application Telegram
any device
L'utilisateur envoie un mot à l'instant où il le rencontre : un mot coréen, une romanisation ou une simple description en anglais
SaaS
Externe · broker de messages
API Bot Telegram
api.telegram.org
Met les updates en file jusqu'à ce que le bot les récupère · aucun port entrant requis
desktop · mobile
Client · révision
Obsidian
Spaced Repetition plugin
Rend chaque note en deux flashcards : word::meaning et une phrase à trous
Python · asyncio
Cœur · boucle de polling
bot vocab-catcher
bot.py · single file
Fait du long-polling sur Telegram, enrichit chaque mot via Gemini, construit la note et le cloze, l'écrit dans le coffre
long-poll getUpdatesenrichissement en mode JSONconstructeur de clozewatchdog auto-killsync git (branche VPS)
gemini-2.5-flash
Externe · LLM
Google Gemini
google-genai SDK
Un appel JSON renvoie l'orthographe, la signification, la phrase d'exemple, le thème, la forme de surface
Markdown files
Stockage · notes
Coffre Obsidian
OBSIDIAN_VAULT_PATH
Une note par mot, taguée #flashcards/<lang>/<theme>
private repo
Externe · sync
Dépôt git du coffre
SSH deploy key
Le VPS pousse chaque note · les appareils récupèrent via le plugin Obsidian Git

L'utilisateur envoie un mot au bot depuis l'application Telegram ; l'API Bot Telegram le conserve jusqu'à ce que le bot le récupère via le long-polling getUpdates. Le gestionnaire effectue un unique appel en mode JSON à Gemini, qui renvoie l'orthographe, la signification, une phrase d'exemple, un thème, et la forme exacte du mot telle qu'elle apparaît dans cette phrase. À partir de là, le bot construit une note Markdown avec une carte word::meaning et une carte à trous (cloze), l'écrit dans le coffre Obsidian, et (sur la branche VPS) la commit et la pousse vers le dépôt git du coffre en arrière-plan. Le bot répond Saved: ..., et les appareils de l'utilisateur récupèrent la note et la révisent dans Obsidian.

Déploiement : Bot · laptop → agent utilisateur launchd macOS (branche macos-background-service) · Bot · toujours actif → VPS · systemd avec Restart=always (branche vps-deployment)

Résultats

  • ~120 lignes · tout le cœur dans un seul fichier · bot.py
  • 0 port ouvert · long-polling uniquement · pas de webhook, de domaine ni de TLS
  • 2 cartes / mot · définition + cloze, générées automatiquement
  • auto-réparation · le watchdog quitte en cas de blocage · le superviseur redémarre

Compétences démontrées : sortie structurée LLM (mode JSON) · conception crash-only avec redémarrage par superviseur · synchronisation git best-effort derrière un verrou asynchrone · variantes de déploiement en branches git additives

Problèmes et solutions

Une contrainte guide tout : capturer un mot doit prendre quelques secondes, et le pipeline ne doit jamais mourir silencieusement.

▒ Sortie structurée plutôt que parsing de prose

Problème : Une réponse Gemini en texte libre exigerait un parsing de chaînes fragile, et un mauvais parsing signifie une flashcard corrompue que l'utilisateur ne découvre que des semaines plus tard, en pleine révision.

Mode JSON forcé (response_mime_type: application/json) plus une instruction système fixant les clés exactes. Le gestionnaire consomme la réponse avec un simple json.loads.

  • Rejeté : parser une réponse en prose avec des regex : une corruption silencieuse est pire qu'un échec visible
  • Une réponse mal formée échoue bruyamment ('Failed to process'), donc l'utilisateur renvoie simplement le mot
  • Chaque étape en aval devient un accès trivial au dictionnaire, sans code de parsing défensif

▩ Cartes cloze pour une langue qui se conjugue

Problème : Le coréen fléchit : la forme du dictionnaire d'un verbe apparaît rarement telle quelle dans une vraie phrase, donc masquer la forme du dictionnaire dans l'exemple échoue.

Le prompt demande aussi à Gemini la forme de surface exacte telle qu'elle apparaît dans sa propre phrase d'exemple. La génération du cloze est alors un simple remplacement de chaîne littérale.

  • Rejeté : écrire des règles de morphologie en Python : sans espoir pour un paramètre LANGUAGE arbitraire
  • Si la forme de surface ne correspond pas, la carte cloze est ignorée plutôt qu'enregistrée de travers
  • Déléguer la linguistique au modèle garde le code agnostique à la langue : une seule variable d'environnement fait passer le coréen au japonais ou à l'espagnol

░ Long-polling plutôt que webhooks

Problème : Le bot tourne sur un laptop ou un VPS bon marché ; un webhook exige un point de terminaison HTTPS public, un domaine et l'entretien d'un certificat pour un outil mono-utilisateur.

Long-polling via run_polling avec des tentatives de bootstrap infinies. Telegram met les messages en file côté serveur pendant que le bot est hors ligne et les délivre à la reconnexion.

  • Rejeté : l'infrastructure webhook : aucun bénéfice au volume de messages d'un seul utilisateur
  • Accepté la limite d'un seul poller par token (Telegram renvoie 409 Conflict), documentée dans le runbook
  • Rien n'est exposé à Internet, donc il n'y a aucune surface d'attaque à maintenir

▥ Un watchdog qui tue son propre processus

Problème : Le poller peut se bloquer silencieusement après une veille/reprise du laptop ou un changement de réseau : le processus paraît vivant pour launchd tout en ne recevant rien.

Une tâche asyncio vérifie updater.running toutes les 30 s et appelle os._exit(1) en cas de blocage ; launchd ou systemd redémarre le processus avec un état neuf.

  • Rejeté : la logique de reconnexion en cours de processus : lutter contre les rouages internes de la bibliothèque est fragile
  • Conception crash-only : le redémarrage est l'unique chemin de récupération, donc il est toujours bien testé
  • Le même truc fonctionne à l'identique sous launchd et sous systemd

▨ Variantes de déploiement en branches, pas en flags

Problème : Trois façons de tourner (dev au premier plan, agent d'arrière-plan macOS, VPS avec sync git) rempliraient le fichier unique de flags de configuration et de chemins de code morts.

main reste minimal ; l'installateur launchd vit sur une branche, et la branche VPS ajoute vault_sync.py plus trois lignes dans bot.py.

  • Rejeté : les feature flags par variable d'environnement : chaque déploiement transporterait le code mort des autres
  • Accepté de garder les branches synchronisées avec main, maintenu bon marché par des diffs strictement additifs (66 lignes)
  • Chaque checkout se lit exactement comme le code qu'il exécute, rien d'hypothétique

▣ Sync git qui ne bloque jamais un enregistrement

Problème : Sur le VPS, le coffre doit atteindre les appareils de l'utilisateur, mais un push échoué ne doit ni perdre la note ni faire planter le gestionnaire de messages.

Sync best-effort : la note atterrit d'abord sur le disque, puis le commit + push tournent dans un thread derrière un verrou asyncio ; les échecs sont journalisés, jamais levés.

  • Rejeté : Obsidian Sync et Syncthing : git offre gratuitement l'historique plus l'authentification par clé de déploiement sur un dépôt privé
  • Le verrou sérialise les messages concurrents pour que l'index git ne fasse jamais de course
  • Un push échoué s'auto-répare : le commit en attente part avec le push du mot suivant