Con StoryPixAI il mio obiettivo era di creare un’applicazione web interattiva che permettesse agli utenti di generare storie per bambini, arricchite da immagini generate da modelli di intelligenza artificiale. Per realizzare ciò, ho utilizzato diversi servizi AWS come Lambda, API Gateway, DynamoDB, S3 e Cognito per l’autenticazione. Il codice dell’infrastruttura è gestito con Terraform, e il deployment è automatizzato tramite GitLab CI. In questo post, vi svelerò i retroscena di questo progetto appassionante, dalle scelte tecnologiche alle sfide incontrate.

Introduzione

Come architetto di infrastrutture cloud e DevOps esperto, sono sempre stato affascinato dalle nuove tecnologie e dal loro potenziale di trasformare la nostra vita quotidiana. L’emergere dell’IA generativa ha suscitato in me una curiosità crescente, e ho sentito la necessità di immergermi in questo universo in piena effervescenza.

È così che è nato StoryPixAI, un progetto personale che mi ha permesso di esplorare le infinite possibilità dell’IA per creare storie personalizzate e illustrazioni magiche per i bambini. Questo progetto è stata per me l’occasione di mettermi nei panni di uno sviluppatore full-stack, di un prompt engineer, di un product owner e anche di un designer UX/UI, condividendo la mia passione per la tecnologia con i miei cari.

In questo post sul blog, condividerò le mie scelte tecnologiche e le sfide affrontate durante questa avventura affascinante.

Ma prima di tutto, un assaggio!

Per darvi un’idea del potenziale di StoryPixAI, ecco alcune storie generate automaticamente, in diverse lingue.
Ogni storia è accompagnata da illustrazioni, rendendo il racconto ancora più immersivo per i bambini:

L’IA al servizio della creatività: un percorso di sperimentazione

La mia avventura con StoryPixAI è iniziata con un Proof of Concept (PoC) semplice: una funzione Lambda che interagiva con OpenAI per generare testo e DALL-E per creare immagini. Questo primo successo mi ha incoraggiato a andare oltre ed esplorare altri modelli di IA tramite Bedrock di AWS.

GPT-4 e GPT-4-o: i narratori agili

Fin dall’inizio del progetto, GPT-4 di OpenAI si è imposto come una scelta ovvia per la generazione di testo. La sua capacità di comprendere le sfumature del linguaggio naturale e di produrre racconti coerenti e creativi mi ha permesso di creare storie coinvolgenti, adatte all’età e agli interessi dei bambini. Ho potuto sperimentare con diversi stili di scrittura, dal racconto di fiabe all’avventura spaziale, passando per storie di animali e racconti fantastici.

Quando è stato lanciato GPT-4-0, ho rapidamente integrato questo nuovo modello a StoryPixAI. Sono stato impressionato dalla sua velocità di generazione aumentata, che ha permesso di ridurre notevolmente il tempo di attesa per la generazione, e dal netto miglioramento della qualità delle storie generate, con racconti ancora più fluidi, coerenti e immaginativi. GPT-4-0 è così diventato una risorsa importante per StoryPixAI, offrendo un’esperienza utente più rapida e piacevole.

DALL-E 3: l’illustratore di riferimento

Se i modelli di generazione di testo offrivano risultati soddisfacenti, la scelta dello strumento di generazione di immagini si è rivelata più cruciale. Dopo numerosi tentativi, DALL-E 3 si è imposto come il modello di riferimento per StoryPixAI. La sua capacità di creare illustrazioni originali, dettagliate e perfettamente adatte alle storie generate da GPT-4 è stata un fattore determinante per il successo del progetto.

Bedrock di AWS: la porta aperta verso la sperimentazione

Desiderando non limitarmi a OpenAI, ho utilizzato Bedrock di AWS per integrare facilmente altri modelli di IA generativa in StoryPixAI. Questa piattaforma mi ha permesso di testare Claude di Anthropic e Mistral per la generazione di testo, e Stable Diffusion per la creazione di immagini.

Sebbene questi modelli abbiano dato risultati interessanti, ho infine scelto di concentrarmi su GPT-4 e GPT-4-0 per la loro velocità e qualità di generazione di testo, e su DALL-E 3 per la sua capacità di produrre illustrazioni perfettamente adatte alle storie. È importante notare che il prompt utilizzato per generare le immagini è in gran parte elaborato dal modello di testo stesso, il che assicura una coerenza tra il racconto e l’illustrazione.

