PyDA Course
Intermédiaire Semi-guidé ⏱ 75 minutes ⚡ +100 XP +15 XP par étape

Transcripteur et Résumeur de Réunions

clitext-processingcsvregex

Chaque réunion se termine de la même manière : quelqu’un se porte volontaire pour écrire les notes, oublie qui possédait quoi, et les tâches à faire s’évaporent d’ici lundi. Ce projet construit la moitié résumeur d’un vrai pipeline de réunion, il prend une transcription de réunion (le texte que produit ton outil de conversion voix-vers-texte) et la transforme en le rapport que les humains veulent vraiment : une répartition par locuteur montrant qui a dominé, chaque décision qui a été prise, et une liste par personne de tâches à faire extraites automatiquement des verbes et des noms des responsables dans la transcription.

Ceci suppose Python 101, chaînes, entrées-sorties de fichiers, boucles et fonctions. Rien au-delà : pas de ML, pas de traitement audio, pas d’API externes pour le pipeline central (la vraie reconnaissance vocale a besoin d’une clé, et il y a une étape optionnelle pour cela). C’est optionnel et non noté ; voir Projets du monde réel pour la liste complète.

🎯 Ce que tu vas faire

  1. Définir un format de fichier de transcription (horodatage, locuteur, intervention) et analyser chaque ligne en une intervention structurée.
  2. Grouper les interventions par locuteur et calculer la part de chacun dans le temps de parole.
  3. Trouver les décisions, les phrases qui concluent avec des verbes comme « agreed », « decided », « confirmed ».
  4. Extraire les tâches à faire, « X fera Y », avec le nom du responsable associé à chaque tâche.
  5. Assembler le tout en un seul fichier de résumé lisible et pointer l’outil vers de vraies minutes de réunion.

Où exécuter ceci

En local avec uv est le chemin principal ici, le pipeline est du pur traitement de texte sur un fichier que tu contrôles, donc toute la boucle « dépose une transcription, récupère summary.txt » est une habitude de terminal, et le CSV que tu génères est un fichier que tu peux ouvrir dans n’importe quelle feuille de calcul.

GitHub Codespaces fonctionne à l’identique : ouvre codespaces.new/abderrahim-lectures/python-data-analysis-course et les commandes exactes ci-dessous tournent dans un onglet de navigateur avec Node, Python et uv préinstallés.

Google Colab, Kaggle Notebooks et Binder sont un cadre vraiment adapté à chaque étape ci-dessous, pas de secrets, pas de GPU, et tout le pipeline tient en quelques cellules qui tournent sur la transcription d’exemple fournie par le cours (une réunion réaliste tapée à la main). L’avertissement honnête : le notebook utilise cette transcription d’exemple fixe plutôt que de l’audio que tu enregistres. La vraie reconnaissance vocale aurait besoin d’une clé API gratuite, et cette partie est couverte par une étape optionnelle, pour le résumeur lui-même, un notebook l’exécute pour de vrai.

Open In Colab Open In Kaggle Binder

Configuration

Tout ce dont tu as besoin avant de transcrire un seul mot : uv, et une transcription d’exemple réaliste à mâchonner.

Installe uv et mets en place le projet

macOS / Linux (terminal) :

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows (PowerShell) :

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

Ferme et rouvre ton terminal, puis :

uv --version
mkdir meeting-transcriber && cd meeting-transcriber
uv init --bare

Zéro package supplémentaire, ce projet est en pur bibliothèque standard.

Écris une transcription d’exemple réaliste

Colle ceci dans transcript.txt (chaque ligne : [MM:SS] Locuteur: mots, le format exporté par la plupart des outils de transcription, et facile à lire à la main) :

[00:00] Priya: Let's review where we stand on the launch.
[00:08] Tom: Design shipped the landing page yesterday.
[00:15] Priya: Great. We agreed the beta opens next Monday.
[00:22] Tom: I'll block out Thursday to prep the demo video.
[00:30] Zara: I will draft the onboarding email today.
[00:38] Priya: Please send it to me for a quick pass.
[00:44] Tom: We decided the pricing page stays as-is.
[00:52] Zara: So action items: Tom owns the video, I own the email.
[01:00] Priya: And I'll publish the changelog on Friday. Meeting's at 30 minutes? No sooner.
[01:06] Zara: Wait, that's not a decision.

