Avec StoryPixAI mon objectif était de créer une application web interactive permettant aux utilisateurs de générer des histoires pour enfants, enrichies par des images générées par des modèles d’intelligence artificielle. Pour réaliser cela, j’ai utilisé plusieurs services AWS tels que Lambda, API Gateway, DynamoDB, S3 et Cognito pour l’authentification. Le code de l’infrastructure est géré avec Terraform, et le déploiement est automatisé via GitLab CI. Dans ce billet, je vous dévoile les coulisses de ce projet passionnant, des choix technologiques aux défis rencontrés.

Introduction

En tant qu’architecte d’infrastructures cloud et DevOps expérimenté, j’ai toujours été fasciné par les nouvelles technologies et leur potentiel à transformer notre quotidien. L’émergence de l’IA générative a suscité en moi une curiosité grandissante, et j’ai ressenti le besoin de plonger dans cet univers en pleine effervescence.

C’est ainsi qu’est né StoryPixAI, un projet personnel qui m’a permis d’explorer les possibilités infinies de l’IA pour créer des histoires personnalisées et des illustrations magiques pour les enfants. Ce projet a été pour moi l’occasion de me mettre dans la peau d’un développeur full-stack, d’un prompt engineer, d’un product owner et même d’un designer UX/UI, tout en partageant ma passion pour la technologie avec mes proches.

Dans ce billet de blog, je vous partagerai mes choix technologiques et les défis relevés au cours de cette aventure passionnante.

Mais avant tout, une mise en bouche !

Pour vous donner un avant-goût du potentiel de StoryPixAI, voici quelques histoires générées automatiquement, dans plusieurs langues.
Chaque histoire est accompagnée d’illustrations, rendant le récit encore plus immersif pour les enfants :

L’IA au service de la créativité : un parcours d’expérimentation

Mon aventure avec StoryPixAI a commencé par un Proof of Concept (PoC) simple : une fonction Lambda qui interagissait avec OpenAI pour générer du texte et DALL-E pour créer des images. Ce premier succès m’a encouragé à aller plus loin et à explorer d’autres modèles d’IA via Bedrock d’AWS.

GPT-4 et GPT-4-o : les conteurs agiles

Dès le début du projet, GPT-4 d’OpenAI s’est imposé comme un choix évident pour la génération de texte. Sa capacité à comprendre les nuances du langage naturel et à produire des récits cohérents et créatifs m’a permis de créer des histoires captivantes, adaptées à l’âge et aux intérêts des enfants. J’ai pu expérimenter avec différents styles d’écriture, du conte de fées à l’aventure spatiale, en passant par les histoires d’animaux et les récits fantastiques.

Lorsque GPT-4-0 a été lancé, j’ai rapidement intégré ce nouveau modèle à StoryPixAI. J’ai été impressionné par sa vitesse de génération accrue, qui a permis de réduire considérablement le temps d’attente pour la génération, et par l’amélioration notable de la qualité des histoires générées, avec des récits encore plus fluides, cohérents et imaginatifs. GPT-4-0 est ainsi devenu un atout majeur pour StoryPixAI, offrant une expérience utilisateur plus rapide et plus agréable.

DALL-E 3 : l’illustrateur de référence

Si les modèles de génération de texte offraient des résultats satisfaisants, le choix de l’outil de génération d’images s’est avéré plus crucial. Après de nombreux essais, DALL-E 3 s’est imposé comme le modèle de référence pour StoryPixAI. Sa capacité à créer des illustrations originales, détaillées et parfaitement adaptées aux histoires générées par GPT-4 a été un facteur déterminant dans la réussite du projet.

Bedrock d’AWS : la porte ouverte vers l’expérimentation

Souhaitant ne pas me limiter à OpenAI, j’ai utilisé Bedrock d’AWS pour intégrer facilement d’autres modèles d’IA générative à StoryPixAI. Cette plateforme m’a permis de tester Claude d’Anthropic et Mistral pour la génération de texte, et Stable Diffusion pour la création d’images.

Bien que ces modèles aient donné des résultats intéressants, j’ai finalement choisi de me concentrer sur GPT-4 et GPT-4-0 pour leur rapidité et leur qualité de génération de texte, et sur DALL-E 3 pour sa capacité à produire des illustrations parfaitement adaptées aux histoires. Il est important de noter que le prompt utilisé pour générer les images est en grande partie élaboré par le modèle de texte lui-même, ce qui assure une cohérence entre le récit et l’illustration.

Le défi de l’API asynchrone et de DynamoDB

Une fois le PoC validé, j’ai entrepris de créer une API pour rendre StoryPixAI accessible via une interface web. C’est à ce stade que j’ai rencontré mon premier défi majeur : la limitation de timeout d’API Gateway. Pour contourner cette contrainte et permettre la génération d’histoires plus longues et plus complexes, j’ai dû mettre en place une architecture asynchrone.

Amazon DynamoDB est alors entré en jeu. J’ai utilisé cette base de données NoSQL pour stocker les tâches de génération d’histoires en cours, ainsi que leurs résultats une fois terminées. Grâce à cette approche, l’API pouvait renvoyer une réponse immédiate à l’utilisateur, qui pouvait ensuite consulter l’état de sa requête et récupérer l’histoire générée une fois prête.

CORS et l’interface utilisateur : des obstacles à surmonter

La mise en place de l’interface web a également été source de défis. J’ai dû me familiariser avec les subtilités de CORS (Cross-Origin Resource Sharing) pour permettre à mon frontend de communiquer avec l’API. J’ai également consacré du temps à améliorer l’expérience utilisateur en ajoutant des fonctionnalités telles que la sélection des modèles d’IA et des styles d’images.

Le prompting : un art à maîtriser

Tout au long du développement de StoryPixAI, j’ai affiné mes compétences en prompting, cet art de formuler les bonnes requêtes pour guider les modèles d’IA. J’ai appris à adapter mes prompts en fonction des modèles utilisés, des paramètres de l’histoire et des attentes des utilisateurs. Cette étape a été cruciale pour obtenir des résultats de qualité et garantir une expérience utilisateur satisfaisante.

Une infrastructure robuste et automatisée sur AWS

StoryPixAI repose sur une infrastructure serverless hébergée sur Amazon Web Services (AWS), offrant une combinaison idéale de flexibilité, d’évolutivité et d’optimisation des coûts. Cette architecture, entièrement automatisée grâce à Terraform et GitLab CI/CD, permet un déploiement rapide et fiable de l’application.

Les services AWS au cœur de StoryPixAI

alt text

L’architecture de StoryPixAI s’articule autour des services AWS suivants :

  • Amazon S3 (Simple Storage Service) : Stockage des fichiers statiques du site web (HTML, CSS, JavaScript) et des histoires générées ainsi que leurs illustrations associées.
  • Amazon CloudFront : Un réseau de diffusion de contenu (CDN) qui accélère la distribution du contenu de StoryPixAI aux utilisateurs du monde entier en le mettant en cache dans des emplacements géographiquement proches d’eux.
  • Amazon API Gateway : La porte d’entrée sécurisée de l’application. Elle gère les requêtes des utilisateurs, assure leur authentification via Amazon Cognito, et les achemine vers les fonctions Lambda appropriées.
  • AWS Lambda : Des fonctions serverless qui constituent le moteur de StoryPixAI. Elles orchestrent la génération d’histoires, la création d’images, la gestion des tâches asynchrones et l’interaction avec DynamoDB et les autres services AWS.
  • Amazon DynamoDB : Une base de données NoSQL flexible et performante utilisée pour stocker des informations essentielles au fonctionnement de l’application.
  • Amazon Cognito : Un service de gestion des identités et des accès qui sécurise l’application en permettant aux utilisateurs de se connecter et en contrôlant leurs autorisations. Il assure que seuls les utilisateurs authentifiés peuvent accéder aux fonctionnalités de génération d’histoires.
  • Amazon Bedrock : Une plateforme qui simplifie l’accès et l’utilisation de modèles d’IA générative de différents fournisseurs, tels qu’Anthropic (Claude) et Stability AI (Stable Diffusion). Bedrock permet d’intégrer facilement ces modèles dans l’application sans avoir à gérer leur infrastructure sous-jacente.
  • Autres services AWS : StoryPixAI utilise également d’autres services AWS, tels qu’IAM (Identity and Access Management) pour la gestion fine des autorisations d’accès aux ressources, CloudWatch pour la surveillance et les journaux (crucial pour le débogage et l’analyse des performances), et Systems Manager Parameter Store (SSM Parameter Store) pour stocker les informations sensibles comme les clés d’API, garantissant ainsi la sécurité de l’application.

