Agrégateur de presse française inspiré de Ground News : pour chaque événement d'actualité, qui le couvre selon l'orientation politique des rédactions, comment le cadrage diverge entre bords, et quels sujets sont des angles morts ("blindspots") d'un côté du spectre.
100 % gratuit et local : aucune API payante, aucun compte tiers, aucun cloud facturé. Le seul téléchargement est le modèle d'embeddings (~500 Mo, une fois).
python3 -m venv .venv && source .venv/bin/activate
pip install torch --index-url https://download.pytorch.org/whl/cpu # CPU-only, ~10x plus léger
pip install -e .Python ≥ 3.11 requis.
python run.py ingest # fetch des flux RSS (titres + chapôs, rien d'autre)
python run.py cluster # regroupement par événement (seuil : --threshold, défaut 0.92)
python run.py analyze # blindspots + contraste de vocabulaire (sans écraser les catégories v2)
python run.py render # génération du site statique dans site/
python run.py pipeline # les quatre dans l'ordre (+ purge > 30 jours)
python run.py serve # sert site/ sur http://127.0.0.1:8000
python run.py inspect # calibration : clusters aléatoires + paires en zone grise
python run.py check-leaks # vérifie qu'aucun chapô RSS ne fuit dans le HTML public
python run.py check-classifications # vérifie le référentiel sources/orientations/stylesEn local, un cron toutes les 30 minutes :
*/30 * * * * cd /chemin/vers/spectre && .venv/bin/python run.py pipeline >> pipeline.log 2>&1Le workflow .github/workflows/pipeline.yml exécute le pipeline toutes les
30 minutes et déploie site/ sur GitHub Pages via artifact
(upload-pages-artifact + deploy-pages) — le HTML ne transite jamais par git.
La base SQLite est compactée (checkpoint WAL + VACUUM) puis committée dans le
repo à chaque run où elle change ; la purge à 30 jours et la mise à NULL des
embeddings hors fenêtre de 72 h la gardent petite.
Mise en route :
- Créer le repo GitHub et pousser le projet (
spectre.dbpeut être absent au premier run, il sera créé). - Settings → Pages → Source : "GitHub Actions" (pas "Deploy from a branch").
- Ajuster
REPO_URLdansspectre/render.py(lien du footer). - Lancer le premier run à la main : onglet Actions → pipeline → Run workflow
(
workflow_dispatch).
*/30 * * * * = toutes les 30 min
UTC, pas heure de Paris) et le déclenchement est souvent décalé de plusieurs
minutes selon la charge de la plateforme — normal, pas un bug. Le
concurrency group avec cancel-in-progress: false garantit qu'un run qui
committe la base n'est jamais annulé par le suivant.
En plus des statistiques, python run.py analyze --ollama génère une analyse
qualitative du cadrage (résumé neutre, angle par bord, omissions) via un LLM
local — Ollama, aucun service payant. Modèle par
défaut : llama3.2:3b (validé sur machine 8 Go ; qwen3:4b dispo via la variable d'env mais trop lent sous ~6 Go de VRAM), configurable :
ollama pull llama3.2:3b # le défaut
export SPECTRE_OLLAMA_MODEL=qwen3:4b # si ≥ 16 Go de RAM (think désactivé automatiquement)Garde-fous : clusters de 4 à 15 articles avec ≥ 2 orientations seulement, sortie JSON validée strictement (schéma, longueurs max), rejet de toute réponse qui recopie ≥ 8 mots consécutifs d'un titre, cache (re-analyse uniquement si la composition du cluster a changé de ≥ 2 articles). Les sections publiées sont étiquetées comme générées par IA.
Les runners GitHub n'ont pas Ollama : les analyses qualitatives se font sur ta machine et voyagent via la base committée.
git pull --rebase # la base bouge toutes les 30 min (cron)
python run.py analyze --ollama # analyse les clusters actifs
# RELIS les sections générées (pages cluster en local) avant de committer :
# sur un petit modèle, ~30 % des sorties échouent à la relecture humaine
# (inversions factuelles, hallucinations) malgré la validation automatique.
git add spectre.db && git commit -m "chore: analyses qualitatives" && git push
# si un cron est passé entre-temps : git pull --rebase puis git push à nouveauLe prochain déploiement (cron ou workflow_dispatch) rendra les sections.
Normal et attendu : le payload d'un cluster encore actif peut être écrasé
plus tard si le cluster évolue (≥ 2 nouveaux articles) ; et un cluster sorti
de la fenêtre de 72 h garde son analyse stockée telle quelle, elle n'est
jamais recalculée (ses chapôs ont été purgés).
- Ingestion : flux RSS uniquement, titres + chapôs. Pas de scraping du texte intégral (droits voisins). Dédup par guid puis URL canonique sans trackers. Les extraits RSS (chapôs) ne sont conservés que le temps du traitement (72 h), jamais publiés : passé la fenêtre de clustering, ils sont effacés de la base — le repo public ne redistribue donc pas les contenus de presse, il ne contient que titres, liens et résultats d'analyse.
- Clustering : embeddings
intfloat/multilingual-e5-small(local, CPU, préfixequery:), clustering incrémental glouton sur fenêtre 72 h, seuil 0.92 calibré sur données réelles (les similarités E5 sont compressées vers le haut : médiane du corpus 0.81), règle anti-chaînage (similarité exigée au centroïde ET au membre le plus proche). - Blindspots : couverture en sources distinctes par bord, normalisée par le nombre de sources actives du bord. Score −1 (gauche seule) à +1 (droite seule), angle mort à ≥ 80 % d'un seul côté.
- Style éditorial : chaque source porte une dominante
factuel,mixteouopinionpour distinguer les fils d'actualité bruts des médias plus éditorialisés. Cette dimension complète l'axe politique sans le remplacer. - Contraste de vocabulaire : log-odds ratio avec prior de Dirichlet informatif (Monroe et al. 2008, "Fightin' Words"), prior estimé sur tout le corpus, plancher de 50 tokens par bord sous lequel on affiche "corpus insuffisant" plutôt qu'un faux contraste.
- La classification des orientations est indicative et débattable — voir la page "À propos" du site. Propriétaires sourcés d'après la carte "Médias français, qui possède quoi" (Le Monde diplomatique / Acrimed).
- Fusion thématique résiduelle : deux événements proches du même domaine peuvent partager un cluster ; une divergence élevée peut signaler ce défaut.
- Le Parisien ne publie que des titres (pas de chapô) ; Les Échos, Le Point et
Politis n'ont pas de flux RSS accessible (
active: false). - Le HTML public ne contient QUE titres, liens vers les originaux et nos
analyses — jamais les chapôs RSS (vérifié par
check-leaksen CI avant chaque déploiement).
Les instantanés hebdomadaires (data/archive/*.json) sont publiés sous
CC-BY 4.0 : événements, répartitions par bord, scores de blindspot et de
divergence, termes marquants — jamais de contenu de presse. Schéma documenté
dans data/archive/README.md. Réutilisation bienvenue (chercheurs,
data-journalistes) avec attribution.
- Taille du repo : la base SQLite est committée à chaque run apportant du
contenu neuf (les runs sans nouvel article ne committent pas). SQLite étant
binaire, les deltas git sont médiocres : surveiller
du -sh .gitde temps en temps. Si l'historique devient encombrant : squash périodique de la branche, ou migrer le stockage de la base vers un artifact/release GitHub. - Santé des flux : le footer du site affiche « flux RSS : X/Y OK au dernier
passage » (rouge si un flux est tombé). Le détail par flux est dans la table
fetch_log. - Blindspots : les clusters
sportetfaits-divers(catégorisation par slug d'URL) sont écartés des colonnes principales — leur couverture déséquilibrée est structurelle, pas éditoriale.
- Sujets récurrents (« réforme des retraites » suivie sur des mois) : gelé volontairement tant qu'il n'y a pas ≥ 4 semaines d'archives — la détection par bigrammes dominants inter-semaines ne peut pas se calibrer sans données. Revoir en août 2026.