AWS Transcribe × AWS Bedrock でリアルタイム音声文字起こし＆要約を実装してみた！

## 🔥 はじめに

今回は音声データをリアルタイムで文字起こしし、その内容を要約するシステムを構築してみました！

今回は **AWS Transcribe Streaming** を使ってリアルタイム文字起こしを行い、その後 **AWS Bedrock** の LLM を活用して要約を実施します。

📌 実装の流れは以下の通りです：

1. Node.js でマイク入力を取得し、AWS Transcribe で文字起こし
2. 取得したテキストを AWS Bedrock に渡して要約
3. 結果をコンソールに表示

それでは、さっそく実装していきましょう！



## ✅ 必要なもの

- **AWS アカウント**（AWS Transcribe & AWS Bedrock の利用を有効化）
- **Node.js 環境**（v18 以上推奨）
- **AWS SDK for JavaScript (v3)**

## 🚀 実装手順

### 1️⃣ 必要なパッケージをインストール

まず、プロジェクトをセットアップし、必要なパッケージをインストールします。

```bash
npm init -y
npm install express node-record-lpcm16 @aws-sdk/client-transcribe-streaming @aws-sdk/client-bedrock-runtime @aws-sdk/credential-provider-ini
```

### 2️⃣ Node.js サーバーのセットアップ

以下のコードを `server.mjs` として作成します。

```jsx
const express = require("express");
const recorder = require("node-record-lpcm16");
const { TranscribeStreamingClient, StartStreamTranscriptionCommand } = require("@aws-sdk/client-transcribe-streaming");
const { BedrockRuntimeClient, InvokeModelCommand } = require('@aws-sdk/client-bedrock-runtime');
const { fromIni } = require('@aws-sdk/credential-provider-ini');
const { PassThrough } = require('stream');  // 追加

// =============== 設定項目を適宜書き換えてください ===============
const REGION = "ap-northeast-1";  // Bedrock対応リージョン
const MODEL_ID = "anthropic.claude-3-5-sonnet-20240620-v1:0"; // 使用するBedrockモデルIDの例
// ============================================================

// AWSクライアント
const credentials = fromIni(); 
const transcribeClient = new TranscribeStreamingClient({ region: REGION, credentials });
const bedrockClient = new BedrockRuntimeClient({ region: REGION, credentials });

// Expressサーバー
const app = express();
const port = 3000;

// 録音ボタン・結果表示
app.get("/", (req, res) => {
  // Node.jsで直接マイク録音→ストリーミング
  const html = `
  <html>
    <body>
      <h1>リアルタイム文字起こし & 要約デモ</h1>
      <p>このページを表示しても録音は開始されません。<br/>
         サーバー側で起動しているNode.jsプロセスがマイクに直接アクセスするデモです。</p>
      <p>コンソールを確認し、「Ctrl+C」等で録音終了後、Bedrockで要約を実行し結果が表示されます。</p>
    </body>
  </html>`;
  res.send(html);
});

let recording;
let transcriptTextAll = ""; 

// 録音開始用のエンドポイント
app.get("/start", async (req, res) => {
  try {
    console.log("録音を開始します。Ctrl+Cで停止してください...");

    // 1. 録音を開始
    recording = recorder.record({
      sampleRate: 16000,
      channels: 1,
      audioType: 'raw',
      verbose: true,
      recordProgram: 'rec',
      silence: 0
    });

    // 2. 録音ストリームの取得を確認
    const recordStream = recording.stream();
    if (!recordStream) {
      throw new Error("録音ストリームの初期化に失敗しました");
    }

    // 3. AWS Transcribe用のコマンドを作成
    const command = new StartStreamTranscriptionCommand({
      LanguageCode: "ja-JP",
      MediaEncoding: "pcm",
      MediaSampleRateHertz: 16000,
      AudioStream: async function* () {
        try {
          for await (const chunk of recordStream) {
            yield { AudioEvent: { AudioChunk: chunk } };
          }
        } catch (error) {
          console.error("音声ストリーム処理エラー:", error);
          throw error;
        }
      }(),
      EnablePartialResultsStabilization: true,
      PartialResultsStability: "medium",
    });

    // 4. AWS Transcribeでのストリーミングの開始
    const transcribeStream = await transcribeClient.send(command);

    // 5. 文字起こし結果の処理
    if (transcribeStream.TranscriptResultStream) {
      (async () => {
        try {
          for await (const event of transcribeStream.TranscriptResultStream) {
            const results = event.TranscriptEvent?.Transcript?.Results || [];
            for (const result of results) {
              if (result.IsPartial === false && result.Alternatives?.[0]) {
                const text = result.Alternatives[0].Transcript;
                transcriptTextAll += text + "\n";
                console.log("【確定】", text);
              } else if (result.IsPartial === true && result.Alternatives?.[0]) {
                const partial = result.Alternatives[0].Transcript;
                console.log("（途中）", partial);
              }
            }
          }
        } catch (error) {
          console.error("文字起こしストリーム処理エラー:", error);
        }
      })();
    }

    res.send("録音開始しました。コンソールを確認して下さい。\n停止はCtrl+Cで。");
  } catch (error) {
    console.error("録音開始エラー:", error);
    if (recording) {
      recording.stop();
    }
    res.status(500).send("録音の開始に失敗しました");
  }
});

// 録音終了 (Ctrl+Cなどでサーバーごと終了→後処理で要約)
process.on("SIGINT", async () => {
  if (!recording) {
    console.log("録音が開始されていません。");
    process.exit(0);
  }
  if (recording) {
    recording.stop();
  }

  await new Promise(r => setTimeout(r, 1000));

  console.log("最終的なトランスクリプトをBedrockで要約します...");
  
  // 文字起こしテキストの確認
  if (!transcriptTextAll || transcriptTextAll.trim() === '') {
    console.log("文字起こしテキストが空のため、要約をスキップします。");
    process.exit(0);
  }

  console.log("=== 文字起こし結果 ===");
  console.log(transcriptTextAll);
  console.log("\n=== 要約処理開始 ===");

  try {
    const bedrockParams = {
      modelId: MODEL_ID,
      contentType: "application/json",
      accept: "application/json",
      body: JSON.stringify({
        anthropic_version: "bedrock-2023-05-31",
        max_tokens: 300,
        messages: [
          {
            role: "user",
            content: `以下の文章を簡潔に要約してください:\n\n${transcriptTextAll}`
          }
        ]
      })
    };

    const bedrockClient = new BedrockRuntimeClient({ region: REGION, credentials });
    const response = await bedrockClient.send(new InvokeModelCommand(bedrockParams));
    
    const responseBody = JSON.parse(new TextDecoder().decode(response.body));
    const summaryText = responseBody.content[0].text || "(要約取得失敗)";
    console.log("=== 要約結果 ===");
    console.log(summaryText);
  } catch (e) {
    console.error("Bedrock要約時にエラー:", e);
    if (e.response) {
      console.error("エラーレスポンス:", JSON.stringify(e.response, null, 2));
    }
  }

  process.exit(0);
});

// サーバー起動
app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`);
});


