🎤

【Whisper APIの20MB制限に注意!】Next.jsのAPIRoutes(Node.js)で音声ファイルを分割していく

2023/04/23に公開

OpenAI社のWhisperAPIでの文字起こしが話題ですね。とても便利のようでキャッチアップついてに私も以下のようなサービスを早朝ハッカソンで作って見たのですが
https://jedi-ai.vercel.app/

何点か課題を感じ、ガチのサービスに乗っけて提供する上で考えがまとまったので記事として書いて見ます

参考になった方、興味深いと思った方は是非フォロー&Likesをお願いします👍

では早速お話ししていきます。

WhisperAPIについて

https://openai.com/research/whisper
speech to textと言われているように、音声データを文字に変換してくれるAIです。かなりの精度の高さで、実際に使ってみるととてもびっくりします。良いと思う点を以下に何点か列挙して見ます

  • まじで高精度。実用化できる程度の精度です
  • 実装も他のOpenAIのAPIと同じでとても楽
    • inputから読み取ったデータを送るだけ、受け取るだけです
  • 値段がまじで安い(写真参考: 1分あたり0.006USD≒0.80円)
    • 値段

一方でいくつかの制約もあります。今回はその制約に注目してみました
その代表的なものは「20MB」制限です
20MBを超えるファイルは受け取ってもらえません。

しかし、長くなってしまった会議や、大学の講義(1コマ90分)とかをiPhoneのボイスメモアプリで録音していると、軽々と20MBを超えてきます

サービスとして提供すること等を考えると、20MB以上のファイルをユーザーが入力した場合には

  • 入力をお断りする
  • ファイル分割をする

のいずれかでやっていく必要がありますが、ファイル分割をして対応してあげた方が網羅できるユースケースが増えて良いと思います。

ではどうやってファイル分割するんだ?

結論から行くと ffmpeg というオープンソースを使うことで解決できます。iPhoneのボイスメモアプリで録音しているファイルはm4aという拡張子なのですが、これをmp3にも脳筋で変換してくれるので、特に難しいことをする必要はありません。

ステップ1. ffmpegを入れる

fluent-ffmpegおよび ffmpeg-static を入れて設定するファイルを /ffmpeg.ts として作成します

import ffmpeg from 'fluent-ffmpeg'
import pathToFfmpeg from 'ffmpeg-static'

ffmpeg.setFfmpegPath(pathToFfmpeg || '')

export default ffmpeg
export { pathToFfmpeg }

ステップ2. Node.jsで読み込む(Next.jsならAPIRoutesから読み出す)

import ffmpeg from '..../ffmpeg'

...
  const ffmpegCommand = ffmpeg()
    .input(inputFile)
    .saveToFile(outputFilePattern)
    .addOutputOption('-f segment')
    .addOutputOption('-segment_time 10')
    .on('error', err => {
      res.status(500).json({ data: [] })
    })
    .on('end', async () => {
      const files = fs.readdirSync(tmpDir)

      try {
        // fsなどで、ファイルを読み込みtmp以下に書き込むなどする
        res.status(200).json({ data: fileData })
      } catch (e) {
        res.status(500).json({ data: [] })
      } finally {
      	// fsなどでtmp以下のファイルを削除する
      }
    })

  await new Promise((resolve, reject) => {
    ffmpegCommand.on('end', resolve)
    ffmpegCommand.on('error', reject)
  })

上記2ステップになります。

最初に、 const ffmpegCommand = ffmpeg()は、ffmpegコマンドを実行するためのffmpegインスタンスを作成しています。

input(inputFile)は、分割するビデオファイルのパスを指定します。saveToFile(outputFilePattern)は、分割されたビデオファイルのファイル名パターンを指定します。 addOutputOption('-f segment')および addOutputOption('-segment_time 10')は、セグメントファイル形式でビデオファイルを分割し、10秒ごとに新しいセグメントを作成するためのffmpegオプションを指定します。

次に、 .on('error', err => {...})と .on('end', async () => {...})は、それぞれffmpegコマンドのエラーと完了を処理するためのイベントリスナーを定義しています。エラーが発生した場合、500ステータスコードを持つJSONレスポンスを返します。完了した場合、分割されたファイルが格納される一時ディレクトリからファイルを読み取り、それをJSONレスポンスとしてクライアントに返します。

最後に、 await new Promise(...)は、ffmpegコマンドの完了またはエラーのどちらかが発生するまで、Promiseを待機します。

以上がコードの解説です。

データのやりとり

データ自体はCloudStorageなどにmp3ファイルをアップし、ダウンロードリンクを通してクライアント・サーバー間でのやり取りをするのがいいと思います。ChatGPTくんに理由を説明させました

一般的に、40MBを超えるファイルをAPIで直接送受信することは避けた方が良いでしょう。その理由は、次のようなものがあります。

ファイルが大きいため、送受信に時間がかかり、APIのパフォーマンスが低下する可能性があります。
大きなファイルを直接送受信すると、ネットワーク接続の安定性によっては、ファイルの送信中に接続が切断された場合、完全なファイルの送信が保証されない可能性があります。
ファイルをAPIを介して送受信することは、APIサーバーのディスク容量を占有し、不必要な負荷をかける可能性があります。
そのため、ファイルの送受信にはCloud Storageなどの外部ストレージサービスを利用することをお勧めします。Cloud Storageを使用することで、APIサーバーから外部ストレージへのファイルの送受信が可能になります。これにより、APIサーバーのパフォーマンスが低下することがなくなり、ファイルの完全な送信が保証されます。また、外部ストレージサービスは通常、高い信頼性とスケーラビリティを持ち、安全性の高いファイルストレージが可能です。

最後に

オーディオ周りの処理はどうしても難しく感じてしまいますが、ffmpegを使えばとても楽に拡張子を変換することもできますし、ファイル分割も可能です。whisperAPIに関してのノウハウもある程度貯まってきたので、気が向いたら全ソースコードを公開したいと思います。

何か質問があれば、DMください
https://twitter.com/masakasuno1

Discussion