Terraform : l’automatisation au service de l’infrastructure

Pour gérer cette infrastructure complexe, j’ai fait le choix de Terraform, un outil d’Infrastructure as Code (IaC) qui permet de décrire l’infrastructure sous forme de code déclaratif. Grâce à Terraform, j’ai pu automatiser la création, la modification et la destruction des ressources AWS, garantissant ainsi un environnement cohérent, reproductible et facile à gérer. Cela simplifie considérablement le processus de déploiement et réduit le risque d’erreurs humaines.

GitLab CI/CD : des déploiements fluides et sans accroc

Pour assurer un déploiement continu et fiable de StoryPixAI, j’ai mis en place un pipeline CI/CD (Intégration Continue / Déploiement Continu) sur GitLab. Ce pipeline automatise les tests, la construction et le déploiement de l’application à chaque modification du code source, permettant ainsi de détecter et de corriger rapidement les erreurs et de livrer de nouvelles fonctionnalités en toute confiance. Cette approche garantit que l’application est toujours à jour et minimise les temps d’arrêt.

Cette combinaison d’AWS, Terraform et GitLab CI/CD m’a permis de construire une infrastructure robuste, évolutive et facile à maintenir, me laissant ainsi plus de temps pour me concentrer sur l’aspect créatif du projet et l’amélioration de l’expérience utilisateur.

Architecture Globale du projet StoryPixAI

Avant de plonger dans le code, voici un aperçu de l’architecture de l’application :

  1. Site Statique sur S3 : Un site web statique hébergé sur un bucket S3, accessible via CloudFront pour une distribution globale.
  2. API Gateway : Expose des endpoints pour la génération d’histoires et la vérification du statut.
  3. Fonctions Lambda :
    • StoryPixAI.py : Génère l’histoire et les images associées.
    • status_checker.py : Vérifie le statut de la génération dans DynamoDB.
  4. DynamoDB : Stocke le statut des tâches de génération.
  5. S3 : Stocke les images générées et les pages HTML résultantes.
  6. Cognito : Gère l’authentification des utilisateurs pour sécuriser l’API.

Fonction Lambda StoryPixAI.py

Aperçu Général

La fonction StoryPixAI.py est le cœur de l’application. Elle est responsable de :

  • Générer une histoire basée sur un prompt utilisateur.
  • Créer des instructions détaillées pour guider le modèle d’IA dans la génération de l’histoire.
  • Extraire des résumés pour chaque scène ou élément clé de l’histoire.
  • Générer des images correspondantes à ces résumés.
  • Combiner le texte et les images en une page HTML.
  • Stocker le résultat dans S3 et mettre à jour le statut dans DynamoDB.

Décomposition du Code

Imports et Configuration Initiale

import json
import boto3
import base64
import os
import re
from datetime import datetime
from openai import OpenAI
import logging

# Configuration du logger
logger = logging.getLogger()
logger.setLevel(logging.INFO)

generate_images = False
region_name = os.getenv("AWS_REGION", "us-east-1")

# Création d'un client SSM
ssm = boto3.client("ssm", region_name=region_name)

# Obtention de la clé API OpenAI depuis le SSM
parameter = ssm.get_parameter(Name="/openaikey", WithDecryption=True)

Dans cette section, j’importe les modules nécessaires, configure le logger pour le débogage, et récupère la clé API OpenAI stockée dans AWS Systems Manager Parameter Store (SSM). Cela permet de sécuriser la clé et de ne pas la stocker en clair dans le code.

Fonctions Utilitaires

Correction des Balises
def correct_resume_tags(text):
    # Corrige les balises 'résumé', 'resume', 'titre' et leurs variantes en 'resume' et 'titre' respectivement dans le texte généré.

Cette fonction assure que les balises utilisées pour délimiter les résumés et les titres sont uniformes. Cela est crucial pour l’extraction correcte des résumés plus tard.

Extraction des Résumés
def extract_summaries(text):
    # Extrait les résumés du texte en utilisant des balises spécifiques.

Elle utilise des expressions régulières pour extraire les sections de texte délimitées par [resume] et [end_resume]. Ces résumés serviront de prompts pour la génération d’images.

Génération des Instructions pour les Images
def generate_image_instructions(prompt, style, language):
    # Génère les instructions pour la création d'images.

Cette fonction formate le prompt de manière à guider le modèle de génération d’images en incluant le style et la langue.

Mise à Jour de DynamoDB
def update_dynamodb(request_id, status, result_url=None):
    # Met à jour une entrée dans la table DynamoDB avec l'ID de la requête, le statut et l'URL du résultat.

Elle met à jour la table TaskStatus pour suivre l’état de la génération, ce qui est essentiel pour la fonction status_checker.py.

Analyse Approfondie de generate_story_instructions

La fonction generate_story_instructions est le cœur du projet. Elle génère un ensemble d’instructions détaillées qui seront passées au modèle d’IA pour guider la génération de l’histoire.