Exécute :

wc -l transcript.txt

✅ Liste de vérification

  • uv --version affiche un numéro de version.
  • transcript.txt existe avec 11 lignes, chacune commençant par un horodatage [MM:SS].
  • ✅ Tu peux déjà repérer les verbes de décision (agreed, decided) et les verbes de responsabilité (will, owns, publish), ce sont les mots que l’extracteur apprendra à attraper.

Étape 1 : Analyse la transcription en interventions

La transcription est une liste plate de lignes ; le résumé a besoin d’une liste structurée d’interventions, chacune avec un horodatage, un locuteur et les mots. L’analyse est à un honnête split() : l’horodatage et le locuteur sont des préfixes à largeur quasi fixe, et le message est tout ce qui suit le troisième deux-points.

1.1 Écris l’analyseur d’interventions

# parse.py
from pathlib import Path

def parse_line(line: str) -> dict:
    line = line.strip()
    time_s = line.split("]")[0].lstrip("[")
    rest = line.split("]", 1)[1].strip()
    speaker, _, message = rest.partition(":")
    return {"time": time_s, "speaker": speaker.strip(), "text": message.strip()}

def load_transcript(path: str) -> list[dict]:
    turns = []
    for line in Path(path).read_text(encoding="utf-8").splitlines():
        if line.strip():
            turns.append(parse_line(line))
    return turns

if __name__ == "__main__":
    for t in load_transcript("transcript.txt")[:3]:
        print(t)

partition(":") découpe sur le premier deux-points et renvoie un triplet (avant, ":", après), plus sûr que split(":") parce que le texte d’un locuteur peut lui-même contenir des deux-points (regarde la ligne 11 : « Wait, that’s not a decision. » avec une apostrophe, et imagine une URL ou une heure comme 01:06 dans le message). Les .split("]", 1)[1] démêlent l’horodatage de la même façon : tout ce qui suit le premier ], même si le message contient des crochets.

👟 Indice de départ : Analyse le fichier et imprime les trois premières interventions avant d’écrire quoi que ce soit d’autre, le but est de voir Speaker: Priya et text: Let's review... comme des champs propres, pas des préfixes mutilés.

🎯 Résultat attendu : Trois dicts comme {'time': '00:00', 'speaker': 'Priya', 'text': "Let's review where we stand on the launch."}, sans aucun [ ni ] qui fuit dans le champ horodatage.

🩹 Si ça ne marche pas : Si speaker ressort en Priya avec un espace de tête, le .strip() après partition manque. Si ValueError: not enough values to unpack se déclenche, une ligne n’a pas de :, c’est une transcription authentiquement mal formée, et la correction consiste à décider entre sauter les mauvaises lignes ou lever ; notre strip() + filtre saute les lignes vides, pas les lignes mal formées.

1.2 Vérifie l’analyseur

✅ Liste de vérification

  • load_transcript renvoie 11 interventions pour transcript.txt, chacune un dict avec time, speaker et text.
  • ✅ Une intervention dont le message contient un deux-points (par exemple une URL) s’analyse toujours avec le message entier intact.
  • ✅ Les lignes composées uniquement d’espaces ne créent jamais d’interventions vides.
  • ✅ Tu peux prédire ce que parse_line("[05:00] Sam: A: B") renvoie, et il n’y a qu’une seule bonne réponse pour speaker.

🤔 Question(s) socratique(s)

  • Nous découpons l’horodatage avec split("]", 1). Qu’est-ce qui casserait pour un message comme [00:30] Zara: the link is [here], et partition sur l’horodatage est-il un choix plus robuste ?
  • L’analyseur suppose des horodatages [MM:SS]. Si une transcription utilisait 00:04:32 (de vrais temps d’horloge), quel champ changerait silencieusement de forme, et l’analyseur devrait-il valider le format du temps, ou rester de type lâche ?

