• TOP
  • 記事一覧
  • 【Next.js AWS】音声文字起こし&要約、分析アプリケーション(S3)(Transcribe)(Amazon Bedrock)

【Next.js AWS】音声文字起こし&要約、分析アプリケーション(S3)(Transcribe)(Amazon Bedrock)

更新日:2025/02/02

基本機能

  • 音声ファイルをアップロード
  • アップロードした音声を文字に変換(Transcribe)
  • 変換したテキストをプロンプトを指定してBedrockで処理
  • 結果表示
1. 音声ファイルアップロード 2. 自動文字起こし(Amazon Transcribe) 3. AIによるテキスト分析(Amazon Bedrock)

AWS、Next.jsで実装の理由

AWS SDKの充実

  • Node.js用のSDKが完備

Next.jsのAPI Routes

  • フロントエンドとバックエンドを統合できる
  • APIエンドポイントの作成が簡単

AWSサービス間の連携

  • S3、Transcribe、Bedrockが統一的な認証情報で利用可能

手順

Next.jsプロジェクトの作成

mkdir next-app; cd next-app; npx create-next-app@latest . --typescript --tailwind --app --src-dir --yes

フロントページ next-app\src\app\page.tsx

"use client";
import { useState, FormEvent } from "react";

export default function Home() {
  const [file, setFile] = useState<File | null>(null);
  const [prompt, setPrompt] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [jobName, setJobName] = useState<string | null>(null);
  const [transcribeStatus, setTranscribeStatus] = useState<string | null>(null);
  const [transcribeResult, setTranscribeResult] = useState<string | null>(null);
  const [transcribeText, setTranscribeText] = useState<string | null>(null);
  const [bedrockResult, setBedrockResult] = useState<string | null>(null);

  // transcribeTextが設定された時にBedrockを呼び出す
  const processWithBedrock = async (text: string) => {
    try {
      const response = await fetch("/api/bedrock", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ text, prompt }),
      });

      const data = await response.json();
      if (data.result) {
        setBedrockResult(data.result);
      }
    } catch (error) {
      console.error("Bedrock処理エラー:", error);
    }
  };

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    if (!file) return;

    setIsLoading(true);
    try {
      const formData = new FormData();
      formData.append("file", file);

      const response = await fetch("/api/upload", {
        method: "POST",
        body: formData,
      });

      const data = await response.json();
      if (!response.ok) throw new Error(data.error);

      if (data.jobName) {
        setJobName(data.jobName);
        checkTranscribeStatus(data.jobName);
      }
    } catch (error) {
      console.error("エラー:", error);
    } finally {
      setIsLoading(false);
    }
  };

  const checkTranscribeStatus = async (jobName: string) => {
    try {
      const response = await fetch(`/api/transcribe/status?jobName=${jobName}`);
      const data = await response.json();

      setTranscribeStatus(data.status);

      if (data.status !== "COMPLETED") {
        setTimeout(() => checkTranscribeStatus(jobName), 5000);
      } else if (data.transcript) {
        setTranscribeText(data.transcript);
        // Bedrockでテキスト処理
        await processWithBedrock(data.transcript);
      }
    } catch (error) {
      console.error("ステータス確認エラー:", error);
    }
  };

  return (
    <div className="p-4 max-w-2xl mx-auto">
      <h1 className="text-xl mb-4">音声文字起こし分析</h1>

      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label className="block mb-2">音声ファイルを選択</label>
          <input
            type="file"
            accept="audio/*"
            onChange={(e) => setFile(e.target.files?.[0] || null)}
            className="w-full p-2 border"
            required
          />
          {file && <div className="mt-2 text-sm">選択中: {file.name}</div>}
        </div>

        <div>
          <label className="block mb-2">プロンプト</label>
          <textarea
            value={prompt}
            onChange={(e) => setPrompt(e.target.value)}
            className="w-full p-2 border"
            rows={3}
            placeholder="例:要約してください、分析してください、重要なポイントを箇条書きにしてください" // プレースホルダーを具体的に
            required
          />
        </div>

        <button
          type="submit"
          disabled={isLoading}
          className="px-4 py-2 bg-gray-200 hover:bg-gray-300 disabled:bg-gray-100"
        >
          {isLoading ? "処理中..." : "処理開始"}
        </button>
      </form>

      {/* 処理状態の表示 */}
      {transcribeStatus && (
        <div className="mt-4 p-4 bg-gray-50 rounded">
          <h2 className="font-bold mb-2">処理状態</h2>
          <p>{transcribeStatus}</p>
        </div>
      )}

      {/* 処理結果の表示 */}
      <div className="space-y-4">
        {transcribeText && (
          <div className="mt-4 p-4 bg-gray-50 rounded">
            <h2 className="font-bold mb-2">文字起こし結果</h2>
            <p className="whitespace-pre-wrap">{transcribeText}</p>
          </div>
        )}

        {bedrockResult && (
          <div className="mt-4 p-4 bg-gray-50 rounded border-t-4 border-blue-500">
            <h2 className="font-bold mb-2">AIによる分析結果</h2>{" "}
            {/* 「要約結果」から変更 */}
            <p className="whitespace-pre-wrap">{bedrockResult}</p>
          </div>
        )}
      </div>
    </div>
  );
}