def generate_story_instructions(prompt, language):
    """
    Génère les instructions pour créer une histoire captivante pour enfants.

    Args:
        prompt (str): Texte source pour inspirer l'histoire.
        language (str): Langue de l'histoire.

    Returns:
        str: Instructions formatées pour la génération de l'histoire.
    """
    language_description = get_language_description(language)
    return f"""
    Crée une histoire unique de 1000 à 1500 mots, captivante et riche en descriptions visuelles pour enfants uniquement dans la langue "{language_description}", inspirée par : "{prompt}". Cette histoire doit mêler aventure, magie, et enseigner des valeurs importantes telles que l'amitié, le courage, la persévérance, l'empathie, et la gentillesse.

    L'histoire peut aborder des thèmes comme : l'amitié entre un enfant humain et un animal merveilleux, la découverte d'un monde magique caché, un long voyage vers une contrée enchantée, un enfant qui découvre qu'il/elle possède des pouvoirs magiques spéciaux et doit apprendre à les maîtriser, une quête pour sauver une créature légendaire en danger, un voyage à travers des royaumes féeriques pour briser un sortilège ancien, une aventure sous-marine dans un monde marin peuplé de sirènes et de créatures fantastiques, une mission pour réunir des objets magiques dispersés afin d'empêcher un grand cataclysme, une compétition amicale entre enfants dotés de capacités extraordinaires dans une école de sorcellerie, etc.
    L'histoire peut également explorer : l'acceptation de soi à travers un personnage unique comme un enfant métamorphe, la découverte d'une ancienne civilisation perdue et de ses secrets, une épopée pour retrouver des parents disparus dans un monde parallèle, une lutte contre les forces des ténèbres menaçant d'engloutir un royaume enchanté, etc.
    N'hésites pas à combiner plusieurs de ces idées pour créer une trame narrative riche et captivante. Tu peux aussi t'inspirer de contes ou légendes traditionnels et leur donner un nouvel éclairage fantastique adapté aux enfants.
    Raconte l'histoire au présent pour une immersion maximale.

    Instructions spécifiques :
    - Utilise des phrases courtes et simples, adaptées pour des enfants de moins de 10 ans.
    - Intègre des dialogues dynamiques et réalistes pour rendre l'histoire vivante.
    - Choisis des mots simples pour une meilleure compréhension par de jeunes lecteurs.
    - Crée des personnages diversifiés en termes d'âge, de genre, d'origine ethnique et de capacités. Assure-toi que l'apparence des personnages (cheveux, yeux, taille, etc.) est précisée au niveau du résumé si jamais ils doivent y apparaître pour être cohérent avec le texte de l'histoire.
    - Attribue des traits de personnalité uniques, des intérêts, des peurs et des rêves à chaque personnage pour une caractérisation approfondie.
    - Développe les personnages et leurs relations tout au long de l'histoire en montrant leurs interactions, leurs moments de partage et leur évolution.
    - Crée des conflits émotionnels et intellectuels, au-delà des défis physiques.
    - Décris en détail les défis physiques et les actions des personnages pour les surmonter. Par exemple, lorsqu'ils traversent la forêt, mentionne les branches qui les gênent, les racines sur lesquelles ils trébuchent, la végétation dense qu'ils doivent écarter. Montre leur fatigue, leurs efforts pour avancer, les émotions qu'ils ressentent face à ces difficultés.
    - Fais échouer les personnages principaux à un moment donné. Montre comment ils gèrent cet échec et essaient à nouveau. Décris en détail leurs sentiments de doute, de frustration ou de découragement, et comment ils puisent dans leur détermination et leur amitié pour surmonter cet obstacle. Assure-toi que l'échec est significatif et impacte réellement la progression de l'histoire.
    - Crée des conflits entre les personnages principaux, ou entre les personnages principaux et les personnages secondaires.
    - Ajoute des rebondissements et des défis supplémentaires pour maintenir l'intérêt des jeunes lecteurs. Décris en détail la réaction des personnages face à ces rebondissements, leurs émotions, leurs doutes et leurs efforts pour s'adapter à la nouvelle situation.
    - Résous les conflits de manière créative et non violente, en mettant l'accent sur le pouvoir de la communication et de la coopération.
    - Développe les antagonistes en leur donnant des motivations claires, des traits de personnalité distincts et des capacités redoutables qui les rendent réellement menaçants pour les héros. Décris en détail leurs actions pour contrecarrer ou mettre en échec les héros à plusieurs reprises au cours de l'histoire. Montre comment leur présence et leurs actions sèment le doute, la peur ou le découragement chez les héros avant qu'ils ne parviennent à les surmonter.
    - Assure-toi que le récit comporte une structure narrative claire avec une introduction captivante, de l'action, des conflits, et une résolution.
    - Ajoute un objectif clair pour les personnages à atteindre et un accomplissement significatif à la fin de l'histoire.
    - Inclue des moments de réflexion ou d'émotion pour permettre aux lecteurs de se connecter aux personnages et à leurs aventures.
    - Varie les interactions entre les personnages pour éviter les répétitions et maintenir l'intérêt.
    - Maintiens un bon rythme dans l'histoire en alternant des scènes d'action, de réflexion et d'émotion. Ajoute des éléments de suspense pour maintenir l'intérêt des jeunes lecteurs.
    - Utilise abondamment des descriptions visuelles riches en couleurs, en textures et en formes pour stimuler l'imagination des enfants et créer un monde immersif.
    - Inclue des descriptions sensorielles pour enrichir l'expérience narrative (sons, odeurs, textures).
    - Chaque personnage doit avoir une motivation claire et des traits de caractère distincts.
    - Assure-toi que chaque chapitre se termine par un cliffhanger ou une question ouverte pour maintenir l'intérêt des lecteurs.
    - Ajoute des éléments éducatifs subtils (faits scientifiques, connaissances culturelles) pour enrichir l'histoire sans alourdir le récit.
    - Enrichis les descriptions sensorielles pour permettre aux lecteurs de vraiment "voir", "entendre" et "ressentir" l'environnement des personnages.
    - Personnalise l'histoire avec des noms ou des éléments familiers pour une connexion émotionnelle plus forte.
    - Intègre des questions de réflexion et d'interaction pour engager les enfants.
    - Ajoute des éléments d'humour et des jeux de mots pour rendre l'histoire amusante.
    - Utilise des illustrations mentales vives et détaillées pour stimuler l'imagination.
    - Intègre une leçon morale ou un message éducatif de manière naturelle dans le récit.
    - Intègre des messages positifs et encourageants dans tes histoires, comme l'importance de croire en soi, de poursuivre ses rêves et de surmonter les obstacles.
    - Ajoute des éléments d'humour et de légèreté dans tes histoires pour les rendre plus amusantes et agréables à lire pour les enfants.
    - Intègre des éléments éducatifs dans tes histoires de manière subtile et ludique, comme des métaphores pour enseigner des concepts scientifiques ou des voyages dans différents pays pour enseigner la géographie et les cultures.
    - Ajoute des éléments interactifs dans tes histoires, comme des questions aux enfants, des choix qui influencent l'histoire, ou des petits défis ou jeux à réaliser.

    Ajoute des difficultés et des obstacles significatifs pour rendre l'histoire plus engageante et permettre aux héros de montrer leur courage et leur ingéniosité :
    - Développe les antagonistes en leur donnant des motivations claires, des traits de personnalité distincts et des capacités redoutables qui les rendent réellement menaçants pour les héros. Décris en détail leurs actions pour contrecarrer ou mettre en échec les héros à plusieurs reprises au cours de l'histoire. Montre comment leur présence et leurs actions sèment le doute, la peur ou le découragement chez les héros avant qu'ils ne parviennent à les surmonter.
    - Décris chaque affrontement au niveau quasi "temps réel", avec les actions, réactions, émotions, blessures, etc. détaillées pas à pas, presque comme si on y assistait. Intègre des éléments de surprise, de retournements inattendus au cours de ces affrontements pour augmenter le suspense. Montre comment les capacités et l'ingéniosité des antagonistes poussent les héros dans leurs derniers retranchements.
    - Lorsque les héros échouent, prends le temps de décrire en détail leurs émotions négatives (déception, frustration, colère, tristesse, etc.) et leurs doutes intérieurs. Montre qu'ils remettent en question leur capacité à poursuivre leur quête à la suite de ces échecs cuisants. Fais en sorte qu'ils aient besoin d'un véritable déclic intérieur, motivé par l'amitié ou leurs valeurs, pour se relever et persévérer. Montre comment cela impacte leurs relations entre eux (reproches, disputes, tensions, ou au contraire un élan de solidarité).
    - Décris les affrontements physiques ou psychologiques étape par étape, en montrant les actions, réactions et émotions ressenties de part et d'autre. N'hésite pas à inclure des blessures, de la souffrance ou de la peur pour les héros lors de ces affrontements acharnés. Fais en sorte que la victoire des héros ne soit jamais acquise d'avance et nécessite des sacrifices ou des prises de risque de leur part.
    - Crée des situations où les héros doivent collaborer et utiliser leurs compétences spécifiques pour réussir.
    - Intègre des moments de doute ou de découragement pour montrer la persévérance des héros. Décris leurs luttes internes et comment ils trouvent la force de continuer. Fais en sorte que les héros aient besoin d'un véritable déclic intérieur, motivé par l'amitié ou leurs valeurs, pour se relever et persévérer.
    - Ajoute des moments où l'amitié ou la confiance entre les héros est mise à rude épreuve par les difficultés rencontrées. Montre comment ils doivent surmonter leurs doutes, leur colère ou leur rancune les uns envers les autres pour rester soudés. Décris leurs prises de conscience, leurs excuses et leur cheminement pour renouer des liens forts malgré l'adversité.
    - Place les héros dans des situations où ils doivent faire un choix difficile qui aura des conséquences douloureuses (abandonner un compagnon, renoncer à un rêve, etc.). Montre leur dilemme intérieur, leur déchirement avant de faire ce choix douloureux pour un plus grand bien. N'aie pas peur d'inclure des pertes, des renoncements ou des traumatismes marquants issus de ces choix cornéliens.
    - Fais en sorte que les personnages apprennent et grandissent à travers les difficultés qu'ils rencontrent.
    - Ajoute des rebondissements inattendus qui changent la direction de l'histoire et maintiennent l'intérêt des lecteurs. Décris en détail la réaction des personnages face à ces rebondissements, leurs émotions, leurs doutes et leurs efforts pour s'adapter à la nouvelle situation.
    - Fais en sorte que les antagonistes infligent de véritables blessures physiques et/ou psychologiques aux héros au cours des affrontements. Décris ces blessures, la douleur ressentie, l'impact sur leur moral et leurs capacités à avancer. Montre leur résolution, leur courage pour continuer malgré ces handicaps.
    - Assure-toi que chaque défi est pertinent pour l'histoire et contribue au développement des personnages.
    - Décris en détail chaque énigme ou défi rencontré par les personnages. Par exemple, si les enfants doivent résoudre des énigmes chantées par les vents, précise le contenu de ces énigmes et la manière dont les enfants trouvent les réponses grâce à leur persévérance ou à l'aide de personnages secondaires.
    - Lorsque les personnages surmontent un obstacle, montre le processus complet de leurs tentatives, incluant les échecs et les efforts qu'ils font avant de réussir. Par exemple, détaille comment ils essaient plusieurs méthodes pour résoudre une énigme ou surmonter un défi avant de finalement trouver la solution.
    - Intègre des dialogues et des interactions entre les personnages et les gardiens ou les antagonistes qui posent des défis. Par exemple, si un enfant des vents protège un objet précieux, décris la conversation où il teste la patience des héros et les réactions des enfants face à ce test.
    - Ajoute des descriptions des émotions et des pensées des personnages lorsqu'ils font face à des épreuves difficiles, montrant leur détermination, leurs doutes, et comment ils surmontent ces sentiments pour réussir.
    - Assure-toi que chaque défi est clairement expliqué avec des indices et des solutions logiques que les enfants peuvent comprendre et suivre. Par exemple, spécifie les indices que les héros utilisent pour résoudre les énigmes et comment ces indices les mènent à la solution.


    IMPORTANT : Ne traduisez ni modifiez pas les balises suivantes :
    [titre]Ton titre ici[end_titre] (balises de titre)
    [resume] et [end_resume] (balises de résumé)
    N'ajoutez aucune autre balise que celles spécifiées ci-dessus.

    Voici comment structurer les descriptions visuelles inspirées par : "{prompt}" :
    - Commence chaque description avec la balise [resume] et finis avec la balise [end_resume]. Ne traduisez ni modifiez pas ces balises.
    - Les descriptions doivent se concentrer exclusivement sur les éléments visuels sans inclure d'actions ou de dialogues des personnages.
    - Chaque élément clé mentionné dans le prompt initial doit être décrit de manière unique et détaillée.
    - Ne mentionne chaque élément (personnage, animal, lieu, objet clé) qu'une seule fois dans les descriptions visuelles. Une fois qu'un élément a été décrit, ne le mentionne plus dans les descriptions suivantes, même indirectement.
    - Utilise des descriptions riches en couleurs, en textures et en formes pour stimuler l'imagination visuelle.
    - Inclue des éléments fantastiques, magiques ou surréalistes pour rendre les scènes plus intéressantes et mémorables.
    - Veille à ce que chaque description soit suffisamment détaillée pour permettre la création d'une illustration complète.

    Exemple de structure de descriptions visuelles (ces exemples sont seulement pour référence, ne les utilisez pas tels quels dans l'histoire) :
    [resume]Un koala super sympa avec une fourrure douce et grise, des yeux pétillants et un sourire amical. Il est assis sur une branche d'eucalyptus, grignotant des feuilles et observant son environnement avec curiosité.[end_resume]
    [resume]Un escargot très méchant avec une coquille noire et luisante, et des yeux perçants qui semblent voir à travers tout. Il se déplace lentement mais de manière menaçante, laissant derrière lui une traînée de bave visqueuse.[end_resume]
    [resume]Un arbre magique avec des feuilles d'un bleu profond qui brillent comme des étoiles. Des oiseaux de toutes les couleurs chantent autour des branches, ajoutant une mélodie enchantée à l'atmosphère mystique.[end_resume]

    Assure-toi que chaque description visuelle est riche, détaillée et entièrement nouvelle, sans aucune répétition d'éléments précédents. Évite d'utiliser les exemples fournis ci-dessus et crée des descriptions fraîches pour chaque scène.

    La conclusion de l'histoire doit renforcer les thèmes de l'aventure et de l'amitié avec une touche plus percutante, et être accompagnée d'une dernière description visuelle marquante.
    [resume]Visualise le chemin de retour à travers un paysage unique et magique, différent pour chaque histoire. Par exemple, un pont arc-en-ciel, un sentier lumineux sous une pluie d'étoiles filantes, des pas dans le sable avec un soleil couchant, etc. Assure-toi que la description finale est riche en détails visuels et évoque une atmosphère enchantée et inoubliable.[end_resume]

    Pour varier les débuts d'histoire et éviter la répétition, choisis parmi les exemples suivants, ou laisse libre cours à ton imagination :
    - Une classe à l'école, un voyage en famille, une fête d'anniversaire, une visite chez les grands-parents, un jour de pluie où les enfants jouent à l'intérieur, une sortie en nature, etc.
    - La découverte d'un livre magique, une rencontre inattendue avec un personnage mystérieux, un rêve étrange qui devient réalité, un message secret trouvé dans une bouteille, un animal parlant qui apparaît soudainement, etc.
    - Des personnages principaux différents : une fratrie, des amis, un enfant et son grand-parent, un groupe de camarades de classe, etc.
    - Des lieux de départ variés : une maison en ville, une cabane dans les bois, un appartement au bord de la mer, une ferme, une école, etc.
    - Déclencheur de l'aventure variés aussi : un portail vers un monde magique, un objet mystérieux trouvé dans le grenier, un événement étrange comme une éclipse ou une étoile filante, un animal parlant qui a besoin d'aide, un visiteur de l'espace, etc.

    Cette structure aide à créer un récit harmonieux et visuellement riche, propice à l'illustration et captivant pour les enfants.
    Attention, je te rappelle la langue cible de l'histoire : "{language_description}"
    """

