設計をコードに落とし込む:video_analyzer.pyの全体像
前回の記事で、代表フレーム抽出・CLIP解析・動画特有の技術チェック(解像度/シーン数/ブラー/手ブレ/無音比率)という各パーツの設計は固まりました。
今回はこれらを1本のスクリプトvideo_analyzer.pyとして組み上げ、実際に動かします。
画像用のanalyzer.pyを土台に、処理の骨格自体はほぼそのまま流用できました。
変わるのは「1枚の画像を読み込む」部分が「動画から代表フレームを抽出する」部分に置き換わる点だけです。
def analyze_video(video_path: Path, cfg: dict) -> dict:
duration, w, h, fps, codec = probe_video_meta(video_path, cfg["ffprobe_path"])
frame_paths = extract_scene_frames(video_path, tmp_dir, cfg)
if len(frame_paths) < cfg.get("min_frames", 3):
frame_paths = extract_uniform_frames(video_path, tmp_dir, duration,
count=cfg.get("fallback_frames", 6))
frame_scores = [analyze_single_frame(p) for p in frame_paths] # 画像版と共通の関数
shake_score = calc_shake_score(video_path, cfg)
silence_ratio = calc_silence_ratio(video_path, cfg["ffmpeg_path"])
return {
"duration": duration, "resolution": f"{w}x{h}", "fps": fps, "codec": codec,
"clip_avg": mean(s["clip_score"] for s in frame_scores),
"clip_min": min(s["clip_score"] for s in frame_scores),
"shake_score": shake_score,
"silence_ratio": silence_ratio,
"scene_count": len(frame_paths),
}
ポイントは、代表フレームそれぞれへの評価(analyze_single_frame)を画像版の解析関数からそのまま呼び出している点です。
設計段階で決めた「画像と全く同じCLIP解析を適用する」方針を、実装でもそのまま守っています。
フレーム抽出→CLIP解析→集約の実装
前回コードで示したextract_uniform_framesや手ブレ判定のオプティカルフロー計算は、
今回そのまま関数として切り出し、video_analyzer.pyの冒頭でimportする構成にしました。
以前は検証用に別スクリプトへベタ書きしていたものを、video_utils.pyという共通モジュールに移動させています。
# video_utils.py(設計段階のコードをモジュール化)
# video_analyzer.py
from video_utils import (
extract_scene_frames,
extract_uniform_frames,
calc_shake_score,
calc_silence_ratio
)
こうしてモジュール化しておくと、次回以降に予定している「Excelへの書き込み」「Streamlit UIからの呼び出し」でも同じ関数をそのまま使い回すことができます。 1つのファイルに実装をすべて詰め込まず、関数を切り出しておいたのは、後から見て正解でした。
① ffprobeのパス設定ミス
実装が一通り終わり、テスト動画で最初の実行をしたところ、いきなり次のエラーで落ちました。
FileNotFoundError: [WinError 2] 指定されたファイルが見つかりません: 'ffprobe'
画像用ツールの開発機ではffmpeg一式にPATHが通っていたため気づかなかったのですが、動画解析用にセットアップした別フォルダにはPATH設定が入っていませんでした。
subprocess.run(["ffprobe", ...])のようにコマンド名だけで呼び出していたのが原因です。
環境変数PATHに依存するのをやめ、設定ファイル側で明示的にffmpeg一式のディレクトリを指定する方式に変更しました。
"analyze_video": {
"ffmpeg_dir": "E:\\tools\\ffmpeg\\bin",
...
}
# コード側
FFMPEG_PATH = str(Path(cfg["ffmpeg_dir"]) / "ffmpeg.exe")
FFPROBE_PATH = str(Path(cfg["ffmpeg_dir"]) / "ffprobe.exe")
これで実行環境のPATH設定に左右されず、設定ファイルさえ合わせれば別のPCでもそのまま動く状態になりました。
② subprocess出力の文字コードエラー
ffprobeのパスを直してテストを再開したところ、今度は一部の動画ファイルでだけ次のエラーが発生しました。
UnicodeDecodeError: 'cp932' codec can't decode byte 0x87 in position 12: illegal multibyte sequence
subprocess.run(cmd, capture_output=True, text=True)という書き方をしていたのですが、
text=Trueを指定すると出力はOSのデフォルトエンコーディング(日本語Windowsではcp932)でデコードされます。
ffmpegのログに含まれる一部の記号がcp932で表現できず、そこで例外が発生していました。
ffmpeg・ffprobeの出力は、今回の環境ではUTF-8として扱うことで問題を解消できたため、デコード方式を明示する形に修正しました。
# Before
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
# After
result = subprocess.run(cmd, capture_output=True, timeout=30)
stdout = result.stdout.decode("utf-8", errors="replace")
stderr = result.stderr.decode("utf-8", errors="replace")
errors="replace"を付けているのは、万一デコードできない文字が残っていても、ログ解析自体を止めないようにするためです。
数値の抽出などクリティカルな処理には影響しない箇所なので、多少文字が化けても実害はないと判断しました。
③ Ollamaサーバー未起動によるエラー
文字コード問題を直したあと、CLIP解析の手前で今度は次のエラーが出ました。
ConnectionError: HTTPConnectionPool(host='localhost', port=11434): Max retries exceeded
画像用ツールのbatファイルはOllamaサーバーを自動起動する処理を持っていましたが、video_analyzer.pyは単体でPythonから直接実行してテストしていたため、
Ollamaサーバー自体が起動していませんでした。単純な確認漏れだったのですが、エラーメッセージだけを見ると原因が分かりにくいと感じたため、起動確認とリトライを入れることにしました。
def wait_for_ollama(url: str, retries: int = 3, wait_sec: int = 3) -> bool:
for i in range(retries):
try:
r = requests.get(f"{url}/api/tags", timeout=3)
if r.status_code == 200:
return True
except requests.exceptions.RequestException:
pass
print(f"[INFO] Ollamaサーバーへの接続待機中... ({i+1}/{retries})")
time.sleep(wait_sec)
return False
if not wait_for_ollama(cfg["ollama_url"]):
print("[ERROR] Ollamaサーバーに接続できません。ollama serve を起動してください。")
sys.exit(1)
本質的な原因はサーバーが単に起動していなかっただけですが、「何が起きているか分かるメッセージを出す」ことを優先し、 無言でスタックトレースだけが出る状態は避けるようにしました。
まとめ
設計段階で決めた各パーツ(フレーム抽出・CLIP解析・技術チェック)は、想定通りほぼそのままコードに落とし込むことができました。 むしろ時間がかかったのは、パス設定・文字コード・外部サーバー起動確認といった、解析ロジック本体とは関係のない周辺部分です。 特に文字コード関連は動画ファイルによって発生したりしなかったりするため、原因の切り分けに時間がかかりました。
今回のポイントまとめ
- 設計済みの関数は早い段階でモジュール化しておくと、後工程でそのまま再利用できる
- 外部コマンドのパスは環境変数PATHに依存せず、設定ファイルで明示的に指定する
- subprocessの出力はtext=Trueに任せず、エンコーディングを明示してデコードする
- 外部サーバー(Ollama等)への依存箇所は、ヘルスチェックとリトライを入れて原因を特定しやすくする
次回は、この解析結果をExcelに書き込む際に、画像用の設定ファイル・Excelとどう共存させるかという設計判断について記録します。
FAQ
前の記事 ← 品質判定・管理Excelの設計