검색

생성형 AI로 어린이 이야기 만들기: StoryPixAI의 모험

StoryPixAI를 통해 제 목표는 사용자들이 인공지능 모델로 생성된 이미지로 풍성해진 어린이 이야기를 생성할 수 있는 대화형 웹 애플리케이션을 만드는 것이었습니다. 이를 위해 Lambda, API Gateway, DynamoDB, S3, Cognito 등 여러 AWS 서비스를 사용했습니다. 인프라 코드는 Terraform으로 관리되며, 배포는 GitLab CI로 자동화했습니다. 이 글에서는 기술 선택부터 맞닥뜨린 도전까지 이 흥미로운 프로젝트의 뒷이야기를 공개합니다.

소개

클라우드 인프라 및 DevOps 아키텍트로서 저는 항상 새로운 기술과 그것들이 일상을 어떻게 바꿀지에 대해 큰 흥미를 느껴왔습니다. 생성형 AI의 등장으로 호기심이 커졌고, 이 급성장하는 분야에 직접 뛰어들고자 하는 충동을 느꼈습니다.

그 결과로 탄생한 것이 StoryPixAI입니다. 이 개인 프로젝트는 AI를 활용해 맞춤형 이야기와 마법 같은 일러스트레이션을 어린이들에게 제공할 수 있는 가능성을 탐구할 기회를 주었습니다. 이 프로젝트를 통해 저는 풀스택 개발자, 프롬프트 엔지니어, 프로덕트 오너, 심지어 UX/UI 디자이너의 역할까지 경험하며 기술에 대한 열정을 주변 사람들과 나눌 수 있었습니다.

이 블로그 글에서는 기술적 선택과 이 모험을 통해 해결한 과제들을 공유하겠습니다.

하지만 그 전에, 미리 맛보기로 몇 가지를 보여드리겠습니다!

StoryPixAI의 잠재력을 미리 보여드리기 위해, 여러 언어로 자동 생성된 몇 가지 이야기를 소개합니다.
각 이야기는 일러스트와 함께 제공되어 어린이들이 더 몰입할 수 있도록 합니다:

창의성을 위한 AI: 실험의 여정

StoryPixAI와의 여정은 간단한 PoC(개념 증명)에서 시작되었습니다: OpenAI와 상호작용해 텍스트를 생성하고 DALL-E로 이미지를 만드는 Lambda 함수였습니다. 이 초기 성공은 더 나아가 AWS Bedrock을 통해 다른 AI 모델들을 탐색하도록 독려했습니다.

GPT-4와 GPT-4-o: 민첩한 이야기꾼

프로젝트 초기부터 OpenAI의 GPT-4는 텍스트 생성에 있어 명백한 선택이었습니다. 자연어의 뉘앙스를 이해하고 일관성 있고 창의적인 이야기를 만들어내는 능력 덕분에, 연령과 관심사에 맞춘 매력적인 이야기를 제작할 수 있었습니다. 동화에서 우주 모험, 동물 이야기, 판타지 서사에 이르기까지 다양한 글쓰기 스타일을 실험해볼 수 있었습니다.

GPT-4-0이 출시되었을 때 저는 이를 빠르게 StoryPixAI에 통합했습니다. 생성 속도가 크게 향상되어 대기 시간이 줄었고, 생성된 이야기의 품질도 더욱 원활하고 일관되며 상상력이 풍부해진 점에 감명을 받았습니다. GPT-4-0은 StoryPixAI에 있어 사용자 경험을 더 빠르고 쾌적하게 만드는 중요한 자산이 되었습니다.

DALL-E 3: 표준 일러스트레이터

텍스트 생성 모델이 만족스러운 결과를 제공했다면, 이미지 생성 도구의 선택은 더욱 결정적이었습니다. 여러 차례의 실험 끝에 DALL-E 3이 StoryPixAI의 표준 모델로 자리잡았습니다. DALL-E 3은 원작 이야기와 완벽하게 어울리는 원본적이고 세밀한 일러스트를 생성하는 능력으로 프로젝트 성공에 핵심적인 역할을 했습니다.

AWS Bedrock: 실험의 문을 열다

OpenAI에만 국한되지 않기 위해 AWS Bedrock을 사용하여 StoryPixAI에 다른 생성형 AI 모델들을 쉽게 통합했습니다. 이 플랫폼을 통해 Anthropic의 Claude와 Mistral 같은 텍스트 생성 모델, Stable Diffusion 같은 이미지 생성 모델을 테스트할 수 있었습니다.

