OpenAIのWhisper APIとGPT-4を使って､音声データから文字起こしと要約を作成する

# はじめに

会議の映像や音声データから文字起こしや要約､議事録を作成する方法を探していたところ､以下の記事がとても参考になりました｡OpenAIのWhisper APIを利用して文字起こしし､出力されたものをGPT-4に渡して要約などを行えばよさそうです｡

https://dev.classmethod.jp/articles/openai-api-whisper-about-data-limit/

# 参考情報

https://dev.classmethod.jp/articles/openai-api-whisper-about-data-limit/

https://www.datacamp.com/tutorial/converting-speech-to-text-with-the-openAI-whisper-API

https://platform.openai.com/docs/guides/speech-to-text

https://www.datacamp.com/tutorial/converting-speech-to-text-with-the-openAI-whisper-API

https://r-tech14.com/whisper-api-transcribe/

https://qiita.com/samezi/items/7ed2c1e57213bdcfa707

https://qiita.com/cardene/items/9415a1fbf7274b411ce6

https://qiita.com/sakasegawa/items/3855472a8566ea302a99

https://qiita.com/kakkunman/items/def562b26afa9e69fdeb

https://happy-nap.hatenablog.com/entry/2023/02/11/220235

https://note.com/npaka/n/nda64330358f8

https://github.com/victorGPT/Transcriptify/blob/main/Transcriptify.py

# 環境構築

## 環境
```console
$ python3 -V
Python 3.10.12
```
```
$ ffmpeg -version
ffmpeg version 4.4.2-0ubuntu0.22.04.1 Copyright (c) 2000-2021 the FFmpeg developers
```

## 必要なPythonパッケージのインストール

```console
$ pip install --upgrade pip
$ pip install openai pydub
```

インストールされた主なライブラリは以下のとおりです｡
```console
$ pip freeze | grep -e "openai" -e "pydub"
openai==0.27.8
pydub==0.25.1
```

# Pythonコード

OpenAI APIキーは､環境変数 `OPENAI_API_KEY`で定義します｡

```python:transcriptin.py
import datetime
import logging
import math
import os
import pathlib
import subprocess
import sys
import tempfile

import openai
from pydub import AudioSegment

logging.basicConfig(stream=sys.stdout, level=logging.INFO, force=True)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

openai.api_key = os.getenv("OPENAI_API_KEY")

# target file size = 25MB
TARGET_FILE_SIZE = 25000000
TEMPERATURE = 0

SYSTEM_PROMPT = """
    レポート文章に見出しをつけたり、箇条書きにして議事録を作成して下さい｡
    """
TRANSCRIPT_PROMPT = """
    こんにちは。今日は、いいお天気ですね。
    """
current_time_str = datetime.datetime.now().strftime("%Y%m%d%H%M%S")


# Extract the audio source, and write it into a file.
def extract_audio_from_media(original_file_path):
    logging.debug("original_file_path: %s", original_file_path)

    with tempfile.NamedTemporaryFile(suffix=original_file_path.suffix) as audio_file:
        subprocess.run(
            [
                "ffmpeg",
                "-y",
                "-i",
                str(original_file_path),
                "-codec:a",
                "copy",
                "-vn",
                audio_file.name,
            ],
            check=False,
        )

        audio_file_size = pathlib.Path(audio_file.name).stat().st_size

        logging.debug("audio_file_size: %s", audio_file_size)

        if audio_file_size > TARGET_FILE_SIZE:
            logging.info("This file need to be converted.")

            # break audio file up into 25MB.
            audio_segment = AudioSegment.from_file(str(audio_file.name))
            audio_length_sec = len(audio_segment) / 1000
            target_kbps = int(
                math.floor(TARGET_FILE_SIZE * 8 / audio_length_sec / 1000 * 0.95)
            )

            logging.debug("target_kbps: %s", target_kbps)

            if target_kbps < 8:
                assert f"{target_kbps=} is not supported"

            with tempfile.NamedTemporaryFile(suffix=".mp4") as converted_file:
                subprocess.run(
                    [
                        "ffmpeg",
                        "-y",
                        "-i",
                        str(audio_file.name),
                        "-codec:a",
                        "aac",
                        "-ar",
                        "16000",
                        "-ac",
                        "1",
                        "-b:a",
                        f"{target_kbps}k",
                        converted_file.name,
                    ],
                    check=False,
                )
                converted_file_size = pathlib.Path(converted_file.name).stat().st_size

                logging.debug("converted_file_size: %s", converted_file_size)

                with open(converted_file.name, "rb") as file:
                    generate_transcript(file)
        else:
            with open(audio_file.name, "rb") as file:
                generate_transcript(file)


def generate_transcript(audio_file):
    logging.debug("audio_file: %s", audio_file)

    os.makedirs("./data", exist_ok=True)
    output_file = pathlib.Path(f"./data/transcription_{current_time_str}.txt")

    transcript = openai.Audio.transcribe(
        file=audio_file,
        model="whisper-1",
        # テキスト形式で出力したい場合は､"text"を指定する｡
        response_format="json",
        # 入力言語が日本語の場合｡英語の場合は､"en"を指定する｡
        language="ja",
        prompt=TRANSCRIPT_PROMPT,
    )

    transcript_result = transcript["text"]

    with open(output_file, "wt", encoding="utf-8") as file:
        file.write(str(transcript_result))

    response = openai.ChatCompletion.create(
        model="gpt-4",
        temperature=TEMPERATURE,
        messages=[
            {
                "role": "system",
                "content": SYSTEM_PROMPT,
            },
            {
                "role": "user",
                "content": transcript_result,
            },
        ],
        request_timeout=20,
    )
    response_message = response["choices"][0]["message"]["content"]
    output_file = pathlib.Path(f"./transcription_summary_{current_time_str}.txt")

    with open(output_file, "wt", encoding="utf-8") as file:
        file.write(response_message)


def main():
    # 映像データ
    original_file_path = pathlib.Path("sample.mp4")
    extract_audio_from_media(original_file_path)


if __name__ == "__main__":
    main()
```