Étape 2 : Segmente les intervenants et compte le temps de parole

Une transcription a deux dimensions : qui l’a dit, et combien ils l’ont dit. Cette étape agrège les interventions en totaux par intervenant, mots par intervenant, interventions par intervenant, les nombres qui montrent instantanément si une voix a dévoré la réunion. Le modèle est Counter/groupement par clé, la même forme que « grouper les ventes par région », appliquée au temps de parole.

2.1 Agrège les statistiques par intervenant

# segments.py
from collections import Counter
from parse import load_transcript

def speaker_stats(turns: list[dict]) -> dict[str, dict]:
    stats = {}
    for t in turns:
        s = t["speaker"]
        row = stats.setdefault(s, {"words": 0, "turns": 0})
        row["words"] += len(t["text"].split())
        row["turns"] += 1
    return stats

def word_share(stats: dict[str, dict]) -> dict[str, float]:
    total = sum(r["words"] for r in stats.values()) or 1
    return {s: r["words"] / total for s, r in stats.items()}

if __name__ == "__main__":
    turns = load_transcript("transcript.txt")
    stats = speaker_stats(turns)
    for s, r in sorted(stats.items()):
        print(f"{s:<6} {r['words']:>3} words  {r['turns']} turns  {word_share(stats)[s]:.0%}")

stats.setdefault(s, {...}) renvoie la ligne existante si l’intervenant est déjà vu, ou insère une nouvelle ligne remise à zéro et la renvoie, l’idiome « construire un dict de lignes » qui garde la mutation sur une ligne. Les mots sont comptés avec t["text"].split(), les interventions avec un simple compte, et word_share normalise en un pourcentage robuste à une réunion vide grâce à la garde or 1.

👟 Indice de départ : Exécute les statistiques, puis compte à la main les mots de Priya dans la transcription et confirme que le nombre correspond, la garde contre « fais confiance à l’outil », c’est « tu as déjà compté une fois ».

🎯 Résultat attendu : Trois lignes comme Priya 41 words 5 turns 43%, Tom 30 words 3 turns ...%, Zara ... ... ...% dont les parts de mots totalisent 100 %.

🩹 Si ça ne marche pas : Si un intervenant manque entièrement, ses interventions ont été analysées sous un autre nom (espace de fin, vérifie le .strip() du locuteur à l’étape 1). Si les parts totalisent 99 % ou 101 %, c’est de l’arrondi flottant, pas un bug, affiche avec :.0% ou normalise une fois ; si une part s’imprime en nan%, tu as atteint le cas limite total = 0 et la garde or 1 n’est pas en place.

2.2 Vérifie la segmentation

✅ Liste de vérification

  • ✅ Chaque intervenant distinct de la transcription a exactement une ligne, les doubles mentions du même jour se dédoublonnent en un seul compte cumulé.
  • ✅ Un intervenant à zéro mot (s’il y en a un) affiche 0 words, 0%, jamais une ligne manquante.
  • ✅ Ton total de mots compté à la main sur tout le fichier correspond à la somme des lignes.

🤔 Question(s) socratique(s)

  • La part de mots est une métrique de quantité : la personne qui parle le plus tient la salle. De quoi une analyse « Priya a-t-elle dominé ou simplement questionné ? » aurait-elle besoin que les mots seuls ne peuvent pas dire, indices : longueur moyenne de prise de parole, nombre de questions, et le ratio des déclarations sur les lignes de passation comme « Please send it to me » ?
  • Nous regroupons par chaîne de locuteur exacte ; Priya et priya seraient deux personnes. Où est le bon point de normalisation, à l’analyse, à l’agrégation, ou jamais, et que dit le choix sur l’outil que tu construis ?

Étape 3 : Trouve les décisions

Les résumés qui listent « ce qui s’est passé » s’oublient ; les résumés qui listent des décisions font le compte rendu. Cette étape scanne la transcription pour le langage de la clôture, des verbes comme agreed, decided, confirmed, decided, et extrait la phrase entière comme décision. Basé sur des règles et superficiel, mais c’est exactement ainsi que se comporte la première passe d’un bot de résumé.