```

### 3️⃣ 実行方法

```
node server.mjs
```

録音を開始するには、ブラウザで `http://localhost:3000/start` にアクセスします。
録音を終了するには `Ctrl+C` を押せば、自動的に AWS Bedrock による要約が行われ、結果がコンソールに表示されます。

- ブラウザ画面

    ![image (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3944959/bd14245f-9d2d-44fb-bfb2-2462982c3c74.png)


- コンソールログ
    - PCのマイクで拾った音声を文字起こしして`Ctrl+C` で録音を停止し、その間の文章の要約文が表示される。
    - コードを実行し、ブラウザにアクセスすると以下のようにPCのマイクから入力した音声が文字起こしされてログに表示されます。文章が確定していない場合は（途中）という文字が入り、1分が確定すると【確定】となります。
    今回は**Meta Quest 3Sの使い方**の動画を再生したものです
    
        ![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3944959/9be28e8a-9d69-42ce-812d-71b816e0e2d2.png)

    
    - `Ctrl+C` で録音を停止すると文字起こしの結果が表示されBedrockでの要約が始まります。
    要約が完了すると要約結果が表示されます
    
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3944959/fcc3caa0-216e-4c9f-94e6-7669076f5caa.png)


## 🎯 まとめ

このプロジェクトでは、AWS Transcribe Streaming を活用して **音声のリアルタイム文字起こし** を行い、その後 **AWS Bedrock による自動要約** を実現しました。

📌 今後追加したい機能：

- ✅ **WebSocket を利用したフロントエンドとの連携**
- ✅ **UI を作成し、録音の制御を可能に**
- ✅ **文字起こしの結果を保存し、後から確認できるようにする**

これらを実装すれば、さらに **実用的なシステム** へ進化できます！

🚀 ぜひ試してみてください！



💡 **質問やフィードバックがあれば、ぜひコメントで教えてください！**

## 参考資料・リンク集

- **公式ドキュメント**
    - [AWS Transcribe 公式ドキュメント](https://docs.aws.amazon.com/transcribe/)
    - [AWS Bedrock 公式ドキュメント](https://docs.aws.amazon.com/bedrock/)
    - [AWS SDK for JavaScript v3 リファレンス](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/)
    

（※投稿内容は個人の意見であり、所属組織の公式見解ではありません。）