4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS で提案書を作成するAPIを開発してみた

4
Last updated at Posted at 2026-02-10

 最近、PoCでやっているのはレポートからスライドを生成する開発を行っています。
とはいえ、GammaやGenSparkでもそれなりによいスライドは作成できるので、そちらで問題ない場合がほとんどかもしれません。ただ、自社プロダクトとなるとそれだと意味がなく、ある程度生成内容をコントロールできないと他のサービスをスライドしているだけの意味のないものになるので自分でとっかかりとして開発してみました。

全体アーキテクチャ
slide-api-arch.png

今回は、API Gateway部分の開発のみをまとめておきます。

  • API Gateway:HTTPエンドポイント
  • Lambda:中核ロジック
  • Bedrock:スライド構造を生成
  • pptx生成:python-pptx 等
  • S3:成果物保存

基本方針

LLMにPPTを直接生成させない

  • バイナリ生成は不安定
  • レイアウト崩れが頻発
  • 修正・再利用がほぼ不可能
  • デバッグ不能

LLMは「構造化されたスライド設計」まで

LLMの責務は以下に限定します。

  • スライド構成
  • タイトル
  • 箇条書き

将来的には

  • 図表の指示
  • 話者ノート

→ LLMは「構造化されたスライド設計」まで
例(JSONイメージ):

{
  "slides": [
    {
      "title": "AWS Bedrockとは",
      "bullets": [
        "AWS提供の生成AI基盤",
        "Claude / Titanなどを利用可能"
      ],
      "note": "導入背景を説明"
    }
  ]
}

👉 PPT生成はコード側で制御

Bedrock モデル選定

Claude 3.5 Sonnet(Anthropic)
*AWSの契約が直契約は使えるらしい。自分の環境は代理店契約なのでAWS Novaで代用。

  • 日本語レポートが安定
  • スライド設計用途に最適

Titan Text

  • Claudeに比べ低コスト・表現力弱め

IAM / 認証まわり

  • AWS上でBedrock用IAMユーザー or ロールを作成
  • bedrock:InvokeModel 権限が必要

ローカル環境構築

必須ツール

  • Docker Desktop
  • AWS SAM CLI
  • Python 3.10 / 3.11
  • AWS CLI
brew install aws-sam-cli
brew install awscli

プロジェクト構成(PPT生成API)

lambda-ppt-generator/
├─ template.yaml
├─ src/
│  ├─ app.py          # Lambda handler
│  ├─ bedrock.py      # Bedrock呼び出し
│  ├─ ppt/
│  │   └─ generator.py
│  ├─ mock/
│  │   └─ bedrock.json
│  └─ requirements.txt
└─ events/
   └─ local.json

SAM template.yaml(最小構成)

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  GeneratePptFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: python3.11
      Handler: app.lambda_handler
      CodeUri: src/
      Timeout: 300
      MemorySize: 1024
      Environment:
        Variables:
          LOCAL: true
          USE_BEDROCK_MOCK: "false"
          BEDROCK_REGION: "us-east-1"
          BEDROCK_MODEL_ID: "anthropic.claude-3-5-sonnet-20240620-v1:0"
          OUTPUT_DIR: "/var/task/lambda-tmp"
      Events:
        Api:
          Type: Api
          Properties:
            Path: /generate
            Method: post

API設計

エンドポイント:/generate

openapi: 3.0.3
info:
  title: Slide Generation API
  description: |
    AWS Bedrock + Lambda を利用したスライド自動生成API。
    Markdownやテキストを入力として受け取り、PPTを生成してS3に保存します。
  version: 1.0.0

servers:
  - url: https://api.example.com
    description: Production
  - url: http://localhost:3000
    description: Local (AWS SAM)

paths:
  /generate:
    post:
      summary: Generate PPT slide
      description: |
        Markdownファイルを入力として受け取り、
        AWS Bedrockでスライド構成を生成し、PPTファイルを作成します。
      operationId: generateSlide
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: text file(mark down)
      responses:
        "200":
          description: PPT generation completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: completed
                  download_url:
                    type: string
                    format: uri
                    example: https://s3.amazonaws.com/bucket/ppt/xxx.pptx
                  expires_in:
                    type: integer
                    example: 3600
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: error
                  message:
                    type: string
                    example: Invalid input
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: error
                  message:
                    type: string
                    example: Internal server error

メイン(app.py)

import json
import shutil, os
import uuid
import traceback
import base64
from requests_toolbelt.multipart import decoder
from ppt.generator import generate_ppt
from bedrock import invoke_bedrock,invoke_claude,invoke_nova
import boto3

#from s3 import upload_and_get_signed_url

from dotenv import load_dotenv
load_dotenv()  # ← カレントディレクトリの .env を読む

print("### app.py loaded ###")


def lambda_handler(event, context):
    try:
        print("### START lambda ###")
        
        # ---------- request ----------
        body = event.get("body")
        if body is None:
            return response(400, {"message": "body is required"})

        #レポートファイル(md)を読み込み
        files, fields = parse_multipart(event)
        markdown_bytes = files["file"]["content"]
        try:
            markdown = markdown_bytes.decode("utf-8")
        except UnicodeDecodeError:
            markdown = markdown_bytes.decode("utf-8-sig")  # BOM 対応
            
        #print("### markdown:",markdown)
        
        # ---------- Bedrock ----------
        slides_json = invoke_nova(markdown)

        #スライド用JSON        
        print("slides_json:",slides_json)

        # ---------- PPT generate ----------
        #顧客名
        company_name = "株式会社ストラテジーテック・コンサルティング"
        #スライドタイトル
        title = ""
        
        print("START generate ppt")
        #output_path = f"/tmp/{uuid.uuid4()}.pptx"
        s3_key = 'output.pptx'
        output_dir = "/tmp/"
        download_url = generate_ppt(slides_json, output_dir,s3_key)
        
        print("END lambda")

        return {
            "statusCode": 200,
            "body": json.dumps({
                "status": "completed",
                "download_url": download_url,
                "expires_in": 3600
            })
        }
        
    except Exception as e:
        print("ERROR:", str(e))
        traceback.print_exc()

        return response(500, {
            "message": "internal server error"
        })


