Z StoryPixAI moim celem było stworzenie interaktywnej aplikacji internetowej, która umożliwia użytkownikom generowanie historii dla dzieci, wzbogaconych o obrazy generowane przez modele sztucznej inteligencji. Aby to osiągnąć, użyłem kilku usług AWS, takich jak Lambda, API Gateway, DynamoDB, S3 i Cognito do uwierzytelniania. Kod infrastruktury jest zarządzany za pomocą Terraform, a wdrażanie jest zautomatyzowane za pomocą GitLab CI. W tym wpisie zdradzam kulisy tego ekscytującego projektu, decyzje technologiczne i napotkane wyzwania.

Wprowadzenie

Jako doświadczony architekt infrastruktury chmurowej i DevOps, zawsze fascynowały mnie nowe technologie i ich potencjał do transformacji naszego codziennego życia. Pojawienie się generatywnej sztucznej inteligencji wzbudziło we mnie rosnącą ciekawość i poczułem potrzebę zanurzenia się w tym dynamicznym świecie.

Tak narodził się StoryPixAI, autorski projekt, który pozwolił mi zbadać nieskończone możliwości sztucznej inteligencji w tworzeniu spersonalizowanych historii i magicznych ilustracji dla dzieci. Projekt ten pozwolił mi wcielić się w rolę full-stack developera, prompt engineera, product ownera, a nawet UX/UI designera, jednocześnie dzieląc się moją pasją do technologii z najbliższymi.

W tym wpisie na blogu podzielę się moimi decyzjami technologicznymi i wyzwaniami, które napotkałem w trakcie tej ekscytującej przygody.

Ale najpierw przedsmak!

Aby dać wam przedsmak potencjału StoryPixAI, oto kilka automatycznie wygenerowanych historii, w różnych językach.
Każda historia jest wzbogacona o ilustracje, co sprawia, że opowieści są jeszcze bardziej wciągające dla dzieci:

SI w służbie kreatywności: ścieżka eksperymentacji

Moja przygoda ze StoryPixAI rozpoczęła się od prostego Proof of Concept (PoC): funkcji Lambda, która współpracowała z OpenAI w celu generowania tekstu oraz z DALL-E do tworzenia obrazów. Ten pierwszy sukces zachęcił mnie do pójścia dalej i eksploracji innych modeli SI za pośrednictwem Bedrock AWS.

GPT-4 i GPT-4-o: zwinni opowiadacze

Od samego początku projektu, GPT-4 od OpenAI okazał się oczywistym wyborem do generowania tekstu. Jego zdolność do rozumienia niuansów języka naturalnego i tworzenia spójnych i kreatywnych opowieści pozwoliła mi na tworzenie fascynujących historii, dostosowanych do wieku i zainteresowań dzieci. Mogłem eksperymentować z różnymi stylami pisania, od bajek, przez przygody kosmiczne, po opowieści o zwierzętach i historie fantastyczne.

Kiedy GPT-4-0 został wprowadzony, szybko zintegrowałem ten nowy model ze StoryPixAI. Byłem pod wrażeniem jego zwiększonej prędkości generowania, która pozwoliła znacznie skrócić czas oczekiwania na wygenerowanie, oraz zauważalnej poprawy jakości generowanych opowieści, z jeszcze bardziej płynnymi, spójnymi i pomysłowymi historiami. GPT-4-0 stał się zatem głównym atutem StoryPixAI, oferując szybsze i przyjemniejsze doświadczenie użytkownika.

DALL-E 3: Wzorcowy Ilustrator

Chociaż modele generowania tekstu oferowały zadowalające rezultaty, wybór narzędzia do generowania obrazów okazał się bardziej kluczowy. Po wielu próbach, DALL-E 3 okazał się wzorcowym modelem dla StoryPixAI. Jego zdolność do tworzenia oryginalnych, szczegółowych ilustracji, idealnie dopasowanych do historii wygenerowanych przez GPT-4, była decydującym czynnikiem w sukcesie projektu.

Bedrock AWS: Otwarcie na Eksperymenty

Chcąc nie ograniczać się do OpenAI, wykorzystałem Bedrock AWS, aby łatwo zintegrować do StoryPixAI inne modele generatywnej sztucznej inteligencji. Ta platforma pozwoliła mi przetestować Claude od Anthropic oraz Mistral do generowania tekstu, a także Stable Diffusion do tworzenia obrazów.

Chociaż te modele dawały interesujące wyniki, ostatecznie zdecydowałem się skoncentrować na GPT-4 i GPT-4-0 ze względu na ich prędkość i jakość generowania tekstu oraz na DALL-E 3 ze względu na jego zdolność do tworzenia ilustracji idealnie dopasowanych do historii. Ważne jest, aby zauważyć, że prompt używany do generowania obrazów jest w dużej mierze opracowywany przez sam model tekstowy, co zapewnia spójność między narracją a ilustracją.

