Python入門トップページ


目次

  1. 仮想環境を構築し,watchdog をインストールする
  2. Watchdog でフォルダを監視する
  3. 新しいファイルを検知する
  4. 検知した新しいファイルを待ち行列フォルダに移動する
  5. 文字起こし処理用のワーカープログラムのひな形を作成する
  6. ワーカープログラムを常駐化する
  7. フォルダ監視とワーカープログラムでパイプライン処理する
  8. 処理用フォルダを設置してそこで処理を行う
  9. ファイルサイズの変化を監視する
  10. サイズの大きなファイルのコピーに対応する
  11. タイムスタンプを表示する
  12. Subprocess で外部コマンドを起動する
  13. WhisperX の設定を行う
  14. ワーカープログラムで音声データからの文字起こしを実行する
  15. 文字起こし結果を完了用フォルダに移動する
  16. ログを保存し例外を処理する
  17. サブフォルダに結果を格納する
  18. Ollama をインストールする
  19. Python から Ollama にプロンプトを送る
  20. Ollama 処理を関数化する
  21. 文字起こしデータから議事録を作成する
  22. サイズの大きなテキストファイルにも対応する
  23. ワーカープログラムで文字起こし結果から議事録を生成する
  24. 議事録をマークダウンファイルに出力する
  25. 文字コードを強制的に指定する

フォルダ監視により,音声ファイルから文字起こしと議事録生成を自動化しよう

サブフォルダに結果を格納する

前のページのプログラムでは,同じファイル名の音声ファイルを繰り返し処理したときに,出力用の done フォルダで結果が上書きされてしまいます.例えば WhisperX のモデルを turbo から large に変更するなど,結果の比較を行いたい場合にはこの設計は不便に感じる可能性があります.ここでは完了用フォルダやエラーフォルダに日時情報を含んだサブフォルダを作成し,その中に結果を保存するようにしてみます.

worker.py
from pathlib import Path
import shutil
import time
import datetime
import subprocess

BASE_DIR = Path(__file__).parent
QUEUE_DIR = BASE_DIR / "queue"
WORKING_DIR = BASE_DIR / "working"
DONE_DIR = BASE_DIR / "done"
ERROR_DIR = BASE_DIR / "error"

# ディレクトリを作っておく
QUEUE_DIR.mkdir(parents=True, exist_ok=True)
WORKING_DIR.mkdir(parents=True, exist_ok=True)
DONE_DIR.mkdir(parents=True, exist_ok=True)
ERROR_DIR.mkdir(parents=True, exist_ok=True)

try:
    while True:
        for src in QUEUE_DIR.glob("*.m*"): # 監視するファイルの拡張子
            print(f"[{datetime.datetime.now().strftime('%H:%M:%S.%f')}] ----------------")
            print(f"[{datetime.datetime.now().strftime('%H:%M:%S.%f')}] 処理開始: {src.name}")
            working_file = WORKING_DIR / src.name
            transcription_dir = WORKING_DIR / src.stem
            transcription_dir.mkdir(parents=True, exist_ok=True) # ディレクトリを作っておく
            log_file = transcription_dir / "whisperx.log"
            shutil.move(src, working_file)
            print(f"[{datetime.datetime.now().strftime('%H:%M:%S.%f')}] 作業開始: {working_file}")
            print(f"[{datetime.datetime.now().strftime('%H:%M:%S.%f')}] 出力先 : {transcription_dir}")
            print(f"[{datetime.datetime.now().strftime('%H:%M:%S.%f')}] 処理中...")
            try:
                with open(log_file, "w", encoding="utf-8") as f:
                    subprocess.run([
                        "whisperx",
                        "--model", "turbo", # tubo / large ...
                        "--language", "ja",
                        # "--device", "cuda",  # NVIDIA GPU を利用できる場合
                        "-o", str(transcription_dir),
                        "--diarize",  # 話者分離
                        str(working_file)
                    ],
                    stdout=f,
                    stderr=subprocess.STDOUT,
                    check=True)
            except subprocess.CalledProcessError:
                print(f"[{datetime.datetime.now().strftime('%H:%M:%S.%f')}] WhisperXの処理に失敗しました")
                dt = datetime.datetime.now()
                err_dir_name = f"{dt.strftime('%Y%m%d-%H%M%S')}_{src.stem}"
                err_dst = ERROR_DIR / err_dir_name / src.name
                err_transcription_dir = ERROR_DIR / err_dir_name / src.stem
                err_transcription_dir.mkdir(parents=True, exist_ok=True) # ディレクトリを作っておく
                shutil.move(working_file, err_dst)
                shutil.move(transcription_dir, err_transcription_dir)
                print(f"[{datetime.datetime.now().strftime('%H:%M:%S.%f')}] 失敗: {err_dst}")
                continue
            time.sleep(3)
            dt = datetime.datetime.now()
            done_dir_name = f"{dt.strftime('%Y%m%d-%H%M%S')}_{src.stem}"
            done_file = DONE_DIR / done_dir_name / src.name
            done_transcription_dir = DONE_DIR / done_dir_name
            done_transcription_dir.mkdir(parents=True, exist_ok=True) # ディレクトリを作っておく
            shutil.move(working_file, done_file)
            shutil.move(transcription_dir, done_transcription_dir)
            print(f"[{datetime.datetime.now().strftime('%H:%M:%S.%f')}] 完了: {done_file}")
        time.sleep(1)

