Con StoryPixAI, mi objetivo era crear una aplicación web interactiva que permitiera a los usuarios generar historias para niños, enriquecidas con imágenes generadas por modelos de inteligencia artificial. Para lograrlo, utilicé varios servicios de AWS como Lambda, API Gateway, DynamoDB, S3 y Cognito para la autenticación. El código de la infraestructura se gestiona con Terraform, y el despliegue está automatizado a través de GitLab CI. En esta entrada, les revelo los entresijos de este emocionante proyecto, desde las elecciones tecnológicas hasta los desafíos encontrados.

Introducción

Como arquitecto de infraestructuras en la nube y experto en DevOps, siempre he estado fascinado por las nuevas tecnologías y su potencial para transformar nuestra vida diaria. La aparición de la IA generativa despertó en mí una creciente curiosidad, y sentí la necesidad de sumergirme en este universo en plena efervescencia.

Así nació StoryPixAI, un proyecto personal que me permitió explorar las infinitas posibilidades de la IA para crear historias personalizadas e ilustraciones mágicas para los niños. Este proyecto me brindó la oportunidad de ponerme en la piel de un desarrollador full-stack, un ingeniero de prompts, un product owner e incluso un diseñador UX/UI, compartiendo mi pasión por la tecnología con mis seres queridos.

En esta entrada del blog, compartiré mis elecciones tecnológicas y los desafíos superados durante esta emocionante aventura.

¡Pero primero, un aperitivo!

Para darles una idea del potencial de StoryPixAI, aquí tienen algunas historias generadas automáticamente, en varios idiomas.
Cada historia está acompañada de ilustraciones, haciendo el relato aún más inmersivo para los niños:

La IA al servicio de la creatividad: un recorrido de experimentación

Mi aventura con StoryPixAI comenzó con un Proof of Concept (PoC) simple: una función Lambda que interactuaba con OpenAI para generar texto y DALL-E para crear imágenes. Este primer éxito me animó a ir más allá y explorar otros modelos de IA a través de AWS Bedrock.

GPT-4 y GPT-4-o: los narradores ágiles

Desde el comienzo del proyecto, GPT-4 de OpenAI se impuso como una elección obvia para la generación de texto. Su capacidad para entender los matices del lenguaje natural y producir relatos coherentes y creativos me permitió crear historias cautivadoras, adaptadas a la edad y los intereses de los niños. Pude experimentar con diferentes estilos de escritura, desde cuentos de hadas hasta aventuras espaciales, pasando por historias de animales y relatos fantásticos.

Cuando se lanzó GPT-4-0, rápidamente integré este nuevo modelo en StoryPixAI. Quedé impresionado por su velocidad de generación aumentada, que permitió reducir considerablemente el tiempo de espera para la generación, y por la notable mejora en la calidad de las historias generadas, con relatos aún más fluidos, coherentes e imaginativos. GPT-4-0 se ha convertido así en un activo principal para StoryPixAI, ofreciendo una experiencia de usuario más rápida y más agradable.

DALL-E 3 : el ilustrador de referencia

Si los modelos de generación de texto ofrecían resultados satisfactorios, la elección de la herramienta de generación de imágenes resultó ser más crucial. Después de numerosos ensayos, DALL-E 3 se impuso como el modelo de referencia para StoryPixAI. Su capacidad para crear ilustraciones originales, detalladas y perfectamente adaptadas a las historias generadas por GPT-4 fue un factor determinante en el éxito del proyecto.

Bedrock de AWS: la puerta abierta hacia la experimentación

Deseando no limitarme a OpenAI, utilicé Bedrock de AWS para integrar fácilmente otros modelos de IA generativa a StoryPixAI. Esta plataforma me permitió probar Claude de Anthropic y Mistral para la generación de texto, y Stable Diffusion para la creación de imágenes.

Aunque estos modelos dieron resultados interesantes, finalmente decidí concentrarme en GPT-4 y GPT-4-0 por su rapidez y calidad de generación de texto, y en DALL-E 3 por su capacidad para producir ilustraciones perfectamente adaptadas a las historias. Es importante señalar que el prompt utilizado para generar las imágenes es en gran parte elaborado por el propio modelo de texto, lo que asegura una coherencia entre el relato y la ilustración.