Wyzwanie Asynchronicznego API i DynamoDB

Po zatwierdzeniu PoC podjąłem się stworzenia API, aby udostępnić StoryPixAI przez interfejs webowy. Na tym etapie napotkałem główne wyzwanie: ograniczenie czasu przetwarzania API Gateway. Aby obejść tę przeszkodę i umożliwić generowanie dłuższych i bardziej złożonych opowiadań, musiałem wdrożyć asynchroniczną architekturę.

Wtedy wszedł do gry Amazon DynamoDB. Użyłem tej bazy danych NoSQL do przechowywania zadań generowania historii w toku oraz ich wyników po ich zakończeniu. Dzięki temu podejściu API mogło natychmiast odpowiedzieć użytkownikowi, który później mógł sprawdzić status swojego żądania i pobrać wygenerowaną historię, gdy była gotowa.

CORS i Interfejs Użytkownika: Przeszkody do Pokonania

Wdrożenie interfejsu webowego również było źródłem wyzwań. Musiałem zapoznać się z subtelnościami CORS (Cross-Origin Resource Sharing), aby umożliwić komunikację frontend z API. Poświęciłem również czas na poprawę doświadczenia użytkownika, dodając funkcje takie jak wybór modeli AI i stylów obrazów.

Prompter: Sztuka do Opanowania

Przez cały rozwój StoryPixAI doskonaliłem swoje umiejętności prompteringu, tej sztuki formułowania odpowiednich żądań, aby kierować modelami AI. Nauczyłem się dostosowywać moje promptry w zależności od używanych modeli, parametrów opowiadania i oczekiwań użytkowników. Ten etap był kluczowy, aby uzyskać wysokojakościowe wyniki i zapewnić satysfakcjonujące doświadczenie użytkownika.

Solidna i Zautomatyzowana Infrastruktura na AWS

StoryPixAI opiera się na bezserwerowej infrastrukturze hostowanej na Amazon Web Services (AWS), oferując idealną kombinację elastyczności, skalowalności i optymalizacji kosztów. Ta architektura, w pełni zautomatyzowana dzięki Terraform oraz GitLab CI/CD, umożliwia szybkie i niezawodne wdrażanie aplikacji.

Usługi AWS w Centrum StoryPixAI

alt text

Architektura StoryPixAI opiera się na następujących usługach AWS: * Amazon S3 (Simple Storage Service) : Przechowywanie statycznych plików witryny (HTML, CSS, JavaScript) oraz wygenerowanych historii i powiązanych ilustracji.

  • Amazon CloudFront : Sieć CDN (Content Delivery Network), która przyspiesza dystrybucję treści StoryPixAI do użytkowników na całym świecie, przechowując je w pamięci podręcznej w geograficznie bliskich lokalizacjach.
  • Amazon API Gateway : Bezpieczna brama aplikacji. Zarządza żądaniami użytkowników, uwierzytelnia je za pomocą Amazon Cognito i kieruje do odpowiednich funkcji Lambda.
  • AWS Lambda : Serverless funkcje, które stanowią napędzenie StoryPixAI. Orkiestrują generowanie historii, tworzenie obrazów, zarządzanie zadaniami asynchronicznymi oraz interakcje z DynamoDB i innymi usługami AWS.
  • Amazon DynamoDB : Elastyczna i wydajna baza danych NoSQL używana do przechowywania kluczowych informacji o funkcjonowaniu aplikacji.
  • Amazon Cognito : Usługa zarządzania tożsamościami i dostępem, która zabezpiecza aplikację, umożliwiając użytkownikom logowanie i kontrolę ich uprawnień. Upewnia się, że tylko uwierzytelnieni użytkownicy mają dostęp do funkcji generowania historii.
  • Amazon Bedrock : Platforma upraszczająca dostęp i użycie generatywnych modeli AI od różnych dostawców, takich jak Anthropic (Claude) i Stability AI (Stable Diffusion). Bedrock umożliwia łatwą integrację tych modeli w aplikacji bez konieczności zarządzania ich infrastrukturą.
  • Inne usługi AWS : StoryPixAI korzysta również z innych usług AWS, takich jak IAM (Identity and Access Management) do zarządzania uprawnieniami dostępu do zasobów, CloudWatch do monitorowania i logów (kluczowe do debugowania i analizy wydajności) oraz Systems Manager Parameter Store (SSM Parameter Store) do przechowywania wrażliwych informacji, takich jak klucze API, zapewniając tym samym bezpieczeństwo aplikacji.

