見出し画像

議事録とかやりたくない。子育て優先で仕事を機械任せにする

おっさんパパは、猫の代わりにAI使って、子持ち様とか言わせません。出すもの出しておけば、子供の保育園送り迎えも安泰です。

今回は、勝手に会社の業務を、個人的にDX化します。

でかいサーバとか、サブスククラウドとか不要。基本無料で公開されているツール、ありふれた古いWindwosノートパソコンで処理します。

whisperを使う

OpenAIさんが、サボりたいあなたの為に、whisperなる文字起こしAIモデルを無料で公開していますので、それ使います。Turboモデル等スピード重視のものもありますが、精度重視でlarge-v3モデルでいきます。
なおこのモデル、多言語対応しているので日本語以外もOKです。
利用には、Hugging Faceのアクセストークンを取得する必要があります

Hugging Faceのアクセストークンを取得する手順

  1. Hugging Faceにログイン / アカウント作成

  2. アクセストークンの生成

    • 右上のプロフィールアイコンをクリック

    • 「Settings」 を開く

    • 左メニューの 「Access Tokens」 を選択

    • 「New token」 をクリック

    • 名前を入力し、権限(Scope)を 「read」(または必要に応じて「write」や「admin」)に設定

    • 「Generate a token」 をクリック

  3. トークンのコピー

    • 生成されたトークンをコピー この文字列を使う。

pyannote audioを使う

whisperでは話者分離ができないので、別のpyannote audioというやつでやります。分離精度は50代のおっさんよりましかもというレベル。そこまで話者分離に拘らないので、一応やってみる。文字起こしより、こちらの処理のほうが時間かかる感じなので、話者分離不要ならスピードは上がる。

全体の処理の流れ

最初に全部文字起こしをして、次に話者分離をして、最後に両者を結合する感じにしました。

最初に話者分離をして、本文を短い文にブツ切りにしてから文字起こしするよりは、AIの仕組み的に長い文のほうが精度が高いのではと考えたからです。

pythonのコードと動かし方

さぼりを実現する適当なpythonのコードです。手元にあるのはWindowsの古いノートなので、まともなGPUなんか当然ない。遅いです。

動かすときには当然ですが、pythonが必要ですので、ダウンロードしてインストールしてください。また、必要なライブラリは、pipなどで、適当に入れてください。
また、管理権限がないとか出るなら、Windwosの管理者として実行してください。

更に、このコードのままだと、ffmpegが無いと動きません。適当にインストールしてください。音声ファイルはwavしか使わんから要らないというなら、チャットGPTさんに、このコードから、ffmpeg部分を削除するとやれば、勝手にやってくれます。

Windowsでこのプログラムを動かす手順

1. 必要なソフトウェアをインストールPython 3.9以上 をインストール(公式サイト から)
FFmpeg をインストールし、環境変数 PATH に追加(公式サイト から)

2. 仮想環境を作成(推奨)
コマンドプロンプト(cmd)またはPowerShellで以下を実行:
python -m venv whisper_env whisper_env\Scripts\activate

3. 必要なPythonライブラリをインストール
以下を実行してライブラリをインストール:
pip install numpy librosa soundfile whisper openai ffmpeg-python torch torchvision torchaudio pyannote.audio

4. Hugging Faceのアクセストークンを取得Hugging Face に登録
アクセスキー を取得
HF_AUTH_TOKEN = "" に設定

5. FFmpegが正しく動作するか確認
ffmpeg -version
バージョン情報が表示されればOK。

6. プログラムを実行
python your_script.py

7. 予想される問題と対処法FFmpegが見つからない場合: 環境変数 PATH にFFmpegのパスを追加
ライブラリのインストールエラー: pip install --upgrade pip setuptools を実行

チャットGPT

ラッキーにも、それなりのRAM容量があるGPUがある人は、チャットGPTさんにでも適当にコードを変えてもらってください。GPUなし環境だと、放置しておくと忘れたころに出来る感じですかね。1時間の会議の録音を文字にする処理に、2時間から3時間は余裕でかかります。

import librosa
import numpy as np
import soundfile as sf
import os
import shutil
import subprocess
import whisper
from pyannote.audio.pipelines import SpeakerDiarization
from datetime import timedelta
#import openai チャットGPTにAPIで入れようとしたけど、有料なのでやめた