El desafío de la API asíncrona y de DynamoDB

Una vez validado el PoC, emprendí la creación de una API para hacer StoryPixAI accesible a través de una interfaz web. Fue en esta etapa cuando encontré mi primer desafío importante: la limitación de tiempo de espera de API Gateway. Para sortear esta restricción y permitir la generación de historias más largas y más complejas, tuve que implementar una arquitectura asíncrona.

Entonces entró en juego Amazon DynamoDB. Utilicé esta base de datos NoSQL para almacenar las tareas de generación de historias en curso, así como sus resultados una vez terminadas. Gracias a este enfoque, la API podía devolver una respuesta inmediata al usuario, quien podía luego consultar el estado de su solicitud y recuperar la historia generada una vez lista.

CORS y la interfaz de usuario: obstáculos a superar

La implementación de la interfaz web también fue una fuente de desafíos. Tuve que familiarizarme con las sutilezas de CORS (Cross-Origin Resource Sharing) para permitir que mi frontend se comunicara con la API. También dediqué tiempo a mejorar la experiencia del usuario agregando funcionalidades como la selección de modelos de IA y estilos de imágenes.

El prompting: un arte a dominar

A lo largo del desarrollo de StoryPixAI, perfeccioné mis habilidades en el prompting, ese arte de formular las solicitudes adecuadas para guiar a los modelos de IA. Aprendí a adaptar mis prompts según los modelos utilizados, los parámetros de la historia y las expectativas de los usuarios. Esta etapa fue crucial para obtener resultados de calidad y garantizar una experiencia de usuario satisfactoria.

Una infraestructura robusta y automatizada en AWS

StoryPixAI se basa en una infraestructura serverless alojada en Amazon Web Services (AWS), ofreciendo una combinación ideal de flexibilidad, escalabilidad y optimización de costos. Esta arquitectura, completamente automatizada gracias a Terraform y GitLab CI/CD, permite un despliegue rápido y confiable de la aplicación.

Los servicios AWS en el corazón de StoryPixAI

alt text

La arquitectura de StoryPixAI se articula alrededor de los siguientes servicios de AWS: * Amazon S3 (Simple Storage Service): Almacenamiento de los archivos estáticos del sitio web (HTML, CSS, JavaScript) y de las historias generadas, así como sus ilustraciones asociadas.

  • Amazon CloudFront: Una red de distribución de contenido (CDN) que acelera la distribución del contenido de StoryPixAI a los usuarios de todo el mundo al almacenarlo en ubicaciones geográficas cercanas a ellos.
  • Amazon API Gateway: La puerta de entrada segura de la aplicación. Maneja las solicitudes de los usuarios, asegura su autenticación a través de Amazon Cognito, y las encamina hacia las funciones Lambda apropiadas.
  • AWS Lambda: Funciones serverless que constituyen el motor de StoryPixAI. Orquestan la generación de historias, la creación de imágenes, la gestión de tareas asíncronas y la interacción con DynamoDB y otros servicios de AWS.
  • Amazon DynamoDB: Una base de datos NoSQL flexible y de alto rendimiento utilizada para almacenar información esencial para el funcionamiento de la aplicación.
  • Amazon Cognito: Un servicio de gestión de identidades y accesos que asegura la aplicación permitiendo a los usuarios iniciar sesión y controlando sus autorizaciones. Asegura que solo los usuarios autenticados puedan acceder a las funcionalidades de generación de historias.
  • Amazon Bedrock: Una plataforma que simplifica el acceso y uso de modelos de IA generativa de diferentes proveedores, como Anthropic (Claude) y Stability AI (Stable Diffusion). Bedrock permite integrar fácilmente estos modelos en la aplicación sin tener que gestionar su infraestructura subyacente.
  • Otros servicios de AWS: StoryPixAI también utiliza otros servicios de AWS, tales como IAM (Identity and Access Management) para la gestión precisa de permisos de acceso a los recursos, CloudWatch para monitoreo y registros (crucial para la depuración y análisis de rendimiento), y Systems Manager Parameter Store (SSM Parameter Store) para almacenar información sensible como claves de API, garantizando así la seguridad de la aplicación.

Terraform: la automatización al servicio de la infraestructura