3.1 Scanne les verbes de décision

# decide.py
from parse import load_transcript

DECIDE_VERBS = ("agreed", "decided", "confirmed", "voted", "ruled", "settled")

def find_decisions(turns: list[dict]) -> list[str]:
    decisions = []
    for t in turns:
        for verb in DECIDE_VERBS:
            if verb in t["text"].lower():
                decisions.append(f"{t['time']} {t['speaker']}: {t['text']}")
                break
    return decisions

if __name__ == "__main__":
    for d in find_decisions(load_transcript("transcript.txt")):
        print(d)

Le double nid de boucles (interventions × verbes) est assez petit pour rester honnêtement O(n·m) ; le lower() garantit que agreed correspond à Agreed, et le break assure un verbe par intervention, une ligne qui dit à la fois « agreed » et « confirmed » compte une fois. Une décision est rendue avec son time et son speaker attachés, pour que le résumé garde la provenance (« à 00:15 Priya a décidé… ») plutôt qu’une clause nue.

👟 Indice de départ : Exécute-le, puis compare la sortie au fichier à l’œil, tu vérifies que « We agreed the beta opens next Monday » et « We decided the pricing page stays as-is » apparaissent tous les deux, et que la ligne 9 de Zara (will draft) n’apparaît PAS, « rédiger » est une action, pas une décision, et cette distinction est tout l’intérêt de cette étape.

🎯 Résultat attendu : Deux lignes, l’intervention 00:15 « beta opens next Monday » et l’intervention 00:44 « pricing page stays as-is », et rien des lignes 3, 6 ou 9.

🩹 Si ça ne marche pas : Si une seule décision apparaît, un verbe a été raté parce que la transcription utilisait un synonyme (agree au lieu de agreed), soit étends le tuple, soit mets en minuscules et tronque (essaie startswith sur une racine de verbe) de façon cohérente. Si [00:08] Tom: Design shipped... est inclus, le mot « decided » apparaît dans une phrase ordinaire (« We decided… »), c’est un vrai positif ici, mais un futur verbe shipped serait un faux positif que ton tuple doit éviter en nommant des mots exacts.

3.2 Vérifie les décisions

✅ Liste de vérification

  • ✅ Les deux vraies décisions de l’échantillon apparaissent avec horodatage et locuteur.
  • ✅ Aucune ligne d’action « I will draft… » ou « I’ll block out… » n’est mal classée en décision.
  • ✅ Une intervention ne mentionnant aucun verbe de décision ne contribue rien à la liste.
  • ✅ Tu peux expliquer la ligne délibérée entre « agreed » (décision) et « will draft » (action).

🤔 Question(s) socratique(s)

  • Notre liste de verbes est un tuple fixe, donc une décision formulée comme « Priya : le bêta est lancé » (sans aucun verbe de décision) passe à travers. Quel second signal indépendant, un point d’interrogation, un « right? », un « oui » en retour, pourrait la signaler, et quels faux positifs ajouterait-il ?
  • agreed dans « I agreed with you earlier that the design was rough » n’est pas contextuellement une décision, pourtant notre scan la rapporte. Un faux négatif « aucune décision rapportée » est-il toujours acceptable, et où tracerais-tu la ligne précision/rappel pour un bot de notes (indice : préfère de nombreux vrais positifs à un faux positif occasionnel, aujourd’hui) ?

Étape 4 : Extrais les tâches à faire avec leurs responsables

Les décisions disent ce qui a changé ; les tâches à faire disent qui fait quoi pour quand, et c’est la partie que les gens vivent réellement. Le modèle d’extraction : un responsable apparaît comme un nom immédiatement suivi (à quelques mots près) d’un verbe au futur (will, owns, publish). C’est un proxy superficiel et explicable de ce qu’un transformer ferait par attention, et pour un bot de notes, l’explicable bat la magie.

4.1 Extrais les paires responsable + tâche

# actions.py
from parse import load_transcript

