# Slidia v2 — Étape 2 : la génération IA

**Date :** 2026-08-01
**Branche :** `slidia_v2`
**Dépend de :** `docs/specs/2026-07-31-slidia-v2-socle-design.md` (étape 1, livrée)
**Statut :** design à valider

---

## 1. Contexte

Slidia produit aujourd'hui des présentations jugées peu pertinentes face à Moodia et Scormia.
L'étape 1 a posé les fondations techniques ; cette étape s'attaque à la qualité de la
génération elle-même.

Trois chantiers, indissociables parce qu'ils partagent le même pipeline :

1. Passage à `gpt-5-mini` et réécriture des deux prompts, aujourd'hui écrits pour `gpt-4o`.
2. Un mode « format précis » : quand l'utilisateur fournit déjà une structure rédigée, Slidia
   la recopie au lieu de la réinventer.
3. L'import de PDF, y compris volumineux, que Slidia ne sait pas faire aujourd'hui.

---

## 2. Portée

### Dans la portée

- `SLIDIA_MODEL` → `gpt-5-mini`, `gpt-5-nano` sur les tâches mécaniques.
- Réécriture des prompts de plan et de contenu, avec sorties structurées `json_schema`.
- Détection de brief structuré et mode « format précis ».
- Extraction, segmentation et condensation de PDF ; envoi ciblé des sections sources.
- Comptage exhaustif de la consommation via `TokenUsageAccumulator`.
- Deux profils de génération, « Standard » et « Approfondi ».
- Reprise d'une génération interrompue sans repayer les lots déjà produits.

### Hors portée

- Toute la refonte visuelle de Slidia : accueil, page de choix de méthode, filtres,
  suppression des catégories et de l'épinglage. C'est l'étape 3.
- Scormia et Moodia ne sont pas touchés.
- L'éditeur de présentation, le mode présentateur et l'export PPTX restent inchangés.

### Découpage en deux plans

L'étape est livrée en deux plans successifs, parce que le second s'appuie sur la robustesse du
premier — construire la condensation de documents sur un pipeline qui avale silencieusement les
réponses vides serait bâtir sur du sable.

- **2a — le pipeline texte.** Projection des masques, schémas JSON, bascule `gpt-5-mini`,
  réécriture des deux prompts, les dix défauts du §8 bis, comptage branché, persistance au fil
  de l'eau. Livrable autonome : une génération depuis un brief texte devient meilleure, moins
  chère, et ne peut plus produire une coquille vide facturée.
- **2b — documents et fidélité.** Détection de structure, mode « format précis », extraction et
  segmentation de PDF, condensation, cache par empreinte, branchement provisoire du wizard.

### Rétro-compatibilité des présentations existantes

**Le format persisté de `slidesData` est inchangé.** L'identifiant de slide introduit pour
corriger l'alignement par position (§8 bis, défaut n°4) n'existe que dans la réponse du modèle :
il sert à l'assemblage, puis il est écarté avant `setSlidesData()`. Les présentations déjà en
base restent lisibles à l'identique par l'éditeur, le mode présentateur et l'export PPTX, qui
ne sont pas touchés.

### Migration de schéma

Deux champs sont ajoutés à `SlidiaPresentation` :

- `generationProfile` — `string(20)`, défaut `standard` (§4).
- `sourceHash` — `string(64)`, nullable : empreinte du document source, pour le cache d'analyse
  (§7). Nul pour une présentation créée depuis un brief texte.

Un troisième champ, `errorMessage` (`text`, nullable), stocke la cause d'un échec de génération :
sans lui, une présentation en échec ne peut rien dire à son propriétaire. Ces trois champs
arrivent dans une migration unique, générée via `make:migration`.

### Branchement provisoire assumé

L'interface de création de Slidia est refondue à l'étape 3. Pour que cette étape soit
utilisable et vérifiable en vrai, le pipeline est branché sur le wizard existant
(`templates/slidia/create.html.twig`) avec le strict minimum : une zone de dépôt de PDF dans
l'étape « Brief » et un interrupteur pour le mode « format précis ». Ces deux éléments sont
jetables et seront remplacés par la page de configuration de l'étape 3 ; tout le reste du
travail — services, prompts, routes API — est définitif.

---

## 3. Le principe qui gouverne tout : condenser pour planifier, jamais pour rédiger

L'erreur classique face à un document volumineux serait de le résumer puis de générer les
slides à partir du résumé. Le contenu final serait alors du résumé de résumé : plat, générique,
sans les chiffres ni les formulations de l'auteur.

Le pipeline fait l'inverse.