Para gestionar esta infraestructura compleja, escogí Terraform, una herramienta de Infrastructure as Code (IaC) que permite describir la infraestructura en forma de código declarativo. Gracias a Terraform, pude automatizar la creación, modificación y destrucción de los recursos de AWS, garantizando así un entorno coherente, reproducible y fácil de gestionar. Esto simplifica considerablemente el proceso de despliegue y reduce el riesgo de errores humanos.

GitLab CI/CD: despliegues fluidos y sin contratiempos

Para asegurar un despliegue continuo y fiable de StoryPixAI, implementé un pipeline CI/CD (Integración Continua / Despliegue Continuo) en GitLab. Este pipeline automatiza las pruebas, la construcción y el despliegue de la aplicación con cada modificación del código fuente, permitiendo así detectar y corregir rápidamente los errores y entregar nuevas funcionalidades con plena confianza. Este enfoque garantiza que la aplicación siempre esté actualizada y minimiza los tiempos de inactividad.

Esta combinación de AWS, Terraform y GitLab CI/CD me permitió construir una infraestructura robusta, escalable y fácil de mantener, dejándome así más tiempo para centrarme en el aspecto creativo del proyecto y la mejora de la experiencia del usuario.

Arquitectura Global del proyecto StoryPixAI

Antes de sumergirnos en el código, aquí hay una visión general de la arquitectura de la aplicación:

  1. Sitio Estático en S3: Un sitio web estático alojado en un bucket S3, accesible a través de CloudFront para una distribución global.
  2. API Gateway: Expone endpoints para la generación de historias y la verificación de estado.
  3. Funciones Lambda:
    • StoryPixAI.py: Genera la historia y las imágenes asociadas.
    • status_checker.py: Verifica el estado de la generación en DynamoDB.
  4. DynamoDB: Almacena el estado de las tareas de generación.
  5. S3 : Almacena las imágenes generadas y las páginas HTML resultantes.
  6. Cognito : Gestiona la autenticación de los usuarios para asegurar la API.

Función Lambda StoryPixAI.py

Vista General

La función StoryPixAI.py es el núcleo de la aplicación. Es responsable de:

  • Generar una historia basada en un prompt del usuario.
  • Crear instrucciones detalladas para guiar el modelo de IA en la generación de la historia.
  • Extraer resúmenes para cada escena o elemento clave de la historia.
  • Generar imágenes correspondientes a estos resúmenes.
  • Combinar el texto y las imágenes en una página HTML.
  • Almacenar el resultado en S3 y actualizar el estado en DynamoDB.

Descomposición del Código

Importaciones y Configuración Inicial

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)

En esta sección, importo los módulos necesarios, configuro el logger para la depuración, y recupero la clave API de OpenAI almacenada en AWS Systems Manager Parameter Store (SSM). Esto permite asegurar la clave y no almacenarla en texto claro en el código.

Funciones Utilitarias

Corrección de Etiquetas
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é.

Esta función asegura que las etiquetas utilizadas para delimitar los resúmenes y los títulos sean uniformes. Esto es crucial para la extracción correcta de los resúmenes más tarde.

Extracción de Resúmenes
def extract_summaries(text):
    # Extrait les résumés du texte en utilisant des balises spécifiques.

Utiliza expresiones regulares para extraer las secciones de texto delimitadas por [resume] y [end_resume]. Estos resúmenes servirán como prompts para la generación de imágenes.

Generación de Instrucciones para las Imágenes
def generate_image_instructions(prompt, style, language):
    # Génère les instructions pour la création d'images.

Esta función formatea el prompt de manera que guíe al modelo de generación de imágenes, incluyendo el estilo y el idioma.

Actualización 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.

Actualiza la tabla TaskStatus para seguir el estado de la generación, lo cual es esencial para la función status_checker.py.

Análisis Profundo de generate_story_instructions