def preprocess_audio(file_path, target_sr=16000):
    """
    音声ファイルを前処理(サンプリングレート変換、ノイズ除去、正規化)
    """
    # 音声ファイルの読み込み
    audio, sr = librosa.load(file_path, mono=True, sr=None)
    
    # サンプリングレートの調整
    if sr != target_sr:
        audio = librosa.resample(audio, orig_sr=sr, target_sr=target_sr)
    
    # 音量の正規化
    audio = librosa.util.normalize(audio)
    
    # 処理済み音声の保存
    output_path = r"c:\data\preprocessed_audio.wav"
    sf.write(output_path, audio, target_sr)
    
    return output_path

# 音声ファイルのパス設定 mp3、mp4以外も変換できる
audio_path = r"c:\data\test1.mp3"
wav_path = r"c:\data\wavsound.wav"  # 変換後のWAVファイルの保存先

# Hugging Faceアクセストークン(取得したトークンをここにコピー)
HF_AUTH_TOKEN = 'これが無いと動かない'

# FFmpegの存在確認
if shutil.which("ffmpeg") is None:
    print("FFmpegが見つかりません。")
else:
    print("FFmpegが見つかりました。")

# ファイルの存在確認
if not os.path.exists(audio_path):
    print(f"音声ファイルが見つかりません: {audio_path}")
    exit(1)

# MP4からWAVに変換
try:
    subprocess.run(["ffmpeg", "-i", audio_path, wav_path, "-y"], check=True)
    print(f"ファイルをWAVに変換しました: {wav_path}")
except subprocess.CalledProcessError as e:
    print(f"FFmpegによる変換中にエラーが発生しました: {e}")
    exit(1)

# 音声の前処理
preprocessed_file = preprocess_audio(wav_path)

# Whisperモデルのロード GPUなしのポンコツ仕様。時間がかかるよ。
try:
    model = whisper.load_model("large-v3", device='cpu')
    print("Whisperモデルのロードに成功しました")
except Exception as e:
    print(f"モデルのロード中にエラーが発生しました: {e}")
    exit(1)

# 音声文字起こし
try:
    result = model.transcribe(preprocessed_file, fp16=False, language="ja")
    transcription_segments = result["segments"]
    print("文字起こし結果:")
    for segment in transcription_segments:
        print(f"[{segment['start']:.3f} --> {segment['end']:.3f}] {segment['text']}")
except Exception as e:
    print(f"文字起こし中にエラーが発生しました: {e}")
    exit(1)

# 話者分離パイプラインのロード
try:
    pipeline = SpeakerDiarization.from_pretrained("pyannote/speaker-diarization", use_auth_token=HF_AUTH_TOKEN)
    print("話者ダイアライゼーションパイプラインのロードに成功しました")
except Exception as e:
    print(f"話者ダイアライゼーションパイプラインのロード中にエラーが発生しました: {e}")
    exit(1)

# 音声の話者分離
try:
    diarization = pipeline({'uri': 'filename', 'audio': preprocessed_file})
    diarization_results = [
        {"start": segment.start, "end": segment.end, "speaker": speaker}
        for segment, _, speaker in diarization.itertracks(yield_label=True)
    ]
except Exception as e:
    print(f"話者分離中にエラーが発生しました: {e}")
    exit(1)

def format_time(seconds):
    """秒数をMM:SS.mmm形式に変換"""
    td = timedelta(seconds=seconds)
    minutes, seconds = divmod(td.seconds, 60)
    milliseconds = td.microseconds // 1000
    return f"{minutes}:{seconds:02}.{milliseconds:03}"

def combine_segments(transcription_segments, diarization_segments):
    """
    文字起こし結果と話者分離データを統合
    """
    combined_segments = []
    for segment in transcription_segments:
        start, end, text = segment["start"], segment["end"], segment["text"]
        speakers = [dia["speaker"] for dia in diarization_segments if dia["start"] < end and dia["end"] > start]
        speaker = speakers[0] if speakers else "Unknown"
        combined_segments.append({"speaker": speaker, "start": start, "end": end, "text": text})
    return combined_segments