| Phase | Rôle | Modèle | Entrée |
|---|---|---|---|
| 0. Extraction | Texte du PDF, découpage en sections | *aucun* — `smalot/pdfparser`, en local | le fichier |
| 1. Sommaire | Titre normalisé et synthèse courte par section, avec son ancrage | `gpt-5-nano` | les sections, une fois |
| 2. Plan | Nombre, ordre, rôle et masque de chaque slide | `gpt-5-mini` | le sommaire seul |
| 3. Contenu | Rédaction des slides, lot par lot | `gpt-5-mini` | **le texte source intégral** des sections du lot |

Le sommaire ne sert qu'à décider de la structure. La rédaction s'appuie toujours sur le texte
original. C'est ce qui permet d'employer un modèle bon marché sur la seule phase où il ne
rédige rien.

Effet de bord notable : le pipeline actuel réexpédie le brief entier à chaque lot. Envoyer à
chaque lot les seules sections qui le concernent réduit à la fois le coût **et** la dilution —
un lot qui reçoit 6 000 tokens ciblés produit un meilleur contenu qu'un lot noyé dans 50 000.

### Les trois régimes de volume

| Texte extrait | Traitement |
|---|---|
| < 50 000 caractères | Envoi direct, phases 0 et 1 court-circuitées. Le cas de la grande majorité des usages. |
| 50 000 – 300 000 caractères | Segmentation puis condensation en sommaire avant le plan. |
| > 300 000 caractères | Refus, avec un message indiquant le volume constaté et la limite. |

Repères : 50 000 caractères ≈ 25 pages denses ; 300 000 ≈ 150 pages. Moodia applique déjà un
plafond comparable (`AppLimits::MOODIA_PLAN_PDF_MAX_LENGTH = 50000`), mais refuse au lieu de
condenser. Slidia condense, parce qu'il plafonne de toute façon à 60 slides : un document de
150 pages doit être résumé pour tenir dans un plan, ce n'est pas une optimisation mais une
nécessité.

Coût constaté attendu pour 200 000 caractères produisant 60 slides : **environ 0,06 $**, dont
un demi-centime pour le sommaire. La même présentation coûte aujourd'hui environ dix fois plus
avec `gpt-4o`, et ne tiendrait probablement pas la fenêtre de contexte.

---

## 4. Modèles et effort de raisonnement

| Étape | Modèle | Effort | Justification |
|---|---|---|---|
| Sommaire d'un PDF volumineux | `gpt-5-nano` | `minimal` | Résumé mécanique en volume |
| Plan (phase 1) | `gpt-5-mini` | `low` | Arbitrage réel : combien de slides, quel masque, quel rôle |
| Contenu (phase 2) | `gpt-5-mini` | `minimal` | Remplissage sous contrainte de longueur |
| Contenu en mode « format précis » | `gpt-5-mini` | `minimal` | Transcription : réfléchir n'apporte rien |
| Notes d'intervenant | `gpt-5-nano` | `minimal` | Trois à cinq puces |

Ces valeurs sont un point de départ, pas un dogme. Les tokens de raisonnement sont facturés au
tarif de sortie — le plus cher — et consomment le budget `max_output_tokens` partagé avec la
réponse. La ventilation journalisée par `TokenUsageAccumulator` permettra de remonter d'un cran
sur données réelles plutôt qu'au jugé.

`OpenAIConfig::SLIDIA_PLAN_TEMPERATURE` et `SLIDIA_TEMPERATURE` deviennent inopérantes : la
Responses API ne prend pas `temperature`. Elles sont supprimées, et la variété du plan passe
par l'instruction, pas par un paramètre d'échantillonnage.

### Deux profils de génération

Exprimés en langage métier, jamais sous forme de noms de modèles :

- **Standard** (défaut) — les valeurs du tableau ci-dessus.
- **Approfondi** — effort `medium` sur la phase plan uniquement, avec la mention « consomme
  davantage de tokens ». C'est là que la structure d'une présentation gagne réellement.

Un troisième cran « économique » n'a pas d'intérêt : l'écart de prix est trop faible pour
justifier une dégradation visible.

Cette étape livre le profil **côté serveur** : un paramètre porté par la présentation, valant
`standard` par défaut, qui pilote l'effort transmis à chaque phase. Son exposition à
l'utilisateur relève de l'écran de configuration de l'étape 3. Aucun sélecteur n'est ajouté au
wizard provisoire : ce serait du jetable pour un réglage que personne ne changerait avant que
l'interface ne l'explique.

---

## 5. Réécriture des prompts