La sfida dell’API asincrona e di DynamoDB

Una volta validato il PoC, ho intrapreso la creazione di un’API per rendere StoryPixAI accessibile tramite un’interfaccia web. È a questo punto che ho incontrato la mia prima sfida maggiore: la limitazione del timeout di API Gateway. Per aggirare questo vincolo e permettere la generazione di storie più lunghe e complesse, ho dovuto implementare un’architettura asincrona.

Amazon DynamoDB è quindi entrato in gioco. Ho utilizzato questa database NoSQL per memorizzare i compiti di generazione di storie in corso, così come i loro risultati una volta completati. Grazie a questo approccio, l’API poteva restituire una risposta immediata all’utente, che poteva poi consultare lo stato della sua richiesta e recuperare la storia generata una volta pronta.

CORS e l’interfaccia utente: degli ostacoli da superare

La realizzazione dell’interfaccia web è stata anche fonte di sfide. Ho dovuto familiarizzare con le sottigliezze di CORS (Cross-Origin Resource Sharing) per permettere al mio frontend di comunicare con l’API. Ho anche dedicato del tempo a migliorare l’esperienza utente aggiungendo funzionalità come la selezione dei modelli di IA e degli stili di immagini.

Il prompting: un’arte da padroneggiare

Durante tutto lo sviluppo di StoryPixAI, ho affinato le mie competenze in prompting, quest’arte di formulare le giuste richieste per guidare i modelli di IA. Ho imparato ad adattare i miei prompt in funzione dei modelli utilizzati, dei parametri della storia e delle aspettative degli utenti. Questa fase è stata cruciale per ottenere risultati di qualità e garantire un’esperienza utente soddisfacente.

Un’infrastruttura robusta e automatizzata su AWS

StoryPixAI si basa su un’infrastruttura serverless ospitata su Amazon Web Services (AWS), offrendo una combinazione ideale di flessibilità, scalabilità e ottimizzazione dei costi. Questa architettura, interamente automatizzata grazie a Terraform e GitLab CI/CD, permette un deployment rapido e affidabile dell’applicazione.

I servizi AWS al cuore di StoryPixAI

alt text

L’architettura di StoryPixAI si articola attorno ai seguenti servizi AWS: * Amazon S3 (Simple Storage Service) : Archiviazione dei file statici del sito web (HTML, CSS, JavaScript) e delle storie generate, insieme alle loro illustrazioni associate.

  • Amazon CloudFront : Una rete di distribuzione di contenuti (CDN) che accelera la distribuzione dei contenuti di StoryPixAI agli utenti di tutto il mondo memorizzandoli nella cache in posizioni geograficamente vicine a loro.
  • Amazon API Gateway : La porta d’ingresso sicura dell’applicazione. Gestisce le richieste degli utenti, assicura la loro autenticazione tramite Amazon Cognito e le instrada verso le funzioni Lambda appropriate.
  • AWS Lambda : Funzioni serverless che costituiscono il motore di StoryPixAI. Orchestrano la generazione di storie, la creazione di immagini, la gestione dei compiti asincroni e l’interazione con DynamoDB e altri servizi AWS.
  • Amazon DynamoDB : Un database NoSQL flessibile e performante utilizzato per memorizzare informazioni essenziali al funzionamento dell’applicazione.
  • Amazon Cognito : Un servizio di gestione delle identità e degli accessi che protegge l’applicazione permettendo agli utenti di accedere e controllando le loro autorizzazioni. Garantisce che solo gli utenti autenticati possano accedere alle funzionalità di generazione delle storie.
  • Amazon Bedrock : Una piattaforma che semplifica l’accesso e l’uso dei modelli di IA generativa di diversi fornitori, come Anthropic (Claude) e Stability AI (Stable Diffusion). Bedrock permette di integrare facilmente questi modelli nell’applicazione senza dover gestire la loro infrastruttura sottostante.
  • Altri servizi AWS : StoryPixAI utilizza anche altri servizi AWS, come IAM (Identity and Access Management) per la gestione fine delle autorizzazioni di accesso alle risorse, CloudWatch per la supervisione e i log (cruciali per il debugging e l’analisi delle prestazioni) e Systems Manager Parameter Store (SSM Parameter Store) per memorizzare informazioni sensibili come le chiavi API, garantendo così la sicurezza dell’applicazione.

