最近、PoCでやっているのはレポートからスライドを生成する開発を行っています。
とはいえ、GammaやGenSparkでもそれなりによいスライドは作成できるので、そちらで問題ない場合がほとんどかもしれません。ただ、自社プロダクトとなるとそれだと意味がなく、ある程度生成内容をコントロールできないと他のサービスをスライドしているだけの意味のないものになるので自分でとっかかりとして開発してみました。
今回は、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の指定のフォルダに保存されていることを確認する
最後に
レポートファイルからスライドを生成できるようになりました。
デザインをよくするには、スライドテンプレートファイルを用意し、レイアウトを凝ってあげれば、
それなりのスライドが生成されます。