Les deux prompts actuels font 111 et 118 lignes (`SlidiaAiService.php:128-238` et `:243-360`).
C'est du prompting défensif typique de `gpt-4o` : `RÈGLE ABSOLUE` en capitales, injonctions
répétées, format JSON décrit en prose. Sur un modèle de raisonnement, cette redondance est au
mieux inutile, au pire contre-productive.

Trois transformations :

1. **Le format sort du prompt.** Il est décrit par un `json_schema` transmis via
   `text.format`, avec `strict: true`. Toute la section « Format de sortie » et les rappels de
   structure disparaissent du texte.
2. **Les injonctions sont compressées.** Une règle énoncée une fois, en minuscules, vaut mieux
   que trois rappels en capitales. Objectif indicatif : diviser par deux la longueur des deux
   prompts sans perdre une seule contrainte métier.
3. **Les contraintes métier sont conservées intégralement.** La distribution des masques, les
   volumes par taille de placeholder, les formats visuels par type de slide, la liste noire de
   formulations creuses (« Il est important de noter », « En conclusion »…) : tout cela est le
   fruit d'itérations et ne se jette pas. Chaque règle du prompt actuel doit se retrouver, soit
   dans le nouveau prompt, soit dans le schéma.

Les deux schémas JSON sont versionnés dans `src/Config/Slidia/` plutôt qu'écrits en ligne dans
le service : ils sont volumineux, et les garder à part rend le service lisible.

---

## 6. Le mode « format précis »

### Détection

Un service dédié, `BriefStructureAnalyzer`, analyse le brief **côté serveur, sans appel IA**.
Il cherche des marqueurs de structure déjà rédigée : titres numérotés, en-têtes Markdown,
mentions explicites de slides (`Slide 3 :`, `Diapositive 2`), listes à puces sous des titres
courts, séparateurs répétés.

Il renvoie soit une structure détectée — nombre de sections, titres, contenus, position dans le
texte — soit rien.

Le seuil est volontairement conservateur : mieux vaut ne pas détecter une structure faible que
verrouiller à tort la génération d'un brief en prose. Un texte de trois paragraphes avec un
titre n'est pas un plan de présentation.

### Interface

Quand une structure est détectée, un bandeau apparaît sous la zone de saisie :

> **Structure détectée : 12 slides.** Slidia respectera vos titres et votre texte à la lettre.
> *[interrupteur] Laisser l'IA restructurer*

L'utilisateur n'a rien à apprendre et garde la main. L'état de l'interrupteur est transmis avec
le formulaire.

### Comportement

En mode fidèle, la phase 1 ne demande plus un plan au modèle : le plan **est** la structure
détectée. Le modèle n'intervient que pour associer un masque à chaque slide — un appel court,
avec un schéma restreint à `{layoutPath}` par slide.

La phase 2 reçoit une instruction de transcription : reprendre les titres et les contenus tels
quels, sans reformuler, sans condenser, sans ajouter. Son seul travail est la mise en forme
(puces, gras) et la répartition dans les zones du masque.

Deux libertés, et deux seulement : ajouter une slide de couverture si l'utilisateur n'en a pas
prévu, et une slide de conclusion si sa structure n'en comporte pas. Toutes deux signalées dans
le résultat.

Si un contenu déborde manifestement de la zone qui l'accueille, le contenu est conservé tel
quel — la fidélité prime sur l'esthétique, c'est le sens même du mode. L'utilisateur ajustera
dans l'éditeur.

---

## 7. Traitement des PDF

### Extraction

Réutilisation de `App\Service\Moodia\PdfExtractorService` et `PdfStreamingParser`, déjà en
place et éprouvés, via `smalot/pdfparser`. L'extraction est locale : elle ne coûte rien en API.

Le service Slidia qui l'appelle est distinct : Slidia n'a pas besoin de l'extraction d'images
ni du contexte autour des illustrations que fait Moodia.

### Segmentation

Un service `DocumentSegmenter` découpe le texte extrait en sections, en s'appuyant sur les
sauts de page et les motifs de titres. Chaque section conserve son offset dans le texte
d'origine : c'est cet ancrage qui permet, en phase 3, de renvoyer le texte source exact.

La segmentation est purement locale, sans appel IA, et testable unitairement.

### Mise en cache

Le résultat de l'extraction et du sommaire est mis en cache, indexé par empreinte SHA-256 du
fichier. Le scénario le plus fréquent est celui de l'utilisateur qui n'aime pas le résultat et
relance sur le même document : il ne repaie ni l'analyse, ni l'attente.