Construction du Prompt

Le prompt est conçu pour fournir au modèle d’IA toutes les informations nécessaires pour générer une histoire cohérente, éducative et adaptée aux enfants.

  • Langue : Le paramètre language_description permet de spécifier la langue de l’histoire, assurant ainsi que le texte généré sera dans la langue souhaitée.

  • Thème : Le prompt utilisateur est intégré dans les instructions pour servir de base à l’histoire.

  • Longueur : Une fourchette de 1000 à 1500 mots est spécifiée pour contrôler la longueur de l’histoire.

  • Éléments Clés : Les instructions encouragent l’inclusion d’éléments tels que l’aventure, la magie, et des valeurs éducatives importantes.

Détails des Instructions

Les instructions fournies au modèle sont extrêmement détaillées pour guider la génération de manière précise.

Voici une analyse des différentes parties du prompt :

  1. Structure Narrative : On demande au modèle de structurer l’histoire avec un début captivant, un développement riche en événements, et une conclusion satisfaisante.

  2. Descriptions Visuelles : L’histoire doit être riche en descriptions visuelles pour stimuler l’imagination des enfants.

  3. Personnages : On encourage le développement de personnages attachants avec des personnalités distinctes.

  4. Balises Spécifiques : Des balises telles que [titre]... [end_titre] et [resume]... [end_resume] sont utilisées pour délimiter le titre et les descriptions visuelles.

  5. Éléments Fantastiques : On invite le modèle à inclure des éléments magiques ou fantastiques pour rendre l’histoire plus attrayante.

  6. Valeurs Éducatives : L’histoire doit enseigner des valeurs importantes.

Rôle des Balises

Les balises jouent un rôle crucial dans le traitement ultérieur du texte généré.

  • [titre]… [end_titre] : Encadre le titre de l’histoire. Cela permet de l’extraire facilement pour l’afficher de manière appropriée dans l’interface utilisateur.

  • [resume]… [end_resume] : Encadre les descriptions visuelles détaillées de scènes clés de l’histoire. Ces résumés seront utilisés comme prompts pour la génération d’images.

Traitement Après la Génération

Une fois que le modèle d’IA a généré l’histoire en suivant ces instructions, le code effectue les étapes suivantes :

  1. Correction des Balises : La fonction correct_resume_tags s’assure que toutes les balises sont correctement formatées pour l’extraction.

  2. Extraction des Résumés : La fonction extract_summaries utilise les balises [resume] et [end_resume] pour extraire les descriptions visuelles.

  3. Génération des Images : Chaque résumé est passé à la fonction generate_image pour créer une image correspondante.

  4. Création du Contenu HTML : Le texte de l’histoire et les images générées sont combinés pour créer une page HTML complète.

Impact sur la Génération

En fournissant ces instructions détaillées, le modèle est guidé pour :

  • Respecter le Format : En utilisant les balises spécifiées, le modèle produit un texte structuré qui facilite le traitement automatisé.

  • Générer du Contenu Adapté : Les contraintes sur la langue, le style, et les thèmes garantissent que l’histoire est appropriée pour le public cible.

  • Faciliter la Génération d’Images : En extrayant des descriptions visuelles précises, on obtient des prompts de qualité pour la génération d’images.

Gestion des Balises par le Modèle

Le modèle est explicitement instruit de ne pas traduire ou modifier les balises. Cela est essentiel pour que les balises restent intactes et puissent être utilisées pour le post-traitement. Les instructions insistent sur ce point pour éviter que le modèle, qui pourrait tenter de paraphraser ou de traduire tout le texte, altère les balises.

Génération de l’Histoire

Une fois les instructions détaillées générées par la fonction generate_story_instructions, la prochaine étape est de passer ces instructions au modèle d’IA pour qu’il crée l’histoire.

