FaceFusionのWebUIは開くのに、動画をアップロードしてもプレビューがいつまでも表示されない――この症状をWindows 11とRTX 3080 Ti 12GBのローカル環境で切り分けました。最初はコンテンツフィルターの影響を疑いましたが、結論から言えば、主因はそこではありませんでした。ONNX Runtimeが必要とするcuDNN 9のDLLはPC内に存在していたものの、FaceFusionを起動したPythonプロセスから正しく探索・事前ロードできず、CUDA推論がCPUへフォールバックしていたことが本当の原因です。

今回の結論
WebUIは実際に5桁ポートでHTTP 200まで起動し、動画からのプレビュー画像生成にも成功しました。CUDA実セッション、実プレビュー、1496フレームの動画変換まで確認しているため、「画面だけ開いた」という判定ではありません。
CPUフォールバックからCUDA・cuDNN・RAMディスクを経由してFaceFusionの動画プレビューが復旧する流れ
壊れたCPUフォールバック経路から、DLL登録・CUDA推論・RAMディスクを通る正常なプレビュー経路へ切り替えた構成図(gpt-image-2で作成)

症状:アップロードは通るのにプレビューが出ない

対象はFaceFusion 3.6.1のローカルWebUIです。ソース画像と約50秒の720p動画は選択でき、WebUI自体も起動します。しかし、動画プレビューの更新が極端に遅く、利用者から見ると「何も生成されない」状態でした。

この症状では、次の4つを混同しないことが重要です。

  • Webサーバーが開くこと:GradioがHTTP応答できるか。
  • 動画が読めること:OpenCVやFFmpegが指定時刻のフレームを復号できるか。
  • プレビューUIが値を返すこと:FaceFusionのupdate_preview_image()が実画像を生成できるか。
  • GPUで推論できること:ONNX Runtimeの実セッションがCUDAで動いているか。

HTTP 200だけでは後ろ3項目を証明できません。また、onnxruntime.get_available_providers()CUDAExecutionProviderが並んでいても、それはインストールされたONNX RuntimeがCUDA対応ビルドであることを示すだけです。必要DLLを読み込んだ実セッションがGPUで推論できるかは別に確認する必要があります。

最初の仮説:コンテンツフィルターのせいか

FaceFusionには入力フレームを解析する処理があります。そのため「動画を選べるのにプレビューが出ない」という見え方から、コンテンツ解析が止めている可能性を最初に調べました。

今回のローカルラッパーでは、利用ポリシーを管理するための解析関数が明示的に差し替えられており、ストリーム解析と単一フレーム解析はいずれもFalseを返す状態でした。さらに、プレビュー側が保持しているインポート参照にも差し替えが届いていることを確認しました。動画の指定フレームも(720, 1280, 3)の配列として復号できています。

つまり、コンテンツ解析や動画デコードは「疑う価値のある仮説」でしたが、今回の停止点ではありませんでした。ここで調査を終えず、推論バックエンドの標準エラーを追ったことが解決につながりました。

本当の原因:cudnn64_9.dllを実行時に見つけられない

単フレームの実推論を行うと、ONNX Runtimeがcudnn64_9.dllを読み込めず、CUDA Execution Providerの初期化に失敗していました。その後はCPU Execution Providerへフォールバックするため、完全なクラッシュにはなりません。エラー画面が出ず、ただプレビューが非常に遅くなるのが厄介な点です。

環境にはonnxruntime-gpu 1.24.4が入り、ビルド対象はCUDA 12.8でした。必要なCUDA・cuDNN DLLは共有Conda環境のLibrary\binなどに既に存在していました。したがって、重いCUDAやcuDNNを新たに再インストールする必要はありません。問題はファイルの不在ではなく、ローカル仮想環境から共有ランタイムへのDLL探索経路でした。

確認結果意味
動画フレーム復号(720, 1280, 3)入力動画やFFmpegが主因ではない
コンテンツ解析False今回の停止点ではない
Provider一覧CUDAありCUDA対応ビルドの存在だけを示す
修正前の実推論cudnn64_9.dll読込失敗CPUフォールバックの直接証拠
修正後の実セッションCUDA → CPUの順CUDAが実際に初期化された

修正1:PowerShell側で共有DLLディレクトリを渡す