Le cache est stocké dans `var/storage/slidia/documents/`, avec le même mécanisme de nettoyage
que les vignettes de masques.

---

## 8. Coût, comptage et robustesse

### Comptage

Un `TokenUsageAccumulator` — livré à l'étape 1 — traverse toute la génération : sommaire, plan,
chaque lot de contenu, notes. Un seul débit à la fin, à partir de son total, et la ventilation
part dans les journaux avec, par appel, le modèle, l'étape, l'entrée, l'entrée servie par le
cache, la sortie et le raisonnement.

Rappel de la règle verrouillée à l'étape 1 : les tokens de raisonnement sont déjà compris dans
la sortie. Ils sont facturés une fois, jamais deux.

### Cache de prompt OpenAI

Le préfixe stable — prompt système, puis description des masques — précède systématiquement le
contenu variable du lot. Un `prompt_cache_key` par masque et par génération fiabilise
l'appariement. Gain attendu : quelques millièmes de dollar par présentation. C'est marginal,
mais c'est un ordre de messages, pas du code.

### Reprise sur échec

Aujourd'hui, si la génération casse au quatrième lot sur six, tout est perdu : l'utilisateur
recommence de zéro et l'éditeur repaie les trois premiers lots.

Les slides arrivant déjà une à une par le flux SSE, il suffit de persister `slidesData` au fil
de l'eau et de conserver le plan. À la reprise, seuls les lots manquants sont relancés. C'est
le gain le plus rentable de cette section, à la fois financier et ergonomique.

### Troncature

`TruncatedResponseException`, livrée à l'étape 1 et jamais encore levée en pratique, est
interceptée : un lot tronqué est relancé une fois avec un lot de taille moitié. Si le second
essai échoue également, l'erreur remonte à l'utilisateur avec un message explicite, et les
slides déjà produites sont conservées.

---

## 8 bis. Défauts du pipeline actuel à corriger au passage

Le relevé technique du flux existant a mis au jour des défaillances qui ne relèvent pas de la
qualité de la génération, mais qui la ruinent en pratique. Elles entrent dans le périmètre de
cette étape parce qu'on réécrit précisément ce code — et toutes dans le plan **2a**, puisque le
reste s'appuie dessus.

**1. Une réponse valide mais vide produit une présentation vide, facturée.**
`SlidiaAiService.php:48` et `:119` font `$data['slides'] ?? []`. Si le modèle renvoie un JSON
syntaxiquement correct sans la clé attendue, le plan est vide, aucune slide n'est générée, le
statut passe à `ready`, les tokens sont débités et l'utilisateur est redirigé vers un éditeur
vide — sans le moindre message d'erreur. C'est le mode de défaillance le plus grave du flux
actuel. Le `json_schema` avec `strict: true` le rend structurellement impossible ; s'y ajoute
une validation explicite : un plan vide lève une erreur au lieu de produire une coquille.

**2. Le statut reste `generating` pour toujours après une erreur.**
Le `catch` de `SlidiaController.php:347` émet l'événement mais ne remet pas le statut ni ne
`flush()`. La ligne en base ment indéfiniment. Un `finally` rétablit un statut cohérent.

**3. Aucune trace serveur.** `SlidiaController` n'a pas de logger : un échec de génération ne
laisse rien, et c'est le message d'exception brut qui part au client — avec le risque d'y
exposer une URL d'API ou un extrait de payload. Un logger est injecté ; le client reçoit un
message intelligible, les détails vont dans les journaux.

**4. Les slides sont alignées par position.** `$absoluteIndex = $offset + $i`
(`SlidiaController.php:306`) : si le modèle omet une slide au milieu d'un lot, tout le reste du
lot est décalé et écrase les mauvaises entrées. Le schéma impose désormais un identifiant de
slide, et l'assemblage se fait par cet identifiant, pas par le rang.

**5. `layoutPath` n'est jamais validé.** Il traverse quatre étapes sans contrôle et finit en
accès direct dans `PptxBuilder.php:297` : une valeur inventée par le modèle ne casse rien à la
génération, mais fait échouer l'export PPTX bien plus tard, sans lien apparent avec la cause.
La validation se fait à la sortie de la phase 1, contre la liste réelle des masques ; une
valeur inconnue est remplacée par un masque de repli et journalisée.

**6. Les masques sont envoyés bruts au modèle.** Chaque placeholder transmis porte encore `x`,
`y`, `cx`, `cy`, `defaultText`, `style`, `fillColor`, `gradFill`, `border`, `textInsets`,
`lineSpacing`. Le modèle n'a besoin que du chemin du masque, du type, du nom et de la taille
déduite. C'est un surcoût en tokens payé à **chaque** appel, pour des données que le modèle
ignore. Une projection réduit le payload à ce qui sert réellement.