AWS SDK のセットアップと S3 へのアップロード機能を実装

AWS SDK のインストール

npm install @aws-sdk/client-s3

既存ユーザーにAWS管理ポリシーを直接アタッチ

  • IAMコンソール → ユーザー → 該当ユーザー
  • 「許可を追加」→ 「既存のポリシーを直接アタッチ」
  • 以下の管理ポリシーを選択:
    • AmazonS3FullAccess
    • AmazonTranscribeFullAccess
    • BedrockFullAccess
AmazonS3FullAccess AmazonS3FullAccess AmazonTranscribeFullAccess BedrockFullAccess 次へ

バケットの作成

必須設定項目: 1. バケットタイプ: 汎用 2. リージョン: アジアパシフィック(東京) 3. バケット名: myname-demo-transcribe ※ 他の設定はデフォルトのまま

next-app\src\app\api\upload\route.ts

import { NextResponse } from 'next/server'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { TranscribeClient, StartTranscriptionJobCommand } from '@aws-sdk/client-transcribe'

// S3クライアントの初期化
const s3Client = new S3Client({
  region: process.env.NEXT_PUBLIC_AWS_REGION || 'ap-northeast-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  }
})

// Transcribeクライアントの初期化
const transcribeClient = new TranscribeClient({
  region: process.env.NEXT_PUBLIC_AWS_REGION || 'ap-northeast-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  }
})

export async function POST(request: Request) {
  try {
    const formData = await request.formData()
    const file = formData.get('file') as File
    if (!file) {
      return NextResponse.json({ error: 'No file provided' }, { status: 400 })
    }

    // ファイルをArrayBufferに変換
    const buffer = Buffer.from(await file.arrayBuffer())
    
    // ファイル名に現在時刻を追加して一意にする
    const fileKey = `uploads/${Date.now()}-${file.name}`

    // S3にアップロード
    await s3Client.send(
      new PutObjectCommand({
        Bucket: process.env.NEXT_PUBLIC_S3_BUCKET_NAME,
        Key: fileKey,
        Body: buffer,
        ContentType: file.type,
      })
    )

    // Transcribeジョブを開始
    const jobName = `transcribe-${Date.now()}`
    await transcribeClient.send(
      new StartTranscriptionJobCommand({
        TranscriptionJobName: jobName,
        LanguageCode: 'ja-JP',
        Media: {
          MediaFileUri: `s3://${process.env.NEXT_PUBLIC_S3_BUCKET_NAME}/${fileKey}`
        },
        OutputBucketName: process.env.NEXT_PUBLIC_S3_BUCKET_NAME
      })
    )

    return NextResponse.json({ 
      success: true,
      fileKey,
      jobName,
      bucket: process.env.NEXT_PUBLIC_S3_BUCKET_NAME
    })
  } catch (error) {
    console.error('Upload error:', error)
    return NextResponse.json({ error: 'Upload failed' }, { status: 500 })
  }
}

next-app/.env.example

# AWS Credentials
AWS_ACCESS_KEY_ID=あなたのアクセスキーID
AWS_SECRET_ACCESS_KEY=あなたのシークレットアクセスキー
NEXT_PUBLIC_AWS_REGION=S3バケットにリージョン
NEXT_PUBLIC_S3_BUCKET_NAME=あなたのバケット名

Transcribeの設定

# Transcribe用のSDKをインストール
npm install @aws-sdk/client-transcribe
音声ファイル S3バケット Transcribe 文字起こし処理 結果JSON S3バケット

↓音声ファイル素材

https://coefont.cloud/home

next-app\src\app\api\transcribe\status\route.ts

import { NextResponse } from 'next/server'
import { TranscribeClient, GetTranscriptionJobCommand } from '@aws-sdk/client-transcribe'
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'

const transcribeClient = new TranscribeClient({
  region: process.env.NEXT_PUBLIC_AWS_REGION || 'ap-northeast-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  }
})