def merge_speaker_segments(combined_segments):
    """
    連続する同一話者のセグメントを統合
    """
    merged_segments = []
    for segment in combined_segments:
        if merged_segments and merged_segments[-1]["speaker"] == segment["speaker"]:
            merged_segments[-1]["end"] = segment["end"]
            merged_segments[-1]["text"] += " " + segment["text"]
        else:
            merged_segments.append(segment)
    return merged_segments

# データ統合・整形
combined_segments = combine_segments(transcription_segments, diarization_results)
merged_segments = merge_speaker_segments(combined_segments)

# 統合結果を表示
print("統合された話者分離結果:")
for segment in merged_segments:
    print(f"{segment['speaker']}: [{format_time(segment['start'])} --> {format_time(segment['end'])}] {segment['text']}")

精度

色々試しましたが、なかなかです。「えーと」とか、そんなのは勝手に消してくれますし。

  result = model.transcribe(preprocessed_file, fp16=False, language="ja",initial_prompt='専門用語等を指定')

しかし、実際の会議だと予備知識がないとわからない、固有名詞等があるので、initial_prompt=の部分にそれを指定した方がより正しく認識するようです。
initial_prompt='おっさんパパ株式会社は、今日も開店休業中です。'
これだけで、おっさんパパ株式会社という固有名詞の認識力が上がります。

で、このAIさんでも認識できない部分の音声を、実際に人間であるおっさんパパが聞いてみたのですが、あまり認識できませんでした。

人間が聞いても何言っているか分からないというのは、会議ではよくある事です。発言している当人も、何言っているかわかっていないのかもしれません。そういう部分は、議事録から黙って削除です。

どうせマトモに読む人は居ないので、適当にやりましょう。

構造化はチャットGPTで

文字起こしたものは、発言がそのままなので議事録としては微妙。
そこは、チャットGPTで処理したいところ。
APIは有料なので、出てきたものをコピペでGOです。
会社規定がクラウド利用がだめというなら、文字起こしだけでなく、LLMもローカルのノートPCで頑張りますか?

7bくらいなら動きますが、まともなLLMをローカルで動かすには、GPUいるし、GPUのメモリが沢山いる。比較的お得なアップルのPCでも買うしかない? macbookproとかそういうの。でも高い。

まあ、すっと目を閉じて、チャットGPTでやりゃ簡単です。

テスト

取り合えず、実際に動作するか、適当な音声ファイルをネットから拾ってきて文字起こししてみました。

高崎市議会の総務常任委員会、令和7年1月28日 音声データ [MP3ファイル/11.35MB]の文字起こしをしてみました。

以下その出力された文の一部で、一応、話者分離はできているようです。

SPEAKER_05: [0:00.160 --> 0:41.260] 時間前でございますがお揃いですので、ただいまから総務常任委員会を開会いたします。 この際、初版の報告を申し上げます。 傍聴はあらか じめ許可してあります。 以上で初版の報告を終わります。 それでは本日の会議に入ります。 本日の会議は、会議規則第118条第1項の規定により、 委員一人当たりの発言時間を15分 程度としたいと思いますので、よろしくお願いいたします。 それでは報告事項に入ります。 高崎市公共施設等総合管理計画改定案に関するパブリックコメントの実施について説明を お願いします。 幹事長。
SPEAKER_02: [0:43.840 --> 9:00.400] 高崎市公共施設等総合管理計画改定案がまとまりまして、 2月1日からパブリックコメントを実施いたしますので、 改定案の概要とパブリック コメントの実施概要につきまして、ご報告申し上げます。 この公共施設等総合管理計画は、 国の要請を受け、各地方公共団体において公共施設等の状況を把握し、 長期的な視点で管理するための考え方や取組の方向性を示す計画でございます。 現計画は、平成28年度から令和7年度までの10年間の計画期間となっておりますけれども、 計画策定から一定期間が経過したことや、 先般、国の指針の改定によりまして、 必要とされる項目を追加した計画の見直し要請がございましたので、 改定案を策定し、パブリックコメントを実施するものでございます。 1の改定案の概要ですが、 ①として、各種数値の時点修正を全般的に行いました。 ②として、各種数値の時点修正を全般的に行いました。 ③として、各種数値の時点修正を全 般的に行いました。 ④として、各種数値の時点修正を全般的に行いました。 また、②として、改定に当たり、中長期的な視点から、 公共施設の適正な管理を行うため、 現在の平成28 年度から令和7年度までの10年間の計画期間を、 令和27年度までの30年に延伸いたします。 ③以降は、国の追加項目の要請によるものでございます。 ④の国の全国共通ソフトを用いた 更新費用の試算ですが、 10ページ、11ページをご覧いただきたいと思います。 10ページは、建築系公共施設の更新費用についてでございます。 ④の公共施設の更新費用についてです 。 ④の公共施設の更新費用についてです。 直近3年間の維持管理費用は、 令和3年が約173億円、令和4年度が171億円、 令和5年度が270億円で、平均いたしますと、 年間約205億円を 要しております。 現在の建築系施設をそのまま維持することを前提にいたしまして、 建て替え更新年数を60年、 大規模、 改修実施年数を30年とした、 今後40年間の維持管理経費の試算でございます。