ランチャーでは、既存の共有Conda環境からTensorRT、cuDNN、CUDA Runtime、Library\binの候補を列挙し、存在するディレクトリだけをPATHと専用環境変数へ渡します。

$dllDirectories = @()
if ($condaPrefix) {
    $sitePackages = Join-Path $condaPrefix 'Lib\site-packages'
    $dllDirectories = @(
        (Join-Path $sitePackages 'tensorrt_libs'),
        (Join-Path $sitePackages 'nvidia\tensorrt\bin'),
        (Join-Path $sitePackages 'nvidia\cudnn\bin'),
        (Join-Path $sitePackages 'nvidia\cuda_runtime\bin'),
        (Join-Path $condaPrefix 'Library\bin')
    ) | Where-Object { Test-Path -LiteralPath $_ }
}

$pathEntries = @($dllDirectories) + @($env:PATH -split ';')
$env:PATH = ($pathEntries | Where-Object { $_ } | Select-Object -Unique) -join ';'
$env:FACEFUSION_DLL_DIRECTORIES =
    ($dllDirectories | Select-Object -Unique) -join ';'

PATHだけでも動く構成はありますが、Python 3.8以降のWindows DLL探索や、ライブラリ側の読み込み順によっては不安定です。そのため次のPython側の登録と組み合わせました。

修正2:FaceFusionをimportする前にDLLを登録・事前ロードする

重要なのは処理順です。FaceFusionや推論モジュールを読み込んだ後では遅いため、エントリーポイントの先頭でDLLディレクトリを登録し、cudnn64_9.dllがある完全なランタイムをONNX Runtimeへ事前ロードします。

_DLL_DIRECTORY_HANDLES: list[Any] = []

def _configure_windows_gpu_runtime() -> None:
    if os.name != 'nt':
        return

    dll_directories = os.environ.get(
        'FACEFUSION_DLL_DIRECTORIES', ''
    ).split(os.pathsep)

    for dll_directory in dll_directories:
        if dll_directory and os.path.isdir(dll_directory):
            _DLL_DIRECTORY_HANDLES.append(
                os.add_dll_directory(dll_directory)
            )

    for dll_directory in dll_directories:
        if os.path.isfile(os.path.join(dll_directory, 'cudnn64_9.dll')):
            import onnxruntime
            onnxruntime.preload_dlls(directory=dll_directory)
            break

_configure_windows_gpu_runtime()

from facefusion import content_analyser, core

os.add_dll_directory()の戻り値をグローバル配列へ保持している点も大切です。戻り値のハンドルが破棄されると、登録した探索ディレクトリが早期に閉じられる可能性があります。「関数を呼んだのに後段でまた見失う」状態を避けるため、プロセス寿命まで保持します。

修正3:通常のWebUIではTensorRTを先頭にしない

TensorRTは定常処理を高速化できる一方、初回にはエンジンの構築やキャッシュ作成が入り、プレビュー更新が止まったように見えることがあります。そこで通常利用のbalancedプロファイルはcuda, cpuとし、TensorRTは明示的なthroughputプロファイルへ分離しました。

balanced = @{
    Threads = 4
    Providers = @('cuda', 'cpu')
    PreferRamDisk = $true
    TempFrameFormat = 'jpeg'
    VideoPreset = 'medium'
    VideoQuality = 88
}

quality = @{
    Threads = 2
    Providers = @('cuda', 'cpu')
    PreferRamDisk = $true
    MinimumFreeGiB = 24
    TempFrameFormat = 'png'
    VideoPreset = 'veryslow'
    VideoQuality = 100
    PixelBoost = '1024x1024'
}

日常のプレビューはbalancedで待ち時間を抑え、最終変換はqualityでPNG中間フレーム、品質100、veryslow、1024×1024ピクセルブーストを使います。なお品質100はFaceFusion/エンコーダーの最高設定であり、数学的な可逆圧縮を意味するわけではありません。

RAMディスクは「速くする」前に容量と退避を設計する

動画処理は大量の中間フレームを作ります。今回の高画質変換ではRAMディスクに37.79GiBの空きを確認してから開始し、PNGフレームを配置しました。ランチャーは必要空き容量を下回る場合やディレクトリ作成に失敗した場合、プロジェクト内の通常ディスクへ自動フォールバックします。

