動画設定無しでもエラーにしない

バックエンド側の動画対応が一通り終わったところで、普段使用している画像生成画面から動画も扱えた方が便利と考えて、Streamlit製UIにも動画用の設定項目を追加することにしました。 「動画を作らない日や、そもそも動画を扱わない利用者もいるため、動画設定を行わなくてもエラーにならないこと」です。

単に「入力必須のフォームを追加する」だけでは済まず、空欄という状態を正常系の一つとして扱う設計が必要となります。

この記事で書くこと

  • 画像・動画で処理単位が違う場合のボタン設計の考え方
  • Python側での「空欄なら安全にスキップ」ガードの実装
  • ログ確認ページのプロセス名フィルタで踏んだ落とし穴

ボタンを分けるか、1つにまとめるか

UIを設計する際、まず迷ったのが「画像解析開始」ボタンと「動画解析開始」ボタンを分けるべきかという点でした。 一括で処理したいと考えることもあり得ますが、ほとんどは分けて処理することが多いと考えて、ボタンは分けることにしました。

ページボタン構成理由
AI解析「画像解析開始」「動画解析開始」の2つ画像解析開始:run_analyze_ui.bat、動画解析開始:run_analyze_video_ui.batが処理される
OK/NG振り分け「振り分け開始(画像+動画)」の1つrun_furiwake_ui.batで画像、動画を処理する
手動削除チェック「チェック開始(画像+動画)」の1つrun_shudou_delete_ui.batで画像、動画を処理する

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

画像側の設定は「空欄なら既存値を保持」、動画側の設定は「空欄ならそのまま空欄で保存」と、あえて挙動を変えています。 動画を使わないという意思を明示的に残せるようにするためです。

画面上の画像解析、動画解析処理は下図のようなイメージとなり、別々のボタンから処理を開始します。

画面上の画像解析、動画解析処理

画面上のOK/NG振り分け処理は下図のようなイメージとなり、1つのボタンで画像・動画が処理されます。

画面上のOK/NG振り分け処理

手動削除チェック処理も同様に1つのボタンで画像・動画が処理されます。

画面上の手動削除チェック処理

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]ログを出して正常終了する統一ルールにしています。

一つ工夫したのが、「空欄(未設定)」と「設定されているが実体がない」場合を処理ごとに区別したことです。 動画解析では、動画フォルダが未設定、または存在しない場合は「動画を扱わない」ケースも考えられるため、[SKIP]として正常終了させています。 一方、Excelファイルなど、設定されていること自体が処理実行の前提となるものについては、存在しない場合を誤設定と判断して[ERROR]として扱います

# 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

Q. 画像解析と動画解析のボタンは分けるべきですか?
裏側で呼び出すbatファイルが完全に別物かどうかで判断しました。AI解析ページはrun_analyze_ui.bat/run_analyze_video_ui.batが別スクリプトのためボタンを分け、OK/NG振り分けや手動削除チェックは内部で両方処理する設計のためボタンは1つにまとめています。
Q. 動画設定が空欄のとき、Pythonスクリプトはどう振る舞いますか?
GPU(CLIPモデル)を読み込む前の最も早い段階で空欄チェックを入れ、未設定なら[SKIP]ログを出して終了コード0(正常終了)で終わらせています。「そのメディア種別を扱わない」状態を失敗ではなく正常系として扱うためです。
Q. 「空欄」と「設定されているが実体がない」場合は同じ扱いですか?
いいえ、区別しています。未設定の場合は、その処理が不要という意味なので[SKIP]として正常終了させます。一方、Excelファイルなど、設定されていることが前提となるものが存在しない場合は、誤設定の可能性が高いため[ERROR]として扱います。処理によっては、動画フォルダが存在しない場合を「動画を扱わない」と判断して[SKIP]とするなど、用途に応じて扱いを分けています。

前の記事
← 審査結果の収集
次の記事 ハマったポイント集:Ollama衝突からCRLF破損まで→