Terraform : l’automazione al servizio dell’infrastruttura

Per gestire questa infrastruttura complessa, ho scelto Terraform, uno strumento di Infrastructure as Code (IaC) che permette di descrivere l’infrastruttura sotto forma di codice dichiarativo. Grazie a Terraform, ho potuto automatizzare la creazione, la modifica e la distruzione delle risorse AWS, garantendo così un ambiente coerente, riproducibile e facile da gestire. Ciò semplifica notevolmente il processo di deployment e riduce il rischio di errori umani.

GitLab CI/CD : deployment fluidi e senza intoppi

Per assicurare un deployment continuo e affidabile di StoryPixAI, ho implementato una pipeline CI/CD (Continuous Integration / Continuous Deployment) su GitLab. Questa pipeline automatizza i test, la costruzione e il deployment dell’applicazione ad ogni modifica del codice sorgente, permettendo così di individuare e correggere rapidamente gli errori e di rilasciare nuove funzionalità con fiducia. Questo approccio garantisce che l’applicazione sia sempre aggiornata e minimizza i tempi di inattività.

Questa combinazione di AWS, Terraform e GitLab CI/CD mi ha permesso di costruire un’infrastruttura robusta, scalabile e facile da mantenere, lasciandomi così più tempo per concentrarmi sull’aspetto creativo del progetto e sul miglioramento dell’esperienza utente.

Architettura Globale del progetto StoryPixAI

Prima di immergerci nel codice, ecco una panoramica dell’architettura dell’applicazione:

  1. Sito Statico su S3 : Un sito web statico ospitato su un bucket S3, accessibile tramite CloudFront per una distribuzione globale.
  2. API Gateway : Espone endpoint per la generazione di storie e la verifica dello stato.
  3. Funzioni Lambda :
    • StoryPixAI.py : Genera la storia e le immagini associate.
    • status_checker.py : Verifica lo stato della generazione in DynamoDB.
  4. DynamoDB : Memorizza lo stato delle attività di generazione.
  5. S3 : Archivia le immagini generate e le pagine HTML risultanti.
  6. Cognito : Gestisce l’autenticazione degli utenti per proteggere l’API.

Funzione Lambda StoryPixAI.py

Panoramica Generale

La funzione StoryPixAI.py è il cuore dell’applicazione. È responsabile di:

  • Generare una storia basata su un prompt dell’utente.
  • Creare istruzioni dettagliate per guidare il modello di IA nella generazione della storia.
  • Estrarre riassunti per ogni scena o elemento chiave della storia.
  • Generare immagini corrispondenti a questi riassunti.
  • Combinare il testo e le immagini in una pagina HTML.
  • Archiviare il risultato in S3 e aggiornare lo stato in DynamoDB.

Scomposizione del Codice

Importazioni e Configurazione Iniziale

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)

In questa sezione, importo i moduli necessari, configuro il logger per il debug e recupero la chiave API OpenAI archiviata in AWS Systems Manager Parameter Store (SSM). Questo permette di proteggere la chiave e di non archiviarla in chiaro nel codice.

Funzioni Utilitarie

Correzione dei Tag
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é.

Questa funzione garantisce che i tag utilizzati per delimitare i riassunti e i titoli siano uniformi. Ciò è cruciale per l’estrazione corretta dei riassunti successivamente.

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

Utilizza espressioni regolari per estrarre le sezioni di testo delimitate da [resume] e [end_resume]. Questi riassunti serviranno come prompt per la generazione delle immagini.

Generazione delle Istruzioni per le Immagini
def generate_image_instructions(prompt, style, language):
    # Génère les instructions pour la création d'images.

Questa funzione formatta il prompt in modo da guidare il modello di generazione delle immagini includendo lo stile e la lingua.

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

Aggiorna la tabella TaskStatus per monitorare lo stato della generazione, il che è essenziale per la funzione status_checker.py.

Analisi Approfondita di generate_story_instructions

La funzione generate_story_instructions è il cuore del progetto. Genera un insieme di istruzioni dettagliate che saranno passate al modello di IA per guidare la generazione della storia.

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

Costruzione del Prompt

