chatgpt apiを使って、入力テキストからhtmlスライド(reveal.js)を自動生成して、プレビューも出来るようしてみた。(react + node)
nodeプロジェクト生成
1 2 3 4 |
mkdir chatgpt-backend cd chatgpt-backend npm init -y npm install express openai cors |
server.jsで、入力テキスト → chatgpt api → html出力
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// server.js(Node.jsバックエンド) const express = require('express'); const cors = require('cors'); const { OpenAI } = require('openai'); const app = express(); const port = 3000; app.use(cors()); app.use(express.json()); const openai = new OpenAI({ apiKey: 'sk-' // ※ セキュリティのため .env に移行するのが望ましい }); app.post('/api/generate', async (req, res) => { const userText = req.body.text; try { const chatRes = await openai.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: 'あなたはHTMLスライドを生成するアシスタントです。スライドにはアイコン、絵文字、タイトル、リスト、ビジュアル的強調(色や太字)なども適度に加えてください。htmlファイルとしてそのまま利用できる出力してください。' }, { role: 'user', content: `以下の内容をhttps://unpkg.com/reveal.js/を使ったスライドHTMLにしてください。最初にアジェンダ(視覚的にも引き込まれるように):\n${userText}` } ] }); const html = chatRes.choices[0].message.content; res.json({ html }); } catch (err) { console.error('エラー:', err); res.status(500).send('スライド生成中にエラーが発生しました'); } }); app.listen(port, () => { console.log(`✅ サーバー起動中:http://localhost:${port}`); }); |
バックエンドのnodeサーバ起動
1 |
node server.js |
フロントはreactで作成
1 2 3 |
npx create-react-app lightning-client cd lightning-client npm install |
src/App.jsに記述
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
// Reactフロントエンド App.js(1ファイル構成) import { useState, useEffect, useRef } from "react"; function App() { const [inputText, setInputText] = useState(""); const [htmlText, setHtmlText] = useState(""); const [loading, setLoading] = useState(false); const iframeRef = useRef(null); const generateSlides = async () => { setLoading(true); try { const res = await fetch("http://localhost:3000/api/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: inputText }), }); const data = await res.json(); setHtmlText(data.html); } catch (error) { console.error("スライド生成失敗:", error); } finally { setLoading(false); } }; // iframeの高さを自動調整(オプション) useEffect(() => { if (iframeRef.current) { iframeRef.current.onload = () => { try { const iframeDoc = iframeRef.current.contentDocument || iframeRef.current.contentWindow.document; const height = iframeDoc.body.scrollHeight; iframeRef.current.style.height = height + 50 + 'px'; } catch (e) { console.warn("iframe自動高さ調整失敗:", e); } }; } }, [htmlText]); return ( <div style={{ padding: '20px', fontFamily: 'sans-serif' }}> <h1> 1分LTスライド自動生成</h1> <h2> 1. トーク内容入力</h2> <textarea value={inputText} onChange={(e) => setInputText(e.target.value)} rows={12} style={{ width: '100%', marginBottom: '10px' }} placeholder="エピソードや説明したい内容を入力..." /> <br /> <button onClick={generateSlides} disabled={loading}> {loading ? " 生成中..." : " スライドを生成"} </button> <h2 style={{ marginTop: '20px' }}>️ 2. HTML内容(修正可能)</h2> <textarea value={htmlText} onChange={(e) => setHtmlText(e.target.value)} rows={15} style={{ width: '100%', fontFamily: 'monospace', backgroundColor: '#f9f9f9' }} /> <h2 style={{ marginTop: '20px' }}>️ 3. スライドプレビュー</h2> <iframe ref={iframeRef} srcDoc={htmlText} style={{ width: '100%', height: '600px', border: '1px solid #ccc', backgroundColor: 'white', overflow: 'auto' }} sandbox="allow-scripts" title="スライドプレビュー" ></iframe> </div> ); } export default App; |
フロントエンドのreactサーバ起動
1 |
npm start |
AWSへ移行してみた。フロントはreactのまま(静的reactにして、s3に配置)
lambda + s3 + Secrets Manager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
import json import boto3 import os import requests secrets_client = boto3.client('secretsmanager') s3_client = boto3.client('s3') BUCKET_NAME = '' # S3バケット名 from datetime import datetime, timezone, timedelta def get_api_key(): response = secrets_client.get_secret_value(SecretId='secrets managerの識別ID') secret = json.loads(response['SecretString']) return secret.get('key') # ← ここでキー名を合わせる def lambda_handler(event, context): headers = { 'Content-Type': 'application/json' } # OPTIONSリクエストのCORS対応 if event.get('requestContext', {}).get('http', {}).get('method') == 'OPTIONS': return { 'statusCode': 200, 'headers': headers, 'body': '' } try: body = json.loads(event.get('body', '{}')) input_text = body.get('inputText', '') api_key = get_api_key() payload = { "model": "gpt-4o", "messages": [ { "role": "system", "content": ( "あなたはHTMLスライドを生成するアシスタントです。" "スライドにはアイコン、絵文字、タイトル、リスト、ビジュアル的強調(色や太字)なども適度に加えてください。" "htmlファイルとしてそのまま利用できる出力してください。" ) }, { "role": "user", "content": ( f"以下の内容をhttps://unpkg.com/reveal.js/を使ったスライドHTMLにしてください。" f"最初にアジェンダ(視覚的にも引き込まれるように):\n{input_text}" ) } ] } response = requests.post( "https://api.openai.com/v1/chat/completions", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" }, json=payload ) response.raise_for_status() result = response.json() html = result.get("choices", [{}])[0].get("message", {}).get("content", "<p>生成失敗</p>") # 日本時間 (UTC+9) に変換 jst = timezone(timedelta(hours=9)) now = datetime.now(jst) filename = now.strftime("%Y%m%d_%H%M%S") + ".txt" # S3へログ保存 s3_client.put_object( Bucket=BUCKET_NAME, Key=f"logs/{filename}", Body=json.dumps({ "inputText": input_text }, ensure_ascii=False), ContentType="text/plain", ACL="private" ) return { 'statusCode': 200, 'headers': headers, 'body': json.dumps({ 'html': html }) } except Exception as e: return { 'statusCode': 500, 'headers': headers, 'body': json.dumps({ 'error': str(e) }) } |