이들 모델도 흥미로운 결과를 보여주었지만, 최종적으로 저는 텍스트 생성의 속도와 품질 때문에 GPT-4와 GPT-4-0에 집중하기로 했고, 이미지 생성은 DALL-E 3을 주요 선택지로 유지했습니다. 중요한 점은 이미지 생성에 사용되는 프롬프트의 상당 부분이 텍스트 모델 자체에 의해 작성된다는 점으로, 이는 이야기와 일러스트 간의 일관성을 보장합니다.

비동기 API와 DynamoDB의 도전

PoC를 검증한 뒤, StoryPixAI를 웹 인터페이스로 노출하기 위한 API를 만들기 시작했습니다. 이 단계에서 마주한 첫 큰 도전은 API Gateway의 타임아웃 제한이었습니다. 더 길고 복잡한 이야기를 생성할 수 있도록 이 제약을 우회하기 위해 비동기 아키텍처를 도입해야 했습니다.

이때 Amazon DynamoDB가 등장했습니다. 이 NoSQL 데이터베이스를 사용해 진행 중인 이야기 생성 작업과 완료된 결과를 저장했습니다. 이 접근법 덕분에 API는 사용자에게 즉시 응답을 반환할 수 있었고, 사용자는 자신의 요청 상태를 확인하고 이야기가 준비되면 결과를 받아볼 수 있었습니다.

CORS와 사용자 인터페이스: 극복해야 할 장애물

웹 인터페이스를 구현하는 과정에서도 여러 난관이 있었습니다. 프론트엔드가 API와 통신할 수 있도록 CORS(Cross-Origin Resource Sharing)의 세부사항을 숙지해야 했습니다. 또한 AI 모델 선택과 이미지 스타일 선택 같은 기능을 추가하여 사용자 경험을 개선하는 데 시간과 노력을 들였습니다.

프롬프트 작성: 숙달해야 할 예술

StoryPixAI 개발 전반에 걸쳐 저는 프롬프트 작성 능력을 갈고닦았습니다. 이는 AI 모델을 원하는 방향으로 이끌기 위한 적절한 요청을 구성하는 기술입니다. 사용된 모델, 이야기의 매개변수, 사용자 기대에 따라 프롬프트를 조정하는 법을 배웠습니다. 이 과정은 고품질 결과를 얻고 만족스러운 사용자 경험을 보장하는 데 필수적이었습니다.

AWS 기반의 견고하고 자동화된 인프라

StoryPixAI는 Amazon Web Services(AWS)에 호스팅된 서버리스 인프라를 기반으로 하며, 유연성, 확장성 및 비용 최적화의 이상적인 조합을 제공합니다. Terraform과 GitLab CI/CD로 완전히 자동화된 이 아키텍처는 애플리케이션의 빠르고 안정적인 배포를 가능하게 합니다.

StoryPixAI의 핵심 AWS 서비스

대체 텍스트

StoryPixAI의 아키텍처는 다음 AWS 서비스들을 중심으로 구성됩니다:

  • Amazon S3 (Simple Storage Service): 웹사이트의 정적 파일(HTML, CSS, JavaScript)과 생성된 이야기 및 해당 일러스트레이션 파일을 저장합니다.
  • Amazon CloudFront: 전 세계 사용자에게 StoryPixAI의 콘텐츠를 더 가까운 위치에서 캐시하여 빠르게 배포하는 CDN입니다.
  • Amazon API Gateway: 애플리케이션의 보안 게이트웨이로, 사용자 요청을 관리하고 Amazon Cognito를 통한 인증을 수행한 후 적절한 Lambda 함수로 라우팅합니다.
  • AWS Lambda: 이야기 생성, 이미지 생성, 비동기 작업 관리 및 DynamoDB 및 기타 AWS 서비스와의 상호작용을 오케스트레이션하는 서버리스 함수들입니다.
  • Amazon DynamoDB: 애플리케이션 작동에 필수적인 정보를 저장하는 유연하고 고성능의 NoSQL 데이터베이스입니다.
  • Amazon Cognito: 사용자 인증 및 권한 관리를 통해 인증된 사용자만 이야기 생성 기능에 접근할 수 있도록 애플리케이션 보안을 담당합니다.
  • Amazon Bedrock: Anthropic(Claude) 및 Stability AI(Stable Diffusion) 같은 다양한 공급자의 생성형 AI 모델에 대한 접근과 사용을 단순화하는 플랫폼입니다. Bedrock은 이러한 모델들을 직접 인프라로 관리하지 않고도 애플리케이션에 통합할 수 있게 합니다.
  • 기타 AWS 서비스: StoryPixAI는 또한 IAM(Identity and Access Management)으로 세부적인 권한 관리를 하고, CloudWatch로 모니터링과 로그(디버깅 및 성능 분석에 중요)를 수행하며, **Systems Manager Parameter Store (SSM Parameter Store)**에 API 키와 같은 민감한 정보를 저장하여 애플리케이션의 보안을 강화합니다.

