検索

bloginfrastructureia

生成AIで子ども向けの物語を作る:StoryPixAIの冒険

生成AIで子ども向けの物語を作る:StoryPixAIの冒険

StoryPixAIでは、ユーザーが生成AIモデルによって作成された画像を添えて子ども向けの物語を生成できる、対話型のウェブアプリを作ることを目指しました。その実現のために、Lambda、API Gateway、DynamoDB、S3、認証には Cognito といった複数の AWS サービスを利用しました。インフラのコードは Terraform で管理し、デプロイは GitLab CI によって自動化しています。本記事では、この魅力的なプロジェクトの裏側、技術選定から直面した課題までを紹介します。

はじめに

クラウドインフラと DevOps のアーキテクトとして、私は常に新しい技術とそれらが日常をどう変えるかに魅了されてきました。生成AIの登場は私の興味を強く刺激し、この急速に発展する分野に飛び込んでみたいと感じました。

こうして StoryPixAI は誕生しました。個人的なプロジェクトとして、子ども向けにパーソナライズされた物語と魔法のような挿絵を作るために AI を活用する可能性を探ることができました。このプロジェクトを通じて、フルスタック開発者、プロンプトエンジニア、プロダクトオーナー、さらには UX/UI デザイナーの視点を経験し、家族や友人と技術への情熱を共有する機会となりました。

この記事では、私の技術的選択とこのワクワクする冒険で直面した課題を共有します。

まずはお試しを!

StoryPixAI の潜在力を感じていただくために、自動生成されたいくつかの物語を複数言語で紹介します。
各物語には挿絵が添えられており、子どもたちにとってより没入感のある読み聞かせ体験になります:

創造性のためのAI:実験の道のり