Terraform : automatyzacja infrastruktury

Aby zarządzać tą złożoną infrastrukturą, wybrałem Terraform, narzędzie do Infrastructure as Code (IaC), które pozwala opisywać infrastrukturę w postaci deklaratywnego kodu. Dzięki Terraform mogłem zautomatyzować tworzenie, modyfikowanie i usuwanie zasobów AWS, zapewniając spójne, powtarzalne i łatwe do zarządzania środowisko. To znacząco upraszcza proces wdrażania i redukuje ryzyko błędów ludzkich.

GitLab CI/CD : bezproblemowe wdrożenia

Aby zapewnić ciągłe i niezawodne wdrażanie StoryPixAI, wdrożyłem pipeline CI/CD (Continuous Integration/Continuous Deployment) na GitLab. Ten pipeline automatyzuje testy, budowanie i wdrażanie aplikacji przy każdej modyfikacji kodu źródłowego, umożliwiając szybkie wykrywanie i naprawianie błędów oraz pewne dostarczanie nowych funkcji. To podejście zapewnia, że aplikacja jest zawsze aktualna i minimalizuje przestoje.

To połączenie AWS, Terraform i GitLab CI/CD pozwoliło mi zbudować solidną, skalowalną i łatwą do utrzymania infrastrukturę, zostawiając mi więcej czasu na skupienie się na kreatywnym aspekcie projektu i ulepszaniu doświadczeń użytkowników.

Ogólna Architektura projektu StoryPixAI

Przed zanurzeniem się w kodzie, oto przegląd architektury aplikacji:

  1. Strona statyczna na S3 : Statyczna strona internetowa hostowana w bucket S3, dostępna przez CloudFront dla globalnej dystrybucji.
  2. API Gateway : Udostępnia endpointy do generowania historii i sprawdzania statusu.
  3. Funkcje Lambda :
    • StoryPixAI.py : Generuje historię i powiązane obrazy.
    • status_checker.py : Sprawdza status generacji w DynamoDB.
  4. DynamoDB : Przechowuje status zadań generacji.
  5. S3 : Przechowuje wygenerowane obrazy i wynikowe strony HTML.
  6. Cognito : Zarządza uwierzytelnianiem użytkowników, aby zabezpieczyć API.

Funkcja Lambda StoryPixAI.py

Ogólny Przegląd

Funkcja StoryPixAI.py jest sercem aplikacji. Odpowiada za:

  • Generowanie historii na podstawie promptu użytkownika.
  • Tworzenie szczegółowych instrukcji, aby poprowadzić model AI w generowaniu historii.
  • Wyodrębnienie streszczeń dla każdej sceny lub kluczowego elementu historii.
  • Generowanie obrazów odpowiadających tym streszczeniom.
  • Łączenie tekstu i obrazów w stronę HTML.
  • Przechowywanie wyniku w S3 i aktualizowanie statusu w DynamoDB.

Dekonstrukcja Kodu

Importy i Wstępna Konfiguracja

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)

W tej sekcji importuję niezbędne moduły, konfiguruję logger do debugowania i pobieram klucz API OpenAI przechowywany w AWS Systems Manager Parameter Store (SSM). To pozwala zabezpieczyć klucz i unikać jego przechowywania w kodzie w postaci jawnej.

Funkcje Pomocnicze

Korekta Znaczników
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é.

Ta funkcja zapewnia, że używane znaczniki do delimitacji streszczeń i tytułów są jednolite. Jest to kluczowe dla poprawnego wyodrębnienia streszczeń później.

Wyodrębnienie Streszczeń
def extract_summaries(text):
    # Extrait les résumés du texte en utilisant des balises spécifiques.

Używa wyrażeń regularnych do wyodrębniania sekcji tekstu delimitowanych przez [resume] i [end_resume]. Te streszczenia posłużą jako prompty do generowania obrazów.

Generowanie Instrukcji dla Obrazów
def generate_image_instructions(prompt, style, language):
    # Génère les instructions pour la création d'images.

Ta funkcja formatuje prompt w sposób, który prowadzi model do generowania obrazów, włączając styl i język.

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

Aktualizuje tabelę TaskStatus, aby śledzić stan generacji, co jest kluczowe dla funkcji status_checker.py.

Głębsza Analiza generate_story_instructions

Funkcja generate_story_instructions jest sercem projektu. Generuje zestaw szczegółowych instrukcji, które będą przekazywane modelowi AI, aby poprowadzić generowanie historii.

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

Konstrukcja Promptu