Il prompt è progettato per fornire al modello di IA tutte le informazioni necessarie per generare una storia coerente, educativa e adatta ai bambini.

  • Lingua : Il parametro language_description permette di specificare la lingua della storia, assicurando così che il testo generato sia nella lingua desiderata.

  • Tema : Il prompt dell’utente è integrato nelle istruzioni per fungere da base alla storia.

  • Lunghezza : Una gamma da 1000 a 1500 parole è specificata per controllare la lunghezza della storia.

  • Elementi Chiave : Le istruzioni incoraggiano l’inclusione di elementi come l’avventura, la magia, e valori educativi importanti.

Dettagli delle Istruzioni

Le istruzioni fornite al modello sono estremamente dettagliate per guidare la generazione in modo preciso.

Ecco un’analisi delle diverse parti del prompt:

  1. Struttura Narrativa : Si chiede al modello di strutturare la storia con un inizio coinvolgente, uno sviluppo ricco di eventi e una conclusione soddisfacente.

  2. Descrizioni Visive : La storia deve essere ricca di descrizioni visive per stimolare l’immaginazione dei bambini.

  3. Personaggi : Si incoraggia lo sviluppo di personaggi affascinanti con personalità distinte.

  4. Tag Specifici : Tag come [titre]... [end_titre] e [resume]... [end_resume] sono utilizzati per delimitare il titolo e le descrizioni visive.

  5. Elementi Fantastici : Si invita il modello a includere elementi magici o fantastici per rendere la storia più attraente.

  6. Valori Educativi : La storia deve insegnare valori importanti.

Ruolo dei Tag

I tag giocano un ruolo cruciale nel trattamento successivo del testo generato.

  • [titre]… [end_titre] : Racchiude il titolo della storia. Questo permette di estrarlo facilmente per visualizzarlo in modo appropriato nell’interfaccia utente.

  • [resume]… [end_resume]: Inquadra le descrizioni visive dettagliate delle scene chiave della storia. Questi riassunti saranno utilizzati come prompt per la generazione di immagini.

Trattamento Dopo la Generazione

Una volta che il modello di IA ha generato la storia seguendo queste istruzioni, il codice esegue i seguenti passaggi:

  1. Correzione dei Tag: La funzione correct_resume_tags si assicura che tutti i tag siano formattati correttamente per l’estrazione.

  2. Estrazione dei Riassunti: La funzione extract_summaries utilizza i tag [resume] e [end_resume] per estrarre le descrizioni visive.

  3. Generazione delle Immagini: Ogni riassunto viene passato alla funzione generate_image per creare un’immagine corrispondente.

  4. Creazione del Contenuto HTML: Il testo della storia e le immagini generate vengono combinati per creare una pagina HTML completa.

Impatto sulla Generazione

Fornendo queste istruzioni dettagliate, il modello è guidato per:

  • Rispettare il Formato: Utilizzando i tag specificati, il modello produce un testo strutturato che facilita il trattamento automatizzato.

  • Generare Contenuti Adatti: I vincoli sulla lingua, lo stile e i temi garantiscono che la storia sia appropriata per il pubblico di destinazione.

  • Facilitare la Generazione di Immagini: Estraendo descrizioni visive precise, si ottengono prompt di qualità per la generazione di immagini.

Gestione dei Tag da Parte del Modello

Il modello è esplicitamente istruito a non tradurre o modificare i tag. Questo è essenziale affinché i tag rimangano intatti e possano essere utilizzati per il post-trattamento. Le istruzioni insistono su questo punto per evitare che il modello, che potrebbe tentare di parafrasare o tradurre tutto il testo, alteri i tag.

Generazione della Storia

Una volta generate le istruzioni dettagliate dalla funzione generate_story_instructions, il prossimo passo è passare queste istruzioni al modello di IA affinché crei la storia.

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

Interazione con il Modello OpenAI

  • Client OpenAI: Instanzio un client OpenAI utilizzando la chiave API recuperata in precedenza.

  • Prompting: Il modello riceve una serie di messaggi:

    • Un messaggio di sistema che indica che l’assistente è un esperto in storie per bambini.
    • Il messaggio dell’utente contenente le istruzioni dettagliate generate.
  • Risposta del Modello: Il modello genera una storia basata sulle istruzioni fornite.

Gestione degli Errori

Se si verifica un’eccezione durante la chiamata all’API OpenAI, viene catturata e restituito un messaggio di errore.

Estrazione dei Riassunti e Tag

