「動画設定は空欄でも良い」という要件
バックエンド側(video_analyzer.py、furiwake_okng.py、shudou_delete.py)の動画対応が一通り終わったところで、Streamlit製UIにも動画用の設定項目を追加することになりました。ここでの要件は明確でした。「動画を作らない運用の日もあるので、動画設定は空欄のままでもエラーにならないこと」です。
この要件は地味に見えて、実装方針に大きく影響します。単に「入力必須のフォームを追加する」だけでは済まず、空欄という状態を正常系の一つとして扱う設計が必要になるからです。
この記事で書くこと
- 画像・動画で処理単位が違う場合のボタン設計の考え方
- Python側での「空欄なら安全にスキップ」ガードの実装
- ログ確認ページのプロセス名フィルタで踏んだ落とし穴
ボタンを分けるか、1つにまとめるか
UIを設計する際、まず迷ったのが「画像解析開始」ボタンと「動画解析開始」ボタンを分けるべきかという点でした。結論として、裏側で呼び出すbatファイルが完全に別物かどうかを基準に判断しました。
| ページ | ボタン構成 | 理由 |
|---|---|---|
| AI解析 | 「画像解析開始」「動画解析開始」の2つ | run_analyze_ui.bat / run_analyze_video_ui.batが別スクリプトのため |
| OK/NG振り分け | 「振り分け開始(画像+動画)」の1つ | run_furiwake_ui.batが内部で両方処理する設計のため |
| 手動削除チェック | 「チェック開始(画像+動画)」の1つ | 同上 |
OK/NG振り分けと手動削除チェックについては、ボタンは1つのまま、クリック時に画像用・動画用の両セクションをまとめてstockphoto.jsonに保存してから実行する形にしました。
# 動画側:空欄はそのまま空欄として保存する
# (「動画は扱わない」を明示できるようにするため、画像側のような
# if文でのガードはあえて入れない)
cfg_full.setdefault("furiwake_video", {})
cfg_full["furiwake_video"]["excel_file"] = excel_file_v
cfg_full["furiwake_video"]["source_dir"] = source_dir_v
cfg_full["furiwake_video"].setdefault("dest", {})["ok"] = ok_dest_v
cfg_full["furiwake_video"].setdefault("dest", {})["ng"] = ng_dest_v
画像側の設定は「空欄なら既存値を保持」、動画側の設定は「空欄ならそのまま空欄で保存」と、あえて挙動を非対称にしている点がポイントです。動画を使わないという意思表示を明示的に残せるようにするための判断でした。
Python側での空欄安全ガード
UI側で空欄を許容しても、裏側のPythonスクリプトが空文字列を渡されてクラッシュしては意味がありません。video_analyzer.pyでは、GPU(CLIPモデル)を読み込む前の最も早い段階で空欄チェックを入れました。
if not VIDEO_FOLDER or not VIDEO_FOLDER.strip():
print("[SKIP] video_folder が未設定のため、動画解析をスキップします。")
sys.exit(0)
if not OUTPUT_EXCEL or not OUTPUT_EXCEL.strip():
print("[SKIP] output_excel が未設定のため、動画解析をスキップします。")
sys.exit(0)
if not os.path.isdir(VIDEO_FOLDER):
print(f"[SKIP] 動画フォルダが見つかりません: {VIDEO_FOLDER}")
sys.exit(0)
ここで重要なのは、終了コードを0(正常終了)にすることです。exit(1)にしてしまうと、bat側のエラーハンドリングに引っかかり、画像側の処理まで「失敗」として扱われかねません。「そのメディア種別を扱わない」という状態は失敗ではなく正常系である、という位置づけを終了コードのレベルで明確にしました。
furiwake_okng.py・shudou_delete.pyにも同様のガードを追加し、source_dirやexcel_fileが空欄・存在しない場合はエラーではなく[SKIP]ログを出して正常終了する統一ルールにしています。
ここで一つ工夫したのが、「空欄(未設定)」と「設定されているのに実体がない(誤設定の可能性)」を区別したことです。前者は完全にスキップして正常終了させますが、後者は依然としてエラー扱いのままにしました。
# excel_file / search_root が未設定(空欄)の場合はスキップ
if not excel_file.strip() or not search_root.strip():
print(f"[SKIP] {SECTION}.excel_file / search_root が未設定のためスキップします。")
sys.exit(0)
# 設定されているのに実体が無い場合は、誤設定の可能性が高いのでエラーとして扱う
if not Path(excel_file).exists():
print(f"[ERROR] Excelファイルが見つかりません: {excel_file}")
sys.exit(1)
「空欄なら黙って通す」を全面的に適用してしまうと、フォルダ名のタイプミスのような本当のミスまで握りつぶしてしまいます。 「未入力」と「入力したが間違っている」は別物として扱うべき、という判断です。
ログの絞り込みで踏んだ落とし穴
ログ確認ページには、処理名でログファイルを絞り込むプルダウンがありました。
動画解析用のログファイル名はanalyze_video_YYYYMMDD.logという命名なのですが、既存の絞り込みロジックがstartswithによる前方一致だったため、
「analyze」を選ぶと「analyze_video」のログまで一緒に混ざって表示されてしまう不具合がありました。
# Before: "analyze_video_..." も "analyze" にマッチしてしまう
filtered = [f for f in log_files if f.stem.startswith(process_key)]
# After: プロセス名_日付 の完全一致のみ
pattern = re.compile(rf"^{re.escape(process_key)}_\d{{8}}$")
filtered = [f for f in log_files if pattern.match(f.stem)]
動画用のログエントリを新設したことで初めて表面化した問題でした。既存の「furiwake」と「furiwake_ui」も同じ構造だったため、この修正で副次的に直っています。
出力ファイルページの二重化
出力ファイルページのExcel表示も、画像用・動画用を1行ずつ分けて表示するよう変更しました。 動画用Excelが未設定の場合は「(未設定です)」という案内を出し、AI解析ページへの導線を示すようにしています。 CSVについても、EXAM/OK/NGのCSVは収集が共通のため画像・動画で1つのまま、アップロード用CSV(申請用メタデータ)だけを画像用・動画用に分けて表示しました。
まとめ
今回のUI対応を通じて、「入力を必須にしない」設計は見た目以上に考慮点が多いことを実感しました。 UI側の保存ロジック、バックエンド側の早期リターン、ログの絞り込みロジックのそれぞれに、空欄というケースを正常系として織り込む必要があります。 一つのレイヤーだけ対応しても、別のレイヤーで空欄チェックが漏れていれば結局どこかでエラーが起きるため、UI・設定保存・バックエンドの3層すべてで一貫したルールを持たせることが重要でした。
今回のポイントまとめ
- 処理単位が別スクリプトならボタンも分ける、共通スクリプトならボタンもまとめる
- 「扱わない」状態は失敗ではなく正常系。終了コードで明確に区別する
- UI側の変更は既存機能への副作用(ログ絞り込みなど)にも注意が必要
次回は、実際に動画解析を動かした際に発生した一連のトラブル(Ollamaサーバーの衝突、絵文字による文字コードエラー、Windowsのセキュリティ機能によるブロック、改行コード破損)をまとめて記録します。
FAQ
前の記事 ← 審査結果の収集