1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

kintoneからSpeech-to-Textで文字起こしして要約させる

Last updated at Posted at 2025-12-06

はじめに

文字起こしや要約といった作業は専用の議事録系サービスやNotebookLM等を使ってAIにさせることが当たり前になりました。
しかも最近のGeminiやChatGPTは扱えるコンテキストや量にも大幅に改善され、実用的になってきたと感じます。
しかし、いざ業務でそれを定着させようとすると意外と大変で、「このプロンプト(Gem)を使ってね」とか、管理する悩みはどの職場でも話題の一つになっているのではと思います。
さらに業務でkintoneを利用している場合、関連する情報は一つのアプリ内で集約させたいでしょう。

そこで、毎回決まった要約をするのであれば「誰でも」簡単にできないかと考え、会話を始めるときにkintone上で「録音開始」ボタンを押すだけ、終わったら「録音停止」ボタンを押すだけで、音声ファイルのアップロードから文字起こし、プロンプト実行までを自動におこなうプログラムを書いてみました。

システム構成

全体のアーキテクチャは以下の通りです。 kintoneのWebhookを起点に、AWS Lambdaを経由してGoogle CloudのAIサービスを呼び出します。(なぜGoogle?は多分好みです)

Speech-to-Text V2の非同期のバッチ処理を利用するため、処理の開始(トリガー)と結果の受け取り(ハンドラー)を2つのLambda関数に分けて連携させています。

(細かいところはがっつりと省略しています)

利用した主なサービス

  • kintone: フロントエンド(音声入力・結果表示)
  • AWS Lambda: ロジックの実行環境
  • Google Cloud Speech-to-Text V2: 音声認識モデル
  • Google Cloud Run: 文字起こし完了後にPub/Subへ渡します
  • Google Cloud Pub/Sub: Lambda呼び出しで利用します
  • Google Gemini API: 文字起こしテキストの整形と要約
  • Amazon DynamoDB: ジョブの状態管理

kintoneアプリの設定

kintone側には、以下のフィールドを用意しておきます。

  • 添付ファイル: 音声ファイル用
  • 文字列(複数行): 文字起こし結果用
  • 文字列(複数行): AI要約用

実装

音声ファイルの作成はブラウザ上で行います。今回の記事では割愛しますが、せっかく録音したデータがネットワークトラブルで消えてしまった...といった悲しい状況にはならないように注意して(万一の時に復元できるように)実装します。なお、コーデックはWebM(Opus)にしました。圧縮効率と音質の高さ、そしてなによりGoogle Speech-to-Textでサポートされていることが理由です。

実装は大きく分けて「トリガー関数」「Speech-to-TextからLambdaへのバトンタッチ」「結果処理関数」の3つになります。

1. トリガー用Lambda (Speech-to-Text開始)

まずは、kintoneからリクエストを受け取り、Google Speech-to-Textにジョブを投げる部分です。 Speech-to-Text V2 APIは非同期のバッチ処理に対応しているため、長時間の音声でもタイムアウトを気にせず処理できます。

まずはkintoneから音声をダウンロードし、一時的にGoogle Cloud Storage (GCS) へアップロードします。

    // 1. kintoneから音声ファイルをダウンロード
    // fileKeyはリクエストに含めておく
    const fileData = await kintoneClient.file.downloadFile({
      fileKey: fileKey,
    });
    const audioFileBuffer = Buffer.from(fileData);
    
    // 2. GCSへアップロード (一時保管)
    const gcsFileName = `tmp/${recordId}-${fileKey}-${Date.now()}`;
    await storage.bucket(uploadBucket).file(gcsFileName).save(audioFileBuffer);

注) 今回は実装をシンプルにするためファイルを一度メモリに読み込んでいますが、Lambdaのメモリ制限を超えてエラーになる可能性があります。巨大なファイルを扱う場合は、ストリームを使った転送処理等が必要と思います。