La función generate_story_instructions es el núcleo del proyecto. Genera un conjunto de instrucciones detalladas que serán pasadas al modelo de IA para guiar la generación de la historia.

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}"
    """

Construcción del Prompt

El prompt está diseñado para proporcionar al modelo de IA toda la información necesaria para generar una historia coherente, educativa y adecuada para niños.

  • Idioma : El parámetro language_description permite especificar el idioma de la historia, asegurando así que el texto generado estará en el idioma deseado.

  • Tema : El prompt del usuario se integra en las instrucciones para servir como base de la historia.

  • Longitud : Se especifica un rango de 1000 a 1500 palabras para controlar la longitud de la historia.

  • Elementos Clave : Las instrucciones fomentan la inclusión de elementos como la aventura, la magia y valores educativos importantes.

Detalles de las Instrucciones

Las instrucciones proporcionadas al modelo son extremadamente detalladas para guiar la generación de manera precisa.

Aquí hay un análisis de las diferentes partes del prompt:

  1. Estructura Narrativa : Se pide al modelo estructurar la historia con un inicio cautivador, un desarrollo rico en eventos, y una conclusión satisfactoria.

  2. Descripciones Visuales : La historia debe ser rica en descripciones visuales para estimular la imaginación de los niños.

  3. Personajes : Se fomenta el desarrollo de personajes entrañables con personalidades distintas.

  4. Etiquetas Específicas : Se utilizan etiquetas como [titre]... [end_titre] y [resume]... [end_resume] para delimitar el título y las descripciones visuales.

  5. Elementos Fantásticos : Se invita al modelo a incluir elementos mágicos o fantásticos para hacer la historia más atractiva.

  6. Valores Educativos : La historia debe enseñar valores importantes.

Papel de las Etiquetas

Las etiquetas juegan un papel crucial en el procesamiento posterior del texto generado.

  • [titre]… [end_titre] : Enmarca el título de la historia. Esto permite extraerlo fácilmente para mostrarlo de manera apropiada en la interfaz de usuario.

  • [resume]… [end_resume]: Enmarca las descripciones visuales detalladas de escenas clave de la historia. Estos resúmenes se utilizarán como prompts para la generación de imágenes.

Procesamiento Después de la Generación

Una vez que el modelo de IA ha generado la historia siguiendo estas instrucciones, el código realiza los siguientes pasos:

  1. Corrección de Etiquetas: La función correct_resume_tags se asegura de que todas las etiquetas estén correctamente formateadas para la extracción.

  2. Extracción de Resúmenes: La función extract_summaries utiliza las etiquetas [resume] y [end_resume] para extraer las descripciones visuales.

  3. Generación de Imágenes: Cada resumen se pasa a la función generate_image para crear una imagen correspondiente.

  4. Creación del Contenido HTML: El texto de la historia y las imágenes generadas se combinan para crear una página HTML completa.

Impacto en la Generación

Al proporcionar estas instrucciones detalladas, el modelo es guiado para:

  • Respetar el Formato: Al usar las etiquetas especificadas, el modelo produce un texto estructurado que facilita el procesamiento automatizado.

  • Generar Contenido Adecuado: Las restricciones sobre el idioma, el estilo y los temas garantizan que la historia sea apropiada para el público objetivo.

  • Facilitar la Generación de Imágenes: Al extraer descripciones visuales precisas, se obtienen prompts de calidad para la generación de imágenes.

Gestión de Etiquetas por el Modelo

El modelo está explícitamente instruido para no traducir o modificar las etiquetas. Esto es esencial para que las etiquetas permanezcan intactas y puedan ser utilizadas para el post-procesamiento. Las instrucciones insisten en este punto para evitar que el modelo, que podría intentar parafrasear o traducir todo el texto, altere las etiquetas.

Generación de la Historia

Una vez que las instrucciones detalladas son generadas por la función generate_story_instructions, el siguiente paso es pasar estas instrucciones al modelo de IA para que cree la historia.

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

Interacción con el Modelo OpenAI

  • Cliente OpenAI: Instancio un cliente de OpenAI usando la clave API recuperada previamente.

  • Prompting: El modelo recibe una serie de mensajes:

    • Un mensaje de sistema indicando que el asistente es un experto en historias para niños.
    • El mensaje del usuario que contiene las instrucciones detalladas generadas.
  • Respuesta del Modelo: El modelo genera una historia basada en las instrucciones proporcionadas.

Gestión de Errores

Si ocurre una excepción durante la llamada a la API de OpenAI, es capturada, y se devuelve un mensaje de error.

Extracción de Resúmenes y Etiquetas

Después de la generación de la historia, el siguiente paso consiste en extraer las descripciones visuales utilizando las etiquetas especificadas.

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

Corrección de Etiquetas

El modelo a veces puede alterar ligeramente las etiquetas (por ejemplo, añadir acentos). La función correct_resume_tags se asegura de que todas las etiquetas sean uniformes y estén correctamente formateadas.

Extracción de Resúmenes

La función extract_summaries utiliza una expresión regular para encontrar todas las ocurrencias de texto entre las etiquetas [resume] y [end_resume]. Estos resúmenes son las descripciones visuales detalladas que serán utilizadas para generar las imágenes.

Generación de Imágenes

Una vez que los resúmenes se extraen, cada resumen se utiliza para generar una imagen correspondiente.

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

Función generate_image

La función generate_image llama a la API del modelo de generación de imágenes (por ejemplo, OpenAI DALL·E) para crear una imagen a partir del resumen.

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

Generación de Instrucciones para las Imágenes La función generate_image_instructions adapta el resumen para crear un prompt adecuado para la generación de imágenes.

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.
    """
  • Estilo: El estilo especificado por el usuario (por ejemplo, “acuarela”, “caricatura”) se incluye en el prompt para influir en el renderizado de la imagen.

  • Idioma: La descripción se adapta al idioma elegido, lo que puede ayudar al modelo a entender las sutilezas culturales.

  • Instrucciones Claras: Al precisar que la escena debe ser puramente visual, se evita que el modelo agregue texto o elementos no deseados en la imagen.