except KeyboardInterrupt:
    print(f"[{datetime.datetime.now().strftime('%H:%M:%S.%f')}] 停止します")

ターミナルを2個起動し,それぞれで watch.py と worker.py を実行します.

(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog> python watch.py ⏎
(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog> python worker.py ⏎

2つのプログラムが起動したらマークダウンファイルと音声ファイルを2個ずつ監視用フォルダに設置して処理を開始します.

(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog> python watch.py
[18:06:02.236970] 新しいファイルを検出: C:\Users\rinsaka\Documents\python\watchdog\watch\voice-sample02.m4a
[18:06:02.237319] 新しいファイルの名前: voice-sample02.m4a
[18:06:12.238836] ファイルの受取りが完了しました
[18:06:12.238999] ファイルの移動開始: C:\Users\rinsaka\Documents\python\watchdog\queue\voice-sample02.m4a
[18:06:12.239678] ファイルの移動完了: C:\Users\rinsaka\Documents\python\watchdog\queue\voice-sample02.m4a
[18:06:12.239833] 新しいファイルを検出: C:\Users\rinsaka\Documents\python\watchdog\watch\sample1.md
[18:06:12.239889] 新しいファイルの名前: sample1.md
[18:06:22.241144] ファイルの受取りが完了しました
[18:06:22.241231] ファイルの移動開始: C:\Users\rinsaka\Documents\python\watchdog\queue\sample1.md
[18:06:22.241899] ファイルの移動完了: C:\Users\rinsaka\Documents\python\watchdog\queue\sample1.md
[18:06:22.242041] 新しいファイルを検出: C:\Users\rinsaka\Documents\python\watchdog\watch\sample2.md
[18:06:22.242123] 新しいファイルの名前: sample2.md
[18:06:32.242981] ファイルの受取りが完了しました
[18:06:32.243070] ファイルの移動開始: C:\Users\rinsaka\Documents\python\watchdog\queue\sample2.md
[18:06:32.243550] ファイルの移動完了: C:\Users\rinsaka\Documents\python\watchdog\queue\sample2.md
[18:06:32.243663] 新しいファイルを検出: C:\Users\rinsaka\Documents\python\watchdog\watch\voice-sample01.m4a
[18:06:32.243732] 新しいファイルの名前: voice-sample01.m4a
[18:06:42.244400] ファイルの受取りが完了しました
[18:06:42.244492] ファイルの移動開始: C:\Users\rinsaka\Documents\python\watchdog\queue\voice-sample01.m4a
[18:06:42.245451] ファイルの移動完了: C:\Users\rinsaka\Documents\python\watchdog\queue\voice-sample01.m4a
Ctrl + C
[18:07:30.592455] 停止します
(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog>

ワーカープログラムでは日時情報から始まる新規フォルダが作成され,その中に処理結果が格納されていることが分かります.

(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog> python worker.py
[18:06:13.162973] ----------------
[18:06:13.163229] 処理開始: voice-sample02.m4a
[18:06:13.181613] 作業開始: C:\Users\rinsaka\Documents\python\watchdog\working\voice-sample02.m4a
[18:06:13.181790] 出力先 : C:\Users\rinsaka\Documents\python\watchdog\working\voice-sample02
[18:06:13.181922] 処理中...
[18:06:36.061433] 完了: C:\Users\rinsaka\Documents\python\watchdog\done\20261102-180636_voice-sample02\voice-sample02.m4a
[18:06:37.062362] ----------------
[18:06:37.062565] 処理開始: sample1.md
[18:06:37.064537] 作業開始: C:\Users\rinsaka\Documents\python\watchdog\working\sample1.md
[18:06:37.064637] 出力先 : C:\Users\rinsaka\Documents\python\watchdog\working\sample1
[18:06:37.064657] 処理中...
[18:06:49.754223] WhisperXの処理に失敗しました
[18:06:49.756209] 失敗: C:\Users\rinsaka\Documents\python\watchdog\error\20261102-180649_sample1\sample1.md
[18:06:49.756340] ----------------
[18:06:49.756383] 処理開始: sample2.md
[18:06:49.757133] 作業開始: C:\Users\rinsaka\Documents\python\watchdog\working\sample2.md
[18:06:49.757222] 出力先 : C:\Users\rinsaka\Documents\python\watchdog\working\sample2
[18:06:49.757263] 処理中...
[18:07:00.497862] WhisperXの処理に失敗しました
[18:07:00.515651] 失敗: C:\Users\rinsaka\Documents\python\watchdog\error\20261102-180700_sample2\sample2.md
[18:07:01.516580] ----------------
[18:07:01.516700] 処理開始: voice-sample01.m4a
[18:07:01.517982] 作業開始: C:\Users\rinsaka\Documents\python\watchdog\working\voice-sample01.m4a
[18:07:01.518041] 出力先 : C:\Users\rinsaka\Documents\python\watchdog\working\voice-sample01
[18:07:01.518097] 処理中...
[18:07:21.038238] 完了: C:\Users\rinsaka\Documents\python\watchdog\done\20261102-180721_voice-sample01\voice-sample01.m4a
Ctrl + C
[18:07:31.423532] 停止します
(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog>

完了用フォルダの中身を確認します.

(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog> gci done -Recurse ⏎

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\done

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2026/11/02    18:06                20261102-180636_voice-sample02
d----          2026/11/02    18:07                20261102-180721_voice-sample01

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\done\20261102-180636_voice-sample02

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2026/11/02    18:06                voice-sample02
-a---          2026/10/27    12:39         554089 voice-sample02.m4a

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\done\20261102-180636_voice-sample02\voice-sample02

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2026/11/02    18:06          58602 voice-sample02.json
-a---          2026/11/02    18:06           1113 voice-sample02.srt
-a---          2026/11/02    18:06           1014 voice-sample02.tsv
-a---          2026/11/02    18:06           1005 voice-sample02.txt
-a---          2026/11/02    18:06           1096 voice-sample02.vtt
-a---          2026/11/02    18:06           3355 whisperx.log

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\done\20261102-180721_voice-sample01

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2026/11/02    18:07                voice-sample01
-a---          2026/10/27    12:36         329637 voice-sample01.m4a

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\done\20261102-180721_voice-sample01\voice-sample01

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2026/11/02    18:07          28431 voice-sample01.json
-a---          2026/11/02    18:07            566 voice-sample01.srt
-a---          2026/11/02    18:07            505 voice-sample01.tsv
-a---          2026/11/02    18:07            494 voice-sample01.txt
-a---          2026/11/02    18:07            558 voice-sample01.vtt
-a---          2026/11/02    18:07           2994 whisperx.log

(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog>

エラー用のフォルダにも日時情報からなるサブフォルダが作成されて,その中に結果が格納されています.

(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog> gci error -Recurse ⏎

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\error

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2026/11/02    18:06                20261102-180649_sample1
d----          2026/11/02    18:07                20261102-180700_sample2

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\error\20261102-180649_sample1

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2026/11/02    18:06                sample1
-a---          2026/10/25    18:18             56 sample1.md

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\error\20261102-180649_sample1\sample1

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2026/11/02    18:06                sample1

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\error\20261102-180649_sample1\sample1\sample1

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2026/11/02    18:06           4412 whisperx.log

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\error\20261102-180700_sample2

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2026/11/02    18:07                sample2
-a---          2026/10/25    18:18            115 sample2.md

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\error\20261102-180700_sample2\sample2

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2026/11/02    18:06                sample2

    ディレクトリ:C:\Users\rinsaka\Documents\python\watchdog\error\20261102-180700_sample2\sample2\sample2

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2026/11/02    18:06           4412 whisperx.log

(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog>

監視用 watch フォルダ,待ち行列用 queue フォルダと処理用 working フォルダはいずれも空になっていることが分かります.

(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog> gci watch ⏎
(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog> gci queue ⏎
(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog> gci working ⏎
(py313watchdog) PS C:\Users\rinsaka\Documents\python\watchdog>

ここまでの作業によって,監視フォルダに音声ファイルを設置するだけで自動的に文字起こし結果が完了用フォルダに出力されるパイプライン処理ができました.次のページでは文字起こし結果からローカルの生成AI (LLM) によって自動的に議事録を作成することを目標に作業を進めていきます.

目次に戻る