【JavaScript】ブラウザーで OpenAI API の音声認識 (Speech to Text) でマイク音声を録音してアップロードする方法

前回は「ブラウザーで OpenAI の音声合成 (Text to speech) のダウンロード再生とストリーム再生する方法」を解説しましたが、今回はその逆で、音声ファイルをアップロード、または、マイクから録音した音声をそのままアップロードして、OpenAI API の音声認識 (Speech to Text) を使ってテキスト化する方法を解説します。

OpenAI API の音声認識 (Speech to Text) の概要

OpenAI の 音声認識の REST API の詳細は、API reference の [Audio > Create transcription] に記載されています。curl コマンドであれば次のようにして音声ファイルをアップロードして、その解析結果をテキストで取得することができます。

curl https://api.openai.com/v1/audio/transcriptions \
  -H "Authorization: Bearer sk-axcuw8EQwBnPVLexeKj83G9RblPqWG3qwalWvmdeLoFng2py" \
  -H "Content-Type: multipart/form-data" \
  -F file="@./speech.mp3" \
  -F model="whisper-1" \
  -F language="ja" \
  -F response_format="json"

Authorization ヘッダーの値にある sk-axcuw8EQwBnPVLexeKj83G9RblPqWG3qwalWvmdeLoFng2py の部分は API キーです(この値はこの記事のためにランダムに作った文字列ですので API キーとしては利用できません)。

この curl コマンドは、カレントディレクトリにある speech.mp3 という音声ファイルを OpenAI API のエンドポイントにアップロードします。

パラメータについていくつか解説します。まず、Content-Type"multipart/form-data" です。これは HTML の form タグでファイルをアップロードする場合と同じタイプです。

パラメータ file はアップロードしたい音声ファイルを指しており、ファイルのパスを指定します。指定可能な音声ファイルフォーマットは flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, webm です。

パラメータ mode は音声認識モデルを表しており、現状は "whisper-1" で固定です。パラメータ language は言語の指定で必須パラメータではありませんが、指定したほうが精度が高くなります。なお、この値は "ja" のように言語コードしか指定することができません。 "ja-JP" のように国コードの指定はできませんので注意してください。

パラメータ response_format は応答のフォーマットを指します。デフォルトは "json" ですが、それ以外に "text", "srt", "verbose_json", "vtt" を指定することができます。

実際に上記 curl コマンドで「本日の天気予報です。」という音声ファイルを認識させると、次のような応答が返ってきます。

response_format="json"

{
  "text": "本日の天気予報です。"
}

response_format=”text”

本日の天気予報です。

response_format="srt"

1
00:00:00,000 --> 00:00:02,000
本日の天気予報です。

response_format="verbose_json"

{
  "task": "transcribe",
  "language": "japanese",
  "duration": 1.840000033378601,
  "text": "本日の天気予報です。",
  "segments": [
    {
      "id": 0,
      "seek": 0,
      "start": 0.0,
      "end": 2.0,
      "text": "本日の天気予報です。",
      "tokens": [
        50364,
        8802,
        6890,
        2972,
        6135,
        25870,
        1369,
        230,
        17803,
        4767,
        1543,
        50464
      ],
      "temperature": 0.0,
      "avg_logprob": -0.2016688734292984,
      "compression_ratio": 0.7317073345184326,
      "no_speech_prob": 0.021211111918091774
    }
  ]
}

response_format="vtt"

WEBVTT

00:00:00.000 --> 00:00:02.000
本日の天気予報です。

音声ファイルをアップロードして結果を取得する方法

まずはシンプルにブラウザー上で音声ファイルを受け付け、それを OpenAI API に投げて認識結果を得る方法を見ていきましょう。

次のサンプルでは、HTML に音声ファイルを受け付ける input 要素と送信ボタンの button 要素がマークアップされています。form 要素をマークアップしてフォーム送信しても良いのですが、そのような使い方をするケースはないでしょうから、ここでは fetch を使ってリクエストします。

<input id="afile" type="file" accept="audio/*">
<button type="button" id="btn">送信</button>
const btn_el = document.getElementById('btn');
const afile_el = document.getElementById('afile');

btn_el.addEventListener('click', async (event) => {
    // OpenAI API にリクエストするための FormData オブジェクトを生成
    var fd = new FormData();
    fd.append('file', afile_el.files[0]);
    fd.append('model', 'whisper-1');
    fd.append('language', 'ja');
    fd.append('response_format', 'json');

    // fetch で OpenAI API にリクエスト
    const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
        method: 'POST',
        headers: {
            'Authorization': 'Bearer sk-axcuw8EQwBnPVLexeKj83G9RblPqWG3qwalWvmdeLoFng2py'
        },
        body: fd
    });

    // レスポンスボディをテキストとして取り出す
    const rdata = await response.text();
    console.log(rdata);
});

特に難しい点はないのですが、fetch の使い方で 1 点注意が必要です。curl では明示的に Content-Type"multipart/form-data" を指定しましたが、fetch では指定してはいけません。