def generate_story(prompt, model_type, model_id, language, api_key=None, region_name="us-east-1"):
    instruction = generate_story_instructions(prompt, language)

    if model_type == "openai":
        client = OpenAI(api_key=api_key)
        try:
            response = client.chat.completions.create(
                model=model_id,
                messages=[
                    {
                        "role": "system",
                        "content": "Vous êtes un assistant AI expert des histoires pour enfant.",
                    },
                    {"role": "user", "content": instruction},
                ],
            )
            first_choice_message = response.choices[0].message
            return first_choice_message.content
        except Exception as e:
            return f"Une erreur est survenue : {e}"

    # Gestion des autres modèles (Mistral, Anthropic, Meta) via Amazon Bedrock

Interaction avec le Modèle OpenAI

  • Client OpenAI : J’instancie un client OpenAI en utilisant la clé API récupérée précédemment.

  • Prompting : Le modèle reçoit une série de messages :

    • Un message système indiquant que l’assistant est un expert en histoires pour enfants.
    • Le message utilisateur contenant les instructions détaillées générées.
  • Réponse du Modèle : Le modèle génère une histoire en se basant sur les instructions fournies.

Gestion des Erreurs

Si une exception survient lors de l’appel à l’API OpenAI, elle est capturée, et un message d’erreur est retourné.

Extraction des Résumés et Balises

Après la génération de l’histoire, l’étape suivante consiste à extraire les descriptions visuelles en utilisant les balises spécifiées.

def correct_resume_tags(text):
    # Corrige les balises 'résumé', 'resume', 'titre' et leurs variantes en 'resume' et 'titre' respectivement dans le texte généré.

def extract_summaries(text):
    pattern = r"\[resume\](.*?)\[end_resume\]"
    summaries = re.findall(pattern, text, re.DOTALL)
    return summaries

Correction des Balises

Le modèle peut parfois altérer légèrement les balises (par exemple, ajouter des accents). La fonction correct_resume_tags s’assure que toutes les balises sont uniformes et correctement formatées.

Extraction des Résumés

La fonction extract_summaries utilise une expression régulière pour trouver toutes les occurrences de texte entre les balises [resume] et [end_resume]. Ces résumés sont les descriptions visuelles détaillées qui seront utilisées pour générer les images.

Génération des Images

Une fois les résumés extraits, chaque résumé est utilisé pour générer une image correspondante.

def generate_image_for_each_summary(summaries, model, bucket_name, seed, style, size, quality, language):
    images_urls = []
    for summary in summaries:
        image_data = generate_image(summary, model, seed, style, size, quality, language)
        if image_data is not None:
            image_url = upload_to_s3(image_data, bucket_name)
            images_urls.append(image_url)
        else:
            images_urls.append("")
    return images_urls

Fonction generate_image

La fonction generate_image appelle l’API du modèle de génération d’images (par exemple, OpenAI DALL·E) pour créer une image à partir du résumé.

def generate_image(prompt, model, seed, style, size, quality, language):
    width, height = extract_dimensions(size)

    if model == "openai":
        client = OpenAI(api_key=parameter["Parameter"]["Value"])
        adjusted_prompt = generate_image_instructions(prompt, style, language)

        try:
            response = client.images.generate(
                prompt=adjusted_prompt,
                model=os.environ.get("OPENAI_IMAGE_MODEL"),
                n=1,
                size=size,
                response_format="b64_json",
                quality=quality,
                user="user_id",
            )
            image_data = response.data[0].b64_json
            return image_data
        except Exception as e:
            logger.error(f"Error generating image with OpenAI: {str(e)}", exc_info=True)
            return None

    # Gestion des autres modèles (Titan, Stable Diffusion) via Amazon Bedrock

Génération des Instructions pour les Images

La fonction generate_image_instructions adapte le résumé pour créer un prompt approprié pour la génération d’images.

def generate_image_instructions(prompt, style, language):
    language_description = get_language_description(language)
    return f"""
    Génère un dessin pour enfant dans le style "{style}" basé sur cette description en langue "{language_description}" : {prompt}.
    La scène doit être purement visuelle, sans aucun texte, et conçue pour éveiller l'émerveillement chez les jeunes spectateurs.
    """
  • Style : Le style spécifié par l’utilisateur (par exemple, “aquarelle”, “cartoon”) est inclus dans le prompt pour influencer le rendu de l’image.

  • Langue : La description est adaptée à la langue choisie, ce qui peut aider le modèle à comprendre les nuances culturelles.

  • Instructions Claires : En précisant que la scène doit être purement visuelle, on évite que le modèle ajoute du texte ou des éléments indésirables dans l’image.

Interaction avec l’API OpenAI pour les Images

  • Appel à l’API : La fonction client.images.generate est utilisée pour générer l’image.

  • Paramètres Importants :

    • Prompt : Le prompt ajusté est passé à l’API.
    • Modèle : Le modèle de génération d’images spécifié.
    • Taille : La taille de l’image (par exemple, “1024x1024”).
    • Qualité : La qualité de l’image (standard, HD).
    • Format de Réponse : Les images sont retournées en base64 pour faciliter le stockage et la manipulation.

Gestion des Erreurs

Les erreurs lors de la génération d’images sont capturées et loguées, permettant de diagnostiquer les problèmes.

Création du Contenu HTML

Après avoir généré les images correspondantes aux résumés extraits, l’étape suivante consiste à assembler le texte de l’histoire et les images dans un format présentable pour l’utilisateur. Cela se fait en créant un contenu HTML structuré qui sera affiché sur le site web.

def create_html_with_images(text_data, images_urls, generate_images=True):
    """
    Crée un contenu HTML en intégrant le texte et les images générées.
    """
    # Extraction du titre
    title_match = re.search(r"\[titre\](.*?)\[end_titre\]", text_data)
    if title_match is not None:
        title = title_match.group(1)
        text_data = text_data.replace(title_match.group(0), "")
    else:
        title = "Histoire Générée par l'IA"

    # Initialisation du contenu HTML
    html_content = """
    <html>
    <head>
        <title>Histoire générée par l'IA</title>
        <meta charset='UTF-8'>
        <style>
            /* Styles CSS pour une présentation agréable */
            body { font-family: Arial, sans-serif; margin: 20px; }
            .title { text-align: center; font-size: 2em; margin-bottom: 20px; }
            .center { text-align: center; }
            img { max-width: 100%; height: auto; margin: 20px 0; }
            p { text-align: justify; line-height: 1.6; }
        </style>
    </head>
    <body>
    """
    html_content += f'<div class="title">{title}</div>\n'

    # Séparation du texte en segments basés sur les résumés
    summaries = extract_summaries(text_data)
    segments = re.split(r"\[resume\].*?\[end_resume\]", text_data, flags=re.DOTALL)

    # Assemblage du texte et des images
    for i, segment in enumerate(segments):
        formatted_segment = segment.strip().replace("\n", "<br>")
        html_content += f"<p>{formatted_segment}</p>\n"
        if generate_images and i < len(images_urls) and images_urls[i]:
            image_url = images_urls[i]
            html_content += f'<div class="center"><img src="{image_url}" alt="Image générée"></div>\n'

    html_content += "</body></html>"
    return html_content

Explication en Détail

  1. Extraction du Titre:

    • Utilise une expression régulière pour trouver le texte entre les balises [titre] et [end_titre].
    • Supprime les balises du texte principal après extraction.
    • Si aucun titre n’est trouvé, un titre par défaut est utilisé.
  2. Initialisation du HTML:

    • Le contenu HTML commence avec les balises <html>, <head>, et <body>.
    • Les styles CSS sont inclus pour améliorer la présentation (typo, marges, alignements).
  3. Séparation du Texte:

    • Le texte est divisé en segments en utilisant les balises [resume] et [end_resume].
    • Les segments représentent les parties de l’histoire sans les résumés.
  4. Assemblage:

    • Chaque segment de texte est inséré dans un paragraphe <p>.
    • Si la génération d’images est activée et qu’il y a une image correspondante, l’image est insérée après le paragraphe.
    • Les images sont centrées et adaptées à la taille de l’écran pour une meilleure expérience utilisateur.
  5. Finalisation:

    • Les balises de fermeture </body> et </html> sont ajoutées pour compléter le document HTML.

