こんにちは、久しぶりの投稿です。
唐突ですが、最近議事メモをとるのが、かなりめんどくさく感じておりまして、勉強がてら自動で議事メモを作成するアプリを作成してみました。
アプリの主な実行順序としては以下の通りです。使用言語はpythonとなります。
実行手順
1.動画ファイルから音声データを抽出する。
2.音声データを適当な大きさのファイルに分割する。
3.分割したファイル各々に対して、AIを使って文字起こしを実施する。
4.文字起こししたファイルたちをAIを用いて要約し、議事メモのサマリを作成する。
いくつか前提事項です。
- 本アプリの入力を動画ファイルにしてます。これは近年ウェブ会議を実施する場合、ZoomやMicrosoft Teamsなど専用アプリで実施することが多く、その録画はmp4やwavなどの動画ファイルとして保存されることが多いためです。
- 使用するプログラミング言語はpythonになります。これは音声ファイルを操作したり生成AIとAPIでやり取りしたりするライブラリが充実しているためです。
以下順番に見ていきます。
0.全体構成
各論に入る前に全体の構成を以下に示しておきます。
.
├── README.md
├── activate_transcript_env.sh
├── create_env.sh
├── data
├── deactivate_transcript_env.sh
├── main.py
├── module
│ ├── application_service.py
│ ├── audio_operator
│ │ ├── audio_extractor.py
│ │ ├── audio_splitter.py
│ │ └── setup.py
│ ├── env_settings.py
│ ├── setup.py
│ ├── summarizer
│ │ ├── setup.py
│ │ ├── summarizer.py
│ │ ├── summarizer_factory.py
│ │ ├── text_summarizer_openai.py
│ │ └── text_summarizer_watson.py
│ └── transcriber
│ ├── setup.py
│ ├── speech_to_text.py
└── requirements.txt
なお、*.eggInfoや__pycache__などは省略しています。また
・activate_transcript_env.sh
・create_env.sh
・deactivate_transcript_env.sh
は本アプリを実行するための環境作成/アクティベートするもので、私個人の環境でpython実行する際の都合によるものなので、説明は割愛します。
いくつかコメントします。
- python main.pyを実行します。
- 次にapplication_service.pyが呼ばれ、各コンポーネントが実施されます。
コードは以下のイメージです。
import asyncio
from application_service import MeetingMinutesCreationService
service = MeetingMinutesCreationService()
asyncio.run(service.execute())
import asyncio
from audio_extractor import AudioExtractor
from audio_splitter import AudioSplitter
from summarizer_factory import SummarizerFactory
from speech_to_text import Transcriber
class MeetingMinutesCreationService():
# 対象動画ファイル指定
target_file = "./data/画面収録 2024-02-28 9.05.31.mov"
async def execute(self):
# 音声ファイル抽出
audio_extractor = AudioExtractor(self.target_file)
audio_file_name = audio_extractor.extract_audio()
# 音声ファイルの分割
splitter = AudioSplitter()
separated_files: list[str] = splitter.split_wav(audio_file_name)
# 音声→文字変換
transcriber = Transcriber()
transcribed_files = await transcriber.perform_transcription(separated_files)
# 文字列の要約
summarizer = SummarizerFactory.get_instance().create_summarizer_instance()
summarizer.summarize_text(transcribed_files)
端的にいえば、MeetingMinutesCreationServiceはDDD(ドメイン駆動設計)におけるアプリケーションサービスにあたるもので、フロー制御を行っているものとなり、詳細については各処理に委譲しています。
それでは各処理についてみていきます。
1. 動画ファイルから音声データを抽出する
本コンポーネントはこちらを参考にしています。
まずpythonで音声データを抽出するには、ffmpegというソフトをまずインストールします。ffmpegのインストールについてはこちらを参照してください。
(私はmacを使っているので、brewでインストールしました。)
というのもpythonで動画ファイルの編集をするにはmoviepyというpythonライブラリが必要なのですが、どうやらmoviepyの内部でffmpegを使用してるようであるためです。(ソース:こちら)
moviepyのインストールはpipでインストールできますが、今回はrequirement.txtで他のライブラリ含めて一括でインストールします:
pip install -r requirement.txt
moviepy
これで準備が整いました。早速pythonコードを以下に記します。
import datetime
from moviepy.editor import AudioFileClip
class AudioExtractor():
def __init__(self, video_file) -> None:
self.video_file = video_file
def extract_audio(self) -> str:
print("start extracting at ",datetime.datetime.now())
# 動画からAudioFileClipオブジェクトを生成
audio = AudioFileClip(self.video_file)
# .wavファイルとして保存
audio_file_name = self.video_file.replace('.mov', '.wav')
audio.write_audiofile(audio_file_name)
print("end extracting at ",datetime.datetime.now())
return audio_file_name
かなりシンプルなので、あまり説明を加える必要もないかもしれませんが、、
- 15行目「audio_file_name = self.video_file.replace(‘.mov’, ‘.wav’)」とある通り、動画ファイルはmovファイルにしてます。これはmacのQuickTime Playerで画面録画したファイルの拡張子はデフォルトでmovとなるからです。ここは各個人の環境に合わせていい感じに変更していただければと思います。wavについても同様特段深い意味はありません。
- extract_audioのメソッドでaudio_file_nameをreturnしてますが、これは後続処理に対象ファイル名を渡すためです。
2. 音声データを適当な大きさのファイルに分割する。
次に音声ファイルを分割します。本セクションはこちらを参考にしてます。
これは音声ファイルのサイズが大きすぎると生成AIのトークン上限に引っかかってしまうためです。
こちらで追加でインストールするライブラリは「scipy」です。
moviepy
scipy
早速コードです。
from typing import List
import wave
import struct
from scipy import int16
import numpy as np
import math
class AudioSplitter():
# 分割元ファイルパラメータ
ch: int
width: int
fr: int
fn: int
total_time: float
integer: int
frames: int
num_cut:int
data: bytes
t: int = 500 # 分割単位:sec
def split_wav(self, input_file):
self.__read_file(input_file)
out_files = self.__split_file(input_file)
print("split files = ", out_files)
return out_files
def __read_file(self, input_file) -> None:
wr = wave.open(input_file, "r") # targetファイル読み込み
# wav情報を取得
self.ch = wr.getnchannels()
self.width = wr.getsampwidth()
self.fr = wr.getframerate()
self.fn = wr.getnframes()
self.total_time = 1.0 * self.fn / self.fr
self.integer = math.floor(self.total_time)
self.frames = int(self.ch * self.fr * self.t)
# 小数点切り上げ(1分に満たない最後のシーンを出力するため)
self.num_cut = int(math.ceil(self.integer / self.t))
self.data = wr.readframes(wr.getnframes())
wr.close()
def __split_file(self, input_file) -> List[str]:
out_files: List[str] = []
X = np.frombuffer(self.data, dtype=int16)
for i in range(self.num_cut):
outf = input_file + str(i) + ".wav"
start_cut = int(i * self.frames)
end_cut = int(i * self.frames + self.frames)
Y = X[start_cut:end_cut]
outd = struct.pack("h" * len(Y), *Y)
# 書き出し
ww = wave.open(outf, "w")
ww.setnchannels(self.ch)
ww.setsampwidth(self.width)
ww.setframerate(self.fr)
ww.writeframes(outd)
ww.close()
# 書き出したファイルをout_fileリストに追加
out_files.append(outf)
return out_files
分割単位は一旦500秒としています。その他は特に特筆するところはありません。。
クラスにしているのは、moduleに分割し再利用しやすくするためというのもありますが、私個人が普段JavaやTypescriptを使っている関係で趣味レベルでクラスにしたいだけでして、本質的な部分ではありません。クラスが嫌な場合は適宜修正してしまっても問題ありません。
本日はここまでにします。次回からAIを呼び出して文字起こしを実施していきます。
最後まで読んでいただきありがとうございます。
質問等はコメント欄かお問合せにてよろしくおねがいいたします。