Documentation Dev
Billoff Pipeline V1 — Génération automatique de pages de résiliation SEO. Ce doc couvre l'architecture, les scripts, la logique conditionnelle, et les prochaines étapes pour ton dev.
🗺️ Vue d'ensemble
Le pipeline prend un nom de service + une catégorie en entrée, fait 4 recherches web en temps réel, rédige les textes éditoriaux en parallèle, puis assemble une page HTML complète (~120 Ko) en ~90 secondes.
⚙️ Installation
config.py. Avant tout déploiement, les sortir en variables d'environnement et ajouter config.py au .gitignore.# 1. Cloner / dézipper le projet
unzip billoff-pipeline-v1.zip
cd scripts
# 2. Installer les dépendances Python
pip install -r requirements.txt
# 3. Configurer la clé API OpenAI
export OPENAI_API_KEY="sk-..."
# 4. Vérifier que tout est OK
python3 -c "from openai import OpenAI; print('OK')"
🚀 Quickstart
cd scripts/scripts
# Telecom mobile (pas de blocs partage Spliiit/Sharesub)
python3 01_generate_v1.py \
--service "SFR Mobile" \
--category "telecom-mobile" \
--output ../../billoff-sfr.html
# Streaming vidéo (active les blocs partage)
python3 01_generate_v1.py \
--service "Netflix" \
--category "streaming-video" \
--output ../../billoff-netflix.html
# Re-render depuis JSON existant (0 coût API)
python3 render_page.py \
../data/service_data_sfr-mobile.json \
../../billoff-sfr-rerender.html
🔄 Pipeline 3 phases
Phase 1 — Recherche web (4 passes séquentielles)
Chaque passe appelle gpt-4o-search-preview avec web_search_options. Elle retourne un JSON structuré + une liste d'URLs vérifiées (HTTP HEAD).
Pass 1 → résiliation, remboursement, avis, droits, discussions
Pass 2 → tarifs, plans, essai gratuit, alternatives (suspension/downgrade/FAQ)
Pass 3 → 3-5 concurrents directs avec prix, pros, cons
Pass 4 → infos société (PDG, siège, filiale EU, support, date création)
Phase 2 — Rédaction éditoriale (3 prompts en parallèle)
Utilise ThreadPoolExecutor(max_workers=3) pour paralléliser les 3 appels à gpt-5-mini.
Prompt SEO → title, meta_description, h1, keywords, canonical_path
Prompt Content → intros par section, checklist, dark_patterns, avertissements
Prompt Témoins → 6 avis fictifs réalistes (profils variés, notes 3-5)
Phase 3 — Rendu HTML
render_page.py assemble JSON + éditorial + templates statiques. Les sections sans données suffisantes sont automatiquement skippées via les seuils de compétude (voir ci-dessous).
📁 Arborescence
├── scripts/
│ ├── 01_generate_v1.py ← Point d'entrée — orchestre tout
│ ├── render_page.py ← Moteur de rendu HTML
│ ├── config.py ← Clés API, modèles, prompts complets
│ └── shared.py ← extract_json(), print_cost_summary()
├── templates/
│ ├── _css.html ← Toute la CSS (dark mode inclus)
│ ├── _svg_defs.html ← Bibliothèque icônes SVG inline
│ ├── _scripts.html ← JS (thème, FAQ accordion, sidebar)
│ ├── _footer.html
│ ├── _header.html
│ └── _head_meta.html
└── data/
├── service_data_schema.json ← Schéma JSON de référence
├── service_data_sfr-mobile.json
└── service_data_youtube-premium.json
📜 Scripts détaillés
🎛️ Seuils conditionnels (logique de compétude)
Chaque section de la page a un score de compétude calculé sur ses données réelles. Si ce score est inférieur au seuil, la section est skippée — aucun bloc vide n'est affiché.
_completeness_score(section_key, data) et le dict SECTION_THRESHOLDS sont dans render_page.py lignes ~20-100. Les seuils sont facilement ajustables.| Section | Seuil | Logique de calcul | Type |
|---|---|---|---|
| Hero / Actions / TL;DR | — | Toujours affichées (contenu statique de fallback) | Toujours |
| Guide résiliation | 20% | ≥ 1 méthode avec steps[] non vide | Conditionnel |
| Après résiliation | 30% | (len(keep) + len(lose)) / 6 | Conditionnel |
| Concurrents | 40% | ≥ 2 concurrents avec name + price_monthly | Strict |
| Délais / Rétractation | — | Toujours (données légales statiques comme fallback) | Toujours |
| Alternatives | 30% | ≥ 1 option parmi : can_suspend / can_downgrade / can_share | Conditionnel |
| Droits légaux | 25% | ≥ 1 loi dans consumer_rights.laws[] | Conditionnel |
| Fiche entreprise | 20% | ≥ 1 champ rempli parmi : name, HQ, CEO, support | Conditionnel |
| Témoignages | 50% | ≥ 3 témoignages générés (sur 6 attendus) | Strict |
| Communauté | 30% | ≥ 1 thread de discussion récupéré | Conditionnel |
| FAQ | 25% | ≥ 2 questions-réponses | Conditionnel |
Modifier un seuil
# Dans render_page.py, modifier le dict SECTION_THRESHOLDS :
SECTION_THRESHOLDS = {
"concurrents": 0.40, # ← augmenter si trop de pages s'affichent avec 1 seul concurrent
"temoignages": 0.50, # ← baisser à 0.30 si les témoignages LLM sont souvent absents
"faq": 0.00, # ← mettre 0.0 pour toujours forcer l'affichage FAQ
}
🔗 Liens externes — nofollow
rel="noopener noreferrer nofollow". Les liens internes Billoff (header, breadcrumb, sidebar, CTA) ne portent pas nofollow.| Endroit | Attribut rel appliqué |
|---|---|
| Cartes concurrents (website) | noopener noreferrer nofollow |
| Threads communauté (forum) | noopener noreferrer nofollow |
| Fiche → site officiel + support | noopener noreferrer nofollow |
| Header / breadcrumb / sidebar / CTA | pas de nofollow (liens internes) |
🏷️ Catégories et logique de partage
Les blocs Spliiit / Sharesub / GamsGo ne s'affichent que pour les catégories "partageables".
# Catégories avec blocs partage actifs
streaming-video | music-streaming | software-saas | gaming
# Catégories sans blocs partage
telecom-mobile | internet-fai | energy | insurance | bank-finance | transport | health
📋 Schéma JSON de sortie
Chaque service produit un fichier service_data_[slug].json réutilisable pour tout front-end ou CMS.
{
"service_identity": { name, slug, category, tagline, platforms, ... },
"company_info": { full_legal_name, parent_company, ceo, headquarters, ... },
"pricing": { plans: [{name, price_monthly, features}], free_trial_days, ... },
"cancellation": { difficulty, methods: [{platform, label, steps[]}], dark_patterns },
"post_cancellation": { keep: [], lose: [], access_until_end_of_period },
"refund": { has_refund, refund_policy, cooling_off_days, exceptions[] },
"reviews": { overall_rating, positive_themes[], negative_themes[] },
"competitors": { items: [{name, price_monthly, pros[], cons[], website}] },
"alternatives_to_cancel": { can_suspend, can_downgrade, can_share, sharing_platforms[] },
"consumer_rights": { laws: [{title, description, legal_ref}] },
"faq": { items: [{question, answer}] },
"community_threads":{ items: [{title, author, excerpt, best_answer, url}] },
"testimonials": { items: [{text, author, date, stars}] },
"verified_sources": { items: [url, ...] }
}
📌 TODO — Priorités pour le dev
🔴 Urgent / Sécurité
OpenAI, Gemini et Anthropic sont en clair dans le fichier. Passer en variables d'environnement (.env + python-dotenv) et ajouter config.py au .gitignore immédiatement.
🔴 Infrastructure de base
Créer un endpoint FastAPI (ou Express) qui prend {service, category} → lance 01_generate_v1.py → retourne l'HTML ou le JSON. Ajouter une queue de jobs (Celery ou BullMQ) pour ne pas bloquer (~90s par page).
Si service_data_[slug].json existe et a moins de N jours → skiper la Phase 1 (recherche web, ~$0.10). Régénérer seulement l'HTML avec render_page.py. Économise 83% du coût par re-run.
Le fichier service_data est la source de vérité réutilisable pour le CMS, l'API et tout autre front. Ne pas laisser sur disque en prod.
🟡 Améliorations pipeline
Régénère l'HTML depuis le JSON existant sans rappeler les APIs. Idéal pour itérer sur le design/templates.
Les constantes COUNTRY / CURRENCY / SYMBOL sont déjà dans config.py. Il suffit de les exposer comme paramètres CLI pour supporter DE, ES, UK, IT...
Sauvegarder chaque génération avec timestamp (service_data_sfr-mobile_20260331.json) pour comparer les évolutions de données au fil du temps.
Un service "bank-finance" aura naturellement moins de concurrents streaming. Ajouter une surcharge de seuils par catégorie dans SECTION_THRESHOLDS.
🟢 Qualité & SEO
Chaque page générée contient des données structurées JSON-LD. Vérifier qu'elles passent le validateur Google après chaque refonte du template.
Passer chaque page dans Lighthouse (contraste, alt sur images, ARIA, performance). Le CSS dark mode est déjà en place mais non testé systématiquement.
Certains serveurs renvoient 405 sur HEAD. Ajouter un fallback GET si HEAD échoue avec 405 dans la fonction _run_search() de 01_generate_v1.py.
🧪 Tester sur d'autres services
# Services recommandés pour tester la robustesse :
# 1. Streaming — teste les blocs partage Spliiit/Sharesub
python3 01_generate_v1.py --service "Netflix" --category "streaming-video" --output ../../test-netflix.html
# 2. Music streaming — variante moins de concurrents
python3 01_generate_v1.py --service "Spotify" --category "music-streaming" --output ../../test-spotify.html
# 3. Énergie — teste catégorie sans partage, droits spécifiques
python3 01_generate_v1.py --service "EDF" --category "energy" --output ../../test-edf.html
# 4. SaaS — teste suspension/downgrade, cas international
python3 01_generate_v1.py --service "Adobe Creative Cloud" --category "software-saas" --output ../../test-adobe.html
# 5. Banque — teste droits bancaires, peu d'infos partage
python3 01_generate_v1.py --service "Boursorama" --category "bank-finance" --output ../../test-boursorama.html
Ce qu'il faut vérifier sur chaque page de test
Regarder les logs "⚠️ [section] skipped" dans le terminal. Si trop de sections sautent pour un service connu, baisser le seuil correspondant dans SECTION_THRESHOLDS.
Vérifier que les blocs partage n'apparaissent PAS pour EDF, Boursorama, SFR. Et qu'ils apparaissent bien pour Netflix, Spotify, Adobe.
Inspecter le HTML généré : chaque <a> vers un site concurrent doit avoir rel="noopener noreferrer nofollow".
Le script affiche le coût à la fin. Vérifier que ~$0.12 par page est bien respecté. Un dérapage peut indiquer une passe de recherche qui loope.
💡 Améliorations suggérées
Court terme (1-2 jours)
# Ajouter un mode --dry-run qui simule sans appeler les APIs
python3 01_generate_v1.py --service "Netflix" --category "streaming-video" --dry-run
# Ajouter --verbose pour voir les JSON bruts de chaque passe
python3 01_generate_v1.py --service "Netflix" --category "streaming-video" --verbose
Moyen terme (1-2 semaines)
# API FastAPI avec queue de jobs
POST /api/generate
Body: { "service": "Amazon Prime", "category": "streaming-video" }
→ { "job_id": "abc123", "status": "queued" }
GET /api/job/abc123
→ { "status": "done", "html_url": "/pages/amazon-prime.html", "json_url": "/data/..." }
Long terme (roadmap)
Script qui lit une liste de services CSV et génère toutes les pages en séquence avec rate-limiting API. Idéal pour lancer 100+ pages en une nuit.
Comparer le nouveau JSON vs l'ancien pour détecter les changements de prix, de CEO, de politique de résiliation. Utile pour garder les pages à jour.
Le JSON service_data est structuré pour alimenter directement un CMS headless (Strapi, Payload, Contentful). Le front-end peut alors être découplé du pipeline.