← Retour aux démos

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.

PHASE 01
Recherche web
gpt-4o-search-preview
~50s · 4 passes
PHASE 02
Rédaction éditoriale
gpt-5-mini
~40s · 3 prompts //
PHASE 03
Rendu HTML
render_page.py
instantané
OUTPUT
page.html + data.json
~120 KB · 14 sections
~$0.12 / page

⚙️ Installation

⚠️
Sécurité : Les clés API sont actuellement en dur dans 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

Billoff/
├── 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

01_generate_v1.py
Orchestrateur principal
Gère les 3 phases, la gestion d'erreurs par passe, le calcul des coûts, et la sauvegarde JSON + HTML. Point d'entrée CLI.
render_page.py
Moteur de rendu HTML
~830 lignes. Contient un builder par section. Logique de seuils de compétude intégrée. Peut être appelé standalone depuis un JSON existant.
config.py
Configuration centrale
Clés API (OpenAI, Gemini, Anthropic), noms des modèles, tables de coûts, et les 7 prompts complets (recherche + éditorial).
shared.py
Utilitaires partagés
extract_json() (robuste face aux LLMs), build_result(), print_cost_summary(). Réutilisable dans d'autres variantes du pipeline.

🎛️ 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é.

💡
La fonction _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

Tous les liens externes sont déjà patchés avec rel="noopener noreferrer nofollow". Les liens internes Billoff (header, breadcrumb, sidebar, CTA) ne portent pas nofollow.
EndroitAttribut rel appliqué
Cartes concurrents (website)noopener noreferrer nofollow
Threads communauté (forum)noopener noreferrer nofollow
Fiche → site officiel + supportnoopener noreferrer nofollow
Header / breadcrumb / sidebar / CTApas 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é

!
Sortir les clés API hardcodées de config.py

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

1
Endpoint API POST /api/generate

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).

2
Système de cache JSON

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.

3
Stocker les JSON en base (Postgres ou S3)

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

4
Mode --refresh-only

Régénère l'HTML depuis le JSON existant sans rappeler les APIs. Idéal pour itérer sur le design/templates.

5
Internationalisation (--country / --currency)

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...

6
Versionner les pages générées

Sauvegarder chaque génération avec timestamp (service_data_sfr-mobile_20260331.json) pour comparer les évolutions de données au fil du temps.

7
Affiner les seuils de compétude par catégorie

Un service "bank-finance" aura naturellement moins de concurrents streaming. Ajouter une surcharge de seuils par catégorie dans SECTION_THRESHOLDS.

🟢 Qualité & SEO

8
Valider le Schema.org avec Google Rich Results Test

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.

9
Audit Lighthouse / accessibilité

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.

10
Améliorer la vérification URL (HEAD → GET fallback)

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

🎯
Tester au moins 2-3 services de catégories différentes avant d'aller en production. Les seuils de compétude ont été calibrés sur SFR Mobile + YouTube Premium uniquement.
# 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

A
Sections skippées — cohérence avec les données réelles

Regarder les logs "⚠️ [section] skipped" dans le terminal. Si trop de sections sautent pour un service connu, baisser le seuil correspondant dans SECTION_THRESHOLDS.

B
Blocs partage Spliiit/Sharesub — catégorie correcte

Vérifier que les blocs partage n'apparaissent PAS pour EDF, Boursorama, SFR. Et qu'ils apparaissent bien pour Netflix, Spotify, Adobe.

C
URLs concurrents — toutes en nofollow

Inspecter le HTML généré : chaque <a> vers un site concurrent doit avoir rel="noopener noreferrer nofollow".

D
Coût réel vs estimé

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)

Génération batch automatique

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.

Diff de données entre régénérations

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.

Injection CMS directe

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.

📦 Téléchargements

🗜️
billoff-pipeline-v1.zip Scripts Python + templates HTML + données JSON exemples · ~128 Ko
⬇ Télécharger le ZIP

Scripts individuels

py
01_generate_v1.py Orchestrateur principal — 528 lignes
⬇ Télécharger
py
render_page.py Moteur de rendu HTML — ~900 lignes · nofollow + seuils intégrés
⬇ Télécharger
py
config.py Clés API, modèles, 7 prompts complets
⬇ Télécharger
{}
service_data_schema.json Schéma JSON complet — référence pour l'intégration CMS/API
⬇ Télécharger
{}
service_data_sfr-mobile.json Exemple JSON généré — SFR Mobile (dernier run)
⬇ Télécharger