def response(status_code, body):
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*"
        },
        "body": json.dumps(body, ensure_ascii=False)
    }



def parse_multipart(event):
    headers = event.get("headers") or {}

    # 
    content_type = headers.get("content-type") or headers.get("Content-Type")
    if not content_type:
        raise ValueError("Content-Type header not found")

    body = event.get("body")
    if body is None:
        raise ValueError("body is empty")

    if event.get("isBase64Encoded", False):
        body = base64.b64decode(body)
    else:
        body = body.encode("utf-8")

    multipart_data = decoder.MultipartDecoder(body, content_type)

    files = {}
    fields = {}

    for part in multipart_data.parts:
        disposition = part.headers.get(b"Content-Disposition", b"").decode()

        if "filename=" in disposition:
            name = disposition.split("name=")[1].split(";")[0].strip('"')
            filename = disposition.split("filename=")[1].strip('"')
            files[name] = {
                "filename": filename,
                "content": part.content,
                "content_type": part.headers.get(b"Content-Type", b"").decode()
            }
        else:
            name = disposition.split("name=")[1].strip('"')
            fields[name] = part.content.decode("utf-8")

    return files, fields


Bedrockにリクエストし、レポート内容をPPT向けのjson生成をするスクリプト(bedrock.py)

nova用の関数とcloude用関数を用意

import os
import json

import os
import json
import boto3
import certifi

SYSTEM_PROMPT = """You are a PowerPoint slide generation engine.
あなたはPowerPointスライド生成専用エンジンです。

【最重要ルール】
- 出力は JSON のみ
- 説明文・前置き・後書きは禁止
- ``` や markdown 記号は禁止
- 日本語のみ

【目的】
以下のMarkdownを、PowerPoint生成用のJSONに要約変換してください。

【制約】
- スライドは最大8枚
- 各スライドの bullets は最大5個
- 各 bullet は40文字以内
- 1 bullet は1文のみ
- 長文は要約する
- レポートタイトルをtitleで出力する

【出力JSON形式】
{
  "title": "タイトル",
  "slides": [
    {
      "title": "スライドタイトル",
      "bullets": [
        "箇条書き1",
        "箇条書き2"
      ]
    }
  ]
}

この形式以外の出力は禁止。


"""
def invoke_nova(markdown: str):
    
    print("### invoke_nova START ###")
    
    #ローカルの場合はSSL認証をオフにする
    verify = os.getenv("LOCAL") != True
    
    client = boto3.client(
        "bedrock-runtime"
        ,region_name="ap-northeast-1"
        ,verify=verify               
        )

    response = client.converse(
        modelId="amazon.nova-lite-v1:0",

        system=[
            {"text": SYSTEM_PROMPT}
        ],
        messages=[
            {
                "role": "user",
                "content": [
                    {"text": f"Convert the following Markdown:\n\n{markdown}"}
                ]
            }
        ],

        inferenceConfig={
            "maxTokens": 2048,
            "temperature": 0.2,
            "topP": 0.9
        }
    )

    text = response["output"]["message"]["content"][0]["text"]

    try:
        return json.loads(text)
    except json.JSONDecodeError:
        raise ValueError(f"Invalid JSON returned by model:\n{text}")

def invoke_claude(prompt: str):
    client = boto3.client(
        "bedrock-runtime",
        region_name=os.getenv("BEDROCK_REGION", "us-east-1"),
        verify=False
    )

    response = client.converse(
        modelId=os.getenv("BEDROCK_MODEL_ID"),
        messages=[
            {
                "role": "user",
                "content": [
                    {"text": prompt}
                ]
            }
        ],
        inferenceConfig={
            "maxTokens": 2048,  
            "temperature": 0.2,
            "topP": 0.9
        }
    )
    
    print("### invoke_nova END ###")

    # Claude 3 の返却テキスト
    return response["output"]["message"]["content"][0]["text"]

スライドファイルを生成するスクリプト(ppt/generattor.py)

*AWS SAMとは?

AWS SAM(Serverless Application Model)は
サーバーレスを最速で作るためのAWS公式フレームワークです。

sam local start-api \
  --env-vars env.json \
  --container-host-interface 0.0.0.0 \
  --warm-containers EAGER

生成されたpptファイル確認・取得(ローカル)

docker exec -it <container_id> ls -l /tmp
docker cp <container_id>:/tmp/output.pptx ./output/output.pptx

ローカル実行(API Gateway + Lambda擬似再現)

ビルド

sam build

起動

sam local start-api \
  --env-vars env.json \
  --container-host-interface 0.0.0.0 \
  --warm-containers EAGER

別ターミナルでAPI呼び出し
sample.mdはローカルのレポート内容が記載されたmarkdown.mdファイル

curl -X POST http://127.0.0.1:3000/generate \
  -F "file=@slides/sample.md" \
  -F "language=ja" 

S3の指定のフォルダに保存されていることを確認する

最後に

レポートファイルからスライドを生成できるようになりました。
デザインをよくするには、スライドテンプレートファイルを用意し、レイアウトを凝ってあげれば、
それなりのスライドが生成されます。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?