Repository navigation
15 generation ia
Public : formateurs (usage) et développeurs (partie technique en fin de page).
Le builder formateur intègre plusieurs générations de contenu assistées par IA :
- Générer une leçon à partir d'un document existant, à l'intérieur d'un chapitre déjà créé — via l'API Mistral (choisie notamment pour son hébergement européen, un critère RGPD plus simple à défendre que des fournisseurs hors UE).
- Générer une formation complète (chapitres + leçons + contenu rédigé) à partir d'un thème et/ou d'un document, dès l'écran de création — même moteur Mistral.
- Générer l'audio d'une leçon (lecture du texte à voix haute) — via Piper, un moteur de synthèse vocale auto-hébergé (gratuit, open-source), plutôt qu'une API cloud payante.
Dans tous les cas, rien n'est publié automatiquement : le contenu généré atterrit dans l'éditeur normal du builder, à relire et ajuster par le formateur avant de le proposer aux stagiaires.
Depuis le plan d'une formation (/formateur/mes-modules/{id}/edition), bouton « + Générer une leçon (IA) » à côté de « + Ajouter une leçon ». Une modale permet de :
- choisir le chapitre de destination ;
- donner un titre (optionnel — l'IA en suggère un sinon) ;
- importer un document source : PDF, Word (.docx) ou texte brut, 20 Mo max.
Le contenu généré est essentiellement du texte structuré (titres, paragraphes, listes). Si le document source contient des images, elles sont automatiquement extraites et ajoutées à la fin de la leçon (voir « Images extraites des documents » ci-dessous) — pas de vidéo ou SCORM, qui nécessitent des médias déjà importés dans le module.
Depuis l'écran « Créer une formation », onglet « Générer avec l'IA » (à côté de « Créer manuellement »). Deux champs, dont au moins un est requis :
- un thème en texte libre (500 caractères max) ;
- et/ou un document source (PDF, .docx ou texte brut, 20 Mo max).
L'IA propose 3 à 5 chapitres de 2 à 4 leçons chacune, avec un contenu déjà rédigé pour chaque leçon, ainsi que 3 à 5 objectifs pédagogiques pour la formation. La génération peut prendre jusqu'à 4 minutes — la page affiche un indicateur de chargement pendant l'attente.
Les objectifs générés par l'IA sont enregistrés sur la formation et visibles dans l'onglet « Objectifs » de sa page de présentation (le même endroit où s'affichent les objectifs saisis manuellement). Ils sont modifiables à tout moment dans le panneau « Options de la formation » du plan de la formation (champ « Objectifs pédagogiques », un par ligne) — comme le reste du contenu généré par IA, à relire avant de proposer la formation aux stagiaires.
Quand un document source (PDF ou Word) contient des images, elles sont automatiquement récupérées et rattachées à la formation, sans intervention du formateur :
- Word (.docx) : toutes les images du document (png, jpg, gif, webp), extraites intégralement.
- PDF : seules les images encodées en JPEG sont récupérées (format le plus courant pour des photos/scans). Les autres encodages PDF (captures d'écran ou schémas exportés depuis Word/PowerPoint, par exemple) sont silencieusement ignorés — leur décodage n'est pas pris en charge par la librairie utilisée.
- L'IA ne « voit » pas les images : elle ne peut donc pas les placer près du paragraphe pertinent. Elles sont ajoutées à la fin — de la leçon (leçon unique), ou de la première leçon du premier chapitre (formation complète). À repositionner manuellement si besoin dans l'éditeur.
- Maximum 10 images par document.
- Quota : 3 générations IA par jour et par formateur (leçon depuis document + formation complète partagent le même compteur ; l'audio a son propre compteur séparé, également 3/jour). Au-delà, un message invite à réessayer le lendemain.
- Modération de contenu : un thème ou document manifestement inapproprié (violence, haine, contenu sexuel, etc.) est refusé immédiatement, sans consommer le quota.
- Le contenu généré doit être relu : l'IA peut se tromper ou mal interpréter un document mal structuré.
Depuis l'écran d'édition d'une leçon (/formateur/mes-modules/lectures/{id}/edition), bouton « Générer l'audio » en haut de page (sauf pour les leçons SCORM/slides, verrouillées). Le texte de tous les blocs text et quote de la leçon est lu à voix haute (voix française), produisant un unique fichier audio écoutable directement sur la page — et automatiquement visible aussi côté stagiaire, en haut de la leçon.
- Le bouton devient « Régénérer l'audio » une fois un audio déjà généré : la nouvelle version remplace l'ancienne (un seul fichier audio par leçon).
- Si la leçon ne contient aucun bloc
text/quoteavec du texte réel (uniquement image, vidéo, SCORM...), la génération est refusée avec un message explicite. - Contrairement aux deux autres générations, celle-ci ne passe pas par la modération : elle lit un contenu déjà rédigé et revu par le formateur (ou déjà passé par la modération s'il vient d'une génération IA), pas un nouveau prompt libre.
| Variable | Rôle | Requis |
|---|---|---|
MISTRAL_API_KEY |
Clé API Mistral | Oui (fonctionnalité désactivée sans clé) |
MISTRAL_MODEL |
Modèle de chat utilisé | Non (défaut : mistral-large-latest) |
PIPER_BINARY_PATH |
Chemin vers l'exécutable piper
|
Non (défaut : storage/app/piper/piper/piper) |
PIPER_MODEL_PATH |
Chemin vers le modèle de voix .onnx
|
Non (défaut : storage/app/piper/voices/fr_FR-siwis-medium.onnx) |
Lues via config('services.mistral.*') et config('services.piper.*') (config/services.php). La modération utilise toujours le modèle mistral-moderation-latest, non configurable.
Piper n'est pas un package Composer : c'est un binaire natif (~25 Mo) + un modèle de voix (~60 Mo) à installer séparément sur chaque serveur (dev et production), contrairement à Mistral qui n'est qu'un appel API. Installation utilisée en dev :
mkdir -p storage/app/piper/voices
curl -sL https://github.com/rhasspy/piper/releases/download/2023.11.14-2/piper_linux_x86_64.tar.gz | tar -xz -C storage/app/piper
curl -sL -o storage/app/piper/voices/fr_FR-siwis-medium.onnx \
https://huggingface.co/rhasspy/piper-voices/resolve/main/fr/fr_FR/siwis/medium/fr_FR-siwis-medium.onnx
curl -sL -o storage/app/piper/voices/fr_FR-siwis-medium.onnx.json \
https://huggingface.co/rhasspy/piper-voices/resolve/main/fr/fr_FR/siwis/medium/fr_FR-siwis-medium.onnx.jsonstorage/app/piper/ est exclu de git (comme tout storage/app/*) — à réinstaller sur chaque environnement.
| Fichier | Rôle |
|---|---|
Support/MistralClient.php |
Appels HTTP bruts à l'API Mistral : chat() (complétion JSON) et moderate() (modération). Journalise la conso token de chaque appel (voir plus bas) |
Support/ExtracteurTexteDocument.php |
Extraction de texte (extract()) et d'images (extractImages()) : smalot/pdfparser (PDF), phpoffice/phpword/ZipArchive (.docx), lecture directe (.txt) |
Support/GardeFouPromptIA.php |
Vérifie le thème/document via MistralClient::moderate(), lève une exception si contenu bloquant |
Support/LimiteurGenerationIA.php |
Quota de 3 générations/jour/formateur via RateLimiter (cache Laravel, table cache) |
Actions/GenererLeconIA.php |
Orchestration : document → texte → garde-fous → prompt → blocs de leçon → images |
Actions/GenererStructureFormationIA.php |
Orchestration : thème/document → texte → garde-fous → prompt → module + chapitres + leçons → images |
Actions/ImporterImagesDocument.php |
Extrait les images d'un document et les rattache au module (TeleverserImageModule), renvoie des blocs image prêts à insérer |
Support/PiperTtsClient.php |
Shell-out vers le binaire piper (Symfony\Process), texte en entrée (stdin), fichier WAV en sortie |
Actions/GenererAudioLecon.php |
Orchestration : leçon → texte lisible (blocs text/quote) → quota → Piper → media lesson-audio sur la leçon |
-
ModuleBuilderController::generateLectureIA()(routeformateur.modules.builder.lectures.generate-ia, POSTsections/{section}/lectures/generer-ia) valide le fichier et délègue àGenererLeconIA::execute(), avec leModuledu chapitre ciblé. -
ExtracteurTexteDocumentextrait le texte brut du document. -
MistralClient::chat()appelle l'API Mistral en mode JSON strict, avec un prompt qui structure le document en blocstextpédagogiques et suggère un titre. -
ImporterImagesDocumentextrait les images du même document, les rattache au module (TeleverserImageModule, collectionlesson-images) et ajoute un blocimagepar image trouvée, à la fin des blocs générés. - L'ensemble des blocs passe par le
NettoyeurBlocsModuleexistant (même sanitizer que l'éditeur manuel) avant d'être persisté viaCreerLecon, dans le chapitre choisi.
-
ModuleBuilderController::generateStructureIA()(routeformateur.modules.builder.generate-structure-ia, POST/mes-modules/generer-ia) valide thème/document et appelleGenererStructureFormationIA::execute().set_time_limit(270)est posé avant l'appel : la génération complète peut prendre 1 à 4 minutes. - Si un document est fourni,
ExtracteurTexteDocumenten extrait le texte. Le thème et/ou le texte du document sont combinés dans le prompt utilisateur envoyé à Mistral. -
MistralClient::chat()est appelé avec un timeout de 260 s et jusqu'à 12000 tokens de réponse, en mode JSON strict. Le prompt demande 3 à 5 chapitres de 2 à 4 leçons, chaque leçon avec un contenu déjà rédigé (blocstext), ainsi qu'un tableauobjectifs(3 à 5 chaînes). Historique : ce plafond était initialement à 8000 tokens ; des générations volumineuses tronquaient la réponse JSON (completion_tokenspile à la limite), rendant le JSON invalide et faisant échouer silencieusement la génération (redirectionback()vers une page sans rapport, faute d'URL précédente utile en session) — relevé et corrigé après plusieurs échecs reproductibles en test. -
$decoded['objectifs']est nettoyé (strip_tags, limite 255 car/objectif, 8 objectifs max) puis passé àcreerModuleVide(), qui l'enregistre dansModule::objectifs(colonne castarray). Le module est créé viaCreerModule::creerModuleVide()(variante deCreerModule::execute()sans la structure d'exemple). Si un document est fourni,ImporterImagesDocumenten extrait les images et les rattache au module, avant la création des chapitres/leçons. - Chapitres et leçons sont créés dans une transaction (
CreerChapitre,CreerLecon— même sanitizer que le reste du builder). Les blocsimageextraits à l'étape précédente sont ajoutés à la fin des blocs de la première leçon du premier chapitre. - Redirection vers le plan du module généré.
-
ModuleBuilderController::generateAudioLecture()(routeformateur.modules.builder.lectures.generate-audio, POSTlectures/{lecture}/generer-audio) délègue àGenererAudioLecon::execute(). - Le quota
audioest vérifié (LimiteurGenerationIA::tropDeTentatives($trainerId, 'audio')— compteur indépendant du quotatexte). -
GenererAudioLecon::extraireTexteLisible()parcourtcontent_blocks, ne garde que les blocstext(HTML nettoyé viastrip_tags) etquote, concatène le tout. Si le résultat est vide (leçon uniquement image/vidéo/SCORM),RuntimeExceptionimmédiate. -
PiperTtsClient::synthesize()lance le binaire Piper (texte passé surstdin, fichier.wavtemporaire en sortie), retourne les octets audio. -
ModuleLecture::addMediaFromString()stocke le WAV dans la collectionlesson-audio(singleFile(): toute régénération remplace le fichier précédent, pas d'accumulation).
Aucune modération n'est appliquée ici : le texte lu existe déjà dans la leçon (rédigé ou validé par le formateur), ce n'est pas un nouveau prompt libre.
Point d'attention : Illuminate\Http\Client\ConnectionException (timeout réseau vers Mistral) n'hérite pas de RuntimeException — generateStructureIA() et generateLectureIA() catchent donc ConnectionException séparément (message générique « a pris trop de temps ») de RuntimeException (message spécifique : quota, modération, réponse invalide), sinon un timeout Mistral remonte en erreur 500 au lieu d'un message utilisateur.
Les deux actions IA appliquent systématiquement, dans cet ordre, avant tout appel de génération coûteux :
-
Quota (
LimiteurGenerationIA) : vérifie viaRateLimiter::tooManyAttempts()qu'un formateur n'a pas dépassé 3 générations IA par jour (cléia-generation-formateur:{type}:{id}, fenêtre glissante de 24 h).$typevauttexte(défaut, partagé parGenererLeconIAetGenererStructureFormationIA) ouaudio(GenererAudioLecon) — deux compteurs indépendants. Si dépassé, lève uneRuntimeExceptionimmédiatement, avant tout appel réseau. -
Budget mensuel (
LimiteurBudgetTokensIA) : compare la somme destotal_tokensde la tableconsommations_iadu formateur pour le mois en cours à un plafond configurable (MISTRAL_MONTHLY_TOKEN_LIMIT,config('services.mistral.monthly_token_limit'), défaut 500 000 tokens/mois). Si atteint ou dépassé,RuntimeExceptionimmédiate — avant même l'extraction du document ou l'appel de modération, pour ne pas consommer davantage de tokens inutilement. Blocage dur : pas de dépassement toléré, le compteur se réinitialise au mois suivant (basé surcreated_at, pas de table de reset séparée). -
Modération (
GardeFouPromptIA+MistralClient::moderate()) : envoie le thème et/ou le texte extrait du document à l'endpointPOST /v1/moderations(modèlemistral-moderation-latest). Catégories bloquantes :sexual,hate_and_discrimination,violence_and_threats,dangerous,criminal,selfharm,jailbreaking. Volontairement exclues :health,financial,law,pii— une plateforme de formation professionnelle traite légitimement ces sujets (premiers secours, gestion budgétaire, droit du travail, protection des données), les inclure aurait généré trop de faux positifs. Rejet immédiat (RuntimeExceptionavec le nom des catégories déclenchées) si une catégorie bloquante est détectée àtrue. - Seulement si la modération passe,
LimiteurGenerationIA::enregistrerTentative()incrémente le compteur de quota — juste avant l'appel de génération proprement dit, donc un contenu rejeté par la modération ne consomme pas de quota, mais un timeout Mistral (après modération) en consomme un (l'appel a réellement eu lieu).
La page « Ma consommation IA » du formateur (/formateur/mes-modules/consommation-ia) affiche une barre de progression du plafond mensuel, pour que le formateur comprenne pourquoi la génération est bloquée sans avoir à consulter les logs.
GenererLeconIA::execute() et GenererStructureFormationIA::execute() prennent donc un paramètre int $trainerId en plus du document/thème.
Chaque appel Mistral (chat() et moderate()) est tracé à deux endroits, dans MistralClient::logUsage() :
-
Log fichier : une ligne dans le canal dédié
mistral(config/logging.php, driverdaily, fichierstorage/logs/mistral-{date}.log, conservé 30 jours) — volontairement séparé delaravel.logpour rester discret. Sert de complément brut au dashboard de facturation Mistral (console.mistral.ai). -
Table
consommations_ia(modèleConsommationIA) : un enregistrement durable par appel —formateur_id,type(chat/moderate),model,prompt_tokens,completion_tokens,total_tokens. C'est la source de données des deux tableaux de bord de suivi (le log fichier n'a pas de rétention longue et n'est pas exploitable en UI).
Deux vues exploitent ConsommationIADashboardService (app/Services/ConsommationIADashboardService.php) :
-
Admin —
/admin/pilotage/consommation-ia(admin.pilotage.consommation-ia,ConsommationIAController) : total de tokens et de générations par formateur, tous formateurs confondus, triés par consommation décroissante. -
Formateur —
/formateur/mes-modules/consommation-ia(formateur.modules.builder.consommation-ia,ModuleBuilderController::consommationIA()) : ses propres totaux (générations, tokens prompt/réponse) + historique paginé de ses générations. Lien accessible depuis l'en-tête de la page « Mes créations ».
Volontairement limité aux tokens bruts, sans estimation de coût en euros : les tarifs Mistral varient selon le modèle et changent dans le temps, un coût affiché deviendrait vite trompeur sans mise à jour manuelle régulière.
ExtracteurTexteDocument::extractImages() (max 10 images par document, silencieusement tronqué au-delà) :
-
PDF : parcourt les
XObjectde chaque page viasmalot/pdfparser, filtre lesSmalot\PdfParser\XObject\Imagedont leFiltervautDCTDecode(JPEG) — les autres filtres (FlateDecodebrut,CCITTFaxDecode,JPXDecode...) ne sont pas décodés par la librairie et sont ignorés. Un même XObject peut apparaître deux fois dansPage::getXObjects()(id brut + id numérique nettoyé) : dédoublonné viaspl_object_id(). -
Word (.docx) : un
.docxest une archive ZIP ; les images sont lues directement viaZipArchivesousword/media/*, filtrées par extension (png,jpg,jpeg,gif,webp). - Chaque image extraite est écrite dans un fichier temporaire (
tempnam()), puisImporterImagesDocumentl'enveloppe dans unUploadedFile(mode test) pour réutiliserTeleverserImageModule::execute()tel quel. Le fichier temporaire est supprimé après upload (finally), que l'upload réussisse ou non.
| Package | Usage |
|---|---|
smalot/pdfparser |
Extraction de texte et d'images PDF |
phpoffice/phpword |
Extraction de texte Word .docx (les images .docx sont lues séparément via ZipArchive, sans passer par PhpWord) |