fetch はファイルアップロードと認識すると、自動的に Content-Type"multipart/form-data" だとして処理します。実際にサーバーにリクエストする際には、次のように boundary が追加されます。次のヘッダーは実際に Chrome で試した結果です。

multipart/form-data; boundary=----WebKitFormBoundarywI4sNqf7XZyB6YlS

もし fetch で明示的にリクエストヘッダーの Content-Type"multipart/form-data" を指定してしまうと、それが優先されてしまい、boundary が欠落した状態でサーバーにリクエストされてしまいます。これは fetch でファイルをアップロードする場合にうまくいかない良くある落とし穴ですので注意しましょう。

もし、OpenAI API の API キーをお持ちなら、次のサンプルを試してください。音声を録音した mp3 ファイルを選択して送信ボタンを押すと、その認識結果が表示されます。もし音声ファイルがすぐに用意できないようでしたら、こちらのサンプルをご利用ください。

[サンプルの mp3 ファイル]

OpenAI の API キーはこのサイトのサーバーでは一切保存していませんのでご安心ください。とはいえ、すでに利用中の API キーを使いまわすことはしないでください。OpenAI の API キーを新たに生成し、ここで使った後、破棄 (revoke) してください。

動画の文字起こし

OpenAI API にアップロードできる音声ファイルフォーマットに動画でも使われるフォーマットが含まれていたことにお気づきでしょうか。mp4, mpeg, webm が該当します。実は、OpenAI API の 音声認識 (Speech to Text) は動画ファイルも受け付けます。

そして、レスポンスデータのフォーマットに vtt が指定できる点を忘れてはいけません。vtt とは 正式には WebVTT (The Web Video Text Tracks Format) と呼ばれる字幕フォーマットのことで、W3C で策定されたビデオ字幕フォーマットです。この字幕フォーマットは HTML の video 要素と track 要素を組み合わせることで、ビデオに字幕を表示することができます。

実際にビデオをアップロードして、vtt ファイルを取得し、それを再生してみましょう。

<input id="vfile" type="file" accept="video/*">
<button type="button" id="btn">送信</button>
<video id="video" controls></video>
const btn_el = document.getElementById('btn');
const vfile_el = document.getElementById('vfile');
const video_el = document.getElementById('video');

// 動画ファイルが選択されたら video 要素にセットして確認できるようにする
vfile_el.addEventListener('change', (event) => {
    const file = event.currentTarget.files[0];
    video_el.src = URL.createObjectURL(file);
});

// 送信ボタンが押されたときの処理
btn_el.addEventListener('click', async (event) => {
    // OpenAI API にリクエストするための FormData オブジェクトを生成
    var fd = new FormData();
    fd.append('file', vfile_el.files[0]);
    fd.append('model', 'whisper-1');
    fd.append('language', 'ja');
    fd.append('response_format', 'vtt');

    // fetch で OpenAI API にリクエスト
    const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
        method: 'POST',
        headers: {
            'Authorization': 'Bearer sk-axcuw8EQwBnPVLexeKj83G9RblPqWG3qwalWvmdeLoFng2py'
        },
        body: fd
    });

    // レスポンスの VTT データを取り出す
    const vtt = await response.text();

    // VTT から Blob オブジェクトを生成
    const blob = new Blob([vtt], { type: "text/plain; charset=utf-8" });

    // track 要素を生成
    const track_el = document.createElement('track');
    track_el.srclang = 'ja-JP';
    track_el.kind = 'captions';
    track_el.default = true;

    // track 要素の src 属性に Object URL をセット
    track_el.src = URL.createObjectURL(blob);

    // track 要素を video 要素に追加
    video_el.innerHTML = '';
    video_el.append(track_el);

    // 字幕の準備ができたら動画を再生
    video_el.play();
});

このサンプルはあくまでも実証という意味で用意したものです。実際は使い物にはなりません。なぜなら、OpenAI API でアップロードできるファイルのサイズは 25MB に制限されているからです。もし動画の文字起こしをしたいなら、動画から音声だけを切り出して利用するのが現実的でしょう。

もし、OpenAI API の API キーをお持ちなら、次のサンプルを試してください。mp4 の動画ファイルを選択して送信ボタンを押すと、字幕付きの動画として再生します。

マイクロフォンから録音してアップロードする方法

マイクロフォンから音声を収録するには、Media Capture and Streams APIMediaStream Recording API を組み合わせます。

まず、マイクロフォンから音声を収録するには、getUserMedia() メソッドを使います。

// マイクロフォンから音声を取り込む MediaStream オブジェクトを生成
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });

この時点で、ブラウザーから許可ダイアログが表示されるはずです。

許可すると、マイクロフォンからの音声取得が始まり、マイクロフォンから得られる音声のストリームを表す MediaStream オブジェクト (上記コードでは変数 stream) が得られます。そして、その MediaStream オブジェクトを使って、MediaRecorder コンストラクタから MediaRecorder オブジェクト (以下のコードでは変数 recorder) を生成します。

