V0 · mode d’emploi

Aide

Prototype de fact-checking en direct des discours TNT. Rien n’est publié sur X. Les cartes restent formulées « il a été affirmé à telle heure sur telle chaîne ». Le transcript affiche le nom quand le STT l’identifie.

Test à blanc. Le risque du projet n’est pas le tuner, c’est un fact-check faux ou trop affirmatif. Les cartes servent à relire, valider ou jeter — pas à publier.

L’interface

Cinq pages, toujours accessibles en haut à gauche.

Salle de rédaction

C’est le poste de travail. Deux colonnes : le transcript à gauche, les affirmations à droite. Chaque colonne défile toute seule.

Observateur

La salle montre le même moteur que Philippe pilote par webhook. Start / Stop / Statut dans la barre du haut font la même chose que ses URLs. L’onglet sert aussi à voir transcript, cartes, mails et JSON. Les directs déjà captés restent dans le tableau de bord.

LLM

Gemini fait le preflight et la recherche. Il faut une clé API_GEMINI dans env.local.

Transcript

Chaque ligne est un extrait horodaté. Le flux de Philippe envoie deux pistes :

Le bouton CC affiche ou masque les sous-titres. Le moteur fact-checke le STT ; le CC part en contexte (noms, chiffres), pas comme une 2ᵉ source d’affirmations. Détail du décalage et des délais : Timing.

Chaque ligne STT porte le locuteur s’il est identifié (pastille dans le transcript). unknown devient Animateur : journaliste / plateau. On ne lance le fact-check que si au moins un invité nommé parle dans la fenêtre. Animateur seul = rien, ni preflight ni search.

Tu peux sélectionner des extraits consécutifs (glisser, ou Maj+clic) pour forcer un fact-check manuel, même si le preflight n’a rien sorti — à condition qu’un locuteur y soit reconnu.

Affirmations

Une carte = une affirmation reformulée, puis le verdict et l’explication. En pied : heure, durée d’analyse, tokens in/out, coût.

Direct partagé

On ne pilote pas le tuner. Philippe lance la capture TNT de son côté, puis dit au moteur quand fact-checker. Tous les visiteurs de la salle voient la même session serveur.

Webhook Philippe

Serveur à serveur, token dans WEBHOOK_TOKEN (header X-Webhook-Token, Authorization: Bearer ou ?token=).

Dans la salle, Start / Stop / Statut appellent les mêmes actions sans token : POST /api/session/enable, POST /api/session/disable, GET /api/session/status.

Une fois armé, le suivi du live LEL est automatique (mails + JSON vers /truth). La salle affiche veille tant que personne n’a envoyé Start (bouton ou webhook).

Un redémarrage du serveur (déploiement) désarme le moteur : il faut un nouveau Start.

Livraison

Chaque nouvelle carte (pas une mise à jour) part de deux façons, sans bloquer la salle si ça échoue :

Schéma : lel.media/factcheck v1. Fuseau Europe/Paris. Dedans : affirmation, locuteur, verdict, sources, extraits, tokens, search_enabled / search_used / search_queries, coût. search_queries: 0 veut dire que Gemini n’a pas lancé Search (souvent de la mémoire), pas que le compteur est cassé.

Tableau de bord

Vue d’ensemble des directs persistés : extraits, affirmations, tokens, coût. Ouvre une émission pour voir les cartes. Les heures sont en Europe/Paris.

Les encarts de verdict (Tous, Vrai, Faux…) filtrent la liste en dessous. Tous est sélectionné par défaut.

Seuls les directs TNT sont enregistrés. Un replay d’archive ne crée pas une nouvelle ligne.

Sources

Catalogue des bases que Gemini doit chercher en priorité : INSEE, OCDE, Eurostat, ministères, Parlement, Cour des comptes, data.gouv.fr, Our World in Data, etc.

Ce n’est pas un mur étanche : si aucune source de la liste ne couvre le sujet, Gemini peut élargir. La presse peut contextualiser, elle ne doit pas être la source principale quand une base institutionnelle existe.

Prompts

Page Prompts : les deux consignes envoyées à Gemini. On peut les lire, les modifier, les enregistrer, ou revenir au texte d’origine.

Un prompt enregistré est utilisé dès le prochain appel. Si tu supprimes une variable, l’enregistrement est refusé.

La technique

Moteur Python (FastAPI) dans src/factcheck/. UI locale, SQLite par défaut. L’objectif n’est pas un bus de messages : un SSE suffit pour le live.

Qui fait quoi

Le contrat V0 : le moteur renvoie un résultat structuré (affirmation, chaîne, heure, locuteur, verdict, explication, sources, confiance). Philippe allume et coupe via webhook ; le JSON part vers /truth ; la carte part par mail. Toujours rien sur X.

Flux TNT

Base : https://beta.leselectronslibres.com

Le parseur (ingest.py) tolère plusieurs formes de JSON. Si l’API chaînes est down, l’UI bascule sur une liste figée (BFM, CNEWS, etc.) : le live peut quand même marcher.

Pipeline, de bout en bout

  1. Start (salle ou webhook Philippe) → statut LEL (/ui/capture/state) → si live, abonnement SSE → affichage dans la salle.
  2. Buffer local (pas de LLM) — recolle les extraits en fenêtres.
  3. Locuteur — s’il n’y a que l’animateur / unknown, on s’arrête (pas de LLM).
  4. Preflight — gemini-3.5-flash-lite, sans search : 0 à 2 affirmations vérifiables, ou rien.
  5. Dédoublonnage local — on jette les paraphrases déjà vues.
  6. Fact-check — gemini-3.8-flash + Google Search, seulement s’il reste une affirmation.
  7. Carte à l’écran, SQLite si c’est un direct, POST /truth, mail HTML + JSON joint.