Prompt jest zaprojektowany tak, aby dostarczyć modelowi AI wszystkich niezbędnych informacji do wygenerowania spójnej, edukacyjnej i odpowiedniej dla dzieci historii.

  • Język : Parametr language_description umożliwia określenie języka historii, zapewniając, że wygenerowany tekst będzie w wybranym języku.

  • Temat : Prompt użytkownika jest włączony w instrukcje, aby służyć jako podstawa historii.

  • Długość : Określony jest zakres od 1000 do 1500 słów, aby kontrolować długość historii.

  • Kluczowe Elementy : Instrukcje zachęcają do włączenia elementów takich jak przygoda, magia i ważne wartości edukacyjne.

Szczegóły Instrukcji

Instrukcje dostarczane modelowi są niezwykle szczegółowe, aby precyzyjnie prowadzić generowanie.

Oto analiza różnych części promptu:

  1. Struktura Narracyjna : Model jest proszony o ustrukturyzowanie historii z porywającym początkiem, bogatym w wydarzenia rozwinięciem i satysfakcjonującym zakończeniem.

  2. Opisy Wizualne : Historia musi być bogata w opisy wizualne, aby pobudzać wyobraźnię dzieci.

  3. Postacie : Zachęca się do rozwijania sympatycznych postaci z wyraźnymi osobowościami.

  4. Specyficzne Znaczniki : Znaczniki takie jak [titre]... [end_titre] i [resume]... [end_resume] są używane do delimitacji tytułu i opisów wizualnych.

  5. Elementy Fantastyczne : Model jest zachęcany do włączania elementów magicznych lub fantastycznych, aby uczynić historię bardziej atrakcyjną.

  6. Wartości Edukacyjne : Historia musi przekazywać ważne wartości.

Rola Znaczników

Znaczniki odgrywają kluczową rolę w późniejszym przetwarzaniu wygenerowanego tekstu.

  • [titre]… [end_titre] : Obejmuje tytuł historii. To pozwala łatwo go wyodrębnić i odpowiednio wyświetlić w interfejsie użytkownika.

  • [resume]… [end_resume]: Otacza szczegółowe opisy wizualne kluczowych scen w historii. Te streszczenia będą używane jako podpowiedzi do generowania obrazów.

Przetwarzanie Po Generacji

Po tym, jak model AI wygeneruje historię zgodnie z tymi instrukcjami, kod wykonuje następujące kroki:

  1. Korekta znaczników: Funkcja correct_resume_tags upewnia się, że wszystkie znaczniki są odpowiednio sformatowane do ekstrakcji.

  2. Ekstrakcja streszczeń: Funkcja extract_summaries używa znaczników [resume] i [end_resume] do wyodrębnienia opisów wizualnych.

  3. Generowanie obrazów: Każde streszczenie jest przekazywane do funkcji generate_image, aby stworzyć odpowiadający mu obraz.

  4. Tworzenie treści HTML: Tekst historii i wygenerowane obrazy są łączone, aby stworzyć kompletną stronę HTML.

Wpływ na Generację

Dostarczając te szczegółowe instrukcje, model jest prowadzony do:

  • Przestrzegania formatu: Korzystając z określonych znaczników, model generuje ustrukturalizowany tekst, który ułatwia automatyczne przetwarzanie.

  • Generowanie Odpowiednich Treści: Ograniczenia dotyczące języka, stylu i tematów zapewniają, że historia jest odpowiednia dla grupy docelowej.

  • Ułatwianie Generowania Obrazów: Wyodrębniając precyzyjne opisy wizualne, uzyskujemy wysokiej jakości podpowiedzi do generowania obrazów.

Zarządzanie Znacznikami przez Model

Model jest wyraźnie instruowany, aby nie tłumaczył ani nie modyfikował znaczników. Jest to niezbędne, aby znaczniki pozostały nienaruszone i mogły być używane w przetwarzaniu końcowym. Instrukcje podkreślają ten punkt, aby uniknąć sytuacji, w której model, który mógłby próbować parafrazować lub tłumaczyć cały tekst, zmienia znaczniki.

Generowanie Historii

Po wygenerowaniu szczegółowych instrukcji przez funkcję generate_story_instructions, kolejnym krokiem jest przekazanie tych instrukcji modelowi AI, aby stworzył historię.

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

Interakcja z Modelem OpenAI

  • Klient OpenAI: Inicjuję klienta OpenAI, używając wcześniej pozyskanego klucza API.

  • Prompting: Model otrzymuje serię komunikatów:

    • Komunikat systemowy informujący, że asystent jest ekspertem w opowiadaniu historii dla dzieci.
    • Komunikat użytkownika zawierający wygenerowane szczegółowe instrukcje.
  • Odpowiedź Modelu: Model generuje historię na podstawie dostarczonych instrukcji.

Zarządzanie Błędami