高崎市議会の総務常任委員会

この元の文章を、チャットGPTさんに、そのまま放り込みます。
プロンプトは適当に「以下の音声文字起し文を、話者に注意して構造化された議事録にして下さい」です。
以下が、出力された議事録、そのままです。粒度を変えたいならチャットGPTさんにお願いしてください。

総務常任委員会 議事録

日時: (記載なし)
場所: (記載なし)
出席者: (記載なし)

1. 開会

議長(SPEAKER_05):定刻前ではあるが、出席者が揃ったため総務常任委員会を開会。
傍聴は事前に許可済み。
会議規則第118条第1項に基づき、委員1人あたりの発言時間を15分程度とする。


2. 報告事項

(1)高崎市公共施設等総合管理計画改定案に関するパブリックコメントの実施幹事長(SPEAKER_02):2月1日よりパブリックコメントを実施。
現行計画(平成28年度~令和7年度)を見直し、令和27年度までの30年計画に延長。
主要な改定内容:
各種数値の時点修正。
公共施設の適正管理を目的とした長期視点での見直し。
国の指針に基づく追加項目の反映。
建築系公共施設の更新費用試算の詳細(維持管理費・超寿命化対策)。


質疑応答委員(SPEAKER_03):維持管理費と更新費用試算の数値に大きな乖離があり、国の指針に基づいた内容ではあるが妥当性について疑問。
施設削減の目標を掲げる自治体もあるが、高崎市の方針は?


関西課長(SPEAKER_02):一律的な削減目標は市民生活への影響が大きいため、慎重に進める。
市民や関係団体の意見を尊重しながら適正化を進める。


委員(SPEAKER_03):公民館などの老朽化が進む中、市民の意見を反映するため、パブリックコメントの実施を要望。


委員(SPEAKER_00):目標数値設定がない点に疑問。他自治体では20~30%の削減目標を掲げている。
施設管理に関する専門チームの編成を提案。


幹事会課長(SPEAKER_02):現時点で数値目標は設定せず、地域のニーズやコスト分析を基に慎重に検討。
他自治体の事例では、市民との合意形成が重要との意見が多い。


3. その他

議長(SPEAKER_05):執行部からの報告事項なし。
委員からの質疑応答を受付。


(1)選挙投票所の立ち会いについて委員(SPEAKER_07):期日前投票・投票日当日の立ち会い人について、時間・報酬・意見を確認。


選管次長(SPEAKER_01):立ち会い時間:
投票日当日: 午前7時~午後7時
期日前投票: 午前8時30分~午後8時
報酬:
投票日当日: 1万900円
期日前投票: 9600円
立ち会い時間の長さに関する意見が多い。


委員(SPEAKER_07):高齢の区長にとって負担が大きいため、改善を検討すべき。


(2)若年層の投票率向上施策について委員(SPEAKER_07):18歳へのバースデーカード配布や高校での出前講座の対象選定基準と過去3年間の実績を確認。


選管次長(SPEAKER_01):出前講座の対象校は申請制。
過去3年間の実績について報告(詳細省略)。


4. 閉会

議長(SPEAKER_05):質疑応答を終結。
以上で会議を終了。

高崎市議会の総務常任委員会の文字起こし文を、チャットGPTで議事録っぽく構造化したもの

人の手が入っているのは、文字起こし文をチャットGPTの枠にコピペした部分だけですね。

いいなと思ったら応援しよう!