Pourquoi Cette Approche ?

  • Alignement du Texte et des Images: En insérant les images après les segments de texte correspondants, l’histoire est enrichie visuellement, ce qui est particulièrement important pour les enfants.

  • Flexibilité: Si l’utilisateur choisit de ne pas générer d’images, le code gère ce cas en n’insérant que le texte.

  • Accessibilité: En utilisant des balises sémantiques et des styles adaptés, le contenu est accessible sur différents appareils (ordinateurs, tablettes, smartphones).

Upload sur S3 et Mise à Jour du Statut

Une fois le contenu HTML généré, il est nécessaire de le rendre accessible à l’utilisateur. Cela se fait en téléchargeant le fichier sur un bucket S3 configuré pour l’hébergement de sites web statiques.

def upload_to_s3(content, bucket_name, content_type="image/jpeg"):
    """
    Télécharge le contenu sur S3 et retourne l'URL publique.
    """
    s3_client = boto3.client("s3")
    timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
    if content_type == "image/jpeg":
        file_name = f"generated_images/{timestamp}.jpg"
        content_to_upload = base64.b64decode(content)
    else:
        file_name = f"generated_content/{timestamp}.html"
        content_to_upload = content.encode("utf-8")
        content_type = "text/html; charset=utf-8"

    try:
        s3_client.put_object(
            Bucket=bucket_name,
            Key=file_name,
            Body=content_to_upload,
            ContentType=content_type,
            ACL='public-read'
        )
        return f"https://{os.environ['CLOUDFRONT_DOMAIN']}/{file_name}"
    except Exception as e:
        logger.error(f"Error uploading content to S3: {e}", exc_info=True)
        raise

Détails Techniques

  • Nomination des Fichiers:

    • Les fichiers sont nommés en utilisant un horodatage pour garantir l’unicité.
    • Les images sont stockées dans le dossier generated_images/ et les fichiers HTML dans generated_content/.
  • Téléchargement sur S3:

    • Utilisation du client S3 de boto3 pour interagir avec le service.
    • Le contenu est encodé ou décodé en fonction du type (image ou texte).
    • Le paramètre ACL='public-read' rend le fichier accessible publiquement.
  • Construction de l’URL:

    • L’URL publique est construite en utilisant le domaine CloudFront configuré, ce qui permet une distribution rapide et sécurisée du contenu.
  • Gestion des Exceptions:

    • En cas d’erreur lors du téléchargement, l’exception est loguée et levée pour être traitée par le lambda_handler.

Fonction Principale lambda_handler

La fonction lambda_handler est le point d’entrée de la fonction Lambda. Elle orchestre toutes les étapes décrites précédemment.

def lambda_handler(event, context):
    """
    Point d'entrée de la fonction Lambda.
    """
    try:
        # Récupération des données de la requête
        request_id = event.get("requestId")
        body = json.loads(event.get("body", "{}"))
        prompt = body.get("text", "Texte par défaut")
        # Récupération des autres paramètres (modèles, langue, etc.)

        # Mise à jour du statut dans DynamoDB
        update_dynamodb(request_id, "Processing")

        # Génération de l'histoire
        text_data = generate_story(prompt, story_generation_model, model_id, language, api_key)

        # Correction des balises et extraction des résumés
        text_data = correct_resume_tags(text_data)
        summaries = extract_summaries(text_data)

        # Génération des images
        images_urls = []
        if generate_images and summaries:
            images_urls = generate_image_for_each_summary(
                summaries, image_generation_model, bucket_name, seed, style_with_spaces, size, quality, language
            )

        # Création du contenu HTML
        html_content = create_html_with_images(text_data, images_urls, generate_images)

        # Upload du contenu sur S3
        result_url = upload_to_s3(html_content, bucket_name, content_type="text/html")

        # Mise à jour du statut avec le lien du résultat
        update_dynamodb(request_id, "link", result_url)

        # Retour de la réponse HTTP
        return {
            "isBase64Encoded": False,
            "statusCode": 200,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({"requestId": request_id, "resultUrl": result_url}),
        }

    except Exception as e:
        logger.error(f"Erreur lors de l'exécution de la fonction lambda: {str(e)}", exc_info=True)
        update_dynamodb(request_id, "Failed")
        return {
            "statusCode": 500,
            "body": json.dumps({"message": "Internal server error"}),
            "headers": {"Content-Type": "application/json"},
        }

Explication

  • Traitement de la Requête:

    • Récupère les informations nécessaires depuis l’événement (event) reçu.
    • Les paramètres de la requête incluent le prompt, les modèles sélectionnés, la langue, etc.
  • Mise à Jour du Statut:

    • Avant de commencer le traitement, le statut est mis à jour en “Processing” dans DynamoDB.
  • Génération de l’Histoire:

    • Appel à generate_story avec les paramètres appropriés.
  • Extraction et Traitement:

    • Les balises sont corrigées et les résumés extraits pour la génération d’images.
  • Génération des Images:

    • Si la génération d’images est activée, les images correspondantes sont générées et les URLs collectées.
  • Création du Contenu HTML:

    • Le texte et les images sont combinés pour créer le contenu HTML final.
  • Upload sur S3:

    • Le contenu HTML est uploadé sur S3 et l’URL du résultat est obtenue.
  • Mise à Jour du Statut Final:

    • Le statut est mis à jour en “link” avec l’URL du résultat dans DynamoDB.
  • Retour de la Réponse:

    • La réponse inclut le requestId et l’URL du résultat, permettant au client de vérifier le statut ou d’accéder directement au contenu.
  • Gestion des Exceptions:

    • En cas d’erreur, le statut est mis à jour en “Failed” et une réponse HTTP 500 est retournée.

Fonction Lambda status_checker.py

Aperçu Général

La fonction status_checker.py permet aux utilisateurs de vérifier le statut de leur demande de génération d’histoire. Elle interroge DynamoDB pour récupérer le statut actuel et, si disponible, l’URL du résultat.

Analyse du Code

import boto3
import json
from botocore.exceptions import ClientError

def lambda_handler(event, context):
    """
    Fonction Lambda pour vérifier le statut d'une demande.
    """
    # Initialisation de DynamoDB
    dynamodb = boto3.resource("dynamodb")
    table = dynamodb.Table("TaskStatus")

    # Définition des en-têtes HTTP
    headers = {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET,OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
    }

    # Récupération du requestId depuis les paramètres de la requête
    query_params = event.get("queryStringParameters")
    if not query_params or "requestId" not in query_params:
        return {
            "statusCode": 400,
            "body": json.dumps({"message": "Missing requestId"}),
            "headers": headers,
        }

    request_id = query_params["requestId"]
    try:
        # Récupération de l'élément dans DynamoDB
        response = table.get_item(Key={"requestId": request_id})
        if "Item" in response:
            item = response["Item"]
            return {
                "statusCode": 200,
                "body": json.dumps(
                    {
                        "status": item.get("status", "Unknown"),
                        "resultUrl": item.get("resultUrl", ""),
                    }
                ),
                "headers": headers,
            }
        else:
            return {
                "statusCode": 404,
                "body": json.dumps({"message": "Request ID not found"}),
                "headers": headers,
            }
    except ClientError as e:
        return {
            "statusCode": 500,
            "body": json.dumps({"message": str(e)}),
            "headers": headers,
        }

Détails

  • Récupération du requestId:

    • Le requestId est essentiel pour identifier la demande spécifique de l’utilisateur.
  • Interrogation de DynamoDB:

    • La fonction tente de récupérer l’élément correspondant au requestId.
    • Si l’élément existe, le statut et le resultUrl sont extraits.
  • Construction de la Réponse:

    • Si le statut est disponible, il est retourné avec l’URL du résultat.
    • Si l’élément n’est pas trouvé, une erreur 404 est retournée.
    • En cas d’erreur lors de l’interrogation de la base de données, une erreur 500 est retournée avec un message approprié.
  • En-têtes HTTP:

    • Les en-têtes sont définis pour permettre les requêtes CORS depuis le site web.

Intégration avec API Gateway

Configuration des Endpoints

L’API Gateway expose deux endpoints principaux pour interagir avec les fonctions Lambda :

  1. /generate-image:

    • Méthode : POST
    • Description : Permet aux utilisateurs de lancer la génération d’une histoire et, éventuellement, des images associées.
    • Intégration : Connecté à la fonction Lambda StoryPixAI.py.
  2. /check-status:

    • Méthode : GET
    • Description : Permet aux utilisateurs de vérifier le statut de leur demande en fournissant le requestId.
    • Intégration : Connecté à la fonction Lambda status_checker.py.