**7. Le brief n'est plafonné nulle part côté génération.** `SlidiaConfig::BRIEF_MAX_WORDS` et
`BRIEF_MAX_CHARS` existent mais ne sont lues nulle part ; seul le `maxlength` HTML limite la
saisie. Les trois régimes de volume définis en §3 s'appliquent désormais côté serveur, quelle
que soit l'origine du texte.

**8. Le client ne détecte pas une coupure du flux.** `generate_controller.js:174` sort
silencieusement de la boucle quand le flux se ferme sans événement final : l'interface reste
figée sur sa barre de progression, indéfiniment, sans message. Un flux qui s'interrompt sans
`done` ni `error` affiche désormais une erreur et propose de reprendre — ce que la persistance
au fil de l'eau rend possible.

**9. Rien n'empêche de relancer une génération en cours.**
`generateStream()` ne vérifie pas le statut de la présentation avant de démarrer, et la route
n'a pas de jeton CSRF. Comme le débit des tokens intervient en fin de flux, un simple
rechargement de page lance une seconde génération complète et facture deux fois. Une garde sur
le statut, refusant de démarrer sur une présentation déjà `generating`, coûte trois lignes.

**10. Une génération peut durer une demi-heure dans un process HTTP.** Pire cas actuel pour 60
slides : 120 s pour le plan, puis 6 lots à 300 s. Sans `set_time_limit()`, c'est la limite PHP
qui tranche, au hasard. Scormia a déjà résolu ce problème en sortant sa génération du cycle
HTTP via Messenger. **Cette étape ne fait pas cette bascule** — elle serait justifiée mais
double le périmètre — mais elle borne explicitement le temps d'exécution et rend la reprise
possible, ce qui en atténue l'effet. Le passage en asynchrone est à considérer pour une suite.

---

## 9. Ce que l'étape 1 a laissé à traiter ici

- Inverser l'ordre du `array_merge` dans `OpenAIHttpClient::callResponsesApi()` : les valeurs
  explicites (`model`, `input`, `max_output_tokens`) doivent primer sur `extraParams`.
- Valider l'effort de raisonnement contre les constantes `OpenAIConfig::REASONING_EFFORT_*`
  plutôt que de transmettre en aveugle la chaîne fournie par l'appelant.

---

## 10. Tests

**Unitaires**
- `BriefStructureAnalyzer` : détecte les cinq formes de structure attendues ; ne détecte rien
  sur de la prose ; ne se déclenche pas sous le seuil de confiance.
- `DocumentSegmenter` : segmente sur les titres et les sauts de page ; conserve les offsets ;
  gère un document sans titre.
- Sélection des sections sources par lot : chaque lot reçoit les sections de ses slides, et
  elles seules.
- `computeTargetSlides` : conservé, mais enfin couvert par des tests.
- Construction des schémas JSON : conformité au format attendu par la Responses API.
- Facturation : une génération de six appels produit un débit égal à la somme des six.

**Fonctionnels**
- Un brief structuré déclenche le mode fidèle et conserve les titres à l'identique.
- Un PDF au-delà de la limite est refusé avec un message explicite.
- Une génération interrompue reprend sans relancer les lots déjà produits.

Les appels OpenAI sont simulés par `MockHttpClient` : aucun test ne consomme de crédit.

---

## 11. Points de vigilance

1. **Le mode fidèle ne doit pas devenir un mode dégradé.** Si la détection se déclenche à tort
   sur un brief en prose, l'utilisateur obtient des slides brutes au lieu d'une présentation
   travaillée. Le seuil conservateur et l'interrupteur visible sont les deux garde-fous ; les
   tests de non-détection comptent autant que ceux de détection.
2. **Le sommaire ne doit jamais atteindre la phase de rédaction.** C'est la promesse de qualité
   de toute l'architecture. Un test doit vérifier que le corps envoyé en phase 3 contient bien
   le texte source et non la synthèse.
3. **La facturation ne doit rien laisser passer.** Chaque nouvel appel IA ajouté au pipeline
   doit être enregistré dans l'accumulateur. Le test « somme des appels » est le filet.
4. **`gpt-5-nano` sur la condensation reste à valider.** Si les sommaires produits se révèlent
   trop pauvres pour permettre un bon plan, la bascule se fait sur `gpt-5-mini` en changeant une
   constante — le surcoût reste inférieur à un centime par document.