StoryPixAI の冒険は、シンプルな概念実証(Proof of Concept、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 を導入しました。DynamoDB を NoSQL データベースとして使用し、進行中の生成タスクと完了後の結果を保存するようにしました。このアプローチにより、API はユーザーに即時応答を返し、ユーザーは自分のリクエストの状態を確認して、生成が完了した時点で物語を取得できるようになりました。

CORS とユーザーインターフェース:克服すべき障害

ウェブインターフェースの実装でも幾つかの課題がありました。フロントエンドが API と通信できるようにするため、CORS(クロスオリジンリソース共有)の細かな調整を学ぶ必要がありました。また、AI モデルの選択や画像スタイルの選択など、ユーザー体験を向上させる機能を追加するために時間をかけました。

プロンプティング:習得すべき技術

StoryPixAI の開発を通じて、良いプロンプトを作る技術、すなわちプロンプティングを磨きました。使用するモデル、物語のパラメータ、ユーザーの期待に応じてプロンプトを調整する方法を学びました。この工程は高品質の結果を得て、満足のいくユーザー体験を保証する上で非常に重要でした。

AWS 上の堅牢で自動化されたインフラ

StoryPixAI は Amazon Web Services(AWS)上にホストされたサーバーレスなインフラに基づいており、柔軟性、スケーラビリティ、コスト最適化の理想的な組み合わせを提供します。このアーキテクチャは Terraform と GitLab CI/CD によって完全に自動化されており、アプリケーションの迅速かつ信頼性の高いデプロイを可能にします。

StoryPixAI の中心にある AWS サービス

アーキテクチャ図

StoryPixAI のアーキテクチャは以下の AWS サービスを中心に構成されています:

  • Amazon S3 (Simple Storage Service): ウェブサイトの静的ファイル(HTML、CSS、JavaScript)や、生成された物語とそれに関連するイラストの保存。
  • Amazon CloudFront: コンテンツ配信ネットワーク(CDN)として、地理的に近い場所でキャッシュすることで世界中のユーザーへのコンテンツ配信を高速化。
  • Amazon API Gateway: アプリケーションのセキュアな入口。ユーザーからのリクエストを受け取り、Amazon Cognito による認証を行い、適切な Lambda 関数へルーティングします。
  • AWS Lambda: StoryPixAI のエンジンとなるサーバーレス関数。物語の生成、画像作成、非同期タスク管理、DynamoDB や他の AWS サービスとの連携をオーケストレーションします。
  • Amazon DynamoDB: アプリケーションの運用に必要な情報を保存するための柔軟で高性能な NoSQL データベース。
  • Amazon Cognito: ユーザーのサインインやアクセス制御を提供する ID 管理サービス。認証済みユーザーのみが物語生成機能にアクセスできるようにします。
  • Amazon Bedrock: Anthropic(Claude)や Stability AI(Stable Diffusion)など、複数のベンダーの生成AIモデルへのアクセスと利用を簡素化するプラットフォーム。基盤となるインフラを管理することなくモデルを統合できます。
  • その他の AWS サービス: IAM(Identity and Access Management)で細かなアクセス権を管理、CloudWatch で監視とログ収集(デバッグやパフォーマンス分析に不可欠)、そして Systems Manager Parameter Store (SSM Parameter Store) で API キーなどの機密情報を安全に保管しています。

Terraform:インフラの自動化

この複雑なインフラを管理するために、私は Infrastructure as Code(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モデルとのやり取り

  • Client 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 が画像を生成するために使用されます。

  • 重要なパラメータ

    • Prompt:調整されたプロンプトがAPIに渡されます。
    • Modèle(モデル):指定された画像生成モデル。
    • Taille(サイズ):“1024x1024” のような画像のサイズ。
    • Qualité(品質):標準、HDなどの画像品質。
    • Format de Réponse:画像は保存と操作を容易にするために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"},
        }

説明

  • リクエストの処理

    • 受け取ったイベントから必要な情報を取得します。
    • リクエストパラメータにはプロンプト、選択されたモデル、言語などが含まれます。
  • ステータスの更新

    • 処理開始前に、DynamoDBでステータスを “Processing” に更新します。
  • 物語の生成

    • 適切なパラメータを付けて generate_story を呼び出します。
  • 抽出と処理

    • タグを修正し、画像生成のために要約を抽出します。
  • 画像の生成

    • 画像生成が有効な場合、対応する画像を生成しURLを収集します。
  • HTMLコンテンツの作成

    • テキストと画像を組み合わせて最終的なHTMLコンテンツを作成します。
  • S3へのアップロード

    • HTMLコンテンツをS3にアップロードし、結果のURLを取得します。
  • 最終ステータスの更新

    • DynamoDBでステータスを “link” に更新し、結果のURLを保存します。
  • レスポンスの返却

    • レスポンスには 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関数とやり取りするための主な2つのエンドポイントを公開します:

  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のオーソライザーによって保護されています。
    • クライアントはリクエストのヘッダーに認証トークンを含める必要があります(Authorization)。

S3上の静的サイトとAPIとのやり取り

サイトの構造

静的ウェブサイトはアプリケーションのユーザーインターフェースを提供します。

  • index.html

    • ユーザーがプロンプトを入力し、生成オプションを選択し、リクエストを送信するためのフォームを含みます。
    • APIとのやり取りや認証の管理に必要なスクリプトが含まれます。
  • storypixai.js

    • APIとのやり取りを管理するためのJavaScriptコードを含みます。
    • Cognitoによる認証、フォームの送信、ステータスの追跡、および結果の表示を管理します。

ユーザーフロー

  1. サインイン

    • ユーザーは組み込まれたログインフォームを介してサインインします。
    • 情報はCognitoで検証されます。
  2. リクエストの送信

    • ユーザーはプロンプトと希望のオプションをフォームに入力します。
    • 送信時に POST のリクエストがデータとともにエンドポイント /generate-image に送信されます。
  3. 非同期処理

    • APIは直ちに requestId を返します。
    • 生成処理はバックグラウンドで行われます。
  4. ステータスの確認

    • サイトは定期的にエンドポイント /check-status を照会し、requestId を提供します。
    • ステータスが “link” になったら、結果のURLがユーザーに表示されます。
  5. 結果の表示

    • ユーザーは生成された物語と画像にアクセスするためにリンクをクリックできます。

リクエストとレスポンスの管理

  • 認証付きリクエスト

    • APIへのすべてのリクエストには認証トークンが含まれます。
    • トークンはサイトに含まれるCognito SDKによって管理されます。
  • ステータスの管理

    • 可能なステータスは “Processing”、“link”、“Failed” です。
    • サイトは受け取ったステータスに応じてUIを調整します(例:スピナーの表示、エラーメッセージ、結果へのリンク)。

コンポーネント間の相互接続

各コンポーネントがどのように相互作用するかは次のとおりです:

  • サイト ↔️ API Gateway

    • サイトはAPI Gatewayが公開するエンドポイントにリクエストを送信します。
    • リクエストには認証トークンが含まれてセキュアに送信されます。
  • API Gateway ↔️ Lambda関数

    • API Gatewayは受信したリクエストに基づいて対応するLambda関数を呼び出します。
  • Lambda関数 ↔️ DynamoDB

    • Lambda関数 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 を実行します。

  2. Terraform の削除

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

    必要に応じてインフラを破棄するために、このジョブは terraform destroy を実行します。

  3. 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 キーの安全な追加と削除を管理します。

実行環境

各ジョブは Ubuntu 22.04 ベースの Docker コンテナ内で実行され、Terraform と 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

この CI/CD アプローチの利点

  1. 自動化:コードの変更ごとにパイプラインが自動的にトリガーされ、検証とデプロイが一貫して行われます。

  2. 手動によるコントロール:デプロイや削除のような重要なステップは手動モード(when: manual)に設定されており、実行前に追加の確認が可能です。

  3. シークレットの安全な管理:API キーの管理に AWS Parameter Store を統合することで、機密情報の安全な取り扱いが保証されます。

  4. 柔軟性:ステージ構成により、パイプラインの各ステップを順序立てて論理的に実行できます。

  5. 再現性:標準化された Docker 環境を使用することで、さまざまなシステム間でビルドやテストの再現性が保証されます。

この CI/CD 設定は、StoryPixAI のデプロイを自動化するだけでなく、開発ライフサイクル全体で高い品質と信頼性を維持するのに役立ちます。

結論

StoryPixAI は単なる技術プロジェクト以上のものでした。これは生成系AIの世界での真の冒険であり、技術への情熱と子どもたちのために魔法のような物語を作りたいという思いを融合させる機会でした。

このプロジェクトを通じて、直感的なユーザーインターフェースの設計から、プロンプト設計の習得、AWS と Terraform を使った堅牢なクラウドインフラの構築に至るまで、AI のさまざまな側面を探求する機会を得ました。各ステップは学びの連続であり、技術的な課題に直面することでフルスタック開発と DevOps のスキルを広げることができました。

このブログ記事が、このワクワクする冒険の舞台裏を垣間見る助けになれば幸いです。

重要なポイント

  • 詳細な指示

    • 明確で構造化されたプロンプトは、AI モデルから一貫性があり高品質な結果を得るのに役立ちます。
  • モジュール式アーキテクチャ

    • 各コンポーネント(ウェブサイト、API Gateway、Lambda、DynamoDB、S3、Cognito)は特定の役割を果たし、システムの保守性と拡張性を容易にします。
  • セキュリティとスケーラビリティ

    • AWS のマネージドサービスを活用することで、堅牢なセキュリティと増大する需要への対応能力が確保されます。

プロジェクトリンク:StoryPixAI

この文書は、gpt-5-mini モデルを使用して fr バージョンから ja 言語に翻訳されました。翻訳プロセスの詳細については、https://gitlab.com/jls42/ai-powered-markdown-translator をご覧ください。