const s3Client = new S3Client({
  region: process.env.NEXT_PUBLIC_AWS_REGION || 'ap-northeast-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  }
})

export async function GET(request: Request) {
  try {
    const { searchParams } = new URL(request.url)
    const jobName = searchParams.get('jobName')
    
    if (!jobName) {
      return NextResponse.json({ error: 'Job name is required' }, { status: 400 })
    }

    const command = new GetTranscriptionJobCommand({
      TranscriptionJobName: jobName
    })

    const response = await transcribeClient.send(command)
    const job = response.TranscriptionJob

    // デバッグログを追加
    console.log('Transcribe Job Status:', job?.TranscriptionJobStatus)
    console.log('Transcribe Job:', job)

    // ジョブが完了していない場合は状態のみ返す
    if (job?.TranscriptionJobStatus !== 'COMPLETED') {
      return NextResponse.json({
        status: job?.TranscriptionJobStatus
      })
    }

    // 完了している場合は文字起こし結果を取得
    try {
      console.log('TranscriptFileUri:', job.Transcript?.TranscriptFileUri)
      
      const transcriptPath = job.Transcript?.TranscriptFileUri
        ?.replace(`https://s3.${process.env.NEXT_PUBLIC_AWS_REGION}.amazonaws.com/${process.env.NEXT_PUBLIC_S3_BUCKET_NAME}/`, '');

      console.log('Transcript Path:', transcriptPath)

      if (transcriptPath) {
        const s3Response = await s3Client.send(
          new GetObjectCommand({
            Bucket: process.env.NEXT_PUBLIC_S3_BUCKET_NAME!,
            Key: transcriptPath
          })
        );

        const transcriptJson = await s3Response.Body?.transformToString();
        console.log('Transcript JSON:', transcriptJson)
        
        const transcriptData = JSON.parse(transcriptJson || '{}');

        return NextResponse.json({
          status: 'COMPLETED',
          transcript: transcriptData.results.transcripts[0].transcript
        });
      }
    } catch (error) {
      console.error('Error fetching transcript:', error);
    }

    return NextResponse.json({
      status: 'COMPLETED',
      error: 'Could not fetch transcript'
    });

  } catch (error) {
    console.error('Status check error:', error)
    return NextResponse.json({ error: 'Status check failed' }, { status: 500 })
  }
}

Bedrockの処理

npm install @aws-sdk/client-bedrock-runtime

next-app\src\app\api\bedrock\route.ts

import { NextResponse } from 'next/server'
import { BedrockRuntimeClient, InvokeModelCommand } from '@aws-sdk/client-bedrock-runtime'

const bedrockClient = new BedrockRuntimeClient({
  region: process.env.NEXT_PUBLIC_AWS_REGION || 'ap-northeast-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  }
})

export async function POST(request: Request) {
  try {
    const { text, prompt } = await request.json()

    const response = await bedrockClient.send(
      new InvokeModelCommand({
        modelId: 'amazon.titan-text-express-v1',
        contentType: 'application/json',
        accept: 'application/json',
        body: JSON.stringify({
          inputText: `以下のテキストを${prompt}\n\n${text}`,
          textGenerationConfig: {
            maxTokenCount: 1000,
            temperature: 0.7,
            topP: 0.9,
            stopSequences: []
          }
        })
      })
    )

    const responseBody = JSON.parse(new TextDecoder().decode(response.body))
    console.log('Bedrock response:', responseBody)  // デバッグ用ログ
    return NextResponse.json({ result: responseBody.results[0].outputText })

  } catch (error) {
    console.error('Bedrock error:', error)
    return NextResponse.json({ error: 'Bedrock processing failed' }, { status: 500 })
  }
}

実際のリクエストでは以下のように組み立てられています:

`以下のテキストを${prompt}\n\n${text}`
  1. “以下のテキストを” (固定文)
  2. “要約してください” (ユーザーが入力したプロンプト)
  3. 改行2回
  4. (文字起こしされたテキスト)

インフラ担当者への権限付与の依頼

【目的】
音声ファイルのアップロードから文字起こし、AI分析までを行うアプリケーションの開発

【必要な権限】
1. AmazonS3FullAccess
   - 用途:音声ファイルの保存、文字起こし結果の取得
   
2. AmazonTranscribeFullAccess
   - 用途:音声の文字起こし処理

3. BedrockFullAccess
   - 用途:文字起こしされたテキストのAI分析

【対象ユーザー/ロール】
- ユーザー名:[開発用IAMユーザー名]
人気記事ランキング
話題のキーワードから探す