ChatGPTでGoogle Meetの録画ファイルからの議事録作成を自動化してみた

## TL;DR
- 出席していなかったミーティングの内容をざっくり知りたい。録画ファイルを全部見る時間はない。
- ミーティングの録画ファイル（MP4形式）を文字起こしして、ChatGPTで要約するPythonプログラムを作った。
- 即席プロンプトでもまぁまぁな精度のものが作れた。


## コーディング解説

### MP4ファイルをMP3ファイルに変換
まずはMP4ファイルをMP3の音声ファイルに変換します。
MP4ファイルはGoogleドライブからダウンロードして、ローカルに保存している前提です。
VideoFileClipライブラリを使いました。

```python
from moviepy.editor import VideoFileClip

def extract_audio(mp4_file_path, mp3_file_path):
    # MP4ファイルを読み込む
    video = VideoFileClip(mp4_file_path)
    # 音声部分を抽出し、MP3ファイルとして書き出す
    audio = video.audio
    audio.write_audiofile(mp3_file_path)
```

### MP3ファイルを分割
文字起こしにはOpenAIのWhisperを使うのですが、ファイルサイズ25MBの制限があります。
１時間の会議になると25MBは余裕で超えてしまうので、作成したMP3ファイルを分割して分割したファイルごとにWhisper APIに投げられるようにします。

MP3ファイルの分割にはpydubライブラリを使用しました。
少し余裕を持って20MBごとのファイルに分割するコードにしています。

```python
from pydub import AudioSegment
import math

def split_audio(file_path, size_mb=20):
    """
    指定された音声ファイルを指定されたサイズで分割する。
    :param file_path: 分割する音声ファイルのパス。
    :param size_mb: 分割するサイズ（MB）。
    :return: 分割されたファイルのパスのリスト。
    """
    audio = AudioSegment.from_file(file_path)
    file_size = os.path.getsize(file_path)
    num_parts = math.ceil(file_size / (size_mb * 1024 * 1024))
    length = len(audio)
    part_length = math.ceil(length / num_parts)
    parts = []

    for i in range(num_parts):
        start = i * part_length
        end = min((i + 1) * part_length, length)
        part = audio[start:end]
        part_file_path = f"part_{i + 1}.mp3"
        part.export(part_file_path, format="mp3")
        parts.append(part_file_path)

    return parts
```

### 文字起こし
MP3からの文字起こしはOpenAIのWhisper APIを使用しました。
分割したファイルごとにWhisper APIに投げて、文字起こし結果を連結してfinal_transcript.txtに書き出します。

```python
from openai import OpenAI
from dotenv import load_dotenv
import os

# OpenAIのAPIキー設定
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=OPENAI_API_KEY)

def convert_to_text(file_paths):
    """
    指定された音声ファイルをテキストに変換し、1つのファイルに出力する。
    :param file_paths: 変換する音声ファイルのパスのリスト。
    :return: None
    """
    with open("final_transcript.txt", "w") as final_text_file:
        for i, file_path in enumerate(file_paths):
            with open(file_path, "rb") as audio_file:
                transcript = client.audio.transcriptions.create(
                    model="whisper-1",  # モデル名は利用可能な最新のモデルに置き換えてください
                    file=audio_file,
                    response_format="text"
                )
                final_text_file.write(transcript + "\n")  # transcriptオブジェクトからテキストを抽出

```

### 議事録作成
文字起こししたファイルをインプットにして、ChatGPT APIを使用して要約文を取得します。
今回のコードではgpt-3.5-turboを使用していますが、トークンの上限が16,385と足りなくなる可能性があるので、その場合は適宜gpt-4-turbo（128,000 tokens）などに置き換えてください。

```py
def summarize_text(text):
    with open("config/prompt.txt", "r") as file:
        prompt = file.read()
    try:
        response = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[
                {
                    "role": "system",
                    "content": prompt
                },
                {
                    "role": "user",
                    "content": text
                }
            ],
            temperature=1,
            max_tokens=1500,
            top_p=1,
            frequency_penalty=0,
            presence_penalty=0
        )

        # 正しくレスポンスからテキストを抽出
        if response.choices:
            return response.choices[0].message.content
        else:
            return "No valid response received."
    except Exception as e:
        print(f"An error occurred: {e}")
        return "Error in processing the request."
```

要約文を作成するためのプロンプトはconfig/prompt.txtに外だしする仕様にしました。
今回使用したプロンプトは以下の通りです。

:::note
あなたはプロの編集者です。\n以下の内容を忠実に守り、以下の#入力文を編集して最高の議事録を作成してください。\n\n#前提条件\n以下の#入力文の内容は、会議の文字起こしデータです。\n\n#制約条件\n・ケバ取りを行ってください。\n・文脈として意味が不明な箇所は、文脈的に相応しいと合理的に推測される内容に修正、または削除してください。\n\n#出力形式\n・まず、会議で特に重要な内容について箇条書きで要旨を作成してください。\n・次に、議事録を作成してください。
 
:::

こちらを参考にさせていただきました。

https://www.goatman.co.jp/media/chatgpt/minutes-prompts/


## 実行結果
実際の会議動画を公開するわけにはいかないので、適当なYoutubeの討論動画を使用しました。

https://www.youtube.com/watch?v=wkd6-upXzLI&t=675s

こちらのYoutube動画をMP4ファイルダウンロードしたものを使用してみます。（MP4化は[こちら](URL "https://ja.savefrom.net/21-youtube-to-mp4.html")を使用）

出力された要約結果はこちら。かなり精度の高い議事録かと思います。