Terraform: 인프라 자동화 도구

이 복잡한 인프라를 관리하기 위해 저는 선언적 코드 형태로 인프라를 기술할 수 있는 IaC(인프라 코드) 도구인 Terraform을 선택했습니다. Terraform을 통해 AWS 리소스의 생성, 변경 및 파괴를 자동화함으로써 일관되고 재현 가능하며 관리하기 쉬운 환경을 보장할 수 있었습니다. 이는 배포 프로세스를 크게 단순화하고 휴먼 에러의 위험을 줄여줍니다.

GitLab CI/CD: 매끄럽고 안정적인 배포

StoryPixAI의 연속적이고 신뢰할 수 있는 배포를 위해 GitLab에 CI/CD 파이프라인을 구축했습니다. 이 파이프라인은 코드 변경 시 테스트, 빌드, 배포를 자동화하여 오류를 빠르게 탐지하고 수정할 수 있게 하며, 새로운 기능을 자신 있게 릴리스할 수 있도록 합니다. 이 접근법은 애플리케이션을 항상 최신 상태로 유지하고 다운타임을 최소화합니다.

AWS, Terraform, GitLab CI/CD의 조합은 견고하고 확장 가능하며 유지보수하기 쉬운 인프라를 구축하게 해주었고, 저는 그 결과 창의적인 부분과 사용자 경험 개선에 더 많은 시간을 쏟을 수 있었습니다.

StoryPixAI 프로젝트의 전체 아키텍처

코드로 들어가기 전에 애플리케이션 아키텍처의 개요는 다음과 같습니다:

  1. S3에 호스팅된 정적 사이트: S3 버킷에 호스팅된 정적 웹사이트로, CloudFront를 통해 전 세계에 배포됩니다.
  2. API Gateway: 이야기 생성 및 상태 확인용 엔드포인트를 노출합니다.
  3. Lambda 함수들:
    • StoryPixAI.py : 이야기와 연관된 이미지들을 생성합니다.
    • status_checker.py : DynamoDB에서 생성 상태를 확인합니다.
  4. DynamoDB: 생성 작업의 상태를 저장합니다.
  5. S3: 생성된 이미지와 결과 HTML 페이지를 저장합니다.
  6. Cognito: API 보안을 위해 사용자 인증을 관리합니다.

Lambda 함수 StoryPixAI.py

전반 개요

함수 StoryPixAI.py는 애플리케이션의 핵심입니다. 이 함수는 다음을 담당합니다:

  • 사용자 프롬프트를 기반으로 이야기를 생성합니다.
  • 이야기 생성을 안내하기 위한 상세한 지침을 만듭니다.
  • 이야기의 각 장면이나 주요 요소에 대한 요약을 추출합니다.
  • 이러한 요약에 대응하는 이미지를 생성합니다.
  • 텍스트와 이미지를 결합해 HTML 페이지를 구성합니다.
  • 결과를 S3에 저장하고 DynamoDB의 상태를 업데이트합니다.

코드 분해

임포트 및 초기 설정

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)

이 섹션에서는 필요한 모듈을 임포트하고 디버깅을 위한 로거를 설정하며, AWS Systems Manager Parameter Store(SSM)에 저장된 OpenAI API 키를 불러옵니다. 이를 통해 키를 코드에 평문으로 저장하지 않고 안전하게 관리할 수 있습니다.

유틸리티 함수들

태그 정정
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):
    # Extrait les résumés du texte en utilisant des balises spécifiques.

이 함수는 정규식을 사용하여 [resume][end_resume]로 구분된 텍스트 섹션을 추출합니다. 이 요약들은 이미지 생성용 프롬프트로 사용됩니다.

이미지용 지침 생성
def generate_image_instructions(prompt, style, language):
    # Génère les instructions pour la création d'images.

이 함수는 스타일과 언어를 포함하여 이미지 생성 모델을 안내할 수 있도록 프롬프트를 포맷합니다.

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.

이 함수는 TaskStatus 테이블을 업데이트하여 생성 상태를 추적합니다. 이는 status_checker.py 함수에 필수적입니다.

generate_story_instructions에 대한 심층 분석

함수 generate_story_instructions는 프로젝트의 핵심입니다. 이 함수는 AI 모델에 전달될 일련의 상세 지침을 생성합니다.

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