ACTORS = ("Priya", "Tom", "Zara")
TASK_WORDS = ("will", "owns", "draft", "send", "block", "publish", "write", "set")

def extract_actions(turns: list[dict]) -> list[dict]:
    actions = []
    for t in turns:
        lowered = t["text"].lower()
        for actor in ACTORS:
            if actor.lower() not in lowered:
                continue
            for word in TASK_WORDS:
                if word in lowered:
                    actions.append({"time": t["time"], "owner": actor, "task": t["text"]})
                    break
    return actions

if __name__ == "__main__":
    for a in extract_actions(load_transcript("transcript.txt")):
        print(f"{a['time']}  {a['owner']} -> {a['task']}")

Encore deux boucles imbriquées, mais l’ordre des gardes compte : vérifier actor d’abord et continuer saute une intervention entière si elle ne mentionne pas une personne connue, tout le filtre « responsabilité ». Le break après le premier mot de tâche garde une ligne pour une action même quand elle dit « will draft the video and then send the email ». Le texte de la tâche est l’intervention entière (tu gardes la phrase pour le contexte) ; un outil plus sophistiqué découperait exactement la sous-chaîne, note ceci comme une simplification délibérée.

👟 Indice de départ : Exécute-le puis marque à la main les vraies actions : Tom → vidéo, Zara → e-mail, Priya → changelog. La sortie doit nommer chaque responsable et la bonne intervention, et la ligne 6 « send it to me » ne doit pas voler l’action de Tom.

🎯 Résultat attendu : Trois interventions complètes chacune étiquetée d’un responsable : Tom -> I'll block out Thursday to prep the demo video, Zara -> I will draft the onboarding email today, Priya -> And I'll publish the changelog on Friday, dans l’ordre de la transcription.

🩹 Si ça ne marche pas : Si la ligne « publish » de Priya manque, son nom n’est pas dans ACTORS ou « publish » n’est pas dans TASK_WORDS, ce sont des données que tu contrôles ; ajoute des noms et des verbes, et préfère relancer plutôt que « régler le modèle ». Si la ligne de Tom affiche le responsable Zara, le texte de l’intervention mentionne Zara (ligne 8 : « Zara, Tom owns the video ») et Tom, une ambiguïté authentique, résolue pour l’instant par premier acteur trouvé, et qui mérite ton commentaire TODO, pas un hack.

4.2 Vérifie les actions

✅ Liste de vérification

  • ✅ Chacune des trois vraies actions apparaît une fois, avec le bon responsable et le bon texte d’intervention.
  • ✅ « send it to me » (une demande) n’est pas extrait comme tâche à faire pour Tom ou Zara.
  • ✅ Un locuteur sans verbe d’action (une intervention purement conversationnelle) ne contribue rien.
  • ✅ Tu peux formuler pourquoi « le responsable apparaît dans la même intervention qu’un mot de tâche » est un proxy, pas la vérité.

🤔 Question(s) socratique(s)

  • Le responsable est quiconque dont le nom apparaît dans une intervention, mais dans « Zara, Tom will own the video », le responsable (Tom) et la personne interpellée (Zara) diffèrent. Quelles données permettraient de lever l’ambiguïté, ordre des mots, proximité du verbe, ou position sujet, et quel est le signal le moins cher ?
  • « I will publish the changelog on Friday » assigne au locuteur ; « Tom will publish the changelog » assigne à quelqu’un d’autre. Notre extracteur traite les deux comme « mention = responsable ». Que changerait une vérification speaker == owner dans la confiance accordée à la liste d’actions, et la règle locuteur-d’abord est-elle un bon défaut pour les notes de réunion ?

Étape 5 : Compose le résumé et livre-le

Jusqu’ici tout produit des fragments ; le résumé est le produit. L’étape 5 assemble les statistiques des locuteurs, les décisions et les actions en un seul fichier lisible, la chose que tu collerais réellement dans le chat de groupe après une réunion, et l’enregistre pour que n’importe qui puisse l’ouvrir.

5.1 Assemble et écris le rapport

