logo

20241017

ファイル保存なしでWhisper APIにLINE Messaging APIから音声を渡す

LINE Messaging API でユーザーから送られてきた音声を OpenAI の Whisper API に渡す方法はググるとたくさん出てきますが、私が見た範囲ではどれもサーバー内に一時ファイルを保存しているものだったので、ファイル保存なしでメモリ上でなんとかする方法を探してみました。

想定読者

  • LINE Messaging API の使い方を知っている人
    • LINE ボットの作り方の解説は省きます
  • 一時ファイルの保存はしたくないというこだわりがある人

環境

  • Python 3.11.10
  • line-bot-sdk 3.11.0
  • openai 1.35.15

解説

LINE Messaging API から音声を取得

from linebot import LineBotApi, WebhookHandler

line_bot_api = LineBotApi(...)
handler = WebhookHandler(...)

@handler.add(MessageEvent, message=AudioMessage)
def message_audio(event):
    if event.message.content_provider.type != "line":
        return
    # 音声データの取得
    content = line_bot_api.get_message_content(event.message.id)
    transcription = transcribe(content.content)
    ...

content.contentbytes の実データ、content.content_type が content type です。

API リファレンス

SDK

OpenAI の Whisper API で文字起こし

from io import BytesIO
from openai import OpenAI

def transcribe(audio: bytes):
    client = OpenAI()
    audio_bytes = BytesIO(audio)
    audio_bytes.name = "audio.m4a"  # ここがミソ
    transcription = client.audio.transcriptions.create(
        model="whisper-1", file=audio_bytes
    )

調べたところ、OpenAI の Whisper API は拡張子でファイルのフォーマットを判定しているようでした。なのでいったん BytesIO に変換してファイル名を付与することで解決できます。Android と iPhone の LINE アプリ両方から録音データを送ってみましたがどっちも m4a だったのでハードコードしちゃいましたが、mp3 とかがアップロードされる場合もあるかと思うので本当は content type から拡張子を出し分けるといいんじゃないかと思います。

ダメだった例1: bytes をそのまま渡す

def transcribe(audio: bytes):
    transcription = client.audio.transcriptions.create(
        model="whisper-1", file=audio
    )

ログ消えちゃったけど、この投稿 と同じ "Unrecognized file format. Supported formats: ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm']" っていうエラーが返ったはず。

ダメだった例2: content type と一緒に bytes を渡す

Transcriptions.create() の引数の型定義的にいけるのかなと思って試したけどだめだった。

以下がその型定義ですが、3つめのパターンでいけるかも、と思って

FileTypes = Union[
    # file (or bytes)
    FileContent,
    # (filename, file (or bytes))
    Tuple[Optional[str], FileContent],
    # (filename, file (or bytes), content_type)
    Tuple[Optional[str], FileContent, Optional[str]],
    # (filename, file (or bytes), content_type, headers)
    Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]],
]

https://github.com/openai/openai-python/blob/v1.35.15/src/openai/_types.py#L49-L58

タプルの0番目のファイル名は None、1番目は bytes、2番目に LINE から取得した content.content_type を渡してみましたが Value error, Expected UploadFile, received: <class 'str'> というエラーになってしまい、レスポンスの input フィールドには文字列化されたバイナリデータが入っていました( \x00\x00\x00\x18ftypmp42\x00\x00\x00\x00isommp42\x00\x00\x03Ymoov... )。

def transcribe(audio: bytes, content_type: str):
    transcription = client.audio.transcriptions.create(
        model="whisper-1", file=(None, audio, content_type)
    )

参考文献