프롬프트 구성

프롬프트는 모델이 일관성 있고 교육적이며 어린이에게 적합한 이야기를 생성하는 데 필요한 모든 정보를 제공하도록 설계되었습니다.

  • 언어: language_description 매개변수는 이야기의 언어를 지정할 수 있게 하여 생성되는 텍스트가 원하는 언어로 나오도록 보장합니다.

  • 주제: 사용자 프롬프트는 이야기의 기초로 지침에 통합됩니다.

  • 길이: 이야기 길이를 제어하기 위해 1000에서 1500단어의 범위를 지정했습니다.

  • 핵심 요소: 모험, 마법, 중요한 교육적 가치와 같은 요소의 포함을 장려하는 지침을 제공합니다.

지침의 세부사항

모델을 정밀하게 안내하기 위해 제공되는 지침은 매우 상세합니다.

다음은 프롬프트의 여러 부분에 대한 분석입니다:

  1. 서사 구조: 모델에게 매력적인 시작, 사건이 풍부한 전개, 만족스러운 결말로 이야기를 구성하도록 요청합니다.
  2. 시각적 묘사: 이야기는 어린이의 상상력을 자극할 수 있도록 풍부한 시각적 묘사로 채워져야 합니다.
  3. 등장인물: 개성 있는 성격을 가진 매력적인 등장인물을 개발하도록 권장합니다.
  4. 특정 태그: 제목과 시각적 묘사를 구분하기 위해 [titre]... [end_titre][resume]... [end_resume] 같은 태그를 사용합니다.
  5. 환상적 요소: 이야기를 더 흥미롭게 만들기 위해 마법적이거나 환상적인 요소를 포함하도록 권장합니다.
  6. 교육적 가치: 이야기는 중요한 가치를 가르쳐야 합니다.

태그의 역할 태그는 생성된 텍스트의 후처리에서 중요한 역할을 합니다.

  • [titre]… [end_titre] : 이야기의 제목을 감쌉니다. 이를 통해 사용자 인터페이스에 적절히 표시하기 위해 쉽게 추출할 수 있습니다.

  • [resume]… [end_resume] : 이야기의 주요 장면에 대한 상세한 시각적 설명을 감쌉니다. 이러한 요약은 이미지 생성을 위한 프롬프트로 사용됩니다.

생성 후 처리

AI 모델이 이러한 지침을 따라 이야기를 생성하면, 코드에서는 다음 단계를 수행합니다 :

  1. 태그 수정 : 함수 correct_resume_tags 는 모든 태그가 추출을 위해 올바르게 형식화되었는지 확인합니다.

  2. 요약 추출 : 함수 extract_summaries 는 태그 [resume][end_resume] 를 사용하여 시각적 설명을 추출합니다.

  3. 이미지 생성 : 각 요약은 해당 이미지를 생성하기 위해 함수 generate_image 로 전달됩니다.

  4. HTML 콘텐츠 생성 : 이야기 텍스트와 생성된 이미지가 결합되어 완전한 HTML 페이지를 만듭니다.

생성에 대한 영향

이러한 자세한 지침을 제공함으로써 모델은 다음과 같이 안내됩니다 :

  • 형식 준수 : 지정된 태그를 사용함으로써 모델은 자동 처리를 용이하게 하는 구조화된 텍스트를 생성합니다.

  • 적합한 콘텐츠 생성 : 언어, 스타일 및 주제에 대한 제약은 이야기가 대상 독자에게 적절하도록 보장합니다.

  • 이미지 생성 용이화 : 구체적인 시각적 설명을 추출함으로써 이미지 생성을 위한 질 좋은 프롬프트를 얻습니다.

모델에 의한 태그 처리

모델은 태그를 번역하거나 수정하지 않도록 명시적으로 지시받습니다. 이는 태그가 손상되지 않고 후처리에 사용될 수 있도록 하는 데 필수적입니다. 지침은 모델이 텍스트 전체를 바꾸거나 번역하려는 시도를 하지 않도록 이 점을 강조합니다.

이야기 생성

함수 generate_story_instructions 가 생성한 자세한 지침을 받은 후, 다음 단계는 이러한 지침을 AI 모델에 전달하여 이야기를 생성하는 것입니다.

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

OpenAI 모델과의 상호작용

  • OpenAI 클라이언트 : 이전에 가져온 API 키를 사용하여 OpenAI 클라이언트를 인스턴스화합니다.

  • 프롬프트 전달 : 모델은 일련의 메시지를 받습니다 :

    • 시스템 메시지로 어시스턴트가 아동용 이야기 전문가임을 알립니다.
    • 사용자 메시지로 상세 지침을 포함합니다.
  • 모델 응답 : 모델은 제공된 지침을 바탕으로 이야기를 생성합니다.

