2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-08-17

はじめに

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

参考情報

環境構築

環境

$ 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パッケージのインストール

$ pip install --upgrade pip
$ pip install openai pydub

インストールされた主なライブラリは以下のとおりです。

$ pip freeze | grep -e "openai" -e "pydub"
openai==0.27.8
pydub==0.25.1

Pythonコード

OpenAI APIキーは、環境変数 OPENAI_API_KEYで定義します。

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を書き換えます。

コードについて

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

    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 によると、モデルが文字起こしにおいて句読点を読み飛ばすことがあるそうです。句読点を含む簡単なプロンプトを使用することでこれを避けることができるそうです。コード内ではこんにちは。今日は、いいお天気ですね。というプロンプトを指定しています。同様に、ええとあのーなどのフィラー語を書き起こしたい場合は、フィラー語を含む簡単なプロンプトを追記するとよいそうです。

例:

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

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

例:

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

長時間の音声データ

音声データのファイルサイズが25MBを超える場合、OpenAI APIのドキュメント 'Speech to text'内のLonger inputsにあるようにPyDubをつかったファイル分割や、圧縮オーディオフォーマットの使用、ビットレートの設定変更が必要です。

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

それでも25MBを超えてしまう場合は、Transcriptify
/Transcriptify.py
にあるようにビットレートを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に変更することで切り替えできます。

2
4
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
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?