RAMディスクは高速ですが、再起動や電源断で消えます。そのため入力素材・最終出力・検証ログはRAMディスクへ置かず、中間フレームだけに限定します。処理後は残留ファイル0件まで確認しました。

WebUIは本当に直ったのか:3段階で確認

1. BATからWebUIを起動

利用者が実際にダブルクリックする入口と同じnewFaceFusion.batを使い、5桁ポート17868で起動しました。結果はHTTP 200で、起動ログにもProviders: cuda, cpu、RAMディスクの空き容量、プレビュー用設定が表示されました。

FaceFusion local profile: balanced
URL: http://127.0.0.1:17868
Providers: cuda, cpu
Threads: 4
Temp: R:\Temp2\NewFaceFusion\temp
Frame format: jpeg
Pixel boost: 512x512

HTTP status: 200

2. 実動画からプレビュー画像を生成

HTTP確認とは別に、WebUIが呼ぶupdate_preview_image()へ実際のソース画像と動画を渡しました。返り値は空ではなく、Gradio管理下にimage.webpが作られ、ファイルの存在と描画内容まで確認できました。これにより、アップロード後のプレビュー生成ロジックが最後まで通ることを確認しています。

3. 動画全体を最高画質プロファイルで変換

約49.8秒、1496フレームの720p動画をqualityプロファイルで最後まで処理しました。処理速度は定常値で3.70fps、FaceFusion処理は約6分43秒、起動から出力検証までの総時間は約7分3秒でした。

修正前の単フレーム約8.17秒、cuDNN読込失敗後にCPUフォールバック
修正後の単フレーム約2.27秒、CUDAエラーなし
動画全体1496フレーム、3.70fps、約6分43秒
出力HEVC Main、1280×720、1496フレーム、AAC 48kHz stereo
整合性映像・音声デコードOK、入力と出力のフレーム数一致

1秒、15秒、30秒、45秒の4地点からQAフレームも抽出し、顔置換の継続性を目視確認しました。最終動画のSHA-256も記録しており、後から同一ファイルか再確認できます。

再発時の切り分けチェックリスト

  1. WebUIのHTTP 200だけで完了にしない:実動画を渡してプレビュー画像ファイルの生成まで確認します。
  2. 動画フレームを直接読む:指定時刻の配列形状が取得できるか確認します。
  3. コンテンツ解析の戻り値と参照先を確認する:関数本体だけでなく、プレビュー側がimport済みの参照も対象です。
  4. 標準エラーでDLL名を探す:cudnn64_9.dllなど、最初のCUDA Provider初期化エラーを見落とさないようにします。
  5. Provider一覧だけを信用しない:モデルから実際にInferenceSessionを作り、実推論を1回通します。
  6. 既存ランタイムを再利用する:DLLが既にあるなら、CUDA一式を重複インストールする前に探索パスと事前ロードを直します。
  7. TensorRTの初回構築を分離する:通常プレビューはCUDA優先、長尺バッチだけTensorRTを選べる構成にします。
  8. RAMディスクに最低空き容量とフォールバックを設ける:高速化より先に、容量不足と揮発性への対策を入れます。

まとめ:見えていた症状と、止まっていた層は違った

今回の「動画を上げてもプレビューが生成されない」という症状は、コンテンツフィルターではなく、CUDA推論の初期化失敗とCPUフォールバックによる待ち時間が主因でした。エラーで止まらず遅くなるため、UIロジックの不具合に見えたのです。

解決の要点は、既存の共有CUDA・cuDNNランタイムをランチャーで列挙し、Python側でFaceFusionのimport前にos.add_dll_directory()onnxruntime.preload_dlls()を実行したことです。加えて、通常のWebUIからTensorRT初回構築を外し、高画質変換だけRAMディスクとPNG中間フレームを使う構成に分離しました。

FaceFusionに限らず、Windows上のGPUアプリで「CUDAは選べるのに妙に遅い」「画面は開くが結果が出ない」という場合、機能フラグやUIだけでなく、DLL探索、実セッション、実推論の3層を順に確認すると原因へ早く到達できます。

利用上の注意:顔置換は、本人の同意を得た素材や自分が権利を持つ素材で利用し、なりすまし、誤認を招く公開、権利侵害に使わないでください。技術的に動くことと、利用が適切であることは別の確認事項です。