Dopo la generazione della storia, il passo successivo consiste nell’estrarre le descrizioni visive utilizzando i tag specificati.

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

Correzione dei Tag

Il modello può a volte alterare leggermente i tag (ad esempio, aggiungere accenti). La funzione correct_resume_tags si assicura che tutti i tag siano uniformi e formattati correttamente.

Estrazione dei Riassunti

La funzione extract_summaries utilizza un’espressione regolare per trovare tutte le occorrenze di testo tra i tag [resume] e [end_resume]. Questi riassunti sono le descrizioni visive dettagliate che saranno utilizzate per generare le immagini.

Generazione delle Immagini

Una volta estratti i riassunti, ogni riassunto viene utilizzato per generare un’immagine corrispondente.

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

Funzione generate_image

La funzione generate_image chiama l’API del modello di generazione delle immagini (ad esempio, OpenAI DALL·E) per creare un’immagine a partire dal riassunto.

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

Generazione delle Istruzioni per le Immagini La funzione generate_image_instructions adatta il riassunto per creare un prompt appropriato per la generazione di immagini.

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.
    """
  • Stile: Lo stile specificato dall’utente (ad esempio, “acquerello”, “cartoon”) è incluso nel prompt per influenzare il rendering dell’immagine.

  • Lingua: La descrizione è adattata alla lingua scelta, il che può aiutare il modello a comprendere le sfumature culturali.

  • Istruzioni Chiare: Precisando che la scena deve essere puramente visiva, si evita che il modello aggiunga testo o elementi indesiderati nell’immagine.

Interazione con l’API OpenAI per le Immagini

  • Chiamata all’API: La funzione client.images.generate è utilizzata per generare l’immagine.

  • Parametri Importanti:

    • Prompt: Il prompt aggiustato è passato all’API.
    • Modello: Il modello di generazione di immagini specificato.
    • Dimensione: La dimensione dell’immagine (ad esempio, “1024x1024”).
    • Qualità: La qualità dell’immagine (standard, HD).
    • Formato di Risposta: Le immagini sono restituite in base64 per facilitare lo stoccaggio e la manipolazione.

Gestione degli Errori

Gli errori durante la generazione delle immagini sono catturati e registrati, permettendo di diagnosticare i problemi.

Creazione del Contenuto HTML

Dopo aver generato le immagini corrispondenti ai riassunti estratti, il passo successivo consiste nell’assemblare il testo della storia e le immagini in un formato presentabile per l’utente. Questo viene fatto creando un contenuto HTML strutturato che sarà visualizzato sul sito 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

Spiegazione nel Dettaglio

  1. Estrazione del Titolo:

    • Usa un’espressione regolare per trovare il testo tra i tag [titre] e [end_titre].
    • Rimuove i tag dal testo principale dopo l’estrazione.
    • Se non viene trovato alcun titolo, viene utilizzato un titolo predefinito.
  2. Inizializzazione dell’HTML:

    • Il contenuto HTML inizia con i tag <html>, <head> e <body>.
    • Gli stili CSS sono inclusi per migliorare la presentazione (tipografia, margini, allineamenti).
  3. Separazione del Testo:

    • Il testo è diviso in segmenti usando i tag [resume] e [end_resume].
    • I segmenti rappresentano le parti della storia senza i riassunti.
  4. Assemblaggio:

    • Ogni segmento di testo è inserito in un paragrafo <p>.
    • Se la generazione di immagini è abilitata e c’è un’immagine corrispondente, l’immagine è inserita dopo il paragrafo.
    • Le immagini sono centrate e adattate alla dimensione dello schermo per una migliore esperienza utente.
  5. Finalizzazione:

    • I tag di chiusura </body> e </html> sono aggiunti per completare il documento HTML.

Perché Questo Approccio?

  • Allineamento del Testo e delle Immagini: Inserendo le immagini dopo i segmenti di testo corrispondenti, la storia è arricchita visivamente, il che è particolarmente importante per i bambini.

  • Flessibilità: Se l’utente sceglie di non generare immagini, il codice gestisce questo caso inserendo solo il testo.

  • Accessibilità: Utilizzando tag semantici e stili adattati, il contenuto è accessibile su diversi dispositivi (computer, tablet, smartphone).

Upload su S3 e Aggiornamento dello Stato

Una volta generato il contenuto HTML, è necessario renderlo accessibile all’utente. Questo viene fatto caricando il file su un bucket S3 configurato per l’hosting di siti web statici.

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

Dettagli Tecnici

  • Nominazione dei File:

    • I file sono nominati utilizzando un timestamp per garantire l’unicità.
    • Le immagini sono memorizzate nella cartella generated_images/ e i file HTML in generated_content/.
  • Caricamento su S3:

    • Utilizzo del client S3 di boto3 per interagire con il servizio.
    • Il contenuto è codificato o decodificato in base al tipo (immagine o testo).
    • Il parametro ACL='public-read' rende il file accessibile pubblicamente. - Costruzione dell’URL:
    • L’URL pubblico è costruito utilizzando il dominio CloudFront configurato, il che consente una distribuzione rapida e sicura del contenuto.
  • Gestione delle Eccezioni:

    • In caso di errore durante il download, l’eccezione viene registrata e sollevata per essere gestita dal lambda_handler.

Funzione Principale lambda_handler

La funzione lambda_handler è il punto d’ingresso della funzione Lambda. Orquestra tutti i passaggi descritti in precedenza.

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

Spiegazione

  • Elaborazione della Richiesta:

    • Recupera le informazioni necessarie dall’evento (event) ricevuto.
    • I parametri della richiesta includono il prompt, i modelli selezionati, la lingua, ecc.
  • Aggiornamento dello Stato:

    • Prima di iniziare l’elaborazione, lo stato viene aggiornato a “Processing” in DynamoDB.
  • Generazione della Storia:

    • Chiamata a generate_story con i parametri appropriati.
  • Estrazione e Elaborazione:

    • I tag sono corretti e i riassunti estratti per la generazione di immagini.
  • Generazione delle Immagini:

    • Se la generazione di immagini è abilitata, le immagini corrispondenti sono generate e gli URL raccolti.
  • Creazione del Contenuto HTML:

    • Il testo e le immagini sono combinati per creare il contenuto HTML finale.
  • Caricamento su S3:

    • Il contenuto HTML è caricato su S3 e l’URL del risultato ottenuto.
  • Aggiornamento dello Stato Finale:

    • Lo stato viene aggiornato in “link” con l’URL del risultato in DynamoDB.
  • Ritorno della Risposta:

    • La risposta include il requestId e l’URL del risultato, permettendo al cliente di verificare lo stato o accedere direttamente al contenuto.
  • Gestione delle Eccezioni:

    • In caso di errore, lo stato viene aggiornato a “Failed” e viene restituita una risposta HTTP 500.

Funzione Lambda status_checker.py

Panoramica Generale

La funzione status_checker.py consente agli utenti di verificare lo stato della loro richiesta di generazione di una storia. Interroga DynamoDB per recuperare lo stato attuale e, se disponibile, l’URL del risultato.

Analisi del Codice

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

Dettagli

  • Recupero del requestId:

    • Il requestId è essenziale per identificare la specifica richiesta dell’utente.
  • Interrogazione di DynamoDB:

    • La funzione tenta di recuperare l’elemento corrispondente al requestId.
    • Se l’elemento esiste, lo stato e il resultUrl vengono estratti.
  • Costruzione della Risposta:

    • Se lo stato è disponibile, viene restituito con l’URL del risultato.
    • Se l’elemento non è trovato, viene restituito un errore 404.
    • In caso di errore durante l’interrogazione del database, viene restituito un errore 500 con un messaggio appropriato.
  • Intestazioni HTTP:

    • Le intestazioni sono definite per consentire le richieste CORS dal sito web.

Integrazione con API Gateway

Configurazione degli Endpoints

L’API Gateway espone due endpoint principali per interagire con le funzioni Lambda:

  1. /generate-image:

    • Metodo: POST
    • Descrizione: Consente agli utenti di avviare la generazione di una storia e, eventualmente, delle immagini associate.
    • Integrazione: Connesso alla funzione Lambda StoryPixAI.py.
  2. /check-status:

    • Metodo: GET
    • Descrizione: Consente agli utenti di verificare lo stato della richiesta fornendo il requestId.
    • Integrazione: Connesso alla funzione Lambda status_checker.py.

Autenticazione con Cognito

Per proteggere l’API e controllare l’accesso alle risorse, ho integrato Amazon Cognito.

  • User Pool:

    • Gestisce le credenziali degli utenti.
    • Permette la registrazione, l’accesso e la gestione degli utenti.
  • Authorizer:

    • Configurato in API Gateway per verificare i token JWT emessi da Cognito.
    • Garantisce che solo le richieste autenticate possano accedere agli endpoint protetti. - Integrazione su API Gateway:
    • Gli endpoint /generate-image e /check-status sono protetti dall’autorizzatore Cognito.
    • I client devono includere il token di autenticazione negli header delle loro richieste (Authorization).

Sito Statico su S3 e Interazione con l’API

Struttura del Sito

Il sito web statico serve da interfaccia utente per l’applicazione.

  • index.html:

    • Contiene il modulo che permette agli utenti di inserire il prompt, scegliere le opzioni di generazione e inviare la richiesta.
    • Include gli script necessari per l’interazione con l’API e la gestione dell’autenticazione.
  • storypixai.js:

    • Contiene il codice JavaScript per gestire le interazioni con l’API.
    • Gestisce l’autenticazione con Cognito, l’invio del modulo, il monitoraggio dello stato e la visualizzazione dei risultati.

Flusso di Lavoro Utente

  1. Connessione:

    • L’utente si connette tramite il modulo di login integrato.
    • Le informazioni vengono verificate tramite Cognito.
  2. Invio della Richiesta:

    • L’utente compila il modulo con il prompt e le opzioni desiderate.
    • Al momento della sottomissione, una richiesta POST viene inviata all’endpoint /generate-image con i dati.
  3. Elaborazione Asincrona:

    • L’API ritorna immediatamente un requestId.
    • L’elaborazione della generazione avviene in background.
  4. Verifica dello Stato:

    • Il sito web interroga periodicamente l’endpoint /check-status fornendo il requestId.
    • Una volta ricevuto lo stato “link”, l’URL del risultato viene visualizzato all’utente.
  5. Visualizzazione del Risultato:

    • L’utente può cliccare sul link per accedere alla storia generata con le immagini.

Gestione delle Richieste e delle Risposte

  • Richieste Autenticate:

    • Tutte le richieste verso l’API includono il token di autenticazione.
    • Il token è gestito dal SDK Cognito incluso nel sito web.
  • Gestione degli Stati:

    • Gli stati possibili sono “Processing”, “link”, “Failed”.
    • Il sito adatta la sua interfaccia in base allo stato ricevuto (ad esempio, visualizzazione di un indicatore di caricamento, messaggio di errore, link al risultato).

Interconnessioni tra i Componenti

Ecco come i diversi componenti interagiscono:

  • Sito Web ↔️ API Gateway:

    • Il sito web invia richieste agli endpoint esposti dall’API Gateway.
    • I token di autenticazione sono inclusi per proteggere le richieste.
  • API Gateway ↔️ Funzioni Lambda:

    • L’API Gateway invoca le funzioni Lambda corrispondenti in base alle richieste ricevute.
  • Funzioni Lambda ↔️ DynamoDB:

    • Le funzioni Lambda StoryPixAI.py e status_checker.py interagiscono con DynamoDB per aggiornare e recuperare lo stato delle richieste.
  • Funzione Lambda ↔️ S3:

    • La funzione StoryPixAI.py carica le immagini generate e il contenuto HTML su S3.
  • CloudFront ↔️ S3:

    • CloudFront è utilizzato per distribuire il contenuto memorizzato su S3 in modo rapido e sicuro.
    • Gli URL forniti agli utenti puntano verso il dominio CloudFront.
  • Utente ↔️ Sito Web:

    • L’utente interagisce con il sito web per inviare richieste e visualizzare i risultati.

Esempio di Risultato nei log di Cloudwatch dopo una Richiesta

Ecco un esempio di risultato dei log dopo una richiesta affinché possiate vedere il formato grezzo dei dati generati:

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

Integrazione Continua con GitLab CI/CD

Per garantire uno sviluppo e un deployment fluidi di StoryPixAI, ho implementato una pipeline di integrazione continua (CI) e deployment continuo (CD) utilizzando GitLab CI/CD. Questa configurazione automatizza i processi di build e deployment, garantendo la qualità e l’affidabilità del codice a ogni modifica.

Configurazione della Pipeline

La pipeline è definita nel file .gitlab-ci.yml nella radice del progetto. Ecco una panoramica della sua struttura:

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

variables:
  TERRAFORM_VERSION: "1.5.7-*"
  TF_VAR_region: $AWS_DEFAULT_REGION
``` Questa configurazione definisce le varie fasi della pipeline e le variabili globali utilizzate nel processo CI/CD.

### Job Principali

La pipeline comprende diversi job chiave:

1. **Verifica Terraform**:
   ```yaml
   Verifica Terraform:
     stage: Verifiche
     when: manual
     script:
       - /bin/bash -c "source export.sh && terraform_plan"

Questo job esegue terraform plan per verificare le modifiche previste all’infrastruttura senza applicarle.

  1. Deploy Terraform:

    Deploy Terraform:
      stage: Deployments
      when: manual
      dependencies:
        - Verifica Terraform
      script:
        - /bin/bash -c "source export.sh && terraform_apply"
    

    Dopo la verifica, questo job applica le modifiche all’infrastruttura eseguendo terraform apply.

  2. Cancellazione Terraform:

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

    Questo job permette di distruggere l’infrastruttura se necessario, eseguendo terraform destroy.

  3. Gestione delle Chiavi OpenAI:

    Chiave OpenAI - Aggiunta:
      stage: Prerequisiti opzionati
      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 "Nessuna chiave trovata."
            exit 1
          fi      
    
    Chiave OpenAI - Cancellazione:
      stage: Cancellazioni
      when: manual
      script:
        - /bin/bash -c "source export.sh && manage_openai_key delete"
    

    Questi job gestiscono l’aggiunta e la cancellazione sicura delle chiavi API OpenAI in AWS Parameter Store.

Ambiente di Esecuzione

Ogni job viene eseguito in un container Docker basato su Ubuntu 22.04, con Terraform e AWS CLI installati:

.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

Vantaggi di questo Approccio CI/CD

  1. Automazione: Ogni modifica del codice avvia automaticamente la pipeline, garantendo verifiche e deploy coerenti.

  2. Controllo Manuale: Le fasi critiche come il deploy e la cancellazione sono configurate in modalità manuale (when: manual), offrendo un controllo aggiuntivo prima dell’esecuzione.

  3. Gestione Sicura dei Segreti: L’integrazione con AWS Parameter Store per la gestione delle chiavi API assicura una manipolazione sicura delle informazioni sensibili.

  4. Flessibilità: La struttura a fasi permette un’esecuzione ordinata e logica delle diverse fasi della pipeline.

  5. Riproducibilità: L’uso di un ambiente Docker standardizzato garantisce che i build e i test siano riproducibili su diversi sistemi.

Questa configurazione CI/CD non solo automatizza il deploy di StoryPixAI, ma mantiene anche un alto livello di qualità e affidabilità durante tutto il ciclo di sviluppo.

Conclusione

StoryPixAI è stato molto più di un semplice progetto tecnico. È stata una vera avventura nel mondo dell’IA generativa, permettendomi di combinare la mia passione per la tecnologia con il desiderio di creare storie magiche per i miei figli.

Questo progetto mi ha offerto l’opportunità di esplorare diverse sfaccettature dell’IA, dalla progettazione di un’interfaccia utente intuitiva alla padronanza del prompting, passando per l’implementazione di un’infrastruttura cloud robusta con AWS e Terraform. Ogni fase è stata una fonte di apprendimento, mettendomi di fronte a sfide tecniche stimolanti e costringendomi ad ampliare le mie competenze in sviluppo full-stack e DevOps.

Spero che questo post sul blog vi abbia dato una panoramica dietro le quinte di questa avventura entusiasmante.

Punti Chiave

  • Istruzioni Dettagliate:

    • Prompt chiari e strutturati permettono di ottenere risultati coerenti e di alta qualità dai modelli di IA. - Architettura Modulare :
    • Ogni componente (sito web, API Gateway, Lambda, DynamoDB, S3, Cognito) ha un ruolo specifico, facilitando la manutenzione e l’evoluzione del sistema.
  • Sicurezza e Scalabilità :

    • L’uso dei servizi gestiti di AWS assicura una sicurezza robusta e una capacità di adattarsi a una domanda crescente.

Link del progetto: StoryPixAI

Questo documento è stato tradotto dalla versione fr alla lingua it utilizzando il modello gpt-4o. Per ulteriori informazioni sul processo di traduzione, consultare https://gitlab.com/jls42/ai-powered-markdown-translator