Google Speech-to-Textを使います。2025年12月現在、APIはV1とV2がありますが今回はV2を使います。

import { v2 } from '@google-cloud/speech';
speechClient = new v2.SpeechClient({ credentials });

そしてSpeechClient.batchRecognize を使用して、非同期認識を開始します。

    // 3. Speech-to-Text V2 バッチ処理の開始
    const location = 'global';
    const recognizerName = `projects/${GCLOUD_PROJECT}/locations/${location}/recognizers/_`;

    const request = {
      recognizer: recognizerName,
      config: {
        languageCodes: ['ja-JP'],
        model: 'long', // 長時間録音向けのモデル
        features: {
          enableAutomaticPunctuation: true, // 句読点の自動付与
          enableSpeakerDiarization: true,   // 話者分離
          minSpeakerCount: 2,
          maxSpeakerCount: 6,
        },
      },
      files: [{ uri: `gs://${uploadBucket}/${gcsFileName}` }],
      recognitionOutputConfig: {
        gcsOutputConfig: { uri: `gs://${resultsBucket}/results/${jobId}/` },
      },
    };

    const [operation] = await speechClient.batchRecognize(request);

recognizerName の部分について

const location = 'global';
// 末尾の "_" (アンダースコア) に注目
const recognizerName = `projects/${GCLOUD_PROJECT}/locations/${location}/recognizers/_`;

Google Cloud Speech-to-Text V2 API では、音声認識の設定(言語コードやモデルの指定など)を 「Recognizer(認識器)」 というリソースとして管理する概念が導入されました。

本来であれば事前に「日本語用Recognizer」や「英語用Recognizer」を作成し、そのIDを指定してAPIを叩くのがV2の基本作法だそうです。

しかし、このコードでは末尾に _ (アンダースコア) を指定しています。 これはワイルドカードのような役割を果たし、「事前に作成されたRecognizerリソースは使わず、このリクエストペイロードに含まれるconfigの設定をそのまま使う」ということになります。

上記ソースコードでは話者分離の設定をしていますが、残念ながら2025年12月時点では日本語の話者分離は提供されていません。今後のアップデートを待ちたいところです。今回は後述するLLMの処理のところで話者分離を「推論」させています。

どのレコードの処理かを追跡するため、DynamoDBにジョブ情報を保存します。

    const ddbClient = new DynamoDBClient({});
    const ddbDocClient = DynamoDBDocumentClient.from(ddbClient);
    
    // 4. DynamoDBにジョブ情報を保存
    await ddbDocClient.send(new PutCommand({
        TableName: DYNAMODB_TABLE_NAME,
        Item: {
            jobId,
            kintoneRecordId,
            status: 'pending',
            // ...
        }
    }));

    // ... (後略: kintoneに「開始しました」コメントを投稿)

2. Speech-to-TextからAWSへのバトンタッチ

Speech-to-Textのバッチ処理は、完了すると結果のJSONファイルをGoogle Cloud Storage (GCS) に静かに保存して終了します。これだけではAWS側のLambdaは動き出しません。

そこで今回は、Cloud Run Functions と Pub/Sub を使って、以下のようなイベントリレーを構築しています。

  1. 結果保存: Speech-to-Textが、GCSに json を保存する
  2. 検知: それをトリガーに Cloud Run を自動起動させる
  3. 通知: Cloud run関数がGCSのパス等をメッセージに組み立てて Pub/Sub に投げる
  4. 起動: Pub/Sub(Pushサブスクリプション)が AWS Lambda のURLを叩き、後続の処理を開始させる

間にメッセージングサービス(Pub/Sub)を挟むことで、リトライ制御やバッファリングをGoogle Cloud側に任せつつ、AWS Lambdaを呼び出す構成にしています。

3. 結果処理用Lambda (Geminiによる整形・要約)