## 実行例

Pythonファイルと同じディレクトリ内に動画ファイル(sample.mp4)を配置し､以下の様に実行します｡

```
python3 ./transcriptin.py
```

`./data`が自動作成され､ここに文字起こし結果が出力されます｡出力ファイル名は`transcription_20230821203508.txt`という形式です｡
GTP-4が処理した結果は､Pythonファイルと同じディレクトリ内に出力されます｡出力ファイル名は`transcription_summary_20230821203603.txt`という形式です｡

## プロンプト

要約や議事録作成など､出力したい内容にあわせてSYSTEM_PROMPTを書き換えます｡

## コードについて

- [クラスメソッドさんの記事](https://dev.classmethod.jp/articles/openai-api-whisper-about-data-limit/)をもとに､動画ファイルから音声データの抜き出しとWhisper APIを使った文字起こしを行います｡
- [OpenAI APIのドキュメント 'Speech to text'内のImproving reliability](https://platform.openai.com/docs/guides/speech-to-text/improving-reliability)に書かれている`Post-processing with GPT-4`を参考に､文字起こししたテキストデータをGPT-4に渡し､要約や議事録を出力します｡

文字起こし結果をテキストファイルに出力していますが､中間出力が不要であればこのように直接GPT-4に渡すこともできます｡

```python
    response = openai.ChatCompletion.create(
        model="gpt-4",
        temperature=TEMPERATURE,
        messages=[
            {
                "role": "system",
                "content": SYSTEM_PROMPT,
            },
            {
                "role": "user",
                "content": openai.Audio.transcribe(
                    file=audio_file,
                    model="whisper-1",
                    response_format="text",
                    language="ja",
                    prompt=TRANSCRIPT_PROMPT,
                ),
            },
        ],
    )
```



# 文字起こしの品質向上

[OpenAI APIのドキュメント 'Speech to text'内のPrompting](https://platform.openai.com/docs/guides/speech-to-text/prompting) によると､モデルが文字起こしにおいて句読点を読み飛ばすことがあるそうです｡句読点を含む簡単なプロンプトを使用することでこれを避けることができるそうです｡コード内では`こんにちは。今日は、いいお天気ですね。`というプロンプトを指定しています｡同様に､`ええと`や`あのー`などのフィラー語を書き起こしたい場合は､フィラー語を含む簡単なプロンプトを追記するとよいそうです｡

例:
```text
TRANSCRIPT_PROMPT = """
    こんにちは。今日は、いいお天気ですね。ええと｡あのー｡はいはい｡
"""
```

また､固有名詞やうまく書き起こせなかった語についてもプロンプトに追加することで文字起こしの品質が改善されるそうです｡(例えば､GPT-3という語がGDP 3と出力される場合は､プロンプトに`GTP-3についての議論です｡`などと追記するとよさそうです｡

例:
```text
TRANSCRIPT_PROMPT = """
    こんにちは。今日は、いいお天気ですね。ええと｡あのー｡これは､GTP-3についての議論です｡
"""
```

# 長時間の音声データ

音声データのファイルサイズが25MBを超える場合､[OpenAI APIのドキュメント 'Speech to text'内のLonger inputs](https://platform.openai.com/docs/guides/speech-to-text/longer-inputs)にあるように[PyDub](https://github.com/jiaaro/pydub)をつかったファイル分割や､圧縮オーディオフォーマットの使用､ビットレートの設定変更が必要です｡

元ネタのこの記事にコード内で使用しているビットレートの計算を参考に適切な値を算出するか､

https://dev.classmethod.jp/articles/openai-api-whisper-about-data-limit/

それでも25MBを超えてしまう場合は､[Transcriptify
/Transcriptify.py](https://github.com/victorGPT/Transcriptify/blob/main/Transcriptify.py#L46)にあるようにビットレートを32kbpsにすることでファイルサイズを大幅に小さくできます｡ただし､音質が落ちるので文字起こしの精度も落ちるかもしれません｡､
試しに1時間ほどの映像データから音声を抜き出したところ､約50MBでした｡そのため､ビットレートを32kbpsに設定したところ､約15MBまで小さくなりWhisper APIに渡すことができました｡

# トークン数

GPT-4を指定しているため､扱えるトークン数の上限は8,192です｡そのため､問い合わせと応答のトークン数の合計が上限を超える場合はエラーとなります｡コストや用途を考えると gpt-3.5-turbo-16k (最大トークン数 16,384)を利用した方がメリットがありそうです｡
上記のコードでは､`model="gpt-4"`を`mode=gpt-3.5-turbo-16k`に変更することで切り替えできます｡