// マイクロフォンからの音声を録音する MediaRecorder オブジェクトを生成
const recorder = new MediaRecorder(stream);

MediaRecorder オブジェクトが得られたら、start() メソッドで録音を開始します。

// 録音を開始
// - 1 秒ごとに Blob オブジェクトを生成する (Safari 対策)
recorder.start(1000);

start() メソッドに引数として 1000 を引き渡している点に注目してください。実は、本来はこれは不要なのですが、Safari では期待通りに動作しないため、意図的に 1000 を引き渡しています(おそらく他の数値でも大丈夫と思われます。)。この引数の意味は次に解説します。

MediaRecorder オブジェクトで録音が開始されると、録音データを表すバイナリデータの準備ができると、MediaRecorder オブジェクトで dataavailable イベントが発生します。このイベントのハンドラ ondataavailable プロパティにコールバック関数をセットして、バイナリデータを蓄積しておきます。このバイナリデータは Blob オブジェクトです。

const chunks = [];
recorder.ondataavailable = async (event) => {
    const blob = event.data;
    chunks.push(blob);
};

前述の start() メソッドに引数 1000 を指定しましたが、これは録音データをどの長さで分割するのかを指定しています。1000 は 1000 ミリ秒を表し、1 秒ごとに ondataavailable プロパティにセットしたコールバック関数が呼び出され、1 秒間に相当する録音データの Blob オブジェクトが得られることになります。

ここで得られる Blob オブジェクトの type プロパティの値 (MIME タイプ) を見ると、どのブラウザーがどのコンテナおよびコーデックで録音データを生成しているのかが分かります。筆者が調べたところ下表のとおりになりました。

ブラウザーtype プロパティの値
Chromeaudio/webm;codecs=opus
Edgeaudio/webm;codecs=opus
Firefoxaudio/ogg; codecs=opus
Safariaudio/mp4

以上で録音の開始から、録音データの蓄積までが終わりました。次は録音の終了処理です。録音の終了は MediaRecorder オブジェクト (下記コードの変数 recorder) の stop() メソッドを使います。しかしこのメソッドを呼び出した瞬間に録音が終了されるわけではないため、stop イベントの発生を待ち受けます。以下のコードでは、このイベントの待ち受けに onstop イベントハンドラを使っています。

// 録音を停止する
recorder.stop();

// MediaRecorder オブジェクトで stop イベントが発生したときの処理
recorder.onstop = async () => {
    // 録音データの blob オブジェクトのリストを 1 つの File オブジェクトに変換
    // - ファイル名は何でも良いが、Safari 対策のため .mp4 のファイル名を付けておく
    const file = new File(chunks, 'dummy.mp4');

    // OpenAI API にリクエストするための FormData オブジェクトを生成
    var fd = new FormData();
    fd.append('file', file);
    fd.append('model', 'whisper-1');
    fd.append('language', 'ja');
    fd.append('response_format', 'text');

    // fetch で OpenAI API にリクエスト
    const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
        method: 'POST',
        headers: {
            'Authorization': 'Bearer sk-axcuw8EQwBnPVLexeKj83G9RblPqWG3qwalWvmdeLoFng2py'
        },
        body: fd
    });

    // レスポンスボディをテキストとして取り出す
    const rdata = await response.text();
    console.log(rdata);
};

onstop イベントハンドラにセットしたコールバック関数では、まず、録音データを表す Blob オブジェクトの配列 (上記コードでは変数 chunks) から、それらをマージした File オブジェクトを生成します。

File コンストラクタから File オブジェクトを生成する際には、第二引数にファイル名を指定します。しかし、Safari 以外のブラウザーの場合、仮に音声フォーマットと異なる拡張子のファイル名を指定しても動作します。しかし、Safari では明示的に .mp4 のファイル名を指定しないといけませんので、ここでは固定で "dummy.mp4" を指定しています。

File オブジェクトが生成できたら、fetch で OpenAI API にリクエストして完了です。

もし、OpenAI API の API キーをお持ちなら、次のサンプルを試してください。録音ボタン(赤いボタン)を押すと録音を開始します。再度、ボタンを押すと録音を終了し、OpenAI API にリクエストを送信し、結果を表示します。

まとめ

マイクロフォンから音声を録音してファイルデータにしてリクエストする過程で、Safari にいくつかのクセがありましたが、なんとかメジャーブラウザーすべてので動作したことにはホッとしました。しかし、実際にこれを会議の文字起こしなどに使おうとすると無理が出てきます。

会議の文字起こしなどでは、一定期間でうまく区切って文字起こししないといけません。強制的に区切ってしまうと、区切った前後で音声認識が欠落してまいます。実際に OpenAI API のドキュメントでも注意喚起しています。うまく会話が途切れたタイミングで区切る必要がありますが、ウェブブラウザーだけでは簡単ではなさそうです。今は良い方法を思いつきませんが、何か良い方法が見つかったら記事にしようと思います。

今回は以上で終わりです。最後まで読んでくださりありがとうございました。それでは次回の記事までごきげんよう。

Share