Authentification avec Cognito

Pour sécuriser l’API et contrôler l’accès aux ressources, j’ai intégré Amazon Cognito.

  • User Pool:

    • Gère les informations d’identification des utilisateurs.
    • Permet l’inscription, la connexion et la gestion des utilisateurs.
  • Authorizer:

    • Configuré dans API Gateway pour vérifier les tokens JWT émis par Cognito.
    • Assure que seules les requêtes authentifiées peuvent accéder aux endpoints protégés.
  • Intégration sur API Gateway:

    • Les endpoints /generate-image et /check-status sont protégés par l’authorizer Cognito.
    • Les clients doivent inclure le token d’authentification dans les en-têtes de leurs requêtes (Authorization).

Site Statique sur S3 et Interaction avec l’API

Structure du Site

Le site web statique sert d’interface utilisateur pour l’application.

  • index.html:

    • Contient le formulaire permettant aux utilisateurs de saisir le prompt, de choisir les options de génération, et de soumettre leur demande.
    • Inclut les scripts nécessaires pour l’interaction avec l’API et la gestion de l’authentification.
  • storypixai.js:

    • Contient le code JavaScript pour gérer les interactions avec l’API.
    • Gère l’authentification avec Cognito, la soumission du formulaire, le suivi du statut, et l’affichage des résultats.

Flux de Travail Utilisateur

  1. Connexion:

    • L’utilisateur se connecte via le formulaire de connexion intégré.
    • Les informations sont vérifiées via Cognito.
  2. Soumission de la Demande:

    • L’utilisateur remplit le formulaire avec le prompt et les options souhaitées.
    • Lors de la soumission, une requête POST est envoyée à l’endpoint /generate-image avec les données.
  3. Traitement Asynchrone:

    • L’API retourne immédiatement un requestId.
    • Le traitement de la génération se fait en arrière-plan.
  4. Vérification du Statut:

    • Le site web interroge périodiquement l’endpoint /check-status en fournissant le requestId.
    • Une fois le statut “link” reçu, l’URL du résultat est affichée à l’utilisateur.
  5. Affichage du Résultat:

    • L’utilisateur peut cliquer sur le lien pour accéder à l’histoire générée avec les images.

Gestion des Requêtes et des Réponses

  • Requêtes Authentifiées:

    • Toutes les requêtes vers l’API incluent le token d’authentification.
    • Le token est géré par le SDK Cognito inclus dans le site web.
  • Gestion des Statuts:

    • Les statuts possibles sont “Processing”, “link”, “Failed”.
    • Le site adapte son interface en fonction du statut reçu (par exemple, affichage d’un spinner, message d’erreur, lien vers le résultat).

Interconnexions entre les Composants

Voici comment les différents composants interagissent :

  • Site Web ↔️ API Gateway:

    • Le site web envoie des requêtes aux endpoints exposés par l’API Gateway.
    • Les tokens d’authentification sont inclus pour sécuriser les requêtes.
  • API Gateway ↔️ Fonctions Lambda:

    • L’API Gateway invoque les fonctions Lambda correspondantes en fonction des requêtes reçues.
  • Fonctions Lambda ↔️ DynamoDB:

    • Les fonctions Lambda StoryPixAI.py et status_checker.py interagissent avec DynamoDB pour mettre à jour et récupérer le statut des demandes.
  • Fonction Lambda ↔️ S3:

    • La fonction StoryPixAI.py télécharge les images générées et le contenu HTML sur S3.
  • CloudFront ↔️ S3:

    • CloudFront est utilisé pour distribuer le contenu stocké sur S3 de manière rapide et sécurisée.
    • Les URLs fournies aux utilisateurs pointent vers le domaine CloudFront.
  • Utilisateur ↔️ Site Web:

    • L’utilisateur interagit avec le site web pour soumettre des demandes et visualiser les résultats.

Exemple de Résultat dans les logs CLoudwatch après un Appel de Requête

Voici un exemple de résultat de logs après un appel de requête pour que vous puissiez voir le format brut des données générées :

[INFO]	2024-07-22T19:13:49.764Z	4ec7d759-2fd2-49ca-b929-4f4d12629c73	Texte généré par l'ia : [titre]Tom et Zoé à l'aventure ![end_titre]

Une belle matinée de printemps, Tom et Zoé se retrouvent chez leur grand-mère pour les vacances. Tom a des cheveux bruns et ébouriffés, des yeux verts pétillants et une tache de rousseur sur le nez. Zoé, elle, a de longs cheveux blonds tressés, des yeux bleus comme le ciel et toujours un sourire aux lèvres.

Ce jour-là, alors qu'ils jouent dans le jardin, ils découvrent quelque chose d'étrange près du vieux puits. "Regarde, Zoé, cette lumière étrange !", s'exclame Tom.

"On dirait un passage secret...", murmure Zoé avec fascination. Ils se regardent, surexcités par la perspective d'une aventure. Ils s'approchent prudemment et tombent sur un escalier en colimaçon menant sous terre. Sans hésiter, ils commencent à descendre.

L'escalier les mène à une forêt lumineuse où les arbres sont couverts de feuilles dorées et où des fleurs scintillent de toutes les couleurs de l'arc-en-ciel. Le sol est tapissé de mousse douce et le chant mélodieux des oiseaux résonne autour d'eux.

[resume]Un arbre gigantesque au centre de la clairière, avec des racines enchevêtrées formant des arches naturelles. Ses feuilles changent de couleur au gré du vent, passant du vert émeraude au violet profond. Autour de ses branches, des lucioles dansent et éclairent l'atmosphère d'une lumière douce et féerique.[end_resume]

Alors qu'ils explorent les environs émerveillés, un petit renard roux avec une touffe blanche sur la queue surgit devant eux. "Bonjour, je m'appelle Félix. Êtes-vous perdus ?"

"Non, pas vraiment. Nous cherchons simplement à explorer !" répondent-ils en chœur.

"Alors, vous êtes au bon endroit. Mais attention, quelque chose de précieux est en danger ici. Un méga escargot vole toutes les salades du jardin magique et il faut l'arrêter ! Voulez-vous m'aider ?" demande Félix.

Tom et Zoé, enthousiastes devant cette mission, acceptent sans hésiter.

Félix les guide à travers des sentiers sinueux, où les branches des arbres semblent former des arches protectrices au-dessus de leurs têtes. La route devient de plus en plus difficile à mesure qu'ils s'enfoncent dans la forêt.

[resume]Une rivière cristalline aux eaux claires comme le verre, dans laquelle nagent des poissons multicolores. Les rives sont bordées de galets ronds et lisses, et des nénuphars aux fleurs roses flottent doucement à la surface.[end_resume]

"

Regardez là-bas, derrière ce buisson," chuchote Félix, en pointant une direction. Derrière les plantes, ils aperçoivent une trace de bave visqueuse brillamment éclairée.

"Ça doit être l'escargot," murmure Tom.

Ils suivent la piste de bave jusqu'à une clairière où ils tombent face à face avec le méga escargot. Il est énorme, avec une coquille noire et luisante et des yeux perçants qui semblent voir à travers tout.

"Je suis le protecteur de ces salades !" s'exclame l'escargot d'une voix grondante. "Elles m'appartiennent toutes !"

[resume]Le méga escargot est si grand que sa coquille ressemble à une petite montagne arrondie. Elle est noire avec des motifs argentés en spirale qui brillent sous le soleil. Ses antennes sont longues et frémissent à chaque mouvement. Il laisse derrière lui une traînée de bave qui scintille comme des cristaux de glace.[end_resume]

"Mais ces salades nourrissent tout le monde ici," réplique Zoé courageusement. "Il faut partager !"

L'escargot se met à rire et glisse vers eux lentement mais de manière menaçante. Tom et Zoé échangent un regard, ils savent qu'ils doivent utiliser leur intelligence et leur courage pour résoudre ce problème.

"Il y a sans doute un moyen de convaincre l'escargot !" dit Félix. "Utilisons la magie de cette forêt pour lui montrer une meilleure voie."

Zoé, qui découvre soudain qu'elle possède un pouvoir magique, ferme les yeux et se concentre. Elle sent une énergie chaude circuler en elle. Elle lève la main et des lianes lumineuses surgissent du sol, s'enroulant doucement autour de l'escargot sans lui faire de mal.