次に、Speech-to-Textの完了通知(Pub/Sub経由)を受けて実行される関数です。 ここで、生のアウトプットをGeminiに渡し、読みやすい形に整形させます。

Pub/Subから受け取ったメッセージを元に、GCSから文字起こし結果(JSON)を取得します。

  const pubsubData = JSON.parse(Buffer.from(event.message.data, 'base64').toString());
  const { jobId, bucket, fileName } = pubsubData;

  // 想定外のバケットへのアクセスを防止
  if (bucket !== process.env.GCS_RESULTS_BUCKET_NAME) {
      throw new Error(`Invalid bucket access: ${bucket}`);
  }

  // GCSからファイルをダウンロード
  const [fileContent] = await storage
    .bucket(bucket)
    .file(fileName)
    .download();

  const resultJson = JSON.parse(fileContent.toString());

JSONはこんな感じです

{
  "results": [
    {
      "alternatives": [
        {
          "transcript": "はい、どうもこんにちは。1週間ぶりですね。",
          "confidence": 0.70180243
        }
      ],
      "resultEndOffset": "10.570s",
      "languageCode": "ja-jp"
    },
    {
      "alternatives": [
        { "transcript": "さあ えーと そうかもしれませんね。", 
          "confidence": 0.71600956 
        }
      ],
      "resultEndOffset": "26.580s",
      "languageCode": "ja-jp"
    },
  ]
}

Gemini API を2段階で活用します。

  • 整形: 話者分離させ、かつ自然な会話形式に修正
  • 要約: 整形されたテキストを元に、要点を抽出

前述した通り今回利用しているGoogle Speech-to-Text V2でまだ話者分離はしてくれません(無視されます)。transcriptの部分を抜き出してGeminiに話者を推測させ、あわせて不明瞭な文字起こし結果も修正してもらい、「スピーカーA:」「スピーカーB:」のように整形してもらいます。
乱暴なような手順ですが、結構問題なく使えています。もし会話の中で名前を呼び合っていたりすると、それも利用して文字起こし結果を整形してくれることもあります。プロンプトを色々試してみてください。

最後にkintoneへ結果を反映します。更新対象のアプリIDやレコードIDはDyanamoDBに保存しておいた情報から取得しています。


    // 3. kintoneのレコード更新
    await kintoneClient.record.updateRecord({
        app: appId,
        id: recordId,
        record: {
          [KINTONE_TRANSCRIPTION_FIELD]: { value: correctedTranscription }, // 文字起こし結果
          [KINTONE_SUMMARY_FIELD]: { value: summary }, // 要約結果
        },
    });

    // ... (後略: kintoneに処理完了のコメントを投稿)
};

処理が終わったら、使用したGCSのファイルなどをクリーンアップします。

工夫した点

  • 非同期処理と状態管理
    音声認識は時間がかかるため、API Gatewayのタイムアウト(29秒)には到底収まりません。 そのため、処理を「リクエスト受付」と「結果処理」に分離し、その間をDynamoDBとPub/Subで繋ぐ構成にしました。これにより、AWS Lambdaの実行時間制限(15分)の中でも効率よく処理を回せます。

  • Geminiによる「2段階加工」
    Speech-to-Textの精度は向上していますが、それでも「えーっと」などのフィラーや、同音異義語の誤変換は発生します。 いきなり要約させても最近のLLMは良い感じにしてくれますが、会話のやりとりも確認したいので一度Geminiに「自然な会話文への修正」を依頼しています。その綺麗なテキストを元に「要約」させることで、最終的なアウトプットの質も高まっているのではと思います。

まとめ

kintoneとクラウドサービスを連携させることで、これまで手作業で行っていた「文字起こし」と「要約作成」を、自然な業務の流れの中に自動化することができました。

特にGeminiのようなLLMを間に挟むことで、単なる文字起こし以上の付加価値(要約、ToDo抽出など)をkintoneレコードに直接戻せるのが非常に便利です。

ぜひ試してみてください。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?