오류 처리

OpenAI API 호출 중 예외가 발생하면 예외를 포착하고 오류 메시지를 반환합니다.

요약 및 태그 추출

이야기 생성 후 다음 단계는 지정된 태그를 사용하여 시각적 설명을 추출하는 것입니다.

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

태그 수정

모델이 때때로 태그를 약간 변경할 수 있습니다(예: 악센트 추가). 함수 correct_resume_tags 는 모든 태그가 일관되고 올바르게 형식화되었는지 확인합니다.

요약 추출

함수 extract_summaries 는 정규식을 사용하여 [resume][end_resume] 사이의 모든 텍스트 발생을 찾습니다. 이 요약들은 이미지 생성을 위해 사용될 상세한 시각적 설명입니다.

이미지 생성

요약이 추출되면 각 요약은 해당 이미지를 생성하는 데 사용됩니다.

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

함수 generate_image

함수 generate_image 는 요약으로부터 이미지를 생성하기 위해 이미지 생성 모델 API(예: OpenAI DALL·E)를 호출합니다.

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

이미지용 지침 생성

함수 generate_image_instructions 는 적절한 이미지 생성 프롬프트를 만들기 위해 요약을 조정합니다.

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.
    """
  • 스타일 : 사용자가 지정한 스타일(예: “aquarelle”, “cartoon”)이 프롬프트에 포함되어 이미지 표현에 영향을 줍니다.

  • 언어 : 설명은 선택된 언어에 맞게 조정되어 문화적 뉘앙스를 이해하는 데 도움이 될 수 있습니다.

  • 명확한 지시 : 장면이 순수하게 시각적이어야 한다고 명시함으로써 모델이 이미지에 텍스트나 원치 않는 요소를 추가하지 않도록 합니다.

이미지 생성을 위한 OpenAI API와의 상호작용

  • API 호출 : 함수 client.images.generate 가 이미지를 생성하는 데 사용됩니다.

  • 중요 파라미터 :

    • 프롬프트 : 조정된 프롬프트가 API에 전달됩니다.
    • 모델 : 지정된 이미지 생성 모델.
    • 크기 : 이미지 크기(예: “1024x1024”).
    • 품질 : 이미지 품질(표준, HD).
    • 응답 형식 : 이미지는 저장 및 조작을 용이하게 하기 위해 base64로 반환됩니다.

오류 처리

이미지 생성 중 오류는 포착되어 로그에 기록되어 문제를 진단할 수 있게 합니다.

HTML 콘텐츠 생성

요약에 해당하는 이미지를 생성한 후 다음 단계는 이야기 텍스트와 이미지를 사용자에게 보여줄 수 있는 형식으로 조립하는 것입니다. 이는 사이트에 표시될 구조화된 HTML 콘텐츠를 생성함으로써 이루어집니다.

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

상세 설명

  1. 제목 추출:

    • 정규식을 사용하여 [titre][end_titre] 사이의 텍스트를 찾습니다.
    • 추출 후 본문에서 태그를 제거합니다.
    • 제목이 없으면 기본 제목이 사용됩니다.
  2. HTML 초기화:

    • HTML 콘텐츠는 <html>, <head>, 및 <body> 태그로 시작합니다.
    • 타이포, 여백, 정렬 등을 개선하기 위해 CSS 스타일이 포함됩니다.
  3. 텍스트 분할:

    • 텍스트는 [resume][end_resume] 태그를 사용하여 세그먼트로 나뉩니다.
    • 세그먼트는 요약이 제거된 이야기의 부분을 나타냅니다.
  4. 조립:

    • 각 텍스트 세그먼트는 단락 <p> 에 삽입됩니다.
    • 이미지 생성이 활성화되어 있고 해당 이미지가 있는 경우, 이미지는 단락 뒤에 삽입됩니다.
    • 이미지는 중앙 정렬되고 화면 크기에 맞게 조정되어 더 나은 사용자 경험을 제공합니다.
  5. 마무리:

    • 문서 완성을 위해 닫는 태그 </body></html> 가 추가됩니다.

이 접근법을 사용하는 이유

  • 텍스트와 이미지의 정렬: 텍스트 세그먼트 뒤에 이미지를 삽입함으로써 이야기가 시각적으로 풍부해지며, 이는 특히 어린이에게 중요합니다.

  • 유연성: 사용자가 이미지를 생성하지 않도록 선택하면 코드가 텍스트만 삽입하는 방식으로 이를 처리합니다.

  • 접근성: 의미론적 태그와 적절한 스타일을 사용하여 다양한 기기(컴퓨터, 태블릿, 스마트폰)에서 콘텐츠에 접근할 수 있게 합니다.

S3 업로드 및 상태 업데이트

HTML 콘텐츠가 생성되면 사용자가 접근할 수 있도록 해야 합니다. 이는 정적 웹사이트 호스팅용으로 구성된 S3 버킷에 파일을 업로드하여 수행됩니다.

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

기술적 세부사항

  • 파일 명명:

    • 파일은 고유성을 보장하기 위해 타임스탬프를 사용하여 명명됩니다.
    • 이미지는 generated_images/ 폴더에, HTML 파일은 generated_content/ 에 저장됩니다.
  • S3로 업로드:

    • 서비스와 상호작용하기 위해 boto3 의 S3 클라이언트를 사용합니다.
    • 콘텐츠는 타입(이미지 또는 텍스트)에 따라 인코딩 또는 디코딩됩니다.
    • 파라미터 ACL='public-read' 는 파일을 공개적으로 접근 가능하게 만듭니다.
  • URL 구성:

    • 공개 URL은 구성된 CloudFront 도메인을 사용하여 구성되며, 콘텐츠의 빠르고 안전한 배포를 가능하게 합니다.
  • 예외 처리:

    • 업로드 중 오류가 발생하면 예외가 로깅되고 lambda_handler 에 의해 처리되도록 전달됩니다.

주요 함수 lambda_handler

함수 lambda_handler 는 Lambda 함수의 진입점입니다. 이 함수는 앞서 설명한 모든 단계를 조율합니다.

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

설명

  • 요청 처리:

    • 수신된 이벤트(event)에서 필요한 정보를 가져옵니다.
    • 요청 매개변수에는 프롬프트, 선택된 모델, 언어 등이 포함됩니다.
  • 상태 업데이트:

    • 처리를 시작하기 전에 상태를 DynamoDB에 “Processing”으로 업데이트합니다.
  • 이야기 생성:

    • 적절한 매개변수로 generate_story 를 호출합니다.
  • 추출 및 처리:

    • 태그를 수정하고 이미지를 생성하기 위해 요약을 추출합니다.
  • 이미지 생성:

    • 이미지 생성이 활성화된 경우, 해당 이미지가 생성되고 URL이 수집됩니다.
  • HTML 콘텐츠 생성:

    • 텍스트와 이미지가 결합되어 최종 HTML 콘텐츠가 생성됩니다.
  • S3 업로드:

    • HTML 콘텐츠가 S3에 업로드되고 결과 URL을 얻습니다.
  • 최종 상태 업데이트:

    • 상태를 “link”로 업데이트하고 결과 URL을 DynamoDB에 저장합니다.
  • 응답 반환:

    • 응답에는 requestId 와 결과 URL이 포함되어 클라이언트가 상태를 확인하거나 콘텐츠에 직접 접근할 수 있게 합니다.
  • 예외 처리:

    • 오류 발생 시 상태를 “Failed”로 업데이트하고 HTTP 500 응답을 반환합니다.

Lambda 함수 status_checker.py

개요

함수 status_checker.py 는 사용자가 이야기 생성 요청의 상태를 확인할 수 있게 합니다. 이 함수는 DynamoDB를 조회하여 현재 상태와 가능한 경우 결과 URL을 가져옵니다.

코드 분석

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

세부사항

  • requestId 검색:

    • 특정 사용자의 요청을 식별하는 데 requestId 가 필수적입니다.
  • DynamoDB 조회:

    • 함수는 requestId 에 해당하는 항목을 가져오려고 시도합니다.
    • 항목이 존재하면 상태와 resultUrl 가 추출됩니다.
  • 응답 구성:

    • 상태가 사용 가능하면 결과 URL과 함께 반환됩니다.
    • 항목이 없으면 404 오류가 반환됩니다.
    • 데이터베이스 조회 중 오류가 발생하면 적절한 메시지와 함께 500 오류가 반환됩니다.
  • HTTP 헤더:

    • 웹사이트로부터의 CORS 요청을 허용하도록 헤더가 설정됩니다.

API Gateway와의 통합

엔드포인트 구성

API Gateway는 Lambda 함수와 상호작용하기 위해 주요 두 엔드포인트를 노출합니다 :

  1. /generate-image:

    • 메서드 : POST
    • 설명 : 사용자가 이야기 생성 및 선택적인 관련 이미지 생성을 시작할 수 있게 합니다.
    • 통합 : Lambda 함수 StoryPixAI.py 와 연결됩니다.
  2. /check-status:

    • 메서드 : GET
    • 설명 : 사용자가 requestId 를 제공하여 자신의 요청 상태를 확인할 수 있게 합니다.
    • 통합 : Lambda 함수 status_checker.py 와 연결됩니다.

Cognito로 인증

API를 보호하고 리소스 접근을 제어하기 위해 Amazon Cognito를 통합했습니다.

  • 유저 풀(User Pool):

    • 사용자 자격 증명을 관리합니다.
    • 가입, 로그인 및 사용자 관리를 허용합니다.
  • Authorizer:

    • API Gateway에 구성되어 Cognito가 발급한 JWT 토큰을 검증합니다.
    • 인증된 요청만 보호된 엔드포인트에 접근할 수 있도록 보장합니다.
  • API Gateway 통합:

    • 엔드포인트 /generate-image/check-status 은 Cognito Authorizer로 보호됩니다.
    • 클라이언트는 요청 헤더에 인증 토큰(Authorization)을 포함해야 합니다.

S3의 정적 사이트 및 API와의 상호작용

사이트 구조

정적 웹사이트는 애플리케이션의 사용자 인터페이스를 제공합니다.

  • index.html:

    • 사용자가 프롬프트를 입력하고, 생성 옵션을 선택하며, 요청을 제출할 수 있는 폼을 포함합니다.
    • API와의 상호작용 및 인증 관리를 위한 스크립트를 포함합니다.
  • storypixai.js:

    • API와의 상호작용을 관리하는 JavaScript 코드가 포함됩니다.
    • Cognito 인증, 폼 제출, 상태 추적 및 결과 표시를 처리합니다.

사용자 작업 흐름

  1. 로그인:

    • 사용자는 통합된 로그인 폼을 통해 로그인합니다.
    • 정보는 Cognito를 통해 검증됩니다.
  2. 요청 제출:

    • 사용자는 프롬프트와 원하는 옵션으로 폼을 작성합니다.
    • 제출 시 /generate-image 엔드포인트로 POST 요청이 데이터와 함께 전송됩니다.
  3. 비동기 처리:

    • API는 즉시 requestId 를 반환합니다.
    • 생성 처리는 백그라운드에서 수행됩니다.
  4. 상태 확인:

    • 웹사이트는 주기적으로 /check-status 엔드포인트를 requestId 를 제공하여 조회합니다.
    • 상태가 “link”가 되면 결과 URL이 사용자에게 표시됩니다.
  5. 결과 표시:

    • 사용자는 링크를 클릭하여 이미지가 포함된 생성된 이야기에 접근할 수 있습니다.

요청 및 응답 관리

  • 인증된 요청:

    • API로의 모든 요청에는 인증 토큰이 포함됩니다.
    • 토큰은 사이트에 포함된 Cognito SDK가 관리합니다.
  • 상태 관리:

    • 가능한 상태는 “Processing”, “link”, “Failed” 입니다.
    • 사이트는 받은 상태에 따라 UI를 조정합니다(예: 스피너 표시, 오류 메시지, 결과 링크).

구성 요소 간 상호 연결

다음은 다양한 구성 요소가 어떻게 상호작용하는지입니다 :

  • Site Web ↔️ API Gateway:

    • 사이트는 API Gateway가 노출한 엔드포인트로 요청을 보냅니다.
    • 요청을 보호하기 위해 인증 토큰이 포함됩니다.
  • API Gateway ↔️ Lambda 함수들:

    • API Gateway는 수신된 요청에 따라 해당 Lambda 함수를 호출합니다.
  • Lambda 함수들 ↔️ DynamoDB:

    • 함수 StoryPixAI.pystatus_checker.py 는 요청의 상태를 업데이트하고 조회하기 위해 DynamoDB와 상호작용합니다.
  • Lambda 함수 ↔️ S3:

    • 함수 StoryPixAI.py 는 생성된 이미지와 HTML 콘텐츠를 S3에 업로드합니다.
  • CloudFront ↔️ S3:

    • CloudFront는 S3에 저장된 콘텐츠를 빠르고 안전하게 배포하는 데 사용됩니다.
    • 사용자에게 제공되는 URL은 CloudFront 도메인을 가리킵니다.
  • 사용자 ↔️ 사이트:

    • 사용자는 요청을 제출하고 결과를 확인하기 위해 사이트와 상호작용합니다.

요청 호출 후 CloudWatch 로그 예시

다음은 요청 호출 후 생성된 원시 데이터 형식을 확인할 수 있도록 로그 결과의 예시입니다 :

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

GitLab CI/CD를 통한 지속적 통합

StoryPixAI의 원활한 개발 및 배포를 보장하기 위해 GitLab CI/CD를 사용하여 지속적 통합(CI) 및 지속적 배포(CD) 파이프라인을 구성했습니다. 이 구성은 빌드 및 배포 프로세스를 자동화하여 코드 변경마다 품질과 신뢰성을 보장합니다.

파이프라인 구성

파이프라인은 프로젝트 루트의 파일 .gitlab-ci.yml 에 정의됩니다. 구조는 다음과 같습니다 :

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

variables:
  TERRAFORM_VERSION: '1.5.7-*'
  TF_VAR_region: $AWS_DEFAULT_REGION
``` 이 구성은 파이프라인의 다양한 단계와 CI/CD 프로세스에서 사용되는 전역 변수를 정의합니다.

### 주요 작업

파이프라인은 여러 주요 작업으로 구성됩니다 :

1. **Terraform 검증** :

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

이 작업은 인프라 변경 예정 사항을 적용하지 않고 확인하기 위해 terraform plan을 실행합니다.

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

검증 후, 이 작업은 terraform apply을 실행하여 인프라 변경을 적용합니다.

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

이 작업은 필요 시 인프라를 제거하기 위해 terraform destroy을 실행할 수 있습니다.

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

Clé OpenAI - Supression:
  stage: Suppressions
  when: manual
  script:
    - /bin/bash -c "source export.sh && manage_openai_key delete"

이 작업들은 AWS Parameter Store에서 OpenAI API 키의 안전한 추가 및 삭제를 관리합니다.

실행 환경

각 작업은 Terraform과 AWS CLI가 설치된 Ubuntu 22.04 기반의 Docker 컨테이너에서 실행됩니다 :

.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

이 CI/CD 접근 방식의 장점

  1. 자동화 : 각 코드 변경은 파이프라인을 자동으로 트리거하여 일관된 검증 및 배포를 보장합니다.

  2. 수동 제어 : 배포 및 삭제와 같은 중요한 단계는 수동 모드(when: manual)로 구성되어 실행 전에 추가적인 제어를 제공합니다.

  3. 비밀의 안전한 관리 : API 키 관리를 위해 AWS Parameter Store와 통합함으로써 민감한 정보의 안전한 취급을 보장합니다.

  4. 유연성 : 스테이지 구조는 파이프라인의 다양한 단계를 순서대로 논리적으로 실행할 수 있게 합니다.

  5. 재현성 : 표준화된 Docker 환경을 사용하면 빌드와 테스트가 다양한 시스템에서 재현 가능해집니다.

이 CI/CD 구성은 StoryPixAI의 배포를 자동화할 뿐만 아니라 개발 주기 전반에 걸쳐 높은 품질과 신뢰성을 유지할 수 있게 합니다.

결론

StoryPixAI는 단순한 기술 프로젝트 그 이상이었습니다. 생성형 AI의 세계에서의 진정한 모험이었으며, 기술에 대한 제 열정과 아이들을 위한 마법 같은 이야기를 만들고자 하는 바람을 결합할 수 있게 해주었습니다.

이 프로젝트는 UI 설계에서부터 프롬프트 활용, AWS와 Terraform을 통한 견고한 클라우드 인프라 구성에 이르기까지 AI의 여러 측면을 탐구할 기회를 제공했습니다. 각 단계는 학습의 원천이었고, 도전적인 기술적 문제에 마주하게 하여 풀스택 개발 및 DevOps 역량을 확장하도록 했습니다.

이 블로그 글이 이 흥미로운 여정의 비하인드를 엿볼 수 있는 통찰을 제공했기를 바랍니다.

핵심 포인트

  • 자세한 지침 :

    • 명확하고 구조화된 프롬프트는 AI 모델로부터 일관되고 높은 품질의 결과를 얻을 수 있게 합니다.
  • 모듈화된 아키텍처 :

    • 각 구성요소(웹사이트, API Gateway, Lambda, DynamoDB, S3, Cognito)는 특정 역할을 하여 시스템의 유지보수 및 확장을 용이하게 합니다.
  • 보안 및 확장성 :

    • AWS의 관리형 서비스를 사용함으로써 강력한 보안과 증가하는 수요에 대응할 수 있는 능력을 확보합니다.

프로젝트 링크 : StoryPixAI

이 문서는 gpt-5-mini 모델을 사용하여 fr 버전에서 ko 언어로 번역되었습니다. 번역 과정에 대한 자세한 정보는 https://gitlab.com/jls42/ai-powered-markdown-translator 를 참조하세요.