Deux appels LLM peuvent tourner en parallèle (sémaphore). Le transcript continue d’arriver pendant que Gemini travaille. Les durées : Timing.

Timing : de la phrase à la carte

L’affichage du transcript est immédiat. L’attente ne concerne que le déclenchement du LLM. Les durées ci-dessous sont celles du code actuel, pas une promesse de SLA.

Étapes, dans l’ordre

Étape Durée Ce qui se passe
Tuner / WhisperLive ~30 s, une fois Avant le premier extraits. Le hint de la salle. Ensuite le flux est continu.
Affichage STT / CC 0 s Chaque extraits SSE s’affiche tout de suite. Rien n’est retenu à l’écran.
Buffer STT ~6–15 s de parole 3 extraits + fin de phrase (ou 280 car.). Filets : 720 car. ou 6 s de silence. Voir Buffer.
Hold CC 0–4 s On attend le CC de la même plage de parole (start_at–end_at, ±3 s). Le at STT est l’heure de validation WhisperLive, pas l’heure où c’est dit : sans ces champs, on collerait le bandeau de la mauvaise phrase. 0 s si le CC est déjà là ; 4 s max (mesuré : toujours arrivé en ≤ 3 s).
Preflight Lite ~1–4 s Sans search. Souvent zéro affirmation : on s’arrête là.
Fact-check Flash + search ~8–20 s Seulement s’il reste une phrase. Deux analyses max en parallèle. C’est en général le plus long.

De la phrase dite à la carte

Parcours Après la fin de la fenêtre STT Après le mot dit
CC déjà aligné ~10–25 s (LLM seul) ~20–40 s
Cas courant ~15–35 s ~25–45 s
Pire cas (hold 4 s + search lent) ~20–25 s ~35–45 s

Le hold n’ajoute pas 4 s à chaque fois : c’est un plafond. Une sélection manuelle saute le buffer et le hold (le CC voisin est collé sur la même plage de parole).

Où on a de la marge

Réglages dans buffer.py (CAPTION_WAIT, CAPTION_SLACK, seuils du buffer) et le sémaphore LLM dans pipeline.py.

Buffer : recomposer des phrases

Le STT arrive en miettes. On n’envoie pas chaque miette au modèle. TranscriptBuffer agglomère sans inventer de ponctuation :

Ce n’est pas un parseur syntaxique. C’est un regroupement pour que le preflight voie un bout de discours, pas une miette.

Preflight Flash Lite

Avant tout search, gemini-3.5-flash-lite reçoit la fenêtre et répond à : ce texte est-il fact-checkable ?

Il ne doit sortir que des faits objectivement vérifiables : chiffres, dates, comparaisons mesurables. Il écarte opinions, slogans, promesses, questions de journalistes. Chaque affirmation est reformulée en une phrase autonome (pronoms résolus, STT corrigé via le CC s’il est là, 15–30 mots). C’est cette phrase qui sera vérifiée, pas le brut.

Réponse vide = on s’arrête. Coût typique : 0,30 $ / M tokens in, 2,50 $ / M out. Pas de Google Search à cette étape.

Une sélection manuelle contourne le preflight : on envoie le texte choisi tel quel au fact-check, seulement s’il y a un locuteur reconnu.

Fact-check + search

gemini-3.8-flash avec l’outil Google Search. Le prompt lui donne la liste des sources actives et lui demande de les prioriser.

Gemini n’accepte pas JSON structuré et search en même temps. On laisse le modèle parler, on parse, et s’il dérape on relance un petit appel « réécris ça en JSON strict » (sans search).

Les URL du grounding sont fusionnées avec celles du JSON, les domaines de la liste blanche passent devant.

Tarif intro 3.8 Flash (jusqu’au 31/12/2026, thinking inclus) : 0,75 $ / M in, 3,75 $ / M out.

Dédoublonnage

Même chiffre répété dix fois à l’antenne ≠ dix cartes. ClaimMemory compare les phrases :

La mémoire vit le temps de la session. Un nouveau direct repart de zéro.

Verdicts et relecture

Échelle : vrai, plutôt vrai, mixte, plutôt faux, faux, insuffisant.

Si le verdict est insuffisant ou si la confiance est sous 70 %, la carte passe en « à relire ». Valider / Rejeter est humain, local à la session en cours.

Persistance

SQLite dans data/factcheck.db (ou PostgreSQL si DATABASE_URL). Tables : recordings, segments, checks, data_sources.

Un direct ouvert reste ouvert tant que la session n’est pas coupée (ou le process tué). Relancer uvicorn coupe le live en mémoire et remet le moteur en veille ; les extraits déjà écrits restent. Détail des envois : Livraison.

Coûts affichés

Chaque carte montre tokens in/out, le nombre de requêtes Google Search réellement lancées, et une estimation en dollars (thinking compris + search). Gemini 3 facture chaque requête du modèle, pas l’appel API : une carte peut coûter 2 ou 3 recherches. Tarif : 14 $ / 1 000 requêtes. L’UI affiche ce tarif unitaire ; Google offre 5 000 recherches / mois, donc la facture réelle peut être plus basse en début de période.

Le tableau de bord agrège par émission. Le preflight Lite n’ouvre pas le search : d’où le filtre avant le grounding.

Limites de la V0

Identification vocale côté Philippe : le flux STT envoie speaker (nom, ou unknown = animateur). Le roster vient de /ui/speakers. Sans nom d’invité, Gemini n’est pas appelé.