Jeśli wystąpi wyjątek podczas wywoływania API OpenAI, zostanie on przechwycony, a zwrócony zostanie komunikat o błędzie.

Ekstrakcja streszczeń i znaczników

Po wygenerowaniu historii, następnym krokiem jest wyodrębnienie opisów wizualnych za pomocą określonych znaczników.

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

Korekta Znaczników

Model może czasami nieznacznie zmienić znaczniki (na przykład, dodając akcenty). Funkcja correct_resume_tags upewnia się, że wszystkie znaczniki są jednolite i poprawnie sformatowane.

Ekstrakcja Streszczeń

Funkcja extract_summaries używa wyrażenia regularnego, aby znaleźć wszystkie wystąpienia tekstu między znacznikami [resume] i [end_resume]. Te streszczenia są szczegółowymi opisami wizualnymi, które będą używane do generowania obrazów.

Generowanie Obrazów

Po wyodrębnieniu streszczeń, każde streszczenie jest używane do wygenerowania odpowiadającego mu obrazu.

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

Funkcja generate_image

Funkcja generate_image wywołuje API modelu do generowania obrazów (na przykład, OpenAI DALL·E), aby stworzyć obraz na podstawie streszczenia.

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

Generowanie Instrukcji do Obrazów Funkcja generate_image_instructions dostosowuje streszczenie, aby stworzyć odpowiedni prompt do generowania obrazów.

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.
    """
  • Styl: Styl określony przez użytkownika (np. “akwarela”, “kreskówka”) jest uwzględniany w prompt, aby wpłynąć na wygląd obrazu.

  • Język: Opis jest dostosowywany do wybranego języka, co może pomóc modelowi zrozumieć niuanse kulturowe.

  • Jasne instrukcje: Poprzez określenie, że scena ma być czysto wizualna, unikamy dodawania przez model tekstu lub niepożądanych elementów w obrazie.

Interakcja z API OpenAI do Generowania Obrazów

  • Wywołanie API: Funkcja client.images.generate jest używana do generowania obrazu.

  • Ważne parametry :

    • Prompt: Dostosowany prompt jest przekazywany do API.
    • Model: Określony model do generowania obrazów.
    • Rozmiar: Rozmiar obrazu (np. “1024x1024”).
    • Jakość : Jakość obrazu (standardowa, HD).
    • Format odpowiedzi : Obrazy są zwracane w formacie base64, aby ułatwić ich przechowywanie i manipulację.

Zarządzanie błędami

Błędy przy generowaniu obrazów są wyłapywane i logowane, co pozwala na diagnozowanie problemów.

Tworzenie Treści HTML

Po wygenerowaniu obrazów odpowiadających wyekstrahowanym streszczeniom, następnym krokiem jest zestawienie tekstu historii i obrazów w formacie prezentacyjnym dla użytkownika. Odbywa się to poprzez tworzenie strukturalnej treści HTML, która będzie wyświetlana na stronie internetowej.

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

Szczegółowe wyjaśnienie

  1. Wyciąganie tytułu:

    • Używa wyrażenia regularnego do znalezienia tekstu między znacznikami [titre] a [end_titre].
    • Usuwa znaczniki z głównego tekstu po ekstrakcie.
    • Jeśli nie znaleziono tytułu, używany jest domyślny tytuł.
  2. Inicjalizacja HTML:

    • Treść HTML zaczyna się od znaczników <html>, <head>, i <body>.
    • Style CSS są uwzględniane, aby poprawić wygląd (czcionka, marginesy, wyrównania).
  3. Podział tekstu:

    • Tekst jest podzielony na segmenty przy użyciu znaczników [resume] i [end_resume].
    • Segmenty reprezentują części historii bez streszczeń.
  4. Składanie:

    • Każdy segment tekstu jest wstawiany do paragrafu <p>.
    • Jeśli generowanie obrazów jest włączone i istnieje odpowiadający obraz, obraz jest wstawiany po paragrafie.
    • Obrazy są wycentrowane i dostosowane do rozmiaru ekranu dla lepszej użyteczności.
  5. Finalizacja:

    • Znaczniki zamykające </body> i </html> są dodawane, aby ukończyć dokument HTML.

Dlaczego takie podejście?

  • Dopasowanie tekstu i obrazów: Wstawiając obrazy po odpowiadających im segmentach tekstu, historia jest wzbogacona wizualnie, co jest szczególnie ważne dla dzieci.

  • Elastyczność: Jeśli użytkownik zdecyduje się nie generować obrazów, kod obsługuje ten przypadek, wstawiając tylko tekst.

  • Dostępność: Używając semantycznych znaczników i dostosowanych stylów, treść jest dostępna na różnych urządzeniach (komputery, tablety, smartfony).

Upload do S3 i Aktualizacja Statusu

Po wygenerowaniu treści HTML, konieczne jest jej udostępnienie użytkownikowi. Odbywa się to poprzez przesłanie pliku do bucket S3 skonfigurowanego do hostowania statycznych witryn internetowych.

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

Szczegóły techniczne

  • Nazewnictwo plików:

    • Pliki są nazywane przy użyciu znacznika czasu, aby zapewnić ich unikalność.
    • Obrazy są przechowywane w folderze generated_images/, a pliki HTML w generated_content/.
  • Przesyłanie do S3:

    • Użycie klienta S3 z boto3 do interakcji z usługą.
    • Treść jest kodowana lub dekodowana w zależności od rodzaju (obraz lub tekst).
    • Parametr ACL='public-read' sprawia, że plik jest publicznie dostępny. - Budowa URL:
    • Publiczny URL jest tworzony przy użyciu skonfigurowanej domeny CloudFront, co umożliwia szybkie i bezpieczne dystrybuowanie treści.
  • Zarządzanie Wyjątkami:

    • W przypadku błędu podczas pobierania, wyjątek jest logowany i podnoszony do obsługi przez lambda_handler.

Główna Funkcja lambda_handler

Funkcja lambda_handler jest punktem wejścia funkcji Lambda. Orkiestruje ona wszystkie wcześniej opisane kroki.

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

Wyjaśnienie

  • Przetwarzanie Żądania:

    • Pobiera niezbędne informacje z otrzymanego zdarzenia (event).
    • Parametry żądania obejmują prompt, wybrane modele, język, itd.
  • Aktualizacja Statusu:

    • Przed rozpoczęciem przetwarzania, status jest aktualizowany na “Processing” w DynamoDB.
  • Generowanie Historii:

    • Wywołanie generate_story z odpowiednimi parametrami.
  • Ekstrakcja i Przetwarzanie:

    • Tagi są poprawiane, a streszczenia wydobywane do generowania obrazów.
  • Generowanie Obrazów:

    • Jeśli generowanie obrazów jest włączone, odpowiednie obrazy są generowane, a ich URL-e zbierane.
  • Tworzenie Treści HTML:

    • Tekst i obrazy są łączone, aby stworzyć końcową treść HTML.
  • Upload na S3:

    • Treść HTML jest wgrywana na S3, a URL wyniku jest uzyskiwany.
  • Ostateczna Aktualizacja Statusu:

    • Status jest aktualizowany na “link” z URL-em wyniku w DynamoDB.
  • Zwrot Odpowiedzi:

    • Odpowiedź zawiera requestId oraz URL wyniku, co pozwala klientowi sprawdzić status lub bezpośrednio uzyskać dostęp do treści.
  • Zarządzanie Wyjątkami:

    • W przypadku błędu, status jest aktualizowany na “Failed” i zwracana jest odpowiedź HTTP 500.

Funkcja Lambda status_checker.py

Ogólny Przegląd

Funkcja status_checker.py pozwala użytkownikom sprawdzić status ich zapytania o generowanie historii. Przeszukuje DynamoDB, aby pobrać aktualny status i, jeśli jest dostępny, URL wyniku.

Analiza Kodu

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

Szczegóły

  • Pobieranie requestId:

    • requestId jest kluczowy do identyfikacji konkretnego zapytania użytkownika.
  • Przeszukiwanie DynamoDB:

    • Funkcja próbuje pobrać element odpowiadający requestId.
    • Jeśli element istnieje, status i resultUrl są wydobywane.
  • Tworzenie Odpowiedzi:

    • Jeśli status jest dostępny, jest zwracany z URL-em wyniku.
    • Jeśli element nie zostanie znaleziony, zwracany jest błąd 404.
    • W przypadku błędu podczas przeszukiwania bazy danych, zwracany jest błąd 500 z odpowiednią wiadomością.
  • Nagłówki HTTP:

    • Nagłówki są definiowane, aby umożliwić zapytania CORS z witryny internetowej.

Integracja z API Gateway

Konfiguracja Endpointów

API Gateway udostępnia dwa główne endpointy do interakcji z funkcjami Lambda:

  1. /generate-image:

    • Metoda: POST
    • Opis: Pozwala użytkownikom uruchomić generowanie historii i ewentualnie powiązanych obrazów.
    • Integracja: Połączone z funkcją Lambda StoryPixAI.py.
  2. /check-status:

    • Metoda: GET
    • Opis: Pozwala użytkownikom sprawdzić status ich zapytania, podając requestId.
    • Integracja: Połączone z funkcją Lambda status_checker.py.

Autoryzacja z Cognito

Aby zabezpieczyć API i kontrolować dostęp do zasobów, wprowadziłem integrację z Amazon Cognito.

  • User Pool:

    • Zarządza informacjami uwierzytelniającymi użytkowników.
    • Umożliwia rejestrację, logowanie i zarządzanie użytkownikami.
  • Authorizer:

    • Skonfigurowany w API Gateway, aby weryfikować tokeny JWT wydane przez Cognito.
    • Zapewnia, że tylko uwierzytelnione żądania mogą uzyskać dostęp do chronionych endpointów. - Integracja z API Gateway:
    • Endpoints /generate-image i /check-status są chronione przez authorizera Cognito.
    • Klienci muszą dołączać token uwierzytelniający w nagłówkach swoich żądań (Authorization).

Strona statyczna na S3 i interakcja z API

Struktura strony

Strona internetowa statyczna służy jako interfejs użytkownika dla aplikacji.

  • index.html:

    • Zawiera formularz umożliwiający użytkownikom wprowadzenie promptu, wybór opcji generowania i przesłanie swojego żądania.
    • Zawiera skrypty niezbędne do interakcji z API i zarządzania uwierzytelnianiem.
  • storypixai.js:

    • Zawiera kod JavaScript do zarządzania interakcjami z API.
    • Obsługuje uwierzytelnianie za pomocą Cognito, przesyłanie formularza, śledzenie statusu i wyświetlanie wyników.

Przepływ pracy użytkownika

  1. Logowanie:

    • Użytkownik loguje się za pomocą wbudowanego formularza logowania.
    • Informacje są weryfikowane przez Cognito.
  2. Przesyłanie żądania:

    • Użytkownik wypełnia formularz promptem i wybranymi opcjami.
    • Po przesłaniu, żądanie POST jest wysyłane na endpoint /generate-image z danymi.
  3. Przetwarzanie asynchroniczne:

    • API zwraca natychmiast requestId.
    • Przetwarzanie generowania odbywa się w tle.
  4. Sprawdzenie statusu:

    • Strona internetowa okresowo odpytuje endpoint /check-status, podając requestId.
    • Po otrzymaniu statusu “link” URL wyniku jest wyświetlany użytkownikowi.
  5. Wyświetlanie wyniku:

    • Użytkownik może kliknąć link, aby uzyskać dostęp do wygenerowanej historii z obrazami.

Zarządzanie żądaniami i odpowiedziami

  • Żądania uwierzytelnione:

    • Wszystkie żądania do API zawierają token uwierzytelniający.
    • Token jest zarządzany przez SDK Cognito zawarty w stronie internetowej.
  • Zarządzanie statusami:

    • Możliwe statusy to “Processing”, “link”, “Failed”.
    • Strona dostosowuje swój interfejs w zależności od otrzymanego statusu (na przykład wyświetlanie spinnera, komunikat o błędzie, link do wyniku).

Interakcje między komponentami

Oto jak różne komponenty współdziałają:

  • Strona internetowa ↔️ API Gateway:

    • Strona internetowa wysyła żądania do endpointów udostępnionych przez API Gateway.
    • Tokeny uwierzytelniające są dołączane w celu zabezpieczenia żądań.
  • API Gateway ↔️ Funkcje Lambda:

    • API Gateway wywołuje odpowiednie funkcje Lambda w zależności od otrzymanych żądań.
  • Funkcje Lambda ↔️ DynamoDB:

    • Funkcje Lambda StoryPixAI.py i status_checker.py wchodzą w interakcję z DynamoDB w celu aktualizacji i pobierania statusu żądań.
  • Funkcja Lambda ↔️ S3:

    • Funkcja StoryPixAI.py przesyła wygenerowane obrazy i zawartość HTML do S3.
  • CloudFront ↔️ S3:

    • CloudFront jest używany do szybkiego i zabezpieczonego dystrybuowania zawartości przechowywanej na S3.
    • URL-e dostarczane użytkownikom wskazują na domenę CloudFront.
  • Użytkownik ↔️ Strona internetowa:

    • Użytkownik wchodzi w interakcję ze stroną internetową, aby przesłać żądania i wyświetlić wyniki.

Przykład wyników w logach Cloudwatch po wywołaniu żądania

Oto przykład wyników logów po wywołaniu żądania, abyś mógł zobaczyć surowy format generowanych danych:

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

Ciągła integracja z GitLab CI/CD

Aby zapewnić płynny rozwój i wdrożenie StoryPixAI, zainstalowałem pipeline ciągłej integracji (CI) i ciągłego wdrażania (CD) za pomocą GitLab CI/CD. Ta konfiguracja automatyzuje procesy budowy i wdrażania, zapewniając jakość i niezawodność kodu przy każdej modyfikacji.

Konfiguracja pipeline’a

Pipeline jest zdefiniowany w pliku .gitlab-ci.yml w katalogu głównym projektu. Oto przegląd jego struktury:

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

variables:
  TERRAFORM_VERSION: "1.5.7-*"
  TF_VAR_region: $AWS_DEFAULT_REGION
``` Ta konfiguracja definiuje różne etapy pipeline'u oraz zmienne globalne używane w procesie CI/CD.

### Główne Zadania

Pipeline obejmuje kilka kluczowych zadań:

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

To zadanie wykonuje terraform plan w celu sprawdzenia planowanych zmian w infrastrukturze bez ich stosowania.

  1. Wdrażanie Terraform:

    Wdrażanie Terraform:
      stage: Wdrożenia
      when: manual
      dependencies:
        - Weryfikacja Terraform
      script:
        - /bin/bash -c "source export.sh && terraform_apply"
    

    Po weryfikacji, to zadanie stosuje zmiany w infrastrukturze, wykonując terraform apply.

  2. Usuwanie Terraform:

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

    To zadanie pozwala na zniszczenie infrastruktury, jeśli to konieczne, wykonując terraform destroy.

  3. Zarządzanie Kluczami OpenAI:

    Klucz OpenAI - Dodawanie:
      stage: Wymóg opcjonalny
      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 "Nie znaleziono żadnego klucza."
            exit 1
          fi      
    
    Klucz OpenAI - Usuwanie:
      stage: Usuwanie
      when: manual
      script:
        - /bin/bash -c "source export.sh && manage_openai_key delete"
    

    Te zadania zarządzają bezpiecznym dodawaniem i usuwaniem kluczy API OpenAI w AWS Parameter Store.

Środowisko Wykonawcze

Każde zadanie jest uruchamiane w kontenerze Docker bazującym na Ubuntu 22.04, z zainstalowanymi Terraform i AWS CLI:

.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

Zalety tego podejścia CI/CD

  1. Automatyzacja: Każda zmiana w kodzie automatycznie uruchamia pipeline, zapewniając spójne weryfikacje i wdrożenia.

  2. Ręczna kontrola: Kluczowe etapy, takie jak wdrożenie i usuwanie, są skonfigurowane w trybie manualnym (when: manual), oferując dodatkową kontrolę przed wykonaniem.

  3. Bezpieczne zarządzanie sekretami: Integracja z AWS Parameter Store w celu zarządzania kluczami API zapewnia bezpieczną manipulację wrażliwymi informacjami.

  4. Elastyczność: Struktura etapów pozwala na uporządkowane i logiczne wykonywanie różnych kroków pipeline’u.

  5. Powtarzalność: Użycie znormalizowanego środowiska Docker zapewnia powtarzalność kompilacji i testów na różnych systemach.

Ta konfiguracja CI/CD pozwala nie tylko na automatyzację wdrożenia StoryPixAI, ale także na utrzymanie wysokiego poziomu jakości i niezawodności przez cały cykl rozwoju.

Wniosek

StoryPixAI było czymś więcej niż tylko zwykłym projektem technicznym. To była prawdziwa przygoda w świecie generatywnej AI, pozwalająca mi połączyć moją pasję do technologii z pragnieniem tworzenia magicznych historii dla moich dzieci.

Ten projekt dał mi możliwość eksploracji różnych aspektów AI, od tworzenia intuicyjnego interfejsu użytkownika po opanowanie promptingu, przez wdrażanie solidnej infrastruktury chmurowej z użyciem AWS i Terraform. Każdy etap był źródłem nauki, stawiając mnie przed wymagającymi technicznymi wyzwaniami i zmuszając do poszerzenia moich umiejętności w zakresie full-stack development i DevOps.

Mam nadzieję, że ten wpis na blogu dał wam wgląd za kulisy tej fascynującej przygody.

Kluczowe Punkty

  • Szczegółowe instrukcje:

    • Jasne i strukturalne prompty pozwalają uzyskać spójne i wysokiej jakości wyniki od modeli AI. - Modularna Architektura:
    • Każdy komponent (strona internetowa, API Gateway, Lambda, DynamoDB, S3, Cognito) pełni specyficzną rolę, ułatwiając utrzymanie i rozwój systemu.
  • Bezpieczeństwo i Skalowalność:

    • Wykorzystanie zarządzanych usług AWS zapewnia solidne bezpieczeństwo oraz zdolność do dostosowania się do rosnącego zapotrzebowania.

Link do projektu: StoryPixAI

Ten dokument został przetłumaczony z wersji fr na język pl przy użyciu modelu gpt-4o. Aby uzyskać więcej informacji na temat procesu tłumaczenia, odwiedź https://gitlab.com/jls42/ai-powered-markdown-translator