Interacción con la API de OpenAI para las Imágenes

  • Llamada a la API: La función client.images.generate se utiliza para generar la imagen.

  • Parámetros Importantes:

    • Prompt: El prompt ajustado se pasa a la API.
    • Modelo: El modelo de generación de imágenes especificado.
    • Tamaño: El tamaño de la imagen (por ejemplo, “1024x1024”).
    • Calidad: La calidad de la imagen (estándar, HD).
    • Formato de Respuesta: Las imágenes se devuelven en base64 para facilitar el almacenamiento y la manipulación.

Gestión de Errores

Los errores durante la generación de imágenes se capturan y registran, lo que permite diagnosticar los problemas.

Creación del Contenido HTML

Después de generar las imágenes correspondientes a los resúmenes extraídos, el siguiente paso consiste en ensamblar el texto de la historia y las imágenes en un formato presentable para el usuario. Esto se hace creando un contenido HTML estructurado que se mostrará en el sitio 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

Explicación en Detalle

  1. Extracción del Título:

    • Utiliza una expresión regular para encontrar el texto entre las etiquetas [titre] y [end_titre].
    • Elimina las etiquetas del texto principal después de la extracción.
    • Si no se encuentra ningún título, se utiliza un título por defecto.
  2. Inicialización del HTML:

    • El contenido HTML comienza con las etiquetas <html>, <head> y <body>.
    • Se incluyen los estilos CSS para mejorar la presentación (tipografía, márgenes, alineaciones).
  3. Separación del Texto:

    • El texto se divide en segmentos utilizando las etiquetas [resume] y [end_resume].
    • Los segmentos representan las partes de la historia sin los resúmenes.
  4. Montaje:

    • Cada segmento de texto se inserta en un párrafo <p>.
    • Si la generación de imágenes está activada y hay una imagen correspondiente, la imagen se inserta después del párrafo.
    • Las imágenes se centran y se adaptan al tamaño de la pantalla para una mejor experiencia de usuario.
  5. Finalización:

    • Las etiquetas de cierre </body> y </html> se agregan para completar el documento HTML.

¿Por qué Este Enfoque?

  • Alineación del Texto y las Imágenes: Al insertar las imágenes después de los segmentos de texto correspondientes, la historia se enriquece visualmente, lo cual es particularmente importante para los niños.

  • Flexibilidad: Si el usuario elige no generar imágenes, el código maneja este caso insertando solo el texto.

  • Accesibilidad: Al utilizar etiquetas semánticas y estilos adaptados, el contenido es accesible en diferentes dispositivos (computadoras, tabletas, smartphones).

Subida a S3 y Actualización del Estado

Una vez generado el contenido HTML, es necesario ponerlo a disposición del usuario. Esto se hace subiendo el archivo a un bucket S3 configurado para el alojamiento de sitios web estáticos.

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