# summary.py
from pathlib import Path
from parse import load_transcript
from segments import speaker_stats, word_share
from decide import find_decisions
from actions import extract_actions

def build_summary(turns: list[dict]) -> str:
    stats = speaker_stats(turns)
    lines = [f"MEETING SUMMARY — {len(turns)} turns",
             "\nSpeakers by share:",
             *[f"  {s}: {r['words']} words ({word_share(stats)[s]:.0%})"
               for s, r in sorted(stats.items(), key=lambda kv: -kv[1]['words'])],
             "\nDecisions:", *[f"  [{d}]" for d in find_decisions(turns)],
             "\nAction items:", *[f"  [{a['time']}] {a['owner']}: {a['task']}"
                                  for a in extract_actions(turns)]]
    return "\n".join(lines)

if __name__ == "__main__":
    turns = load_transcript("transcript.txt")
    Path("summary.txt").write_text(build_summary(turns), encoding="utf-8")
    print(build_summary(turns))

Composer un rapport depuis des sous-résultats est l’étape d’« assemblage » et l’habitude à emporter dans n’importe quel outil plus gros : chaque étape précédente reste une petite fonction pure, et build_summary ne fait que les composer, donc le résumé ne peut pas en savoir plus que les parties, et une partie défaillante est une fonction à tester. Trier les locuteurs par mots décroissants (key=lambda kv: -kv[1]['words']) met la voix dominante en premier, ce qui est en soi un constat.

👟 Indice de départ : Écris summary.txt, puis ouvre-le dans un éditeur de texte et lis-le comme si tu avais raté la réunion entière, ton critère pour « ça marche », c’est qu’un inconnu puisse reconstituer la réunion depuis ce seul fichier.

🎯 Résultat attendu : summary.txt contenant l’en-tête avec le nombre d’interventions, les trois locuteurs avec totaux et parts de mots, les deux décisions, et trois tâches à faire sous des titres clairs, lisible de haut en bas sans artefacts Python.

🩹 Si ça ne marche pas : Si le fichier écrit des sections vides, une sous-fonction a renvoyé [], vérifie que les étapes précédentes tournent encore depuis __main__ avant de composer (un import cassé fait transiter None en silence). Si la sortie contient des fragments None, un f-string a heurté un retour None, chaque sous-fonction doit renvoyer une liste/une chaîne, pas None ; exécute le __main__ de chaque étape pour isoler.

5.2 Vérifie le résumé livré

✅ Liste de vérification

  • summary.txt existe et contient les quatre sections sous leurs titres.
  • ✅ Le contenu de chaque section correspond à ce que les étapes individuelles ont imprimé, rien d’ajouté, rien de perdu.
  • ✅ Un inconnu pourrait reconstituer les locuteurs, décisions et responsables de la réunion à partir du seul fichier.
  • ✅ Relancer la construction depuis une transcription différente reproduit le même pipeline proprement.

🤔 Question(s) socratique(s)

  • Le résumé compose des fragments terminés. Que changerait-il si les statistiques des locuteurs devaient s’afficher différemment dans le résumé qu’à l’étape 2 (disons en minutes au lieu de mots) ? De « reformater » ou de « transporter », est-ce le travail de build_summary, et qu’est-ce que cela dit de l’endroit où la logique d’affichage devrait vivre ?
  • summary.txt est un instantané. Qu’est-ce qui le transformerait en quelque chose que tu relancerais après chaque réunion plutôt qu’un one-off (indice : un drapeau --from et un schéma de nommage de dossier meetings/) ? Nomme la décision de configuration avant de l’écrire.

