会議動画から議事録を自動生成する

# はじめに
音声を文字起こしする「[Whisper](https://openai.com/research/whisper)」というAPIがあることを知ったので、使ってみることにしました。
Whisperは、ChatGPTで有名なOpenAIが提供するAPIで、音声ファイルを文字起こしすることができます。
今回は、MP4形式の動画ファイルから議事録を作成するプログラムを作成します。
PCは、Intel Macです！
![議事録.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/626170/8d9d29bb-8602-5df9-dc17-8c31ea7a52d5.jpeg)


# 仮想環境
まず、仮想環境を構築しましょう。
```:ターミナル
$ python3 -m venv ~/venv
$ source ~/venv/bin/activate
(venv)$
```

# モジュールのインストール
次に、モジュールをインストールしましょう。
```:ターミナル
(venv)$ pip install openai
(venv)$ pip install moviepy
(venv)$ brew install ffmpeg
(venv)$ pip install pydub
(venv)$ pip install python-dotenv
```
openai : Whisper、ChatGPT APIを使うためにインストールします。
moviepy : MP4ファイルをMP3ファイルに変換するのに使用します。
pydub : MP3ファイルから音声を抽出するのに使用します。
ffmpeg : 「pydub」を使うのに必要です。Windowsですと、インストールに少し手間がかかります。
python-dotenv : 環境変数を管理するのに使用します。

# プログラム
では、プログラムを書いていきます。
ファイル名は、「mp4_to_minutes.py」とします。
## モジュールのインポート
まずは、モジュールをインポートしましょう。
```python:mp4_to_minutes.py
import os
import time
import openai
import moviepy.editor as mp
from pydub import AudioSegment
from dotenv import load_dotenv
```

## 環境変数
.envファイルを作成し、環境変数を設定しましょう。
```:.env
API_KEY = "OpenAIのAPIキー"
```
「mp4_to_minutes.py」で、環境変数を読み込みましょう。
```python:mp4_to_minutes.py
# .envの読み込み
load_dotenv()

# APIキーを設定
openai.api_key = os.getenv("API_KEY")
```

## MP4をMP3に変換する関数を作成
次に、MP4ファイルをMP3ファイルに変換する関数を書きましょう。
```python:mp4_to_minutes.py
#mp4をmp3に変換し、mp3のファイルパスを返す
def convert_mp4_to_mp3(mp4_file_path):
    mp3_file_path = os.path.splitext(mp4_file_path)[0] + '.mp3'
    audio = mp.AudioFileClip(mp4_file_path)
    audio.write_audiofile(mp3_file_path)
    return mp3_file_path
```

## MP3を文字起こしする関数を作成
次に、MP3ファイルから文字起こしする関数を書きましょう。
```python:mp4_to_minutes.py
#mp3ファイルを文字起こしし、テキストを返す
def transcribe_audio(mp3_file_path):
    with open(mp3_file_path, 'rb') as audio_file:
        transcription = openai.Audio.transcribe("whisper-1", audio_file, language='ja')

    return transcription.text
```
ここで、Whisperを使っています。
Whisperの返り値```transcription```から、文字起こししたテキストを取得するには、```transcription.text```とします。

## テキストをファイルとして保存する関数
次に、文字起こししたテキストや作成した議事録をテキストファイルとして保存したいので、その関数を書きましょう。
```python:mp4_to_minutes.py
#テキストを保存
def save_text_to_file(text, output_file_path):
    with open(output_file_path, 'w', encoding='utf-8') as f:
        f.write(text)
```

## MP3ファイルを分割する関数
次に、MP3ファイルを分割する関数を書きます。
WhisperとChatGPTには、一度に処理できる量には制限があります。
Whisperだと、音声ファイルの上限が25MBまで
ChatGPTには、4096トークンまでという制限があります。
これ以外にも、時間あたりのアクセス数も制限されています。
※今回のプログラムでは、無料プランではなく、クレカを登録した従課金制のプランにアップグレードされていることを前提としています。
このため、今回のプログラムでは、
1. 音声ファイルをチャンクに分割
1. チャンクごとに文字起こし（Whisper）
1. チャンクごとの文字起こしをそれぞれ要約（ChatGPT）
1. チャンクごとの要約をまとめてChatGPTに投げ、議事録を作成

という流れとします。
```python:mp4_to_minutes.py
#mp3ファイルを分割し、保存し、ファイルリストを返す
def split_audio(mp3_file_path, interval_ms, output_folder):
    audio = AudioSegment.from_file(mp3_file_path)
    file_name, ext = os.path.splitext(os.path.basename(mp3_file_path))

    mp3_file_path_list = []

    n_splits = len(audio) // interval_ms
    for i in range(n_splits + 1):
        #開始、終了時間
        start = i * interval_ms
        end = (i + 1) * interval_ms
        #分割
        split = audio[start:end]
        #出力ファイル名
        output_file_name = output_folder + os.path.splitext(mp3_file_path)[0] + "_" + str(i) + ".mp3"
        #出力
        split.export(output_file_name, format="mp3")

        #音声ファイルリストに追加
        mp3_file_path_list.append(output_file_name)

    #音声ファイルリストを出力
    return mp3_file_path_list
```

関数の定義はここまでです。

## MP4ファイルから、文字起こしまで
まずは、MP4ファイルのパスを設定します。
今回のプログラムでは、動画ファイルはルートディレクトリに配置することとします。
```python:mp4_to_minutes.py
mp4_file_path = "sample_long.mp4"
```

次に、用意した関数を用いて、MP4ファイルをMP3ファイルに変換します。
```python:mp4_to_minutes.py
mp3_file_path = convert_mp4_to_mp3(mp4_file_path)
```

次に、用意した関数を用いて、MP3ファイルを分割します。
分割した音声ファイルは```output```ディレクトリに出力します。
分割する間隔は、8分としました。
8分ですと、WhisperとChatGPTのどちらの制限にもかからなかったため、この間隔にしています。
```python:mp4_to_minutes.py
output_folder = "./output/"
interval_ms = 480_000 # 60秒 = 60_000ミリ秒

mp3_file_path_list = split_audio(mp3_file_path, interval_ms, output_folder)
```

次に、分割したチャンクごとに、文字起こしします。
文字起こしには、用意した関数を使用します。
文字起こししたテキストは、空のリスト```transcription_list```に格納していきます。
また、用意した関数を使用して、文字起こししたテキストをファイルに保存します。
```python:mp4_to_minutes.py
transcription_list = []
for mp3_file_path in mp3_file_path_list:
    transcription = transcribe_audio(mp3_file_path)

    transcription_list.append(transcription)

    output_file_path = os.path.splitext(mp3_file_path)[0] + '_transcription.txt'
```

## チャンクごとに要約
次に、チャンクごとに要約します。

要約には、ChatGPTを使用します。
プロンプトには、制約条件として、「誤字・脱字があるため、話の内容を予測して置き換えてください。」という文言を加えています。
音声ファイルから、Whisperを使って文字起こししているので、どうしても誤字・脱字が発生します。
これをうまい具合に処理して欲しいので、この文言を加えています。

要約したテキストは、```pre_summary```という空の文字列に追加していきます。
ここで、配列ではなく文字列としたのは、次の工程でチャンクごとの要約をまとめてChatGPTに投げたいからです。
配列に格納しても、結局ひと続きの文字列に変換する必要があります。

繰り返しの最後に、```time.sleep(60)```と書いてあります。
これは、時間あたりの制限にかからないようにする工夫です。
従課金制のプランにしても、1時間半の動画を入力した際に、制限に引っかかったことがったため、60秒の間隔を確保しています。
```python:mp4_to_minutes.py
pre_summary = ""
for transcription_part in transcription_list:
    prompt = """
    あなたは、プロの要約作成者です。
    以下の制約条件、内容を元に要点をまとめてください。

    # 制約条件
    ・要点をまとめ、簡潔に書いて下さい。
    ・誤字・脱字があるため、話の内容を予測して置き換えてください。

    # 内容
    """ + transcription_part

    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {'role': 'user', 'content': prompt}
        ],
        temperature=0.0,
    )

    pre_summary += response['choices'][0]['message']['content']

    time.sleep(60)
```

## 議事録を作成
次に、議事録を作成します。
ここでも、「誤字・脱字があるため、話の内容を予測して置き換えてください。」という文言を加え、誤字・脱字をいい感じに処理してもらうことを期待します。
また、「見やすいフォーマットにしてください。」という文言を加えることで、「会議名」「参加者」「議題」「日付」「議事録」などのパートに分けて議事録を生成してくれます。
```python:mp4_to_minutes.py
prompt = """
あなたは、プロの議事録作成者です。
以下の制約条件、内容を元に要点をまとめ、議事録を作成してください。

# 制約条件
・要点をまとめ、簡潔に書いて下さい。
・誤字・脱字があるため、話の内容を予測して置き換えてください。
・見やすいフォーマットにしてください。

# 内容
""" + pre_summary


print("議事録を作成中です...")
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
        {'role': 'user', 'content': prompt}
    ],
    temperature=0.0,
)
```

## 議事録を保存
最後に、生成された議事録を保存します。
```
output_file_path = os.path.splitext(mp4_file_path)[0] + '_mitunes.txt'
save_text_to_file(response['choices'][0]['message']['content'], output_file_path)
```
これでプログラムは終わりです。

# おわりに
ここまで読んでいただきありがとうございました。
本記事の内容が、どなたかの役に立つと嬉しいです！
いいね、ストックよろしくお願いします！