Detalles Técnicos

  • Nombres de Archivos:

    • Los archivos se nombran utilizando una marca de tiempo para garantizar la unicidad.
    • Las imágenes se almacenan en la carpeta generated_images/ y los archivos HTML en generated_content/.
  • Subida a S3:

    • Utilización del cliente S3 de boto3 para interactuar con el servicio.
    • El contenido se codifica o decodifica en función del tipo (imagen o texto).
    • El parámetro ACL='public-read' hace que el archivo sea accesible públicamente. - Construcción de la URL:
    • La URL pública se construye utilizando el dominio CloudFront configurado, lo que permite una distribución rápida y segura del contenido.
  • Manejo de Excepciones:

    • En caso de error durante la descarga, la excepción se registra y se lanza para ser manejada por el lambda_handler.

Función Principal lambda_handler

La función lambda_handler es el punto de entrada de la función Lambda. Orquesta todos los pasos descritos anteriormente.

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"},
        }

Explicación

  • Procesamiento de la Solicitud:

    • Recupera la información necesaria desde el evento recibido.
    • Los parámetros de la solicitud incluyen el prompt, los modelos seleccionados, el idioma, etc.
  • Actualización del Estado:

    • Antes de comenzar el procesamiento, el estado se actualiza a “Processing” en DynamoDB.
  • Generación de la Historia:

    • Llama a generate_story con los parámetros apropiados.
  • Extracción y Procesamiento:

    • Se corrigen las etiquetas y se extraen los resúmenes para la generación de imágenes.
  • Generación de las Imágenes:

    • Si la generación de imágenes está activada, se generan las imágenes correspondientes y se recopilan las URLs.
  • Creación del Contenido HTML:

    • El texto y las imágenes se combinan para crear el contenido HTML final.
  • Carga en S3:

    • El contenido HTML se carga en S3 y se obtiene la URL del resultado.
  • Actualización del Estado Final:

    • El estado se actualiza a “link” con la URL del resultado en DynamoDB.
  • Retorno de la Respuesta:

    • La respuesta incluye el requestId y la URL del resultado, permitiendo al cliente verificar el estado o acceder directamente al contenido.
  • Manejo de Excepciones:

    • En caso de error, el estado se actualiza a “Failed” y se retorna una respuesta HTTP 500.

Función Lambda status_checker.py

Descripción General

La función status_checker.py permite a los usuarios verificar el estado de su solicitud de generación de historia. Consulta DynamoDB para recuperar el estado actual y, si está disponible, la URL del resultado.

Análisis del Código

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,
        }

Detalles

  • Recuperación del requestId:

    • El requestId es esencial para identificar la solicitud específica del usuario.
  • Consulta a DynamoDB:

    • La función intenta recuperar el elemento correspondiente al requestId.
    • Si el elemento existe, se extraen el estado y el resultUrl.
  • Construcción de la Respuesta:

    • Si el estado está disponible, se devuelve con la URL del resultado.
    • Si no se encuentra el elemento, se retorna un error 404.
    • En caso de error al consultar la base de datos, se retorna un error 500 con un mensaje apropiado.
  • Encabezados HTTP:

    • Se definen los encabezados para permitir las solicitudes CORS desde el sitio web.

Integración con API Gateway

Configuración de los Endpoints

API Gateway expone dos endpoints principales para interactuar con las funciones Lambda:

  1. /generate-image:

    • Método: POST
    • Descripción: Permite a los usuarios iniciar la generación de una historia y, opcionalmente, las imágenes asociadas.
    • Integración: Conectado a la función Lambda StoryPixAI.py.
  2. /check-status:

    • Método: GET
    • Descripción: Permite a los usuarios verificar el estado de su solicitud proporcionando el requestId.
    • Integración: Conectado a la función Lambda status_checker.py.

Autenticación con Cognito

Para asegurar la API y controlar el acceso a los recursos, integré Amazon Cognito.

  • User Pool:

    • Gestiona la información de identificación de los usuarios.
    • Permite el registro, inicio de sesión y administración de los usuarios.
  • Authorizer:

    • Configurado en API Gateway para verificar los tokens JWT emitidos por Cognito.
    • Asegura que solo las solicitudes autenticadas puedan acceder a los endpoints protegidos. - Integración en API Gateway:
    • Los endpoints /generate-image y /check-status están protegidos por el autorizador Cognito.
    • Los clientes deben incluir el token de autenticación en los encabezados de sus solicitudes (Authorization).