:::note 
### 会議の要旨
1. 弁護士西吉幸氏による黙秘の重要性に関するアドバイスがSNSで拡散されている。
2. 西氏は、取調べ時に黙秘するべきと主張し、黙秘権の利用が無実を守る手段であることを強調。
3. 元警察官の高野篤氏は、黙秘権が使われた場合、犯人が不利になることはないが、防御機会を逃すリスクがあると指摘。
4. 討論中、黙秘することが捜査機関に与える心理的影響と可能な法的後果について検討された。
5. 雑談や逸脱した会話は取り調べ中に応じるべきではないとされ、これに対する戦略的アプローチが提案された。

### 議事録内容
今回の議論では、弁護士の西吉幸氏による、取調べにおける黙秘権の適用とその重要性についてのSNS投稿が焦点とされました。西氏は取調べ時に黙秘すべき理由として、無実を守るためと黙秘が真実を保護する手段であることを挙げ、この戦略で発信しています。特に、黙秘権は憲法や刑事訴訟法により保護されており、被疑者が不利になることはないと説明しました。

一方で、元警察官の高野篤氏は警察の立場から、黙秘が必ずしも被疑者にプラスに働くとは限らず、有効に能動的に供述する機会を逃すことがあると指摘。これにより、状況に応じて黙秘権の利用が適切か否かを見極めることが重要との見解を示しました。

討論はさらに、被疑者が黙秘権を行使した際の捜査機関への影響や、警察側がよく使う心理戦について議論を深めました。ここで、雑談や無関係な話題を振り、被疑者を心理的に追い込む手法が取り上げられ、これにどのように対処すべきかについての戦略が検討されました。

最終的に、西氏と高野氏は、法執行機関とのやりとりにおいて被疑者が自身を守るためには、法的権利を知り、慎重に行動する必要があると共に、強調しました。また、弁護士と相談のもとで行動を決定することが、被疑者にとって最も安全なアプローチであることが確認されました。
:::




## まとめ
今回使用したサンプル動画は、みなさん発言が明確で、かつ議題に関する説明部分もあったので精度の高い結果となりました。
しかし、実際の会議のように複数人が特殊なコンテキストで議論している１時間くらいの動画だと精度はさがります。実際の会議動画（１時間くらい）でも試してみたのですが、５０−６０％くらいの出来といった感じでした。
この辺りは、まだ改善が必要そうです。


というわけで、今後の改善点としては以下を考えてます。
- 要約の精度を上げたい。具体的には、プロンプトの改善、事前ナレッジのインプットにより改善が見込めるはず。
- Google DriveへのリンクURLを入力して実行できるようにする。
- Slackへの自動投稿



## フルコード
最後にフルコードを記載しておきます。

```python
from openai import OpenAI
from pydub import AudioSegment
from dotenv import load_dotenv
import os
import math
from moviepy.editor import VideoFileClip


# OpenAIのAPIキー設定
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=OPENAI_API_KEY)


def extract_audio(mp4_file_path, mp3_file_path):
    # MP4ファイルを読み込む
    video = VideoFileClip(mp4_file_path)
    # 音声部分を抽出し、MP3ファイルとして書き出す
    audio = video.audio
    audio.write_audiofile(mp3_file_path)


def split_audio(file_path, size_mb=20):
    """
    指定された音声ファイルを指定されたサイズで分割する。
    :param file_path: 分割する音声ファイルのパス。
    :param size_mb: 分割するサイズ（MB）。
    :return: 分割されたファイルのパスのリスト。
    """
    audio = AudioSegment.from_file(file_path)
    file_size = os.path.getsize(file_path)
    num_parts = math.ceil(file_size / (size_mb * 1024 * 1024))
    length = len(audio)
    part_length = math.ceil(length / num_parts)
    parts = []

    for i in range(num_parts):
        start = i * part_length
        end = min((i + 1) * part_length, length)
        part = audio[start:end]
        part_file_path = f"part_{i + 1}.mp3"
        part.export(part_file_path, format="mp3")
        parts.append(part_file_path)

    return parts

def convert_to_text(file_paths):
    """
    指定された音声ファイルをテキストに変換し、1つのファイルに出力する。
    :param file_paths: 変換する音声ファイルのパスのリスト。
    :return: None
    """
    with open("final_transcript.txt", "w") as final_text_file:
        for i, file_path in enumerate(file_paths):
            with open(file_path, "rb") as audio_file:
                transcript = client.audio.transcriptions.create(
                    model="whisper-1",  # モデル名は利用可能な最新のモデルに置き換えてください
                    file=audio_file,
                    response_format="text"
                )
                final_text_file.write(transcript + "\n")  # transcriptオブジェクトからテキストを抽出

def summarize_text(text):
    with open("config/prompt.txt", "r") as file:
        prompt = file.read()
    try:
        response = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[
                {
                    "role": "system",
                    "content": prompt
                },
                {
                    "role": "user",
                    "content": text
                }
            ],
            temperature=1,
            max_tokens=1500,
            top_p=1,
            frequency_penalty=0,
            presence_penalty=0
        )

        # 正しくレスポンスからテキストを抽出
        if response.choices:
            return response.choices[0].message.content
        else:
            return "No valid response received."
    except Exception as e:
        print(f"An error occurred: {e}")
        return "Error in processing the request."
        
mp4_file_path = 'sample3.mp4' #会議の録画ファイル（MP4形式）を指定
mp3_file_path = 'sample_output.mp3'
extract_audio(mp4_file_path, mp3_file_path)

parts = split_audio(mp3_file_path, 20)
convert_to_text(parts)

with open("final_transcript.txt", "r") as file:
    full_text = file.read()

summary_text = summarize_text(full_text)
print(summary_text)
```