"Je vais créer un jardin immense juste pour toi," annonce Zoé, "mais tu devras promettre de partager avec tout le monde ici."

L'escargot, touché par la bonté de Zoé, hésite puis accepte. "Je ne savais pas que j'avais blessé autant de monde. Merci de me montrer un autre chemin."

Les lianes lumineuses dessinent alors un magnifique jardin rempli de salades et d'autres délices pour l'escargot. Cependant, le jardin ne s’ouvre que s’il appelle les autres créatures pour partager.

[resume]Un jardin magnifique avec des salades immenses, leurs feuilles vert tendre et croquantes. Des carottes orange vif et des courgettes vertes s'y mêlent, baignant dans une lumière dorée. Des papillons aux ailes irisées volent autour, ajoutant une touche de magie à ce lieu merveilleux.[end_resume]

En voyant cela, l'escargot laisse échapper une larme de reconnaissance et appelle instantanément les animaux de la forêt pour voir le miracle. Les habitants de la forêt acclament Tom et Zoé. Un énorme festin est organisé en leur honneur.

"Merci d'avoir sauvé notre jardin et notre amitié !" s'exclame Félix avec émotion.

Puis, ils se disent au revoir et, guidés par Félix, Tom et Zoé retrouvent le chemin de la maison. Au moment de passer le portail magique, ils se retournent une dernière fois pour admirer le spectacle enchanteur.

[resume]Un pont arc-en-ciel scintillant traverse le ciel, connectant la forêt magique à leur monde. Les couleurs brisées de l'arc iridescent se mélangent sous leurs regards émerveillés, illuminant la verdure environnante sous une lumière douce et chaleureuse. Chaque pas sur le pont résonne d'une mélodie cristalline.[end_resume]

Ils reprennent leur place dans le jardin de leur grand-mère, main dans la main, renforcés par cette aventure. "Tom, tu penses qu'on reverra Félix ?" demande Zoé rêveusement.

"J'espère bien ! Et qui sait quelle nouvelle aventure nous attend !" répond Tom en souriant.

La journée se termine sous le ciel étoilé, et leur amitié est plus forte que jamais, une étoile brillante dans l'univers de leurs rêves et de leurs aventures.

Les défis et les épreuves leur ont appris des valeurs précieuses : l'amitié, le partage, la persévérance, et surtout, la gentillesse.

Et c'est ainsi que Tom et Zoé grandissent, un peu plus chaque jour, devenant eux-mêmes des héros dans leurs cœurs d'enfants.

[resume]Des étoiles filantes traversent un ciel de velours noir, chaque trainée lumineuse ajoutant une touche de mystère à la nuit. Sur le chemin du retour, chaque pas dans le sable semble faire briller les grains comme des diamants sous la douce lumière de la lune. Une douce brise apporte l'odeur salée de la mer, ponctuée par le murmure des vagues au loin.[end_resume]

Fin.

Intégration Continue avec GitLab CI/CD

Pour assurer un développement et un déploiement fluides de StoryPixAI, j’ai mis en place un pipeline d’intégration continue (CI) et de déploiement continu (CD) en utilisant GitLab CI/CD. Cette configuration automatise les processus de construction et de déploiement, garantissant ainsi la qualité et la fiabilité du code à chaque modification.

Configuration du Pipeline

Le pipeline est défini dans le fichier .gitlab-ci.yml à la racine du projet. Voici un aperçu de sa structure :

stages:
  - Pré-requis optionel
  - Vérifications
  - Déploiements
  - Management
  - Suppressions

variables:
  TERRAFORM_VERSION: "1.5.7-*"
  TF_VAR_region: $AWS_DEFAULT_REGION

Cette configuration définit les différentes étapes du pipeline et les variables globales utilisées dans le processus CI/CD.

Jobs Principaux

Le pipeline comprend plusieurs jobs clés :

  1. Vérification Terraform :

    Vérification Terraform:
      stage: Vérifications
      when: manual
      script:
        - /bin/bash -c "source export.sh && terraform_plan"
    

    Ce job exécute terraform plan pour vérifier les changements d’infrastructure prévus sans les appliquer.

  2. Déploiement Terraform :

    Déploiement Terraform:
      stage: Déploiements
      when: manual
      dependencies:
        - Vérification Terraform
      script:
        - /bin/bash -c "source export.sh && terraform_apply"
    

    Après vérification, ce job applique les changements d’infrastructure en exécutant terraform apply.

  3. Suppression Terraform :

    Suppression Terraform:
      stage: Suppressions
      when: manual
      script:
        - /bin/bash -c "source export.sh && terraform_destroy"
    

    Ce job permet de détruire l’infrastructure si nécessaire, en exécutant terraform destroy.

  4. Gestion des Clés OpenAI :

    Clé OpenAI - Ajout:
      stage: Pré-requis optionel
      when: manual
      script:
        - |
          KEYS_FOUND=false
          if [ -n "$OPENAI_KEY" ]; then
            /bin/bash -c "source export.sh && manage_openai_key put $OPENAI_KEY"
            KEYS_FOUND=true
          fi     
          if [ "$KEYS_FOUND" = false ]; then
            echo "Aucune clé trouvée."
            exit 1
          fi      
    
    Clé OpenAI - Supression:
      stage: Suppressions
      when: manual
      script:
        - /bin/bash -c "source export.sh && manage_openai_key delete"
    

    Ces jobs gèrent l’ajout et la suppression sécurisée des clés API OpenAI dans AWS Parameter Store.

Environnement d’Exécution

Chaque job s’exécute dans un conteneur Docker basé sur Ubuntu 22.04, avec Terraform et AWS CLI installés :

.terraform_template: &terraform_template
  image:
    name: ubuntu:22.04
  before_script:
    - apt-get update
    - apt-get install -y gnupg software-properties-common curl
    - curl -fsSL https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
    - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep 'VERSION_CODENAME' /etc/os-release | cut -d'=' -f2) main" | tee /etc/apt/sources.list.d/hashicorp.list
    - apt-get update
    - apt-get install -y terraform=${TERRAFORM_VERSION} python3-pip bash jq zip
    - pip3 install awscli >> /dev/null

Avantages de cette Approche CI/CD

  1. Automatisation : Chaque modification du code déclenche automatiquement le pipeline, assurant des vérifications et des déploiements cohérents.

  2. Contrôle Manuel : Les étapes critiques comme le déploiement et la suppression sont configurées en mode manuel (when: manual), offrant un contrôle supplémentaire avant l’exécution.

  3. Gestion Sécurisée des Secrets : L’intégration avec AWS Parameter Store pour la gestion des clés API assure une manipulation sécurisée des informations sensibles.

  4. Flexibilité : La structure en stages permet une exécution ordonnée et logique des différentes étapes du pipeline.

  5. Reproductibilité : L’utilisation d’un environnement Docker standardisé garantit que les builds et les tests sont reproductibles sur différents systèmes.

Cette configuration CI/CD permet non seulement d’automatiser le déploiement de StoryPixAI, mais aussi de maintenir un haut niveau de qualité et de fiabilité tout au long du cycle de développement.

Conclusion

StoryPixAI a été bien plus qu’un simple projet technique. C’était une véritable aventure dans le monde de l’IA générative, me permettant de combiner ma passion pour la technologie avec le désir de créer des histoires magiques pour mes enfants.

Ce projet m’a offert l’opportunité d’explorer diverses facettes de l’IA, de la conception d’une interface utilisateur intuitive à la maîtrise du prompting, en passant par la mise en place d’une infrastructure cloud robuste avec AWS et Terraform. Chaque étape a été une source d’apprentissage, me confrontant à des défis techniques stimulants et m’obligeant à élargir mes compétences en développement full-stack et en DevOps.

J’espère que ce billet de blog vous a donné un aperçu des coulisses de cette aventure passionnante.

Points Clés

  • Instructions Détaillées :

    • Des prompts clairs et structurés permettent d’obtenir des résultats cohérents et de haute qualité de la part des modèles d’IA.
  • Architecture Modulaire :

    • Chaque composant (site web, API Gateway, Lambda, DynamoDB, S3, Cognito) joue un rôle spécifique, facilitant la maintenance et l’évolution du système.
  • Sécurité et Scalabilité :

    • L’utilisation des services managés d’AWS assure une sécurité robuste et une capacité à s’adapter à une demande croissante.

Lien du projet : StoryPixAI