Sitio Estático en S3 e Interacción con la API

Estructura del Sitio

El sitio web estático sirve como interfaz de usuario para la aplicación.

  • index.html:

    • Contiene el formulario que permite a los usuarios ingresar el prompt, elegir las opciones de generación y enviar su solicitud.
    • Incluye los scripts necesarios para la interacción con la API y la gestión de la autenticación.
  • storypixai.js:

    • Contiene el código JavaScript para gestionar las interacciones con la API.
    • Gestiona la autenticación con Cognito, el envío del formulario, el seguimiento del estado y la visualización de los resultados.

Flujo de Trabajo del Usuario

  1. Conexión:

    • El usuario se conecta a través del formulario de conexión integrado.
    • La información se verifica mediante Cognito.
  2. Envío de la Solicitud:

    • El usuario llena el formulario con el prompt y las opciones deseadas.
    • Al enviar, se envía una solicitud POST al endpoint /generate-image con los datos.
  3. Procesamiento Asíncrono:

    • La API devuelve inmediatamente un requestId.
    • El procesamiento de la generación se realiza en segundo plano.
  4. Verificación del Estado:

    • El sitio web consulta periódicamente el endpoint /check-status proporcionando el requestId.
    • Una vez recibido el estado “link”, se muestra al usuario la URL del resultado.
  5. Visualización del Resultado:

    • El usuario puede hacer clic en el enlace para acceder a la historia generada con las imágenes.

Gestión de Solicitudes y Respuestas

  • Solicitudes Autenticadas:

    • Todas las solicitudes a la API incluyen el token de autenticación.
    • El token es gestionado por el SDK de Cognito incluido en el sitio web.
  • Gestión de Estados:

    • Los estados posibles son “Processing”, “link”, “Failed”.
    • El sitio adapta su interfaz de acuerdo con el estado recibido (por ejemplo, muestra un spinner, mensaje de error, enlace al resultado).

Interconexiones entre los Componentes

Así es como interactúan los diferentes componentes:

  • Sitio Web ↔️ API Gateway:

    • El sitio web envía solicitudes a los endpoints expuestos por el API Gateway.
    • Los tokens de autenticación se incluyen para asegurar las solicitudes.
  • API Gateway ↔️ Funciones Lambda:

    • El API Gateway invoca las funciones Lambda correspondientes según las solicitudes recibidas.
  • Funciones Lambda ↔️ DynamoDB:

    • Las funciones Lambda StoryPixAI.py y status_checker.py interactúan con DynamoDB para actualizar y recuperar el estado de las solicitudes.
  • Función Lambda ↔️ S3:

    • La función StoryPixAI.py sube las imágenes generadas y el contenido HTML a S3.
  • CloudFront ↔️ S3:

    • CloudFront se utiliza para distribuir el contenido almacenado en S3 de manera rápida y segura.
    • Las URLs proporcionadas a los usuarios apuntan al dominio CloudFront.
  • Usuario ↔️ Sitio Web:

    • El usuario interactúa con el sitio web para enviar solicitudes y ver los resultados.

Ejemplo de Resultado en los Logs de Cloudwatch después de una Solicitud

Aquí hay un ejemplo de resultado de logs después de una solicitud para que pueda ver el formato bruto de los datos generados:

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

Integración Continua con GitLab CI/CD

Para asegurar un desarrollo y un despliegue fluidos de StoryPixAI, he implementado un pipeline de integración continua (CI) y despliegue continuo (CD) utilizando GitLab CI/CD. Esta configuración automatiza los procesos de construcción y despliegue, garantizando la calidad y la fiabilidad del código en cada modificación.

Configuración del Pipeline

El pipeline está definido en el archivo .gitlab-ci.yml en la raíz del proyecto. A continuación se muestra una descripción de su estructura:

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

variables:
  TERRAFORM_VERSION: "1.5.7-*"
  TF_VAR_region: $AWS_DEFAULT_REGION