⚠️ Pièges communs

  • Les deux-points dans les messages qui cassent l’analyse. « The demo link: http://… » contient vraiment un deux-points, et split(":") sur la ligne complète sépare le nom du locuteur et le message. partition(":") après avoir retiré l’horodatage est la correction, découpe sur le premier deux-points seulement, jamais sur tous.
  • La dérive de la chaîne de locuteur qui crée des personnes fantômes. Priya vs Priya (espace de fin) ou priya vs Priya créent deux lignes à l’étape 2 et deux responsables à l’étape 4. Normalise les noms exacts une fois, à l’analyse, et laisse chaque étape en aval faire confiance à la chaîne.
  • Le temps de parole mesuré en mots vs en interventions. La part de mots traite un monologue de 40 mots et 5 petites interjections comme des locuteurs égaux. Les deux statistiques existent ; n’en présenter qu’une seule cadre silencieusement la réunion, le résumé devrait montrer mots et interventions, et laisser la domination être une lecture, pas une assertion.
  • La confusion décision/action. « We decided the beta opens Monday » est une décision ; « I’ll block Thursday » est une action. Exactement une de ces choses est impossible si elles sont fusionnées, parce que les responsables sont dénués de sens pour les décisions et que les séquences de verbes induisent en erreur pour les actions, garde les deux analyseurs séparés, comme le font les étapes 3 et 4.
  • Écrire un résumé que personne ne peut auditer. Un résumé sans horodatages ni locuteurs est une opinion ; avec eux, c’est un compte rendu. Chaque puce que notre rapport émet porte [time] et un nom, retire-les et tu as construit un outil qui paraphrase au lieu de documenter.

Ce que tu viens de construire

Un résumeur de réunions fonctionnel : une transcription entre, un summary.txt lisible sort, les locuteurs classés par part, les deux décisions extraites avec provenance, et trois tâches à faire chacune attachée à un vrai responsable. La compétence transférable est toute l’habitude de pipeline de texte : analyser en interventions structurées, agréger et grouper, reconnaître des modèles avec des règles, puis composer un rapport, le même squelette qui alimente le tri des e-mails, l’aiguillage des tickets de support, l’extraction de journaux, et (avec un modèle plus lourd au milieu) chaque « résumeur » LLM dans lequel tu as déjà collé un enregistrement d’appel. Le tien est transparent, testé ligne par ligne, et n’a besoin d’aucune clé API pour gagner son pain.

Exécute une version plus complète sans aucune configuration locale

examples/meeting-transcriber/ dans le dépôt du cours regroupe l’analyseur, les segments, les décisions, les actions et les modules de résumé plus la transcription d’exemple et un notebook qui exécute chaque étape dans l’ordre. Clone-le, ou ouvre tout le dépôt dans un GitHub Codespace, et construis un résumé dans un onglet de navigateur.

Où aller à partir d’ici

  • Vraie reconnaissance vocale (optionnelle). Si tu as une clé API de niveau gratuit pour un service de transcription (ou l’outil compatible whisper de ton ordinateur portable), remplace load_transcript par un appel subprocess qui prend un .m4a et émet du SRT, chaque étape en aval tourne déjà sur la sortie.
  • Export vers CSV. csv.writer transforme les tâches à faire en lignes (owner, task, time) que tu peux trier par responsable ou importer dans un suivi de tâches, le résumé reste lisible par un humain, le CSV devient lisible par une machine, et ce sont deux vues d’une même analyse.
  • Un filtre --speaker Sam qui résume les interventions d’une seule personne, même pipeline, un argument de filtre, instantanément utile pour « à quoi me suis-je engagé moi ? ».
  • Extraction plus lourde via un LLM (optionnelle). Fournis les interventions analysées à un modèle de niveau gratuit avec une invite système comme « renvoie du JSON des décisions et actions », l’extracteur basé sur les règles reste comme repli hors-ligne, le LLM devient la référence, et tu mesureras où l’un bat l’autre.

Partage ton projet avec la classe

Tu as construit quelque chose dont tu es fier, un résumé qui a capturé une vraie réunion, une liste d’actions que tu as réellement utilisée ? examples/student-projects/ est une galerie de projets soumis par d’autres élèves, et son README guide l’ajout du tien via une pull request du début à la fin : fork, branche, commit et ouverture de la PR. Aucune expérience préalable avec git n’est supposée.

Bienvenue dans l’écriture de Python en dehors du navigateur. 🎓

Terminez chaque étape puis marquez ce projet comme terminé pour gagner ses points.