``` Esta configuración define las diferentes etapas del pipeline y las variables globales utilizadas en el proceso CI/CD.

### Jobs Principales

El pipeline incluye varios jobs clave:

1. **Verificación Terraform**:
   ```yaml
   Verificación Terraform:
     stage: Verificaciones
     when: manual
     script:
       - /bin/bash -c "source export.sh && terraform_plan"

Este job ejecuta terraform plan para verificar los cambios de infraestructura previstos sin aplicarlos.

  1. Despliegue Terraform:

    Despliegue Terraform:
      stage: Despliegues
      when: manual
      dependencies:
        - Verificación Terraform
      script:
        - /bin/bash -c "source export.sh && terraform_apply"
    

    Después de la verificación, este job aplica los cambios de infraestructura ejecutando terraform apply.

  2. Eliminación Terraform:

    Eliminación Terraform:
      stage: Eliminaciones
      when: manual
      script:
        - /bin/bash -c "source export.sh && terraform_destroy"
    

    Este job permite destruir la infraestructura si es necesario, ejecutando terraform destroy.

  3. Gestión de Claves OpenAI:

    Clave OpenAI - Añadir:
      stage: Pre-requisito opcional
      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 ]; entonces
            echo "No se encontró ninguna clave."
            exit 1
          fi      
    
    Clave OpenAI - Eliminación:
      stage: Eliminaciones
      when: manual
      script:
        - /bin/bash -c "source export.sh && manage_openai_key delete"
    

    Estos jobs gestionan la adición y eliminación segura de las claves API OpenAI en AWS Parameter Store.

Entorno de Ejecución

Cada job se ejecuta en un contenedor Docker basado en Ubuntu 22.04, con Terraform y AWS CLI instalados:

.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

Ventajas de este Enfoque CI/CD

  1. Automatización: Cada modificación del código desencadena automáticamente el pipeline, asegurando verificaciones y despliegues coherentes.

  2. Control Manual: Las etapas críticas como el despliegue y la eliminación están configuradas en modo manual (when: manual), ofreciendo un control adicional antes de la ejecución.

  3. Gestión Segura de Secretos: La integración con AWS Parameter Store para la gestión de claves API asegura una manipulación segura de la información sensible.

  4. Flexibilidad: La estructura en etapas permite una ejecución ordenada y lógica de las diferentes fases del pipeline.

  5. Reproducibilidad: El uso de un entorno Docker estandarizado garantiza que las compilaciones y las pruebas sean reproducibles en diferentes sistemas.

Esta configuración CI/CD no solo permite automatizar el despliegue de StoryPixAI, sino también mantener un alto nivel de calidad y fiabilidad a lo largo del ciclo de desarrollo.

Conclusión

StoryPixAI ha sido mucho más que un simple proyecto técnico. Ha sido una verdadera aventura en el mundo de la IA generativa, permitiéndome combinar mi pasión por la tecnología con el deseo de crear historias mágicas para mis hijos.

Este proyecto me ha ofrecido la oportunidad de explorar diversas facetas de la IA, desde el diseño de una interfaz de usuario intuitiva hasta el dominio del prompting, pasando por la implementación de una infraestructura en la nube robusta con AWS y Terraform. Cada etapa ha sido una fuente de aprendizaje, enfrentándome a desafíos técnicos estimulantes y obligándome a ampliar mis habilidades en desarrollo full-stack y DevOps.

Espero que esta entrada de blog os haya dado una visión de los bastidores de esta emocionante aventura.

Puntos Claves

  • Instrucciones Detalladas:

    • Las indicaciones claras y estructuradas permiten obtener resultados coherentes y de alta calidad de parte de los modelos de IA. - Arquitectura Modular :
    • Cada componente (sitio web, API Gateway, Lambda, DynamoDB, S3, Cognito) desempeña un papel específico, facilitando el mantenimiento y la evolución del sistema.
  • Seguridad y Escalabilidad :

    • El uso de los servicios gestionados de AWS asegura una seguridad robusta y una capacidad para adaptarse a una demanda creciente.

Enlace del proyecto : StoryPixAI

Este documento ha sido traducido de la versión fr a la lengua es utilizando el modelo gpt-4o. Para más información sobre el proceso de traducción, consulte https://gitlab.com/jls42/ai-powered-markdown-translator