``` ├── LICENSE ├── README.md ├── README_column.md ├── images/ ├── framepack_eichi_error_screenshot1.png ├── framepack_eichi_screenshot1.png ├── framepack_eichi_screenshot2.png ├── framepack_eichi_screenshot3.png ├── run_endframe_ichi.bat ├── version/ ├── v1.0/ ├── run_endframe_ichi.bat ├── webui/ ├── endframe_ichi.py ├── v1.1/ ├── run_endframe_ichi.bat ├── webui/ ├── endframe_ichi.py ├── video_mode_settings.py ├── v1.2/ ├── run_endframe_ichi.bat ├── webui/ ├── endframe_ichi.py ├── video_mode_settings.py ``` ## /LICENSE ``` path="/LICENSE" Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2024 FramePack-eichi Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ## /README.md # FramePack-eichi FramePack-eichiは、lllyasviel師の[lllyasviel/FramePack](https://github.com/lllyasviel/FramePack)のフォークであるnirvash氏の[nirvash/FramePack](https://github.com/nirvash/FramePack)を元にして作成された機能追加バージョンです。nirvash氏の先駆的な改良に基づき、細かい機能が多数搭載されています。 ![FramePack-eichi画面1](images/framepack_eichi_screenshot1.png) ## 📘 名称の由来 **Endframe Image CHain Interface (EICHI)** - **E**ndframe: エンドフレーム機能の強化と最適化 - **I**mage: キーフレーム画像処理の改善と視覚的フィードバック - **CH**ain: 複数のキーフレーム間の連携と関係性の強化 - **I**nterface: 直感的なユーザー体験とUI/UXの向上 「eichi」は日本語の「叡智」(深い知恵、英知)を連想させる言葉でもあり、AI技術の進化と人間の創造性を組み合わせるという本プロジェクトの哲学を象徴しています。 つまり叡智な差分画像から動画を作成することに特化した現地改修仕様です。 ## 🌟 主な機能 - **高品質な動画生成**: 単一画像から自然な動きの動画を生成 ※既存機能 - **柔軟な動画長設定**: 1〜20秒の各セクションモードに対応 ※独自機能 - **セクションフレームサイズ設定**: 0.5秒モードと1秒モードを切り替え可能 ※v1.5で追加 - **オールパディング機能**: すべてのセクションで同じパディング値を使用 ※v1.4で追加 - **マルチセクション対応**: 複数のセクションでキーフレーム画像を指定し複雑なアニメーションを実現 ※nirvash氏追加機能 - **セクションごとのプロンプト設定**: 各セクションに個別のプロンプトを指定可能 ※v1.2で追加 - **赤枠/青枠によるキーフレーム画像効率的コピー**: 2つのキーフレームだけで全セクションをカバー可能に ※v1.7で追加 - **プロンプト管理機能**: プロンプトの保存、編集、再利用が簡単 ※v1.3で追加 - **MP4圧縮設定**: 動画のファイルサイズと品質のバランスを調整可能 ※v1.6.2で本家からマージ - **【試験実装中】Hunyuan LoRAサポート**: モデルのカスタマイズによる独自の表現を追加 ※v1.3で追加 - **出力フォルダ管理機能**: 出力先フォルダの指定とOSに依存しない開き方をサポート ※v1.2で追加 - **ログ機能**: 詳細な経過情報や終了時に処理時間を出したりWindowsだと音を出したりします ※独自機能 - **クロスプラットフォーム対応**: Windows以外の環境でも基本機能が使用可能 ※多分 ![FramePack-eichi画面2](images/framepack_eichi_screenshot2.png) ## 📝 最新アップデート情報 (v1.7.1) ### 主要な変更点 #### 1. 内部計算の最適化 - **0.5秒モードの処理改善**: セクション数計算がより正確になり、動画生成の安定性が向上しました。 #### 2. UIの改善 - **動画モード表示の簡素化**: 動画モード名から冗長な括弧表記を削除(例: "10(5x2)秒" → "10秒") - **レイアウトの整理**: UI全体が整理され、より使いやすくなりました ### 終点およびキーフレーム画像の持続性問題(対応検討中) 一部のユーザーから以下の問題が報告されており、現在対応を検討中です: - 生成中止後に再度生成を開始すると、終点およびキーフレーム画像が使用されないことがある - 画像を一度削除して再アップロードすると問題が解決するが、生成を開始するまで問題に気づきにくい - この問題が確認された場合、一度画像を閉じて再度アップロードし直してください - v1.5.1でStartボタンを押した場合に明示的に画像を再取得するように見直し実施しました ### Hunyuan LoRA対応状況 暫定対応状態となります: - v1.6ではLoRA適用ロジックが統一され、高VRAMモードと低VRAMモードで同じ直接適用方式を使用 - VRAM管理の基準値が変更され(60GB→100GB)、より多くのユーザーが低VRAMモードで動作可能に - 使用にはVRAM16GBでも少し厳しめだが、処理自体よりも開始前のディスク読込の方が長い。メモリは多め推奨 - VRAMの多いPCで正常に動作しないという問い合わせも来ており、抜本見直しも検討中です ## 💻 インストール方法 ### 前提条件 - Windows 10/11(Linux/Macでも基本機能は多分動作可能) - NVIDIA GPU (RTX 30/40シリーズ推奨、最低8GB VRAM) - CUDA Toolkit 12.6 - Python 3.10.x - 最新のNVIDIA GPU ドライバー ※ Linuxでの動作はv1.2で強化され、オープン機能も追加されましたが、一部機能に制限がある場合があります。 ### 手順 #### 公式パッケージのインストール まず、元のFramePackをインストールする必要があります。 1. [公式FramePack](https://github.com/lllyasviel/FramePack?tab=readme-ov-file#installation)からWindowsワンインストーラーをダウンロードします。 「Click Here to Download One-Click Package (CUDA 12.6 + Pytorch 2.6)」をクリックします。 2. ダウンロードしたパッケージを解凍し、`update.bat`を実行してから`run.bat`で起動します。 `update.bat`の実行は重要です。これを行わないと、潜在的なバグが修正されていない以前のバージョンを使用することになります。 3. 初回起動時に必要なモデルが自動的にダウンロードされます(約30GB)。 既にダウンロード済みのモデルがある場合は、`framepack\webui\hf_download`フォルダに配置してください。 4. この時点で動作しますが、高速化ライブラリ(Xformers、Flash Attn、Sage Attn)が未インストールの場合、処理が遅くなります。 ``` Currently enabled native sdp backends: ['flash', 'math', 'mem_efficient', 'cudnn'] Xformers is not installed! Flash Attn is not installed! Sage Attn is not installed! ``` 処理時間の違い: ※RAM:32GB、RXT4060Ti(16GB)の場合 - ライブラリ未インストール時: 約4分46秒/25ステップ - ライブラリインストール時: 約3分17秒〜3分25秒/25ステップ 5. 高速化ライブラリをインストールするには、[Issue #138](https://github.com/lllyasviel/FramePack/issues/138)から`package_installer.zip`をダウンロードし、解凍してルートディレクトリで`package_installer.bat`を実行します(コマンドプロンプト内でEnterを押す)。 6. 再度起動してライブラリがインストールされたことを確認します: ``` Currently enabled native sdp backends: ['flash', 'math', 'mem_efficient', 'cudnn'] Xformers is installed! Flash Attn is not installed! Sage Attn is installed! ``` 作者が実行した場合、Flash Attnはインストールされませんでした。 注: Flash Attnがインストールされていなくても、処理速度にはほとんど影響がありません。テスト結果によると、Flash Attnの有無による速度差はわずかで、「Flash Attn is not installed!」の状態でも約3分17秒/25ステップと、すべてインストールされている場合(約3分25秒/25ステップ)とほぼ同等の処理速度を維持できます。 Xformersが入っているかどうかが一番影響が大きいと思います。 #### FramePack-eichiのインストール 1. `run_endframe_ichi.bat`をFramePackのルートディレクトリに配置します。 2. 以下のファイルとフォルダを`webui`フォルダに配置します: - `endframe_ichi.py` - メインアプリケーションファイル - `eichi_utils` フォルダ - ユーティリティモジュール(v1.3.1で見直し、v1.6.2でUI関連モジュール追加) - `__init__.py` - `frame_calculator.py` - フレームサイズ計算モジュール - `keyframe_handler.py` - キーフレーム処理モジュール - `keyframe_handler_extended.py` - キーフレーム処理モジュール - `preset_manager.py` - プリセット管理モジュール - `settings_manager.py` - 設定管理モジュール - `ui_styles.py` - UIスタイル定義モジュール(v1.6.2で追加) - `video_mode_settings.py` - 動画モード設定モジュール - `lora_utils` フォルダ - LoRA関連モジュール(v1.3で追加、v1.3.2で改良) - `__init__.py` - `dynamic_swap_lora.py` - LoRA管理モジュール(フック方式は廃止され、直接適用方式のみサポート) - `lora_loader.py` - LoRAローダーモジュール - `lora_check_helper.py` - LoRA適用状況確認モジュール(v1.3.2で追加) 3. `run_endframe_ichi.bat`を実行すると、FramePack-eichiのWebUIが起動します。 #### Linux向けインストール方法 Linuxでは、以下の手順で実行可能です: 1. 上記の必要なファイルとフォルダをダウンロードして配置します。 2. ターミナルで次のコマンドを実行します: ```bash python endframe_ichi.py ``` #### Google Colab向けインストール方法 - [Google Colab で FramePack を試す](https://note.com/npaka/n/n33d1a0f1bbc1)をご参考ください #### Mac(mini M4 Pro)向けインストール方法 - [話題のFramePackをMac mini M4 Proで動かしてみた件](https://note.com/akira_kano24/n/n49651dbef319)をご参考ください ## 🚀 使い方 ### 基本的な動画生成 ※既存機能 1. **画像をアップロード**: 「Image」枠に画像をアップロード 2. **プロンプトを入力**: キャラクターの動きを表現するプロンプトを入力 3. **設定を調整**: 動画長やシード値を設定 4. **生成開始**: 「Start Generation」ボタンをクリック ### 高度な設定 - **生成モード選択**: ※独自機能 - **通常モード**: 一般的な動画生成 - **ループモード**: 最終フレームが最初のフレームに戻る循環動画を生成 - **オールパディング選択**: ※v1.4で追加、この値が小さいほど1セッションでの動きが激しくなる - **オールパディング**: すべてのセクションで同じパディング値を使用 - **パディング値**: 0〜3の整数値 - **動画長設定**: ※独自機能の拡張 - **1~20秒** [続きはこちら](README_column.md#-高度な設定) ## 🛠️ 設定情報 設定に関する詳細情報は[こちら](README_column.md#-%EF%B8%8F-設定情報)をご覧ください。 ## 🔧 トラブルシューティング ### h11エラーについて ツールを立ち上げ、初回に画像をインポートする際に以下のようなエラーが多数発生することがあります: ※コンソールにエラーが表示され、GUI上では画像が表示されません。 **実際には画像はアップロードされており、サムネイルの表示に失敗しているケースが大半のため、そのまま動画生成いただくことも可能です。** ![FramePack-eichiエラー画面1](images/framepack_eichi_error_screenshot1.png) ``` ERROR: Exception in ASGI application Traceback (most recent call last): File "C:\xxx\xxx\framepack\system\python\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 404, in run_asgi ``` このエラーは、HTTPレスポンスの処理中に問題が発生した場合に表示されます。 前述のとおり、Gradioが起動し終わっていない起動初期の段階で頻発します。 解決方法: 1. 画像を一度「×」ボタンで削除して、再度アップロードしてみてください。 2. 同じファイルで何度も失敗する場合: - Pythonプロセスを完全に停止してからアプリケーションを再起動 - PCを再起動してからアプリケーションを再起動 エラーが継続する場合は、他の画像ファイルを試すか、画像サイズを縮小してみてください。 ### メモリ不足エラー 「CUDA out of memory」や「RuntimeError: CUDA error」が表示される場合: 1. `gpu_memory_preservation` の値を大きくする(例: 12-16GB) 2. 他のGPUを使用するアプリケーションを閉じる 3. 再起動して再試行 4. 画像解像度を下げる(640x640前後推奨) ### 動画表示の問題 一部のブラウザ(特にFirefoxなど)やmacOSで生成された動画が表示されない問題があります: - 症状: Gradio UI上で動画が表示されない、Windowsでサムネイルが表示されない、一部のプレイヤーで再生できない - 原因: `\framepack\webui\diffusers_helper\utils.py`内のビデオコーデック設定の問題 **こちらについては本家のMP4 Compression機能をマージし解消しました** ## 🤝 謝辞 このプロジェクトは以下のプロジェクトの貢献に基づいています: - [lllyasviel/FramePack](https://github.com/lllyasviel/FramePack) - 原作者lllyasviel師の素晴らしい技術と革新性に感謝します - [nirvash/FramePack](https://github.com/nirvash/FramePack) - nirvash氏の先駆的な改良と拡張に深く感謝します ## 📄 ライセンス 本プロジェクトは[Apache License 2.0](LICENSE)の下で公開されています。これは元のFramePackプロジェクトのライセンスに準拠しています。 ## 📝 更新履歴 ### 2025-04-28: バージョン1.7.1 - **内部計算の最適化**: - 0.5秒モードのセクション数計算が正確になり、動画生成の安定性が向上 - フレーム計算パラメータ(latent_window_size)が5から4.5に変更 - **UIの改善**: - 動画モード名の簡素化: 括弧付き表記を削除(例: "10(5x2)秒" → "10秒") - レイアウトの整理: UI全体が整理され、より使いやすくなりました ### 2025-04-28: バージョン1.7.0 - **キーフレーム画像コピー機能の大幅改良**: - 赤枠/青枠による視覚的な区別を導入 - **赤枠(セクション0)** ⇒ すべての偶数番号セクション(0,2,4,6...)に自動コピー - **青枠(セクション1)** ⇒ すべての奇数番号セクション(1,3,5,7...)に自動コピー - 動的セクション数計算の精度向上により、選択した動画長に応じて正確にコピー範囲を調整 - ユーザーが必要に応じてコピー機能のオン/オフを切り替え可能に - **コピー処理の安全性向上**: - セクション境界チェックを強化し、無効な位置へのコピーを防止 - 詳細なログ出力によりコピー動作の追跡が容易に ### 2025-04-27: バージョン1.6.2 - **MP4圧縮設定の追加**: ※本家からマージ - 動画のファイルサイズと品質のバランスを調整可能に - 0〜100の範囲で設定可能(0=無圧縮、16=デフォルト、高い値=高圧縮・低品質) - 黒画面問題対策として最適な設定値を提供 - **コード品質の向上**: - UIスタイル定義を`ui_styles.py`として分離し、保守性と可読性を改善 - CSSスタイルの一元管理によりUI一貫性を向上 - **フレームサイズ計算の微調整**: - 0.5秒モードの計算精度を向上(`latent_window_size=4.5`を使用) - セクション数と動画長の計算精度が向上し、より安定した動画生成が可能に ### 2025-04-26: バージョン1.6.1 - **短時間動画モードの拡充**: - 2秒モード: 60フレーム(2秒×30FPS)、2セクション、コピー不要 - 3秒モード: 90フレーム(3秒×30FPS)、3セクション、キーフレーム0→1にコピー - 4秒モード: 120フレーム(4秒×30FPS)、4セクション、キーフレーム0→1,2にコピー - **v1.5.1の基本構造に回帰**: - オリジナルのモード名表記(カッコ付き)を維持 - キーフレームガイドHTMLを復活 - 元々の関数構造・処理方法を維持 ### 2025-04-26: バージョン1.6.0 ※却下版 - **UI/UX改善**: - セクション設定をアコーディオンとして分離し、必要時のみ展開可能に - 動画モード名の簡素化(例:「10(5x2)秒」→「10秒」) - キーフレーム強調表示の削除 - 不要な機能(EndFrame影響度調整、キーフレーム自動コピー機能)を内部設定に変更 - **技術的な改善**: - VRAM管理の基準値変更(60GB→100GB) - セクション計算ロジックの改善(直接秒数から計算する機能追加) - フレーム数・セクション数の一貫性向上(表示秒数との整合性確保) - デバッグ出力の最適化 ### 2025-04-25: バージョン1.5.1 - **短い動画生成向けの「1秒」モードを追加**: - 1秒(約30フレーム @ 30fps)に対応 - **デフォルト動画長を変更**: - 「6秒」から「1秒」にデフォルト値を変更 - **入力画像のコピー動作の最適化**: - 通常モードでの入力画像からのコピー処理を停止 - ループモードでのみLastにコピーする処理を有効化 - **キーフレーム自動コピー機能のデフォルト値を変更**: - デフォルトでオフに設定し、より細かな制御を可能に - 必要な場合はオンにすることで自動コピー可能 - **画像処理の安定性向上**: - Startボタンを押した時に画像が正しく再取得されるよう改善 - プレビュー画像の明示的なクリア処理を追加 ### 2025-04-24: バージョン1.5.0 - **フレームサイズ設定の追加**: - 0.5秒モードと1秒モードを切り替え可能 - フレームサイズによってlatent_window_sizeを動的に調整 ### 2025-04-24: バージョン1.4.0 - **オールパディング機能の追加**: - すべてのセクションで同じパディング値を使用 - この値が小さいほど1セッションでの動きが激しくなる ### 2025-04-24: バージョン1.3.3 - **LoRA適用機能の再見直し**: - 直接LoRAロードモードはパラメタの設定にキー一致が必要なため却下。DynamicSwapに統一して様子見 - 取得したパラメタ数のログ表示実装 ### 2025-04-24: バージョン1.3.2 - **LoRA適用機能の統一**: - 低VRAMモード(DynamicSwap)でも高VRAMモードと同様に直接LoRAを適用する方式に統一 - フック方式を廃止し、より安定した直接適用方式のみをサポート - 互換性のためにインターフェースは維持しつつ、内部実装を改善 - **デバッグ・検証機能の強化**: - LoRAの適用状況を確認するための専用ユーティリティを追加(lora_check_helper.py) - 詳細なログ出力とデバッグ情報の提供 ### 2025-04-24: バージョン1.3.1 - **コードベースのリファクタリング**: 保守性と拡張性向上のため、コードを複数のモジュールに整理 - `eichi_utils`: キーフレーム処理、設定管理、プリセット管理、ビデオモード設定を管理 - `lora_utils`: LoRA関連の機能を集約 ### 2025-04-23: バージョン1.3 - **【調査中】Hunyuan LoRAサポートの追加**: モデルをカスタマイズして独自の表現を追加 - **【試験実装】EndFrame影響度設定の追加**: 最終フレームの影響力を0.01〜1.00の範囲で調整可能に(nirvash氏の知見) ### 2025-04-23: バージョン1.2 - **「20(4x5)秒」モードを追加**: 5パート構成の長時間動画生成に対応 - **セクションごとのプロンプト機能を追加**: 各セクションに個別のプロンプトを設定可能に【試験実装】 - **出力フォルダ管理機能を強化**: 出力先フォルダの指定とOSに依存しない開き方をサポート - **設定ファイル管理の改善**: JSONベースの設定ファイルで設定を永続化 - **クロスプラットフォーム対応の強化**: Windows以外の環境での動作を改善 ### 2025-04-22: バージョン1.1 - **「16(4x4)秒」モードを追加**: 4パート構成の長時間動画生成に対応 - **生成処理中の進捗表示を改善**: セクション情報(例:「セクション: 3/15」)が表示されるようになり、特に長い動画生成時の進捗が把握しやすくなりました - **設定ファイルの構造化**: 動画モード設定が別ファイルに分離され、拡張性が向上 ### 2025-04-21: 初版リリース - プロンプト管理機能の最適化 - キーフレームガイド機能の追加 --- **FramePack-eichi** - Endframe Image CHain Interface より直感的で、より柔軟な動画生成を目指して ## /README_column.md # FramePack-eichi 拡張ドキュメント このドキュメントはFramePack-eichiのメインREADMEの詳細版として、各機能や設定の詳細情報を提供します。 ## 🌟 高度な設定 ### キーフレーム設定 ※nirvash氏追加機能 - **Image**: メインの開始キーフレーム - **Final Frame**: 最終フレーム(オプション) - **セクション設定**: 各セクションのキーフレーム画像とプロンプトを個別設定可能 ### キーフレーム自動コピー機能 ※v1.7で強化 - **赤枠(セクション0)**: 偶数番号セクション(0,2,4,6...)に自動コピー - **青枠(セクション1)**: 奇数番号セクション(1,3,5,7...)に自動コピー - チェックボックスでオン/オフ切替可能 - これにより2つのキーフレーム設定だけで全セクションをカバー可能 ### セクションごとのプロンプト ※v1.2で追加【試験実装】 - 各セクションに個別のプロンプトを設定可能 - セクション固有のプロンプトは、そのセクションの生成時のみ使用される - 空白の場合は共通プロンプトが使用される - 注意: この機能は試験実装であり、効果の保証はできません ### Hunyuan LoRAの設定 ※v1.3で追加【調査中】 - 「LoRAを使用する」チェックボックス: LoRAの有効/無効を切り替え - LoRAファイル選択: 使用するLoRAファイルを選択 - 適用強度スライダー: LoRAの影響度を0.0〜1.0で調整 - フォーマット選択: HunyuanVideo/Diffusers/Musubiなどのフォーマットを選択 - 注意: Hunyuan LoRA使用時はプログレスバーが始まる前に読込のための待ち時間が発生し、全体の処理時間が長くなります - LoRAは種類が多様でありサンプル数も少ないため、有識者の方は試行願います ### 出力フォルダ設定 ※v1.2で追加 - 出力先フォルダ名を指定可能 - 「保存および出力フォルダを開く」ボタンで、設定保存とフォルダオープンが可能 - 設定はアプリ再起動時も保持される ### MP4圧縮設定 ※v1.6.2で本家からマージ - スライダーで0〜100の範囲で設定可能(0=無圧縮、16=デフォルト、高い値=高圧縮・低品質) - 数値が小さいほど高品質になりますが、ファイルサイズは大きくなります - 黒画面が出る場合は16に設定することで解決できる場合があります ![FramePack-eichi画面3](images/framepack_eichi_screenshot3.png) ## 🧠 キーフレーム画像設定の仕組みと考え方 ### FramePackの動作原理 FramePackの最大の特徴は「未来から過去へ」というユニークな動画生成アプローチにあります。一般的な動画生成AIは最初のフレームから順番に未来へ生成していくため、長い動画になるほど画質の劣化や一貫性の低下が起きてしまいます。 FramePackでは、まず入力画像から最終フレームを生成し、そこから逆方向に各フレームを作成していきます。これにより、長時間の動画でも高い画質と一貫性を維持できます。 ### FramePack-eichiの拡張機能 FramePack-eichiでは、複数のキーフレーム画像を戦略的に配置することで、さらに品質を向上させています: 1. **最終セクションの急激な変化を防止**: - 元のendframeでの初回(最後の1秒)セクションのみの画像セットは、最終セクション(最初の1秒付近)で急に画像が変わる問題がありました - FramePack-eichiは、ならば全部のセクションにキーフレーム画像をぶち込んでやろうという安直な力業に出ています - 特に重要なキーフレームを赤枠で強調表示し、これらに画像を設定することで、自動コピーするようにしています - FramePackは上述の通り、最終セクションから動画を生成するため、セクションの並び及びキーフレーム場増も最後尾から設定する形になります - 6秒モードではFramePackが本気を出すのが間に合わず、ループモードのキーフレーム画像までに到達せずにループが終わることもあります - 8秒モードでは6秒モードよりも画像の遷移が緩やかになります - どちらの場合も(後述の複数シーン用も含む)、差分の画像が大きい程、動きの変化が大きくなり、より滑らかな動きを生成できます 2. **ループ機能の最適化**: - ループモードでは、最初のキーフレームがFinal Frameに自動コピーされます - v1.5.1から入力画像からの通常モードでのコピー処理は停止され、ループモードのみ画像コピーが有効になっています - キーフレーム画像1にループの開始姿勢を設定することで、滑らかな循環動画を作成できます 3. **セクションごとのプロンプト設定**:※v1.2で追加【試験実装】 - 各セクションに固有のプロンプトを設定することで、セクションごとに異なる動きや表現を実現 - 例えば、「歩く」→「座る」→「手を振る」といった動きの変化を自然に表現可能 - プロンプトの影響は微妙ですが、キーフレーム画像と組み合わせることで効果的 4. **短時間動画モードの追加**:※v1.6.1で追加 - 1秒、2秒、3秒、4秒の短時間動画モードに対応 - それぞれのモードに最適化されたセクション数とコピーパターンを設定 - 短時間での表現に特化した制御が可能に ### 基本的なキーフレーム画像の関係性について **Image(入力画像)、Final Frame(最終フレーム)とキーフレーム画像の関係**: 1. **優先順位について**: - 基本的には、一番最後のセクション以外は、直線に生成した前のセクションの画像を元にします - 今回のセクションにキーフレーム画像が設定されている場合はそれを、されていない場合は他の画像から推測される中間状態が使用されます - **最終のセクションにキーフレーム画像が設定されている場合、Imageより優先されます。** このような構造により、細かなセクションごとの制御が可能になり、より自然で一貫性のある動きを実現しています。 ### v1.7での革新:赤枠/青枠キーフレームシステム v1.7ではキーフレーム画像のコピー機能が大幅に改良され、より効率的で直感的なシステムが導入されました: 1. **赤枠/青枠による視覚的区別**: - **赤枠(セクション0)**: すべての偶数番号セクション(0,2,4,6...)に自動コピー - **青枠(セクション1)**: すべての奇数番号セクション(1,3,5,7...)に自動コピー 2. **キーフレーム設定の効率化**: - わずか2つのキーフレーム設定だけで全セクションをカバー可能に - 以前は各セクションに個別に設定する必要があったものが、パターンベースの自動コピーにより大幅に効率化 3. **動的セクション数への対応**: - 選択した動画長とフレームサイズに基づいて正確にセクション数を計算 - 計算されたセクション数に合わせて自動的にコピー先を調整 4. **チェックボックスによる柔軟な制御**: - キーフレーム自動コピー機能のオン/オフを簡単に切り替え可能 - 複雑な動画では必要に応じてオフにして、各セクションを個別に制御することも可能 このシステムにより、特に長い動画(10秒以上)の作成時に、キーフレーム設定の手間が大幅に削減されます。 ### プロンプト設定のコツ キーフレーム画像と同様に重要なのがプロンプト設定です: 1. **プロンプトの基本構造**: - 主体 → 動き → その他要素の順で記述すると効果的 - 例: `The character walks gracefully, with clear movements, across the room.` 2. **動きの指定レベル**: - プロンプトなし: ほとんど動きが生まれません - 簡単な動き: `moves back and forth, side to side`程度でも基本的な動きは生成されます - 具体的な動き: `dances powerfully, with clear movements, full of energy`のように詳細に指定すると、より複雑な動きが生成されます 3. **注意点**: - 「ダンス」など大きな動きを表す単語を使うと、予想以上に大げさな動きになることがあります - 実用的なプロンプトの例: - 穏やかな動き: `The character breathes calmly, with subtle body movements.` - 中程度の動き: `The character walks forward, gestures with hands, with natural posture.` - 複雑な動き: `The character performs dynamic movements with energy and flowing motion.` 4. **プロンプトの深層構造(LLAMAとCLIPの分離)**: - FramePack内部では、プロンプトが以下の2つの異なるモデルで処理されます: - **LLAMAモデル(256トークン制限)**: - テキストの詳細な理解と文脈処理を担当 - ビデオの全体的な内容とシーケンスの制御に使用 - 文字数目安: 約1000-1300文字(英語)または200-400文字(日本語) - シーンの文脈や物語性の制御に関わる - **CLIPモデル(77トークン制限)**: - 画像とテキストの関連付けに特化したモデル - ビデオフレームの具体的な視覚的特徴の生成に影響 - 文字数目安: 約300-400文字(英語)または50-150文字(日本語) - スタイル、被写体、視覚的属性の制御に関わる 5. **効果的なプロンプト記述のストラテジー**: - **最初の300-400文字(英語)/50-150文字(日本語)**: - LLAMAとCLIPの両方で処理される重要な「視覚パート」 - ここには主な視覚的要素、スタイル、被写体、全体的なトーンを記述 - 例: `A young woman with long flowing hair, cinematic lighting, detailed facial features, soft expressions, gentle movements` - **後半の600-900文字(英語)/150-250文字(日本語)**: - LLAMAのみで処理される「物語パート」 - ここには動きの詳細、シーンの文脈、シーケンス情報を記述 - 例: `The camera slowly pans from left to right. The woman gradually turns her head, her expressions changing from neutral to a slight smile. There is a sense of emotional buildup as if emotional music is playing in the background.` 6. **セクションごとのプロンプト活用法**: ※v1.2で追加【試験実装】 - セクション固有のプロンプトは簡潔に、そのセクションで重要な動きに焦点を当てる - 長い文章よりも、明確で具体的な指示が効果的 - 例: セクション1「歩く動作」、セクション2「座る動作」、セクション3「手を振る動作」 - 注意: セクションプロンプトの効果は微妙であり、画像設定との組み合わせが重要 7. **LoRAを活用したスタイル調整**: ※v1.3で追加【試験実装】 - LoRAの選択とプロンプトを組み合わせることで、特定のスタイルや表現を強調 ※多分 - LoRAの効果は適用強度で調整可能(0.1-0.3は微妙な効果、0.5-0.8は顕著な効果) - プロンプトとLoRAの選択が一致すると効果が最大化 ### 効果的な差分画像の選択 FramePackの動画生成品質は、キーフレーム画像の選択に大きく左右されます。理想的な差分画像を選ぶために重要なポイント: 1. **最適な差分レベル**: - **過度に小さい差分**: ほとんど同じ画像(「いわゆる叡智な差分」)を使用すると、ほとんど動きが生成されません - **過度に大きい差分**: 全く関連性のない画像を使うと、自然な動きにつながりません - **理想的な差分**: 同じキャラクターの異なるポーズなど、AIが関連性を見出せる程度の変化が最適 2. **関連性の保持**: - 例えば単に画像を左右反転させたものは、AIによって全く異なる画像と認識され、自然な動きにつながりません - 顔の向き、手の位置、体のポーズなどの変化は理想的な差分要素です - 背景や服装は可能な限り一貫性を保つことで、AIがキャラクターの動きに集中できます - 奇しくも画像生成AIにて似通ったプロンプトで作成したキャラクターの揺らぎが理想的な差分要素の一つです 3. **理想的な差分画像の特徴**: - 同一キャラクターが軽く体勢を変えたもの - 表情の微妙な変化(無表情→微笑み、など。ただし、顔の位置が変わらないと動きが弱い) - 手や腕の自然な動きを伴うポーズの変化 - 頭の向きの緩やかな変化 4. **実験的アプローチ**: - 差分画像の選択は科学というよりも美術的な側面が強いため、トライ&エラーが重要 - 始めは似たポーズの差分で試し、徐々に差分の大きさを調整していくのが効果的 - 成功した組み合わせをメモしておくことで、将来の作品に応用できます 5. **画像生成AIとの組み合わせ**: - 理想的な差分画像がない場合、画像生成AIを使って同一キャラクターの異なるポーズを生成するのも有効 - プロンプトで指定するポーズの変化は控えめにし、大幅な変化は避けることでより自然な動きを実現 このような構造化されたアプローチは、両方のモデルの強みを最大限に活かし、より表現力豊かな動画を生成するのに役立ちます。 ### EndFrame影響度とオールパディングの違い、基本と実践 #### 二つの機能の根本的な違い FramePack-eichiには動画の動きを制御する二つの重要な機能があります。「EndFrame影響度調整」と「オールパディング」です。この二つは一見類似しているように見えますが、動作原理と効果は全く異なります。 ##### 1. EndFrame影響度調整(v1.3で導入) **何に作用するか**:これは**最終フレーム(Final Frame)自体の強さ**を直接変更します。 **技術的仕組み**: - 最終フレームの潜在表現(latent representation)に指定した値を正確に掛け算します - コード上では `modified_end_frame_latent = end_frame_latent * end_frame_strength` として実装されています - 値の範囲は0.01~1.00で、1.00がデフォルト(変更なし)です **効果**: - 値を1.0から0.5に下げると、最終フレームの影響力が「全体的に」正確に半分になります - 値を0.3にすると、最終フレームの影響力が「全体的に」正確に30%になります - 最終フレームの直接的な影響を弱めるため、最初のフレーム(Input Image)の特性がより早く現れるようになります ##### 2. オールパディング(v1.4で導入) **何に作用するか**:これは**各セクション間の接続方法**を変更します。 **技術的仕組み**: - 通常、セクション間のパディング値は `[3, 2, 2, 2, 1, 0]` のように自動計算されます - オールパディングを有効にすると、この値が指定した一つの値に統一されます (例: `[1.5, 1.5, 1.5, 1.5, 1.5, 0]`) - 値の範囲は0.2~3.0で、1.0が標準的な値です - 最終セクション(0番目)は常に0に強制されます **効果**: - 値を1.5など高くすると、各セクションが前のセクションをより強く参照するため、変化が稀になります - 値を0.5など低くすると、各セクションが前のセクションをあまり参照しなくなるため、変化が多くなります - 変化量の「分布」が変わりますが、全体のフレームの強さは変わりません #### 具体的な違いを説明する例え 違いをわかりやすくするため、三つの比喩で説明します: **音楽の例え**: - **EndFrame影響度**:オーケストラの「メインテーマの音量」を調整するようなものです。音量を0.5にすればメインテーマ全体が単純に半分の大きさになります。 - **オールパディング**:「各楽章の間のトランジションの滑らかさ」を調整するものです。強い値ではトランジションが滑らかに、弱い値では各楽章の変化が急になります。 **料理の例え**: - **EndFrame影響度**:「メインの調味料の量」を調整するようなものです。ソース自体の量を0.5にすると、当然食材本来の味が強く出ます。 - **オールパディング**:「各調理工程で前の工程の味をどれだけ引き継ぐか」を調整するものです。高い値では各工程の特徴が更に混ざり、低い値では各工程が個別の味になります。 **グラデーションの例え**: - **EndFrame影響度**:赤から青へのグラデーションで、「赤の原色の強さ」を調整するようなものです。赤の強さを0.5にすると、薄い赤(ピンクに近い)から青へのグラデーションになります。 - **オールパディング**:同じグラデーションで、「色の混ざり方」を調整するものです。高い値では赤から青への変化が積み重なり緩やかに、低い値では赤から紫、青への変化が急になります。 #### 技術的な追加説明 ソースコードレベルにおいても、二つの機能は全く異なる場所で動作しています: ``` // EndFrame影響度調整の実装 // 最終フレームの潜在表現自体に直接乗数を掛ける if end_frame_strength != 1.0: modified_end_frame_latent = end_frame_latent * end_frame_strength history_latents[:, :, 0:1, :, :] = modified_end_frame_latent else: history_latents[:, :, 0:1, :, :] = end_frame_latent ``` ``` // オールパディングの実装 // セクション間のパディング配列を統一値で上書き if use_all_padding: padding_value = round(all_padding_value, 1) # 小数点1桁に固定 latent_paddings = [padding_value] * total_latent_sections # 最後のセクションは強制的に0 latent_paddings[-1] = 0 else: # 通常のパディング値計算 latent_paddings = reversed(range(total_latent_sections)) if total_latent_sections > 4: latent_paddings = [3] + [2] * (total_latent_sections - 3) + [1, 0] ``` このように、EndFrame影響度は最終フレームの潜在表現そのものに単純乗算を行いますが、オールパディングは各セクション間のパディング配列を同じ値で書き換える最適化処理を行っています。 #### 組み合わせた場合の効果 この二つのパラメータはまったく別の仕組みで動作するため、以下のように組み合わせて使用できます: 1. **EndFrame影響度0.5 + 通常パディング**: - 最終フレームの影響が半減する - 各セクション間の移行は標準的 - 結果:最終フレームから最初のフレームへの移行が全体的に早くなる 2. **EndFrame影響度1.0 + オールパディング1.5**: - 最終フレームの影響は通常通り - セクション間の移行がより滑らかに - 結果:セクション分けがあまり目立たない、純粋な移行になる 3. **EndFrame影響度0.3 + オールパディング0.5**: - 最終フレームの影響が大幅に弱まる - 各セクションが独立性を強め、変化量が増える - 結果:非常に活発な動きと早い変化が生まれる #### EndFrame影響度とオールパディングの選択ガイド シーンに応じた適切な設定値のガイドライン: ##### EndFrame影響度の適切な値 - **大きな差分の画像**:0.3〜0.6(緩やかな変化を実現) - **中程度の差分**:0.5〜0.8(バランスの取れた移行) - **小さな差分**:0.8〜1.0(標準に近い影響度) - **顔の表情変化**:0.7〜0.8(自然な表情の移り変わり) - **体や手の大きな動き**:0.3〜0.5(より自然な中間フレーム) ##### オールパディングの適切な値 - **滑らかなトランジション**:1.5〜2.0(セクション境界が目立たない純粋な移行) - **標準的な動き**:1.0(バランスの取れた移行) - **活発な動き**:0.5〜0.7(各セクションでの変化が大きくなる) - **極端な動き**:0.2〜0.4(非常に活発で予測不能な動きを生成) ##### 実践的な活用テクニック - **影響度0.5 + オールパディング0.5**: よりダイナミックな動きが欲しい場合 - **影響度0.3 + 短時間モード**: 素早い変化のループアニメーション作成時 - **影響度0.8 + 長時間モード**: ゆっくりと変化する穏やかな動きの表現 - **超低影響度(0.01〜0.1)**: 最終フレームをほぼ無視し、最初のフレームを「目標」とする発想の転換 - **高いオールパディング値(2.0以上) + 影響度0.5**: セクションの境界を目立たずに最終フレームの影響を弱めます ##### 調整プロセスと実験。環境 - まずはデフォルト値(1.0)で生成 - 動きが不自然な場合、値を0.7程度に下げて試す - さらに調整が必要なら0.5、0.3と段階的に実験 - 同じ画像ペアで影響度だけを変えて比較検証するのも効果的 - 結果の探求には、続けてオールパディング値を変更してみるとさらに指定した通りの動きに近づくでしょう EndFrame影響度とパディング値は、料理に例えると「最初に使う調味料の量」と「各工程での前の段階からの参照量」のような関係です。両方を適切に組み合わせることで、あなたが目指す理想的な動きを実現できます。 ## 🛠️ 設定情報 ### 基本設定 (Windows用batファイル) - **ポート設定**: `--port` パラメータ(デフォルト: 8001) - WebUIが使用するポート番号 - 他のアプリケーションと競合する場合は変更してください - **サーバーアドレス**: `--server` パラメータ(デフォルト: '127.0.0.1') - ローカルネットワーク内でアクセスする場合は `0.0.0.0` に変更 - **自動ブラウザ起動**: `--inbrowser` オプション - 起動時に自動的にブラウザを開きます ### パフォーマンス設定 - **GPUメモリ保存設定**: `gpu_memory_preservation` スライダー(デフォルト: 10GB) ※既存機能 - 小さい値 = より多くのVRAMを使用 = 高速処理 - 大きい値 = より少ないVRAMを使用 = 安定動作 - 仕組み: 設定値が小さいほどトランスフォーマーモデル用により多くのVRAMを解放 - 計算方法: VRAM上限(マージン込み)から設定値を引いた残りがツールの使用可能VRAM量(最低6GBは確保) - 例: VRAM 16GBの場合、マージンを入れて14GBとすると「14-(10-6)=10GB」を使用 - 下限値の6GBに設定すると「14-(6-6)=14GB」となり、ほぼ上限まで使用(1セクション約10秒処理時間短縮、メモリスワップリスクあり) - 推奨値: - 8GB VRAM: 7-8GB - 12GB VRAM: 6-8GB - 16GB以上: 6GB前後 - 注意: 他のアプリケーションを同時実行している場合は値を大きくしてください - このツールではメモリスワップ対策にバックグラウンドで他の画像生成系ツールが動いていいよう3GBのマージンを確保しています - LoRAを使用する場合、更にマージンを用意した方が良いでしょう - **高VRAMモード**: 自動検出(v1.5.1: 60GB以上、v1.6: 100GB以上の空きVRAMがある場合)※機能改善 - 有効時: モデルを常にGPUに保持し、メモリ転送のオーバーヘッドを削減 - 効果: 最大20%の処理速度向上 - v1.6では基準値が引き上げられ、ほとんどの環境で低VRAMモードが使用されるようになりました - 低VRAMモードでも高VRAMモードと同じ直接適用方式が使用され、機能の一貫性が向上しています ### 生成設定 - **フレームサイズ設定**: `frame_size` ドロップダウンメニュー(デフォルト: 1秒) ※v1.5で追加 - 0.5秒: 0.5秒分のフレームを生成 セクション回数、処理時間はほぼ倍増します - オールバディング0モードで各フレームの画像に差分を与えることで更に激しい動きが可能となります - 1秒: 1秒分のフレームを生成 - **ステップ数**: `steps` スライダー(デフォルト: 25) ※既存機能 - 値を増やすと品質が向上しますが、処理時間も比例して増加 - 推奨範囲: 20-30(20でもほぼ同等の品質が得られることが多い) - 15以下: 明らかな品質低下が発生 - **TeaCache**: `use_teacache` チェックボックス(デフォルト: 有効) ※既存機能 - 有効: 処理が約15-20%高速化 - 副作用: 手や指先などの細部表現が若干劣化する場合あり - 用途: 一般的な動画生成では有効推奨、細部表現が重要な場合は無効化 - **乱数シード値**: `seed` 数値入力または「Use Random Seed」チェックボックス ※nirvash氏追加機能 - 同じシード値 = 再現可能な結果 - ランダムシード: 毎回異なる動き生成 - 注意: プロンプトや画像が変わると同じシードでも結果は変化 - **Distilled CFG Scale**: `gs` スライダー(デフォルト: 10.0) ※既存機能 - 蒸留ガイダンススケール値 - 小さい値 = より自由な動き(プロンプトからの逸脱増加) - 大きい値 = プロンプトに忠実(動きが制限される場合あり) - 推奨: デフォルト値の維持(変更は上級者向け) - **MP4圧縮設定**: `mp4_crf` スライダー(デフォルト: 16) ※v1.6.2で本家からマージ - 範囲: 0〜100(0=無圧縮、100=最大圧縮) - 数値が小さいほど高品質な動画になるが、ファイルサイズは大きくなる - 数値が大きいほど圧縮率が高く、ファイルサイズは小さくなるが、画質は低下 - 黒画面問題が発生する場合は16に設定することで解決できる場合がある - 用途: 保存用なら低い値(0〜10)、Web共有用なら中程度の値(16〜30) ### LoRA設定 (v1.3で追加、v1.6で改良) - **LoRA使用**: `use_lora` チェックボックス(デフォルト: 無効) - 有効: LoRAファイルを使用してモデルをカスタマイズ - LoRA使用時はカウンターが始まる前の待ち時間が長くなる場合があります - **LoRAファイル**: ファイル選択コンポーネント - 使用するLoRAファイルを指定 - 対応フォーマット: FramePack形式のみ(v1.3.2で確認) - **LoRA強度**: `lora_strength` スライダー(デフォルト: 0.8) - 範囲: 0.0〜1.0 - 小さい値: 軽微な効果 - 大きい値: 強い効果 - 最適値は各LoRAファイルにより異なります - **LoRAフォーマット**: ラジオボタン - HunyuanVideo: Hunyuan Video向けのLoRA形式 - Diffusers: Diffusers形式のLoRA ### フレーム設定 - **動画長**: ラジオボタン + `total_second_length` スライダー ※独自機能の拡張 - **1秒**: 超短時間動画(約30フレーム @ 30fps)- v1.5.1で追加 - **2秒**: 短時間動画(約60フレーム @ 30fps)- v1.6.1で追加 - **3秒**: 短時間動画(約90フレーム @ 30fps)- v1.6.1で追加 - **4秒**: 短時間動画(約120フレーム @ 30fps)- v1.6.1で追加 - **6秒**: 標準モード(約180フレーム @ 30fps) - **8秒**: 標準モード(約240フレーム @ 30fps) - **10秒**: 長時間動画(約300フレーム @ 30fps) - **12秒**: 長時間動画(約360フレーム @ 30fps) - **16秒**: 長時間動画(約480フレーム @ 30fps) - **20秒**: 長時間動画(約600フレーム @ 30fps) - **キーフレーム自動コピー**: `enable_keyframe_copy` チェックボックス(デフォルト: 無効 - v1.5.1で変更) ※独自機能 - 有効: キーフレーム画像が他のセクションに自動コピー - 無効: 各キーフレームを個別に設定する必要あり - 用途: 複雑な動きを設計する上級者は無効にすることもあり ### 出力設定 - **出力フォルダ**: 出力フォルダ設定欄(デフォルト: 'outputs')※v1.2で追加 - 生成された動画と画像の保存先 - 入力欄に直接フォルダ名を入力可能 - 「保存および出力フォルダを開く」ボタンでフォルダを開ける - 設定はJSON形式で保存され、再起動後も維持される - **セクション静止画保存**: `save_section_frames` チェックボックス(デフォルト: 無効) ※nirvash氏追加機能 - 有効: 各セクションの最終フレームが静止画として保存 - 用途: 各セクションの接続部を確認したい場合に有用 - **セクション動画保存**: `keep_section_videos` チェックボックス(デフォルト: 無効) ※独自機能 - 有効: 各セクションの動画ファイルが保持される、「End」で終了した場合は残ります - 無効: 最終的な完成動画のみ保存(中間ファイルは削除)、ゴミ箱に入らないので注意 - 用途: 各セクションの動きを個別に確認したい場合に有用 ### プロンプト管理 - **プリセット保存**: 「保存」ボタン - 名前を付けて現在のプロンプトを保存 - 名前を空にして保存すると起動時デフォルトプロンプトとして設定 - **プリセット適用**: 「反映」ボタン - 選択したプリセットのプロンプトを現在の生成設定に適用 - **プリセット管理**: - 削除: 不要なプリセットを削除(デフォルトプリセットは削除不可) - クリア: 編集フィールドをクリア ## /images/framepack_eichi_error_screenshot1.png Binary file available at https://raw.githubusercontent.com/git-ai-code/FramePack-eichi/refs/heads/main/images/framepack_eichi_error_screenshot1.png ## /images/framepack_eichi_screenshot1.png Binary file available at https://raw.githubusercontent.com/git-ai-code/FramePack-eichi/refs/heads/main/images/framepack_eichi_screenshot1.png ## /images/framepack_eichi_screenshot2.png Binary file available at https://raw.githubusercontent.com/git-ai-code/FramePack-eichi/refs/heads/main/images/framepack_eichi_screenshot2.png ## /images/framepack_eichi_screenshot3.png Binary file available at https://raw.githubusercontent.com/git-ai-code/FramePack-eichi/refs/heads/main/images/framepack_eichi_screenshot3.png ## /run_endframe_ichi.bat ```bat path="/run_endframe_ichi.bat" @echo off call environment.bat cd %~dp0webui "%DIR%\python\python.exe" endframe_ichi.py --server 127.0.0.1 --inbrowser :done pause ``` ## /version/v1.0/run_endframe_ichi.bat ```bat path="/version/v1.0/run_endframe_ichi.bat" @echo off call environment.bat cd %~dp0webui "%DIR%\python\python.exe" endframe_ichi.py --server 127.0.0.1 --inbrowser :done pause ``` ## /version/v1.0/webui/endframe_ichi.py ```py path="/version/v1.0/webui/endframe_ichi.py" from diffusers_helper.hf_login import login import os import random import time # クロスプラットフォーム対応のための条件付きインポート try: import winsound HAS_WINSOUND = True except ImportError: HAS_WINSOUND = False import json from datetime import datetime, timedelta os.environ['HF_HOME'] = os.path.abspath(os.path.realpath(os.path.join(os.path.dirname(__file__), './hf_download'))) import gradio as gr import torch import traceback import einops import safetensors.torch as sf import numpy as np import argparse import math from PIL import Image from diffusers import AutoencoderKLHunyuanVideo from transformers import LlamaModel, CLIPTextModel, LlamaTokenizerFast, CLIPTokenizer from diffusers_helper.hunyuan import encode_prompt_conds, vae_decode, vae_encode, vae_decode_fake from diffusers_helper.utils import save_bcthw_as_mp4, crop_or_pad_yield_mask, soft_append_bcthw, resize_and_center_crop, state_dict_weighted_merge, state_dict_offset_merge, generate_timestamp from diffusers_helper.models.hunyuan_video_packed import HunyuanVideoTransformer3DModelPacked from diffusers_helper.pipelines.k_diffusion_hunyuan import sample_hunyuan from diffusers_helper.memory import cpu, gpu, get_cuda_free_memory_gb, move_model_to_device_with_memory_preservation, offload_model_from_device_for_memory_preservation, fake_diffusers_current_device, DynamicSwapInstaller, unload_complete_models, load_model_as_complete from diffusers_helper.thread_utils import AsyncStream, async_run from diffusers_helper.gradio.progress_bar import make_progress_bar_css, make_progress_bar_html from transformers import SiglipImageProcessor, SiglipVisionModel from diffusers_helper.clip_vision import hf_clip_vision_encode from diffusers_helper.bucket_tools import find_nearest_bucket parser = argparse.ArgumentParser() parser.add_argument('--share', action='store_true') parser.add_argument("--server", type=str, default='127.0.0.1') parser.add_argument("--port", type=int, default=8001) parser.add_argument("--inbrowser", action='store_true') args = parser.parse_args() print(args) free_mem_gb = get_cuda_free_memory_gb(gpu) high_vram = free_mem_gb > 60 print(f'Free VRAM {free_mem_gb} GB') print(f'High-VRAM Mode: {high_vram}') text_encoder = LlamaModel.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='text_encoder', torch_dtype=torch.float16).cpu() text_encoder_2 = CLIPTextModel.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='text_encoder_2', torch_dtype=torch.float16).cpu() tokenizer = LlamaTokenizerFast.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='tokenizer') tokenizer_2 = CLIPTokenizer.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='tokenizer_2') vae = AutoencoderKLHunyuanVideo.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='vae', torch_dtype=torch.float16).cpu() feature_extractor = SiglipImageProcessor.from_pretrained("lllyasviel/flux_redux_bfl", subfolder='feature_extractor') image_encoder = SiglipVisionModel.from_pretrained("lllyasviel/flux_redux_bfl", subfolder='image_encoder', torch_dtype=torch.float16).cpu() transformer = HunyuanVideoTransformer3DModelPacked.from_pretrained('lllyasviel/FramePackI2V_HY', torch_dtype=torch.bfloat16).cpu() vae.eval() text_encoder.eval() text_encoder_2.eval() image_encoder.eval() transformer.eval() if not high_vram: vae.enable_slicing() vae.enable_tiling() transformer.high_quality_fp32_output_for_inference = True print('transformer.high_quality_fp32_output_for_inference = True') transformer.to(dtype=torch.bfloat16) vae.to(dtype=torch.float16) image_encoder.to(dtype=torch.float16) text_encoder.to(dtype=torch.float16) text_encoder_2.to(dtype=torch.float16) vae.requires_grad_(False) text_encoder.requires_grad_(False) text_encoder_2.requires_grad_(False) image_encoder.requires_grad_(False) transformer.requires_grad_(False) if not high_vram: # DynamicSwapInstaller is same as huggingface's enable_sequential_offload but 3x faster DynamicSwapInstaller.install_model(transformer, device=gpu) DynamicSwapInstaller.install_model(text_encoder, device=gpu) else: text_encoder.to(gpu) text_encoder_2.to(gpu) image_encoder.to(gpu) vae.to(gpu) transformer.to(gpu) stream = AsyncStream() outputs_folder = './outputs/' os.makedirs(outputs_folder, exist_ok=True) # プリセット保存用フォルダの設定 webui_folder = os.path.dirname(os.path.abspath(__file__)) presets_folder = os.path.join(webui_folder, 'presets') os.makedirs(presets_folder, exist_ok=True) @torch.no_grad() def worker(input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, save_section_frames, keep_section_videos, section_settings=None): # 処理時間計測の開始 process_start_time = time.time() total_latent_sections = (total_second_length * 30) / (latent_window_size * 4) total_latent_sections = int(max(round(total_latent_sections), 1)) job_id = generate_timestamp() # セクション处理の詳細ログを出力 latent_paddings = reversed(range(total_latent_sections)) if total_latent_sections > 4: latent_paddings = [3] + [2] * (total_latent_sections - 3) + [1, 0] print(f"\u25a0 セクション生成詳細:") print(f" - 生成予定セクション: {list(latent_paddings)}") print(f" - 各セクションのフレーム数: 約{latent_window_size * 4 - 3}フレーム") stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Starting ...')))) try: # セクション設定の前処理 def get_section_settings_map(section_settings): """ section_settings: DataFrame形式のリスト [[番号, 画像, プロンプト], ...] → {セクション番号: (画像, プロンプト)} のdict """ result = {} if section_settings is not None: for row in section_settings: if row and row[0] is not None: sec_num = int(row[0]) img = row[1] prm = row[2] if len(row) > 2 else "" result[sec_num] = (img, prm) return result section_map = get_section_settings_map(section_settings) section_numbers_sorted = sorted(section_map.keys()) if section_map else [] def get_section_info(i_section): """ i_section: int section_map: {セクション番号: (画像, プロンプト)} 指定がなければ次のセクション、なければNone """ if not section_map: return None, None, None # i_section以降で最初に見つかる設定 for sec in range(i_section, max(section_numbers_sorted)+1): if sec in section_map: img, prm = section_map[sec] return sec, img, prm return None, None, None # Clean GPU if not high_vram: unload_complete_models( text_encoder, text_encoder_2, image_encoder, vae, transformer ) # Text encoding stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Text encoding ...')))) if not high_vram: fake_diffusers_current_device(text_encoder, gpu) # since we only encode one text - that is one model move and one encode, offload is same time consumption since it is also one load and one encode. load_model_as_complete(text_encoder_2, target_device=gpu) llama_vec, clip_l_pooler = encode_prompt_conds(prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2) if cfg == 1: llama_vec_n, clip_l_pooler_n = torch.zeros_like(llama_vec), torch.zeros_like(clip_l_pooler) else: llama_vec_n, clip_l_pooler_n = encode_prompt_conds(n_prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2) llama_vec, llama_attention_mask = crop_or_pad_yield_mask(llama_vec, length=512) llama_vec_n, llama_attention_mask_n = crop_or_pad_yield_mask(llama_vec_n, length=512) # Processing input image stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Image processing ...')))) def preprocess_image(img): H, W, C = img.shape height, width = find_nearest_bucket(H, W, resolution=640) img_np = resize_and_center_crop(img, target_width=width, target_height=height) img_pt = torch.from_numpy(img_np).float() / 127.5 - 1 img_pt = img_pt.permute(2, 0, 1)[None, :, None] return img_np, img_pt, height, width input_image_np, input_image_pt, height, width = preprocess_image(input_image) Image.fromarray(input_image_np).save(os.path.join(outputs_folder, f'{job_id}.png')) # VAE encoding stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'VAE encoding ...')))) if not high_vram: load_model_as_complete(vae, target_device=gpu) start_latent = vae_encode(input_image_pt, vae) # end_frameも同じタイミングでencode if end_frame is not None: end_frame_np, end_frame_pt, _, _ = preprocess_image(end_frame) end_frame_latent = vae_encode(end_frame_pt, vae) else: end_frame_latent = None # create section_latents here section_latents = None if section_map: section_latents = {} for sec_num, (img, prm) in section_map.items(): if img is not None: # 画像をVAE encode img_np, img_pt, _, _ = preprocess_image(img) section_latents[sec_num] = vae_encode(img_pt, vae) # CLIP Vision stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'CLIP Vision encoding ...')))) if not high_vram: load_model_as_complete(image_encoder, target_device=gpu) image_encoder_output = hf_clip_vision_encode(input_image_np, feature_extractor, image_encoder) image_encoder_last_hidden_state = image_encoder_output.last_hidden_state # Dtype llama_vec = llama_vec.to(transformer.dtype) llama_vec_n = llama_vec_n.to(transformer.dtype) clip_l_pooler = clip_l_pooler.to(transformer.dtype) clip_l_pooler_n = clip_l_pooler_n.to(transformer.dtype) image_encoder_last_hidden_state = image_encoder_last_hidden_state.to(transformer.dtype) # Sampling stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Start sampling ...')))) rnd = torch.Generator("cpu").manual_seed(seed) num_frames = latent_window_size * 4 - 3 history_latents = torch.zeros(size=(1, 16, 1 + 2 + 16, height // 8, width // 8), dtype=torch.float32).cpu() history_pixels = None total_generated_latent_frames = 0 latent_paddings = reversed(range(total_latent_sections)) if total_latent_sections > 4: # In theory the latent_paddings should follow the above sequence, but it seems that duplicating some # items looks better than expanding it when total_latent_sections > 4 # One can try to remove below trick and just # use `latent_paddings = list(reversed(range(total_latent_sections)))` to compare latent_paddings = [3] + [2] * (total_latent_sections - 3) + [1, 0] for i_section, latent_padding in enumerate(latent_paddings): # 先に変数を定義 is_first_section = i_section == 0 is_last_section = latent_padding == 0 use_end_latent = is_last_section and end_frame is not None latent_padding_size = latent_padding * latent_window_size # 定義後にログ出力 print(f"\n\u25a0 セクション{i_section}の処理開始 (パディング値: {latent_padding})") print(f" - 現在の生成フレーム数: {total_generated_latent_frames * 4 - 3}フレーム") print(f" - 生成予定フレーム数: {num_frames}フレーム") print(f" - 最初のセクション?: {is_first_section}") print(f" - 最後のセクション?: {is_last_section}") # set current_latent here # セクションごとのlatentを使う場合 if section_map and section_latents is not None and len(section_latents) > 0: # i_section以上で最小のsection_latentsキーを探す valid_keys = [k for k in section_latents.keys() if k >= i_section] if valid_keys: use_key = min(valid_keys) current_latent = section_latents[use_key] print(f"[section_latent] section {i_section}: use section {use_key} latent (section_map keys: {list(section_latents.keys())})") print(f"[section_latent] current_latent id: {id(current_latent)}, min: {current_latent.min().item():.4f}, max: {current_latent.max().item():.4f}, mean: {current_latent.mean().item():.4f}") else: current_latent = start_latent print(f"[section_latent] section {i_section}: use start_latent (no section_latent >= {i_section})") print(f"[section_latent] current_latent id: {id(current_latent)}, min: {current_latent.min().item():.4f}, max: {current_latent.max().item():.4f}, mean: {current_latent.mean().item():.4f}") else: current_latent = start_latent print(f"[section_latent] section {i_section}: use start_latent (no section_latents)") print(f"[section_latent] current_latent id: {id(current_latent)}, min: {current_latent.min().item():.4f}, max: {current_latent.max().item():.4f}, mean: {current_latent.mean().item():.4f}") if is_first_section and end_frame_latent is not None: history_latents[:, :, 0:1, :, :] = end_frame_latent if stream.input_queue.top() == 'end': stream.output_queue.push(('end', None)) return print(f'latent_padding_size = {latent_padding_size}, is_last_section = {is_last_section}') indices = torch.arange(0, sum([1, latent_padding_size, latent_window_size, 1, 2, 16])).unsqueeze(0) clean_latent_indices_pre, blank_indices, latent_indices, clean_latent_indices_post, clean_latent_2x_indices, clean_latent_4x_indices = indices.split([1, latent_padding_size, latent_window_size, 1, 2, 16], dim=1) clean_latent_indices = torch.cat([clean_latent_indices_pre, clean_latent_indices_post], dim=1) clean_latents_pre = current_latent.to(history_latents) clean_latents_post, clean_latents_2x, clean_latents_4x = history_latents[:, :, :1 + 2 + 16, :, :].split([1, 2, 16], dim=2) clean_latents = torch.cat([clean_latents_pre, clean_latents_post], dim=2) if not high_vram: unload_complete_models() # GPUメモリ保存値を明示的に浮動小数点に変換 preserved_memory = float(gpu_memory_preservation) if gpu_memory_preservation is not None else 6.0 print(f'Setting transformer memory preservation to: {preserved_memory} GB') move_model_to_device_with_memory_preservation(transformer, target_device=gpu, preserved_memory_gb=preserved_memory) if use_teacache: transformer.initialize_teacache(enable_teacache=True, num_steps=steps) else: transformer.initialize_teacache(enable_teacache=False) def callback(d): preview = d['denoised'] preview = vae_decode_fake(preview) preview = (preview * 255.0).detach().cpu().numpy().clip(0, 255).astype(np.uint8) preview = einops.rearrange(preview, 'b c t h w -> (b h) (t w) c') if stream.input_queue.top() == 'end': stream.output_queue.push(('end', None)) raise KeyboardInterrupt('User ends the task.') current_step = d['i'] + 1 percentage = int(100.0 * current_step / steps) hint = f'Sampling {current_step}/{steps}' desc = f'Total generated frames: {int(max(0, total_generated_latent_frames * 4 - 3))}, Video length: {max(0, (total_generated_latent_frames * 4 - 3) / 30) :.2f} seconds (FPS-30). The video is being extended now ...' stream.output_queue.push(('progress', (preview, desc, make_progress_bar_html(percentage, hint)))) return generated_latents = sample_hunyuan( transformer=transformer, sampler='unipc', width=width, height=height, frames=num_frames, real_guidance_scale=cfg, distilled_guidance_scale=gs, guidance_rescale=rs, # shift=3.0, num_inference_steps=steps, generator=rnd, prompt_embeds=llama_vec, prompt_embeds_mask=llama_attention_mask, prompt_poolers=clip_l_pooler, negative_prompt_embeds=llama_vec_n, negative_prompt_embeds_mask=llama_attention_mask_n, negative_prompt_poolers=clip_l_pooler_n, device=gpu, dtype=torch.bfloat16, image_embeddings=image_encoder_last_hidden_state, latent_indices=latent_indices, clean_latents=clean_latents, clean_latent_indices=clean_latent_indices, clean_latents_2x=clean_latents_2x, clean_latent_2x_indices=clean_latent_2x_indices, clean_latents_4x=clean_latents_4x, clean_latent_4x_indices=clean_latent_4x_indices, callback=callback, ) if is_last_section: generated_latents = torch.cat([start_latent.to(generated_latents), generated_latents], dim=2) total_generated_latent_frames += int(generated_latents.shape[2]) history_latents = torch.cat([generated_latents.to(history_latents), history_latents], dim=2) if not high_vram: # 減圧時に使用するGPUメモリ値も明示的に浮動小数点に設定 preserved_memory_offload = 8.0 # こちらは固定値のまま print(f'Offloading transformer with memory preservation: {preserved_memory_offload} GB') offload_model_from_device_for_memory_preservation(transformer, target_device=gpu, preserved_memory_gb=preserved_memory_offload) load_model_as_complete(vae, target_device=gpu) real_history_latents = history_latents[:, :, :total_generated_latent_frames, :, :] if history_pixels is None: history_pixels = vae_decode(real_history_latents, vae).cpu() else: section_latent_frames = (latent_window_size * 2 + 1) if is_last_section else (latent_window_size * 2) overlapped_frames = latent_window_size * 4 - 3 current_pixels = vae_decode(real_history_latents[:, :, :section_latent_frames], vae).cpu() history_pixels = soft_append_bcthw(current_pixels, history_pixels, overlapped_frames) # 各セクションの最終フレームを静止画として保存(セクション番号付き) if save_section_frames and history_pixels is not None: try: if i_section == 0 or current_pixels is None: # 最初のセクションは history_pixels の最後 last_frame = history_pixels[0, :, -1, :, :] else: # 2セクション目以降は current_pixels の最後 last_frame = current_pixels[0, :, -1, :, :] last_frame = einops.rearrange(last_frame, 'c h w -> h w c') last_frame = last_frame.cpu().numpy() last_frame = np.clip((last_frame * 127.5 + 127.5), 0, 255).astype(np.uint8) last_frame = resize_and_center_crop(last_frame, target_width=width, target_height=height) if is_first_section and end_frame is None: Image.fromarray(last_frame).save(os.path.join(outputs_folder, f'{job_id}_{i_section}_end.png')) else: Image.fromarray(last_frame).save(os.path.join(outputs_folder, f'{job_id}_{i_section}.png')) except Exception as e: print(f"[WARN] セクション{ i_section }最終フレーム画像保存時にエラー: {e}") if not high_vram: unload_complete_models() output_filename = os.path.join(outputs_folder, f'{job_id}_{total_generated_latent_frames}.mp4') save_bcthw_as_mp4(history_pixels, output_filename, fps=30) print(f'Decoded. Current latent shape {real_history_latents.shape}; pixel shape {history_pixels.shape}') print(f"\u25a0 セクション{i_section}の処理完了") print(f" - 現在の累計フレーム数: {int(max(0, total_generated_latent_frames * 4 - 3))}フレーム") print(f" - レンダリング時間: {max(0, (total_generated_latent_frames * 4 - 3) / 30) :.2f}秒") print(f" - 出力ファイル: {output_filename}") stream.output_queue.push(('file', output_filename)) if is_last_section: # 処理終了時に通知 if HAS_WINSOUND: winsound.PlaySound("SystemExclamation", winsound.SND_ALIAS) else: print("\n✓ 処理が完了しました!") # Linuxでの代替通知 # 全体の処理時間を計算 process_end_time = time.time() total_process_time = process_end_time - process_start_time hours, remainder = divmod(total_process_time, 3600) minutes, seconds = divmod(remainder, 60) time_str = "" if hours > 0: time_str = f"{int(hours)}時間 {int(minutes)}分 {seconds:.1f}秒" elif minutes > 0: time_str = f"{int(minutes)}分 {seconds:.1f}秒" else: time_str = f"{seconds:.1f}秒" print(f"\n全体の処理時間: {time_str}") stream.output_queue.push(('progress', (None, f"全体の処理時間: {time_str}", make_progress_bar_html(100, '処理完了')))) # 中間ファイルの削除処理 if not keep_section_videos: # 最終動画のフルパス final_video_path = output_filename final_video_name = os.path.basename(final_video_path) # job_id部分を取得(タイムスタンプ部分) job_id_part = job_id # ディレクトリ内のすべてのファイルを取得 files = os.listdir(outputs_folder) deleted_count = 0 for file in files: # 同じjob_idを持つMP4ファイルかチェック if file.startswith(job_id_part) and file.endswith('.mp4') and file != final_video_name: file_path = os.path.join(outputs_folder, file) try: os.remove(file_path) deleted_count += 1 print(f"[削除] 中間ファイル: {file}") except Exception as e: print(f"[エラー] ファイル削除時のエラー {file}: {e}") if deleted_count > 0: print(f"[済] {deleted_count}個の中間ファイルを削除しました。最終ファイルは保存されています: {final_video_name}") stream.output_queue.push(('progress', (None, f"{deleted_count}個の中間ファイルを削除しました。最終動画は保存されています。", make_progress_bar_html(100, '処理完了')))) break except: traceback.print_exc() if not high_vram: unload_complete_models( text_encoder, text_encoder_2, image_encoder, vae, transformer ) stream.output_queue.push(('end', None)) return def process(input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, use_random_seed, save_section_frames, keep_section_videos, section_settings): global stream assert input_image is not None, 'No input image!' # 動画生成の設定情報をログに出力 total_latent_sections = int(max(round((total_second_length * 30) / (latent_window_size * 4)), 1)) mode_name = "通常モード" if mode_radio.value == "通常" else "ループモード" print(f"\n==== 動画生成開始 =====") print(f"\u25c6 生成モード: {mode_name}") print(f"\u25c6 動画長: {total_second_length}秒") print(f"\u25c6 生成セクション数: {total_latent_sections}回") print(f"\u25c6 サンプリングステップ数: {steps}") print(f"\u25c6 TeaCache使用: {use_teacache}") # セクションごとのキーフレーム画像の使用状況をログに出力 valid_sections = [] if section_settings is not None: for i, sec_data in enumerate(section_settings): if sec_data and sec_data[1] is not None: # 画像が設定されている場合 valid_sections.append(sec_data[0]) if valid_sections: print(f"\u25c6 使用するキーフレーム画像: セクション{', '.join(map(str, valid_sections))}") else: print(f"\u25c6 キーフレーム画像: デフォルト設定のみ使用") print(f"=============================\n") if use_random_seed: seed = random.randint(0, 2**32 - 1) # UIのseed欄もランダム値で更新 yield None, None, '', '', gr.update(interactive=False), gr.update(interactive=True), gr.update(value=seed) else: yield None, None, '', '', gr.update(interactive=False), gr.update(interactive=True), gr.update() stream = AsyncStream() # GPUメモリの設定値をデバッグ出力し、正しい型に変換 gpu_memory_value = float(gpu_memory_preservation) if gpu_memory_preservation is not None else 6.0 print(f'Using GPU memory preservation setting: {gpu_memory_value} GB') async_run(worker, input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_value, use_teacache, save_section_frames, keep_section_videos, section_settings) output_filename = None while True: flag, data = stream.output_queue.next() if flag == 'file': output_filename = data yield output_filename, gr.update(), gr.update(), gr.update(), gr.update(interactive=False), gr.update(interactive=True), gr.update() if flag == 'progress': preview, desc, html = data yield gr.update(), gr.update(visible=True, value=preview), desc, html, gr.update(interactive=False), gr.update(interactive=True), gr.update() if flag == 'end': yield output_filename, gr.update(visible=False), gr.update(), '', gr.update(interactive=True), gr.update(interactive=False), gr.update() break def end_process(): stream.input_queue.push('end') # プリセット管理関連の関数 def initialize_presets(): """初期プリセットファイルがない場合に作成する関数""" preset_file = os.path.join(presets_folder, 'prompt_presets.json') # デフォルトのプロンプト default_prompts = [ 'A character doing some simple body movements.', 'A character uses expressive hand gestures and body language.', 'A character walks leisurely with relaxed movements.', 'A character performs dynamic movements with energy and flowing motion.', 'A character moves in unexpected ways, with surprising transitions poses.', ] # デフォルト起動時プロンプト default_startup_prompt = "A character doing some simple body movements." # 既存ファイルがあり、正常に読み込める場合は終了 if os.path.exists(preset_file): try: with open(preset_file, 'r', encoding='utf-8') as f: presets_data = json.load(f) # 起動時デフォルトがあるか確認 startup_default_exists = any(preset.get("is_startup_default", False) for preset in presets_data.get("presets", [])) # なければ追加 if not startup_default_exists: presets_data.setdefault("presets", []).append({ "name": "起動時デフォルト", "prompt": default_startup_prompt, "timestamp": datetime.now().isoformat(), "is_default": True, "is_startup_default": True }) presets_data["default_startup_prompt"] = default_startup_prompt with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) return except: # エラーが発生した場合は新規作成 pass # 新規作成 presets_data = { "presets": [], "default_startup_prompt": default_startup_prompt } # デフォルトのプリセットを追加 for i, prompt_text in enumerate(default_prompts): presets_data["presets"].append({ "name": f"デフォルト {i+1}: {prompt_text[:20]}...", "prompt": prompt_text, "timestamp": datetime.now().isoformat(), "is_default": True }) # 起動時デフォルトプリセットを追加 presets_data["presets"].append({ "name": "起動時デフォルト", "prompt": default_startup_prompt, "timestamp": datetime.now().isoformat(), "is_default": True, "is_startup_default": True }) # 保存 try: with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) except: # 保存に失敗してもエラーは出さない(次回起動時に再試行される) pass def load_presets(): """プリセットを読み込む関数""" preset_file = os.path.join(presets_folder, 'prompt_presets.json') # 初期化関数を呼び出し(初回実行時のみ作成される) initialize_presets() max_retries = 3 retry_count = 0 while retry_count < max_retries: try: with open(preset_file, 'r', encoding='utf-8') as f: file_contents = f.read() if not file_contents.strip(): print(f"読み込み時に空ファイルが検出されました: {preset_file}") # 空ファイルの場合は再初期化を試みる initialize_presets() retry_count += 1 continue data = json.loads(file_contents) print(f"プリセットファイル読み込み成功: {len(data.get('presets', []))}件") return data except (json.JSONDecodeError, UnicodeDecodeError) as e: # JSONパースエラーの場合はファイルが破損している可能性がある print(f"プリセットファイルの形式が不正です: {e}") # ファイルをバックアップ backup_file = f"{preset_file}.bak.{int(time.time())}" try: import shutil shutil.copy2(preset_file, backup_file) print(f"破損したファイルをバックアップしました: {backup_file}") except Exception as backup_error: print(f"バックアップ作成エラー: {backup_error}") # 再初期化 initialize_presets() retry_count += 1 except Exception as e: print(f"プリセット読み込みエラー: {e}") # エラー発生 retry_count += 1 # 再試行しても失敗した場合は空のデータを返す print("再試行しても読み込みに失敗しました。空のデータを返します。") return {"presets": []} def get_default_startup_prompt(): """起動時に表示するデフォルトプロンプトを取得する関数""" print("起動時デフォルトプロンプト読み込み開始") presets_data = load_presets() # プリセットからデフォルト起動時プロンプトを探す for preset in presets_data["presets"]: if preset.get("is_startup_default", False): startup_prompt = preset["prompt"] print(f"起動時デフォルトプロンプトを読み込み: '{startup_prompt[:30]}...' (長さ: {len(startup_prompt)}文字)") # 重複しているかチェック # 例えば「A character」が複数回出てくる場合は重複している可能性がある if "A character" in startup_prompt and startup_prompt.count("A character") > 1: print("プロンプトに重複が見つかりました。最初のセンテンスのみを使用します。") # 最初のセンテンスのみを使用 sentences = startup_prompt.split(".") if len(sentences) > 0: clean_prompt = sentences[0].strip() + "." print(f"正規化されたプロンプト: '{clean_prompt}'") return clean_prompt return startup_prompt # 見つからない場合はデフォルト設定を使用 if "default_startup_prompt" in presets_data: default_prompt = presets_data["default_startup_prompt"] print(f"デフォルト設定から読み込み: '{default_prompt[:30]}...' (長さ: {len(default_prompt)}文字)") # 同様に重複チェック if "A character" in default_prompt and default_prompt.count("A character") > 1: print("デフォルトプロンプトに重複が見つかりました。最初のセンテンスのみを使用します。") sentences = default_prompt.split(".") if len(sentences) > 0: clean_prompt = sentences[0].strip() + "." print(f"正規化されたデフォルトプロンプト: '{clean_prompt}'") return clean_prompt return default_prompt # フォールバックとしてプログラムのデフォルト値を返す fallback_prompt = "A character doing some simple body movements." print(f"プログラムのデフォルト値を使用: '{fallback_prompt}'") return fallback_prompt def save_preset(name, prompt_text): """プリセットを保存する関数""" presets_data = load_presets() if not name: # 名前が空の場合は起動時デフォルトとして保存 # 既存の起動時デフォルトを探す startup_default_exists = False for preset in presets_data["presets"]: if preset.get("is_startup_default", False): # 既存の起動時デフォルトを更新 preset["prompt"] = prompt_text preset["timestamp"] = datetime.now().isoformat() startup_default_exists = True # 起動時デフォルトを更新 break if not startup_default_exists: # 見つからない場合は新規作成 presets_data["presets"].append({ "name": "起動時デフォルト", "prompt": prompt_text, "timestamp": datetime.now().isoformat(), "is_default": True, "is_startup_default": True }) print(f"起動時デフォルトを新規作成: {prompt_text[:50]}...") # デフォルト設定も更新 presets_data["default_startup_prompt"] = prompt_text preset_file = os.path.join(presets_folder, 'prompt_presets.json') try: # JSON直接書き込み with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) # プロンプトの値を更新 if 'prompt' in globals(): prompt.value = prompt_text return "プリセット '起動時デフォルト' を保存しました" except Exception as e: print(f"プリセット保存エラー: {e}") import traceback traceback.print_exc() return f"保存エラー: {e}" # 通常のプリセット保存処理 # 同名のプリセットがあれば上書き、なければ追加 preset_exists = False for preset in presets_data["presets"]: if preset["name"] == name: preset["prompt"] = prompt_text preset["timestamp"] = datetime.now().isoformat() preset_exists = True # 既存のプリセットを更新 break if not preset_exists: presets_data["presets"].append({ "name": name, "prompt": prompt_text, "timestamp": datetime.now().isoformat(), "is_default": False }) # 新規プリセットを作成 preset_file = os.path.join(presets_folder, 'prompt_presets.json') try: # JSON直接書き込み with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) # ファイル保存成功 return f"プリセット '{name}' を保存しました" except Exception as e: print(f"プリセット保存エラー: {e}") # エラー発生 return f"保存エラー: {e}" def delete_preset(preset_name): """プリセットを削除する関数""" if not preset_name: return "プリセットを選択してください" presets_data = load_presets() # 削除対象のプリセットを確認 target_preset = None for preset in presets_data["presets"]: if preset["name"] == preset_name: target_preset = preset break if not target_preset: return f"プリセット '{preset_name}' が見つかりません" # デフォルトプリセットは削除できない if target_preset.get("is_default", False): return f"デフォルトプリセットは削除できません" # プリセットを削除 presets_data["presets"] = [p for p in presets_data["presets"] if p["name"] != preset_name] preset_file = os.path.join(presets_folder, 'prompt_presets.json') try: with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) return f"プリセット '{preset_name}' を削除しました" except Exception as e: return f"削除エラー: {e}" # 既存のQuick Prompts(初期化時にプリセットに変換されるので、互換性のために残す) quick_prompts = [ 'A character doing some simple body movements.', 'A character uses expressive hand gestures and body language.', 'A character walks leisurely with relaxed movements.', 'A character performs dynamic movements with energy and flowing motion.', 'A character moves in unexpected ways, with surprising transitions poses.', ] quick_prompts = [[x] for x in quick_prompts] css = make_progress_bar_css() + """ .title-suffix { color: currentColor; opacity: 0.05; } .highlighted-keyframe { border: 4px solid #ff3860 !important; box-shadow: 0 0 10px rgba(255, 56, 96, 0.5) !important; background-color: rgba(255, 56, 96, 0.05) !important; } /* セクション番号ラベルの強調表示 */ .highlighted-label label { color: #ff3860 !important; font-weight: bold !important; } """ block = gr.Blocks(css=css).queue() with block: gr.HTML('

FramePack-eichi

') # モード選択用のラジオボタンと動画長選択用のラジオボタンを横並びに配置 with gr.Row(): with gr.Column(scale=1): mode_radio = gr.Radio(choices=["通常", "ループ"], value="通常", label="生成モード", info="通常:一般的な生成 / ループ:ループ動画用") with gr.Column(scale=1): length_radio = gr.Radio(choices=["6秒", "8秒", "10(5x2)秒", "12(4x3)秒"], value="6秒", label="動画長", info="キーフレーム画像のコピー範囲と動画の長さを設定") with gr.Row(): with gr.Column(): input_image = gr.Image(sources='upload', type="numpy", label="Image", height=320) end_frame = gr.Image(sources='upload', type="numpy", label="Final Frame (Optional)", height=320) with gr.Row(): start_button = gr.Button(value="Start Generation") end_button = gr.Button(value="End Generation", interactive=False) prompt = gr.Textbox(label="Prompt", value=get_default_startup_prompt(), lines=6) with gr.Row(): gr.Markdown("※プリセット名を空にして「保存」すると起動時デフォルトになります") # 互換性のためにQuick Listも残しておくが、非表示にする with gr.Row(visible=False): example_quick_prompts = gr.Dataset(samples=quick_prompts, label='Quick List', samples_per_page=1000, components=[prompt]) example_quick_prompts.click(lambda x: x[0], inputs=[example_quick_prompts], outputs=prompt, show_progress=False, queue=False) with gr.Group(): use_teacache = gr.Checkbox(label='Use TeaCache', value=True, info='Faster speed, but often makes hands and fingers slightly worse.') # Use Random Seedの初期値 use_random_seed_default = True seed_default = random.randint(0, 2**32 - 1) if use_random_seed_default else 1 use_random_seed = gr.Checkbox(label="Use Random Seed", value=use_random_seed_default) n_prompt = gr.Textbox(label="Negative Prompt", value="", visible=False) # Not used seed = gr.Number(label="Seed", value=seed_default, precision=0) def set_random_seed(is_checked): if is_checked: return random.randint(0, 2**32 - 1) else: return gr.update() use_random_seed.change(fn=set_random_seed, inputs=use_random_seed, outputs=seed) total_second_length = gr.Slider(label="Total Video Length (Seconds)", minimum=1, maximum=120, value=6, step=1) latent_window_size = gr.Slider(label="Latent Window Size", minimum=1, maximum=33, value=9, step=1, visible=False) # Should not change steps = gr.Slider(label="Steps", minimum=1, maximum=100, value=25, step=1, info='Changing this value is not recommended.') cfg = gr.Slider(label="CFG Scale", minimum=1.0, maximum=32.0, value=1.0, step=0.01, visible=False) # Should not change gs = gr.Slider(label="Distilled CFG Scale", minimum=1.0, maximum=32.0, value=10.0, step=0.01, info='Changing this value is not recommended.') rs = gr.Slider(label="CFG Re-Scale", minimum=0.0, maximum=1.0, value=0.0, step=0.01, visible=False) # Should not change gpu_memory_preservation = gr.Slider(label="GPU Memory to Preserve (GB) (smaller = more VRAM usage)", minimum=6, maximum=128, value=9, step=0.1, info="空けておくGPUメモリ量を指定。小さい値=より多くのVRAMを使用可能=高速、大きい値=より少ないVRAMを使用=安全") # セクションごとの動画保存チェックボックスを追加(デフォルトOFF) keep_section_videos = gr.Checkbox(label="完了時にセクションごとの動画を残す", value=False, info="チェックがない場合は最終動画のみ保存されます(デフォルトOFF)") # セクションごとの静止画保存チェックボックスを追加(デフォルトOFF) save_section_frames = gr.Checkbox(label="セクションごとの静止画を保存", value=False, info="各セクションの最終フレームを静止画として保存します(デフォルトOFF)") # キーフレームコピー機能のオンオフ切り替え enable_keyframe_copy = gr.Checkbox(label="キーフレーム自動コピー機能を有効にする", value=True, info="オフにするとキーフレーム間の自動コピーが行われなくなります") # セクション設定(DataFrameをやめて個別入力欄に変更) section_number_inputs = [] section_image_inputs = [] section_prompt_inputs = [] # 空リストにしておく with gr.Group(): gr.Markdown("### セクション設定. セクション番号は動画の終わりからカウント.(任意。指定しない場合は通常のImage/プロンプトを使用)") for i in range(9): with gr.Row(): section_number = gr.Number(label=f"セクション番号{i+1}", value=[0, 1, 2, 3, 4, 5, 6, 7, 8][i], precision=0) section_image = gr.Image(label=f"キーフレーム画像{i+1}", sources="upload", type="numpy", height=200) section_number_inputs.append(section_number) section_image_inputs.append(section_image) # 重要なキーフレームの説明 with gr.Row(): with gr.Column(): note_html = gr.HTML("""
■ キーフレーム画像設定ガイド:
※ 重要なキーフレームは赤枠で強調表示されます
""") # section_settingsは9つの入力欄の値をまとめてリスト化 def collect_section_settings(*args): # args: [num1, img1, num2, img2, ...] return [[args[i], args[i+1], ""] for i in range(0, len(args), 2)] section_settings = gr.State([[None, None, ""] for _ in range(9)]) section_inputs = [] for i in range(9): section_inputs.extend([section_number_inputs[i], section_image_inputs[i]]) # section_inputsをまとめてsection_settings Stateに格納 def update_section_settings(*args): return collect_section_settings(*args) # section_inputsが変化したらsection_settings Stateを更新 for inp in section_inputs: inp.change(fn=update_section_settings, inputs=section_inputs, outputs=section_settings) # モードと動画長の切り替え時の処理 def handle_mode_length_change(mode=None, length=None): # Image, FinalFrame の2つをクリア base_updates = [gr.update(value=None) for _ in range(2)] # キーフレーム画像1〜9の更新リスト(値をクリアし、デフォルトでは強調なし) keyframe_updates = [] for i in range(9): keyframe_updates.append(gr.update(value=None, elem_classes="")) # セクション番号ラベルのリセット(フォーマット用に事前に作成) label_updates = [] for i in range(9): section_number_inputs[i].elem_classes = "" # 動画長に応じて特定のキーフレーム枠を強調 if length == "10(5x2)秒": # キーフレーム1と5を強調 keyframe_updates[0] = gr.update(value=None, elem_classes="highlighted-keyframe") keyframe_updates[4] = gr.update(value=None, elem_classes="highlighted-keyframe") # 対応するセクション番号ラベルも強調 section_number_inputs[0].elem_classes = "highlighted-label" section_number_inputs[4].elem_classes = "highlighted-label" # Markdownはイベントハンドラ内で動的に追加できないため、別のアプローチで実装 elif length == "12(4x3)秒": # キーフレーム1、4、7を強調 keyframe_updates[0] = gr.update(value=None, elem_classes="highlighted-keyframe") keyframe_updates[3] = gr.update(value=None, elem_classes="highlighted-keyframe") keyframe_updates[6] = gr.update(value=None, elem_classes="highlighted-keyframe") # 対応するセクション番号ラベルも強調 section_number_inputs[0].elem_classes = "highlighted-label" section_number_inputs[3].elem_classes = "highlighted-label" section_number_inputs[6].elem_classes = "highlighted-label" elif mode == "ループ": # 6秒/8秒でもループモードならキーフレーム1を強調 # キーフレーム1のみ強調 keyframe_updates[0] = gr.update(value=None, elem_classes="highlighted-keyframe") # 対応するセクション番号ラベルも強調 section_number_inputs[0].elem_classes = "highlighted-label" # 動画長に応じて秒数を設定 video_length = 6.0 if length == "8秒": video_length = 8.0 # デフォルト elif length == "10(5x2)秒": video_length = 10.8 elif length == "12(4x3)秒": video_length = 12.0 # 画像クリアと動画長設定を返す return base_updates + keyframe_updates + [gr.update(value=video_length)] # 入力画像が変更されたときのモードと動画長別処理 def process_image_change(img, mode, length): if img is None: return [gr.update() for _ in range(10)] # FinalFrame + キーフレーム1〜9 # コピー対象画像数を決定 copy_count = 4 # 6秒は4枚 if length == "8秒": copy_count = 6 # 8秒は6枚 elif length == "10(5x2)秒": copy_count = 4 # 10秒は2パートなので前半・後半それぞれ4枚ずつ elif length == "12(4x3)秒": copy_count = 3 # 12秒は3パートなので各パート3枚ずつ if mode == "通常": if length == "10(5x2)秒": # 10(5x2)秒の場合はキーフレーム5~8にのみコピー keyframe_updates = [gr.update() for _ in range(4)] + \ [gr.update(value=img) for _ in range(4)] + \ [gr.update()] # 9番目用 elif length == "12(4x3)秒": # 12(4x3)秒の場合はキーフレーム7~9にのみコピー keyframe_updates = [gr.update() for _ in range(6)] + \ [gr.update(value=img) for _ in range(3)] else: # 通常の動画長の場合は頭からコピー keyframe_updates = [gr.update(value=img) for _ in range(copy_count)] + \ [gr.update() for _ in range(9 - copy_count)] return [gr.update()] + keyframe_updates else: # ループモード:FinalFrameにコピー return [gr.update(value=img)] + [gr.update() for _ in range(9)] # キーフレーム画像1の統合ハンドラ(ループモード・通常モード両対応) def process_keyframe1_unified(img, mode, length, enable_copy=True): if img is None or not enable_copy: return [gr.update() for _ in range(8)] # 全キーフレーム更新なし if mode == "通常": if length == "10(5x2)秒": # 通常モード+10(5x2)秒: キーフレーム2~4にコピー return [gr.update(value=img) for _ in range(3)] + [gr.update() for _ in range(5)] elif length == "12(4x3)秒": # 通常モード+12(4x3)秒: キーフレーム2~3にコピー return [gr.update(value=img) for _ in range(2)] + [gr.update() for _ in range(6)] else: # 他の通常モード: 何もしない return [gr.update() for _ in range(8)] else: # ループモード # コピー対象画像数を決定 copy_count = 3 # 6秒は2〜4(3枚) if length == "8秒": copy_count = 5 # 8秒は2〜6(5枚) elif length == "10(5x2)秒": copy_count = 3 # 10秒は前半部分のみ(2~4の3枚) elif length == "12(4x3)秒": copy_count = 2 # 12秒は最初のパート分(2~3の2枚) # ループモード: 必要数のキーフレーム画像にコピー return [gr.update(value=img) for _ in range(copy_count)] + \ [gr.update() for _ in range(8 - copy_count)] # モード変更時の処理 mode_radio.change(fn=handle_mode_length_change, inputs=[mode_radio, length_radio], outputs=[input_image, end_frame] + section_image_inputs + [total_second_length]) # 動画長変更時の処理 length_radio.change(fn=handle_mode_length_change, inputs=[mode_radio, length_radio], outputs=[input_image, end_frame] + section_image_inputs + [total_second_length]) # input_imageが変更された時の処理 # 入力画像が変更されたときのコピー機能をオフにする場合のウラッパー関数 def process_image_change_wrapper(img, mode, length, enable_copy): if not enable_copy: return [gr.update() for _ in range(10)] # FinalFrame + キーフレーム1~9 return process_image_change(img, mode, length) input_image.change(fn=process_image_change_wrapper, inputs=[input_image, mode_radio, length_radio, enable_keyframe_copy], outputs=[end_frame] + section_image_inputs) # キーフレーム画像4が変更されたときの処理(12(4x3)秒用) def process_keyframe4_change(img, mode, length, enable_copy=True): if img is None or not enable_copy: return [gr.update() for _ in range(5)] # キーフレーム画像5~9向け # 12(4x3)秒モードの場合(通常モードとループモードの両方) if length == "12(4x3)秒": # キーフレーム画像4→キーフレーム画像5~6にコピー(2枚) return [gr.update(value=img) for _ in range(2)] + [gr.update() for _ in range(3)] # それ以外は何もしない # キーフレーム画像5~9の現在の状態を維持 return [gr.update() for _ in range(5)] # キーフレーム画像5が変更されたときの処理(10(5x2)秒ループモード用) def process_keyframe5_change(img, mode, length, enable_copy=True): if img is None or mode != "ループ" or length != "10(5x2)秒" or not enable_copy: return [gr.update() for _ in range(4)] # キーフレーム画像6~9向け # キーフレーム画像5→キーフレーム画像6~8にコピー(3枚) return [gr.update(value=img) for _ in range(3)] + [gr.update()] # キーフレーム画像7が変更されたときの処理(12(4x3)秒ループモード用) def process_keyframe7_change(img, mode, length, enable_copy=True): if img is None or mode != "ループ" or length != "12(4x3)秒" or not enable_copy: return [gr.update() for _ in range(2)] # キーフレーム画像8~9向け # キーフレーム画像7→キーフレーム画像8~9にコピー(2枚) return [gr.update(value=img) for _ in range(2)] # キーフレーム画像4のchange処理追加 section_image_inputs[3].change(fn=process_keyframe4_change, inputs=[section_image_inputs[3], mode_radio, length_radio, enable_keyframe_copy], outputs=section_image_inputs[4:]) # キーフレーム画像5のchange処理追加 section_image_inputs[4].change(fn=process_keyframe5_change, inputs=[section_image_inputs[4], mode_radio, length_radio, enable_keyframe_copy], outputs=section_image_inputs[5:]) # キーフレーム画像7のchange処理追加 section_image_inputs[6].change(fn=process_keyframe7_change, inputs=[section_image_inputs[6], mode_radio, length_radio, enable_keyframe_copy], outputs=section_image_inputs[7:]) # キーフレーム画像1のchange処理(統合版) section_image_inputs[0].change(fn=process_keyframe1_unified, inputs=[section_image_inputs[0], mode_radio, length_radio, enable_keyframe_copy], outputs=section_image_inputs[1:]) with gr.Column(): result_video = gr.Video(label="Finished Frames", autoplay=True, show_share_button=False, height=512, loop=True) progress_desc = gr.Markdown('', elem_classes='no-generating-animation') progress_bar = gr.HTML('', elem_classes='no-generating-animation') preview_image = gr.Image(label="Next Latents", height=200, visible=False) # プロンプト管理パネルの追加 with gr.Group(visible=True) as prompt_management: gr.Markdown("### プロンプト管理") # 編集画面を常時表示する with gr.Group(visible=True): # 起動時デフォルトの初期表示用に取得 default_prompt = "" default_name = "" for preset in load_presets()["presets"]: if preset.get("is_startup_default", False): default_prompt = preset["prompt"] default_name = preset["name"] break with gr.Row(): edit_name = gr.Textbox(label="プリセット名", placeholder="名前を入力...", value=default_name) edit_prompt = gr.Textbox(label="プロンプト", lines=5, value=default_prompt) with gr.Row(): # 起動時デフォルトをデフォルト選択に設定 default_preset = "起動時デフォルト" # プリセットデータから全プリセット名を取得 presets_data = load_presets() choices = [preset["name"] for preset in presets_data["presets"]] default_presets = [name for name in choices if any(p["name"] == name and p.get("is_default", False) for p in presets_data["presets"])] user_presets = [name for name in choices if name not in default_presets] sorted_choices = [(name, name) for name in sorted(default_presets) + sorted(user_presets)] preset_dropdown = gr.Dropdown(label="プリセット", choices=sorted_choices, value=default_preset, type="value") with gr.Row(): save_btn = gr.Button(value="保存", variant="primary") apply_preset_btn = gr.Button(value="反映", variant="primary") clear_btn = gr.Button(value="クリア") delete_preset_btn = gr.Button(value="削除") # メッセージ表示用 result_message = gr.Markdown("") ips = [input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, use_random_seed, save_section_frames, keep_section_videos, section_settings] start_button.click(fn=process, inputs=ips, outputs=[result_video, preview_image, progress_desc, progress_bar, start_button, end_button, seed]) end_button.click(fn=end_process) # プリセット保存関数 def save_button_click_handler(name, prompt_text): """保存ボタンクリック時のハンドラ関数""" # 重複チェックと正規化 if "A character" in prompt_text and prompt_text.count("A character") > 1: sentences = prompt_text.split(".") if len(sentences) > 0: prompt_text = sentences[0].strip() + "." # 重複を検出したため正規化 # ファイルパスを準備 preset_file = os.path.join(presets_folder, 'prompt_presets.json') try: # プリセットデータを読み込む if os.path.exists(preset_file): with open(preset_file, 'r', encoding='utf-8') as f: presets_data = json.load(f) else: presets_data = {"presets": [], "default_startup_prompt": ""} # 名前が空の場合は起動時デフォルトとして処理 if not name: # 既存の起動時デフォルトを探す startup_default_exists = False for preset in presets_data["presets"]: if preset.get("is_startup_default", False): # 既存の起動時デフォルトを更新 preset["prompt"] = prompt_text preset["timestamp"] = datetime.now().isoformat() startup_default_exists = True break if not startup_default_exists: # 見つからない場合は新規作成 presets_data["presets"].append({ "name": "起動時デフォルト", "prompt": prompt_text, "timestamp": datetime.now().isoformat(), "is_default": True, "is_startup_default": True }) # デフォルト設定も更新 presets_data["default_startup_prompt"] = prompt_text message = "起動時デフォルトプロンプトを保存しました" else: # 通常のプリセット保存処理 # 同名のプリセットがあれば上書き、なければ追加 preset_exists = False for preset in presets_data["presets"]: if preset["name"] == name: preset["prompt"] = prompt_text preset["timestamp"] = datetime.now().isoformat() preset_exists = True break if not preset_exists: presets_data["presets"].append({ "name": name, "prompt": prompt_text, "timestamp": datetime.now().isoformat(), "is_default": False }) message = f"プリセット '{name}' を保存しました" # JSONファイルに保存 with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) # ドロップダウンを更新するための情報を作成 choices = [preset["name"] for preset in presets_data["presets"]] default_presets = [n for n in choices if any(p["name"] == n and p.get("is_default", False) for p in presets_data["presets"])] user_presets = [n for n in choices if n not in default_presets] sorted_choices = [(n, n) for n in sorted(default_presets) + sorted(user_presets)] # メッセージとドロップダウン更新情報を返す # プロンプトへの自動コピーは不要なのでgr.update()を返す return message, gr.update(choices=sorted_choices), gr.update() except Exception as e: print(f"プリセット保存エラー: {e}") import traceback traceback.print_exc() return f"保存エラー: {e}", gr.update(), gr.update() # 保存ボタンのクリックイベントを接続 with block: save_btn.click( fn=save_button_click_handler, inputs=[edit_name, edit_prompt], outputs=[result_message, preset_dropdown, prompt] ) # クリアボタン処理 def clear_fields(): return gr.update(value=""), gr.update(value="") with block: clear_btn.click( fn=clear_fields, inputs=[], outputs=[edit_name, edit_prompt] ) # プリセット読込処理 def load_preset_handler(preset_name): # プリセット選択時に編集欄のみを更新 for preset in load_presets()["presets"]: if preset["name"] == preset_name: return gr.update(value=preset_name), gr.update(value=preset["prompt"]) return gr.update(), gr.update() # プリセット選択時に編集欄に反映 def load_preset_handler_wrapper(preset_name): # プリセット名がタプルの場合も処理する if isinstance(preset_name, tuple) and len(preset_name) == 2: preset_name = preset_name[1] # 値部分を取得 return load_preset_handler(preset_name) with block: preset_dropdown.change( fn=load_preset_handler_wrapper, inputs=[preset_dropdown], outputs=[edit_name, edit_prompt] ) # 反映ボタン処理 - 編集画面の内容をメインプロンプトに反映 def apply_to_prompt(edit_text): """編集画面の内容をメインプロンプトに反映する関数""" # 編集画面のプロンプトをメインに適用 return gr.update(value=edit_text) # プリセット削除処理 def delete_preset_handler(preset_name): # プリセット名がタプルの場合も処理する if isinstance(preset_name, tuple) and len(preset_name) == 2: preset_name = preset_name[1] # 値部分を取得 result = delete_preset(preset_name) # プリセットデータを取得してドロップダウンを更新 presets_data = load_presets() choices = [preset["name"] for preset in presets_data["presets"]] default_presets = [name for name in choices if any(p["name"] == name and p.get("is_default", False) for p in presets_data["presets"])] user_presets = [name for name in choices if name not in default_presets] sorted_names = sorted(default_presets) + sorted(user_presets) updated_choices = [(name, name) for name in sorted_names] return result, gr.update(choices=updated_choices) with block: apply_preset_btn.click( fn=apply_to_prompt, inputs=[edit_prompt], outputs=[prompt] ) delete_preset_btn.click( fn=delete_preset_handler, inputs=[preset_dropdown], outputs=[result_message, preset_dropdown] ) block.launch( server_name=args.server, server_port=args.port, share=args.share, inbrowser=args.inbrowser, ) ``` ## /version/v1.1/run_endframe_ichi.bat ```bat path="/version/v1.1/run_endframe_ichi.bat" @echo off call environment.bat cd %~dp0webui "%DIR%\python\python.exe" endframe_ichi.py --server 127.0.0.1 --inbrowser :done pause ``` ## /version/v1.1/webui/endframe_ichi.py ```py path="/version/v1.1/webui/endframe_ichi.py" from diffusers_helper.hf_login import login import os import random import time # クロスプラットフォーム対応のための条件付きインポート try: import winsound HAS_WINSOUND = True except ImportError: HAS_WINSOUND = False import json import traceback from datetime import datetime, timedelta os.environ['HF_HOME'] = os.path.abspath(os.path.realpath(os.path.join(os.path.dirname(__file__), './hf_download'))) # 設定モジュールをインポート(ローカルモジュール) import os.path from video_mode_settings import ( VIDEO_MODE_SETTINGS, get_video_modes, get_video_seconds, get_important_keyframes, get_copy_targets, get_max_keyframes_count, get_total_sections, generate_keyframe_guide_html, handle_mode_length_change, process_keyframe_change, MODE_TYPE_NORMAL, MODE_TYPE_LOOP ) # インデックス変換のユーティリティ関数追加 def ui_to_code_index(ui_index): """UI表示のキーフレーム番号(1始まり)をコード内インデックス(0始まり)に変換""" return ui_index - 1 def code_to_ui_index(code_index): """コード内インデックス(0始まり)をUI表示のキーフレーム番号(1始まり)に変換""" return code_index + 1 import gradio as gr import torch import einops import safetensors.torch as sf import numpy as np import argparse import math from PIL import Image from diffusers import AutoencoderKLHunyuanVideo from transformers import LlamaModel, CLIPTextModel, LlamaTokenizerFast, CLIPTokenizer from diffusers_helper.hunyuan import encode_prompt_conds, vae_decode, vae_encode, vae_decode_fake from diffusers_helper.utils import save_bcthw_as_mp4, crop_or_pad_yield_mask, soft_append_bcthw, resize_and_center_crop, state_dict_weighted_merge, state_dict_offset_merge, generate_timestamp from diffusers_helper.models.hunyuan_video_packed import HunyuanVideoTransformer3DModelPacked from diffusers_helper.pipelines.k_diffusion_hunyuan import sample_hunyuan from diffusers_helper.memory import cpu, gpu, get_cuda_free_memory_gb, move_model_to_device_with_memory_preservation, offload_model_from_device_for_memory_preservation, fake_diffusers_current_device, DynamicSwapInstaller, unload_complete_models, load_model_as_complete from diffusers_helper.thread_utils import AsyncStream, async_run from diffusers_helper.gradio.progress_bar import make_progress_bar_css, make_progress_bar_html from transformers import SiglipImageProcessor, SiglipVisionModel from diffusers_helper.clip_vision import hf_clip_vision_encode from diffusers_helper.bucket_tools import find_nearest_bucket parser = argparse.ArgumentParser() parser.add_argument('--share', action='store_true') parser.add_argument("--server", type=str, default='127.0.0.1') parser.add_argument("--port", type=int, default=8001) parser.add_argument("--inbrowser", action='store_true') args = parser.parse_args() print(args) free_mem_gb = get_cuda_free_memory_gb(gpu) high_vram = free_mem_gb > 60 print(f'Free VRAM {free_mem_gb} GB') print(f'High-VRAM Mode: {high_vram}') # 元のモデル読み込みコード try: text_encoder = LlamaModel.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='text_encoder', torch_dtype=torch.float16).cpu() text_encoder_2 = CLIPTextModel.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='text_encoder_2', torch_dtype=torch.float16).cpu() tokenizer = LlamaTokenizerFast.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='tokenizer') tokenizer_2 = CLIPTokenizer.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='tokenizer_2') vae = AutoencoderKLHunyuanVideo.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='vae', torch_dtype=torch.float16).cpu() except Exception as e: print(f"モデル読み込みエラー: {e}") print("プログラムを終了します...") import sys sys.exit(1) # 他のモデルも同様に例外処理 try: feature_extractor = SiglipImageProcessor.from_pretrained("lllyasviel/flux_redux_bfl", subfolder='feature_extractor') image_encoder = SiglipVisionModel.from_pretrained("lllyasviel/flux_redux_bfl", subfolder='image_encoder', torch_dtype=torch.float16).cpu() transformer = HunyuanVideoTransformer3DModelPacked.from_pretrained('lllyasviel/FramePackI2V_HY', torch_dtype=torch.bfloat16).cpu() except Exception as e: print(f"モデル読み込みエラー (追加モデル): {e}") print("プログラムを終了します...") import sys sys.exit(1) vae.eval() text_encoder.eval() text_encoder_2.eval() image_encoder.eval() transformer.eval() if not high_vram: vae.enable_slicing() vae.enable_tiling() transformer.high_quality_fp32_output_for_inference = True print('transformer.high_quality_fp32_output_for_inference = True') transformer.to(dtype=torch.bfloat16) vae.to(dtype=torch.float16) image_encoder.to(dtype=torch.float16) text_encoder.to(dtype=torch.float16) text_encoder_2.to(dtype=torch.float16) vae.requires_grad_(False) text_encoder.requires_grad_(False) text_encoder_2.requires_grad_(False) image_encoder.requires_grad_(False) transformer.requires_grad_(False) if not high_vram: # DynamicSwapInstaller is same as huggingface's enable_sequential_offload but 3x faster DynamicSwapInstaller.install_model(transformer, device=gpu) DynamicSwapInstaller.install_model(text_encoder, device=gpu) else: text_encoder.to(gpu) text_encoder_2.to(gpu) image_encoder.to(gpu) vae.to(gpu) transformer.to(gpu) stream = AsyncStream() outputs_folder = './outputs/' os.makedirs(outputs_folder, exist_ok=True) # プリセット保存用フォルダの設定 webui_folder = os.path.dirname(os.path.abspath(__file__)) presets_folder = os.path.join(webui_folder, 'presets') os.makedirs(presets_folder, exist_ok=True) # 統一的なキーフレーム処理関数群 # 1. 統一的なキーフレーム変更ハンドラ def unified_keyframe_change_handler(keyframe_idx, img, mode, length, enable_copy=True): """すべてのキーフレーム処理を統一的に行う関数 Args: keyframe_idx: UIのキーフレーム番号-1 (0始まりのインデックス) img: 変更されたキーフレーム画像 mode: モード ("通常" or "ループ") length: 動画長 ("6秒", "8秒", "10(5x2)秒", "12(4x3)秒", "16(4x4)秒") enable_copy: コピー機能が有効かどうか Returns: 更新リスト: 変更するキーフレーム画像の更新情報のリスト """ if img is None or not enable_copy: # 画像が指定されていない、またはコピー機能が無効の場合は何もしない max_keyframes = get_max_keyframes_count() remaining = max(0, max_keyframes - keyframe_idx - 1) return [gr.update() for _ in range(remaining)] # video_mode_settings.pyから定義されたコピーターゲットを取得 targets = get_copy_targets(mode, length, keyframe_idx) # 結果の更新リスト作成 max_keyframes = get_max_keyframes_count() updates = [] # このキーフレーム以降のインデックスに対してのみ処理 for i in range(keyframe_idx + 1, max_keyframes): # コピーパターン定義では相対インデックスでなく絶対インデックスが使われているため、 # iがtargets内にあるかをチェック if i in targets: # コピー先リストに含まれている場合は画像をコピー updates.append(gr.update(value=img)) else: # 含まれていない場合は変更なし updates.append(gr.update()) return updates # 2. モード変更の統一ハンドラ def unified_mode_length_change_handler(mode, length, section_number_inputs): """モードと動画長の変更を統一的に処理する関数 Args: mode: モード ("通常" or "ループ") length: 動画長 ("6秒", "8秒", "10(5x2)秒", "12(4x3)秒", "16(4x4)秒") section_number_inputs: セクション番号入力欄のリスト Returns: 更新リスト: 各UI要素の更新情報のリスト """ # 基本要素のクリア(入力画像と終了フレーム) updates = [gr.update(value=None) for _ in range(2)] # すべてのキーフレーム画像をクリア section_image_count = get_max_keyframes_count() for _ in range(section_image_count): updates.append(gr.update(value=None, elem_classes="")) # セクション番号ラベルをリセット for i in range(len(section_number_inputs)): section_number_inputs[i].elem_classes = "" # 重要なキーフレームを強調表示 important_kfs = get_important_keyframes(length) for idx in important_kfs: ui_idx = code_to_ui_index(idx) update_idx = ui_idx + 1 # 入力画像と終了フレームの2つを考慮 if update_idx < len(updates): updates[update_idx] = gr.update(value=None, elem_classes="highlighted-keyframe") if idx < len(section_number_inputs): section_number_inputs[idx].elem_classes = "highlighted-label" # ループモードの場合はキーフレーム1も強調(まだ強調されていない場合) if mode == MODE_TYPE_LOOP and 0 not in important_kfs: updates[2] = gr.update(value=None, elem_classes="highlighted-keyframe") if 0 < len(section_number_inputs): section_number_inputs[0].elem_classes = "highlighted-label" # 動画長の設定 video_length = get_video_seconds(length) # 最終的な動画長設定を追加 updates.append(gr.update(value=video_length)) return updates # 3. 入力画像変更の統一ハンドラ def unified_input_image_change_handler(img, mode, length, enable_copy=True): """入力画像変更時の処理を統一的に行う関数 Args: img: 変更された入力画像 mode: モード ("通常" or "ループ") length: 動画長 ("6秒", "8秒", "10(5x2)秒", "12(4x3)秒", "16(4x4)秒") enable_copy: コピー機能が有効かどうか Returns: 更新リスト: 終了フレームとすべてのキーフレーム画像の更新情報のリスト """ if img is None or not enable_copy: # 画像が指定されていない、またはコピー機能が無効の場合は何もしない section_count = get_max_keyframes_count() return [gr.update() for _ in range(section_count + 1)] # +1 for end_frame # ループモードかどうかで処理を分岐 if mode == MODE_TYPE_LOOP: # ループモード: FinalFrameに入力画像をコピー updates = [gr.update(value=img)] # end_frame # キーフレーム画像は更新なし section_count = get_max_keyframes_count() updates.extend([gr.update() for _ in range(section_count)]) else: # 通常モード: FinalFrameは更新なし updates = [gr.update()] # end_frame # 動画長/モードに基づいてコピー先のキーフレームを取得 # これが設定ファイルに基づく方法 copy_targets = [] # 特殊処理のモードでは設定によって異なるキーフレームにコピー if length == "10(5x2)秒": # 10(5x2)秒の場合は5~8にコピー (インデックス4-7) copy_targets = [4, 5, 6, 7] elif length == "12(4x3)秒": # 12(4x3)秒の場合は7~9にコピー (インデックス6-8) copy_targets = [6, 7, 8] elif length == "16(4x4)秒": # 16(4x4)秒の場合は10~12にコピー (インデックス9-11) copy_targets = [9, 10, 11] else: # 通常の動画長の場合は最初のいくつかのキーフレームにコピー if length == "6秒": copy_targets = [0, 1, 2, 3] # キーフレーム1-4 elif length == "8秒": copy_targets = [0, 1, 2, 3, 4, 5] # キーフレーム1-6 # キーフレーム画像の更新リスト作成 section_count = get_max_keyframes_count() for i in range(section_count): if i in copy_targets: updates.append(gr.update(value=img)) else: updates.append(gr.update()) return updates # 4. デバッグ情報表示関数 - コメントアウト部分を関数として維持 def print_keyframe_debug_info(): """キーフレーム設定の詳細情報を表示""" # print("\n[INFO] =========== キーフレーム設定デバッグ情報 ===========") # # # 設定内容の確認表示 # print("\n[INFO] 動画モード設定の確認:") # for mode_key in VIDEO_MODE_SETTINGS: # mode_info = VIDEO_MODE_SETTINGS[mode_key] # print(f" - {mode_key}: {mode_info['display_seconds']}秒, {mode_info['frames']}フレーム") # # # 重要キーフレームの表示(UIインデックスに変換) # important_kfs = mode_info['important_keyframes'] # important_kfs_ui = [code_to_ui_index(kf) for kf in important_kfs] # print(f" 重要キーフレーム: {important_kfs_ui}") # # # コピーパターンの表示 # for mode_type in ["通常", "ループ"]: # if mode_type in mode_info["copy_patterns"]: # print(f" {mode_type}モードのコピーパターン:") # for src, targets in mode_info["copy_patterns"][mode_type].items(): # src_ui = code_to_ui_index(int(src)) # targets_ui = [code_to_ui_index(t) for t in targets] # print(f" キーフレーム{src_ui} → {targets_ui}") # # print("[INFO] =================================================\n") pass @torch.no_grad() def worker(input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, save_section_frames, keep_section_videos, section_settings=None): # 処理時間計測の開始 process_start_time = time.time() # 既存の計算方法を保持しつつ、設定からセクション数も取得する total_latent_sections = (total_second_length * 30) / (latent_window_size * 4) total_latent_sections = int(max(round(total_latent_sections), 1)) # 現在のモードを取得(UIから渡された情報から) # セクション数を全セクション数として保存 total_sections = total_latent_sections job_id = generate_timestamp() # セクション处理の詳細ログを出力 latent_paddings = reversed(range(total_latent_sections)) if total_latent_sections > 4: latent_paddings = [3] + [2] * (total_latent_sections - 3) + [1, 0] # 全セクション数を事前に計算して保存(イテレータの消費を防ぐため) latent_paddings_list = list(latent_paddings) total_sections = len(latent_paddings_list) latent_paddings = latent_paddings_list # リストに変換したものを使用 print(f"\u25a0 セクション生成詳細:") print(f" - 生成予定セクション: {latent_paddings}") print(f" - 各セクションのフレーム数: 約{latent_window_size * 4 - 3}フレーム") print(f" - 合計セクション数: {total_sections}") stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Starting ...')))) try: # セクション設定の前処理 def get_section_settings_map(section_settings): """ section_settings: DataFrame形式のリスト [[番号, 画像, プロンプト], ...] → {セクション番号: (画像, プロンプト)} のdict """ result = {} if section_settings is not None: for row in section_settings: if row and row[0] is not None: sec_num = int(row[0]) img = row[1] prm = row[2] if len(row) > 2 else "" result[sec_num] = (img, prm) return result section_map = get_section_settings_map(section_settings) section_numbers_sorted = sorted(section_map.keys()) if section_map else [] def get_section_info(i_section): """ i_section: int section_map: {セクション番号: (画像, プロンプト)} 指定がなければ次のセクション、なければNone """ if not section_map: return None, None, None # i_section以降で最初に見つかる設定 for sec in range(i_section, max(section_numbers_sorted)+1): if sec in section_map: img, prm = section_map[sec] return sec, img, prm return None, None, None # Clean GPU if not high_vram: unload_complete_models( text_encoder, text_encoder_2, image_encoder, vae, transformer ) # Text encoding stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Text encoding ...')))) if not high_vram: fake_diffusers_current_device(text_encoder, gpu) # since we only encode one text - that is one model move and one encode, offload is same time consumption since it is also one load and one encode. load_model_as_complete(text_encoder_2, target_device=gpu) llama_vec, clip_l_pooler = encode_prompt_conds(prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2) if cfg == 1: llama_vec_n, clip_l_pooler_n = torch.zeros_like(llama_vec), torch.zeros_like(clip_l_pooler) else: llama_vec_n, clip_l_pooler_n = encode_prompt_conds(n_prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2) llama_vec, llama_attention_mask = crop_or_pad_yield_mask(llama_vec, length=512) llama_vec_n, llama_attention_mask_n = crop_or_pad_yield_mask(llama_vec_n, length=512) # Processing input image stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Image processing ...')))) def preprocess_image(img): H, W, C = img.shape height, width = find_nearest_bucket(H, W, resolution=640) img_np = resize_and_center_crop(img, target_width=width, target_height=height) img_pt = torch.from_numpy(img_np).float() / 127.5 - 1 img_pt = img_pt.permute(2, 0, 1)[None, :, None] return img_np, img_pt, height, width input_image_np, input_image_pt, height, width = preprocess_image(input_image) Image.fromarray(input_image_np).save(os.path.join(outputs_folder, f'{job_id}.png')) # VAE encoding stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'VAE encoding ...')))) if not high_vram: load_model_as_complete(vae, target_device=gpu) start_latent = vae_encode(input_image_pt, vae) # end_frameも同じタイミングでencode if end_frame is not None: end_frame_np, end_frame_pt, _, _ = preprocess_image(end_frame) end_frame_latent = vae_encode(end_frame_pt, vae) else: end_frame_latent = None # create section_latents here section_latents = None if section_map: section_latents = {} for sec_num, (img, prm) in section_map.items(): if img is not None: # 画像をVAE encode img_np, img_pt, _, _ = preprocess_image(img) section_latents[sec_num] = vae_encode(img_pt, vae) # CLIP Vision stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'CLIP Vision encoding ...')))) if not high_vram: load_model_as_complete(image_encoder, target_device=gpu) image_encoder_output = hf_clip_vision_encode(input_image_np, feature_extractor, image_encoder) image_encoder_last_hidden_state = image_encoder_output.last_hidden_state # Dtype llama_vec = llama_vec.to(transformer.dtype) llama_vec_n = llama_vec_n.to(transformer.dtype) clip_l_pooler = clip_l_pooler.to(transformer.dtype) clip_l_pooler_n = clip_l_pooler_n.to(transformer.dtype) image_encoder_last_hidden_state = image_encoder_last_hidden_state.to(transformer.dtype) # Sampling stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Start sampling ...')))) rnd = torch.Generator("cpu").manual_seed(seed) num_frames = latent_window_size * 4 - 3 history_latents = torch.zeros(size=(1, 16, 1 + 2 + 16, height // 8, width // 8), dtype=torch.float32).cpu() history_pixels = None total_generated_latent_frames = 0 latent_paddings = reversed(range(total_latent_sections)) if total_latent_sections > 4: # In theory the latent_paddings should follow the above sequence, but it seems that duplicating some # items looks better than expanding it when total_latent_sections > 4 # One can try to remove below trick and just # use `latent_paddings = list(reversed(range(total_latent_sections)))` to compare latent_paddings = [3] + [2] * (total_latent_sections - 3) + [1, 0] for i_section, latent_padding in enumerate(latent_paddings): # 先に変数を定義 is_first_section = i_section == 0 is_last_section = latent_padding == 0 use_end_latent = is_last_section and end_frame is not None latent_padding_size = latent_padding * latent_window_size # 定義後にログ出力 print(f"\n\u25a0 セクション{i_section}の処理開始 (パディング値: {latent_padding})") print(f" - 現在の生成フレーム数: {total_generated_latent_frames * 4 - 3}フレーム") print(f" - 生成予定フレーム数: {num_frames}フレーム") print(f" - 最初のセクション?: {is_first_section}") print(f" - 最後のセクション?: {is_last_section}") # set current_latent here # セクションごとのlatentを使う場合 if section_map and section_latents is not None and len(section_latents) > 0: # i_section以上で最小のsection_latentsキーを探す valid_keys = [k for k in section_latents.keys() if k >= i_section] if valid_keys: use_key = min(valid_keys) current_latent = section_latents[use_key] print(f"[section_latent] section {i_section}: use section {use_key} latent (section_map keys: {list(section_latents.keys())})") print(f"[section_latent] current_latent id: {id(current_latent)}, min: {current_latent.min().item():.4f}, max: {current_latent.max().item():.4f}, mean: {current_latent.mean().item():.4f}") else: current_latent = start_latent print(f"[section_latent] section {i_section}: use start_latent (no section_latent >= {i_section})") print(f"[section_latent] current_latent id: {id(current_latent)}, min: {current_latent.min().item():.4f}, max: {current_latent.max().item():.4f}, mean: {current_latent.mean().item():.4f}") else: current_latent = start_latent print(f"[section_latent] section {i_section}: use start_latent (no section_latents)") print(f"[section_latent] current_latent id: {id(current_latent)}, min: {current_latent.min().item():.4f}, max: {current_latent.max().item():.4f}, mean: {current_latent.mean().item():.4f}") if is_first_section and end_frame_latent is not None: history_latents[:, :, 0:1, :, :] = end_frame_latent if stream.input_queue.top() == 'end': stream.output_queue.push(('end', None)) return print(f'latent_padding_size = {latent_padding_size}, is_last_section = {is_last_section}') indices = torch.arange(0, sum([1, latent_padding_size, latent_window_size, 1, 2, 16])).unsqueeze(0) clean_latent_indices_pre, blank_indices, latent_indices, clean_latent_indices_post, clean_latent_2x_indices, clean_latent_4x_indices = indices.split([1, latent_padding_size, latent_window_size, 1, 2, 16], dim=1) clean_latent_indices = torch.cat([clean_latent_indices_pre, clean_latent_indices_post], dim=1) clean_latents_pre = current_latent.to(history_latents) clean_latents_post, clean_latents_2x, clean_latents_4x = history_latents[:, :, :1 + 2 + 16, :, :].split([1, 2, 16], dim=2) clean_latents = torch.cat([clean_latents_pre, clean_latents_post], dim=2) if not high_vram: unload_complete_models() # GPUメモリ保存値を明示的に浮動小数点に変換 preserved_memory = float(gpu_memory_preservation) if gpu_memory_preservation is not None else 6.0 print(f'Setting transformer memory preservation to: {preserved_memory} GB') move_model_to_device_with_memory_preservation(transformer, target_device=gpu, preserved_memory_gb=preserved_memory) if use_teacache: transformer.initialize_teacache(enable_teacache=True, num_steps=steps) else: transformer.initialize_teacache(enable_teacache=False) def callback(d): preview = d['denoised'] preview = vae_decode_fake(preview) preview = (preview * 255.0).detach().cpu().numpy().clip(0, 255).astype(np.uint8) preview = einops.rearrange(preview, 'b c t h w -> (b h) (t w) c') if stream.input_queue.top() == 'end': stream.output_queue.push(('end', None)) raise KeyboardInterrupt('User ends the task.') current_step = d['i'] + 1 percentage = int(100.0 * current_step / steps) hint = f'Sampling {current_step}/{steps}' # セクション情報を追加(現在のセクション/全セクション) section_info = f'セクション: {i_section+1}/{total_sections}, ' desc = f'{section_info}Total generated frames: {int(max(0, total_generated_latent_frames * 4 - 3))}, Video length: {max(0, (total_generated_latent_frames * 4 - 3) / 30) :.2f} seconds (FPS-30). The video is being extended now ...' stream.output_queue.push(('progress', (preview, desc, make_progress_bar_html(percentage, hint)))) return generated_latents = sample_hunyuan( transformer=transformer, sampler='unipc', width=width, height=height, frames=num_frames, real_guidance_scale=cfg, distilled_guidance_scale=gs, guidance_rescale=rs, # shift=3.0, num_inference_steps=steps, generator=rnd, prompt_embeds=llama_vec, prompt_embeds_mask=llama_attention_mask, prompt_poolers=clip_l_pooler, negative_prompt_embeds=llama_vec_n, negative_prompt_embeds_mask=llama_attention_mask_n, negative_prompt_poolers=clip_l_pooler_n, device=gpu, dtype=torch.bfloat16, image_embeddings=image_encoder_last_hidden_state, latent_indices=latent_indices, clean_latents=clean_latents, clean_latent_indices=clean_latent_indices, clean_latents_2x=clean_latents_2x, clean_latent_2x_indices=clean_latent_2x_indices, clean_latents_4x=clean_latents_4x, clean_latent_4x_indices=clean_latent_4x_indices, callback=callback, ) if is_last_section: generated_latents = torch.cat([start_latent.to(generated_latents), generated_latents], dim=2) total_generated_latent_frames += int(generated_latents.shape[2]) history_latents = torch.cat([generated_latents.to(history_latents), history_latents], dim=2) if not high_vram: # 減圧時に使用するGPUメモリ値も明示的に浮動小数点に設定 preserved_memory_offload = 8.0 # こちらは固定値のまま print(f'Offloading transformer with memory preservation: {preserved_memory_offload} GB') offload_model_from_device_for_memory_preservation(transformer, target_device=gpu, preserved_memory_gb=preserved_memory_offload) load_model_as_complete(vae, target_device=gpu) real_history_latents = history_latents[:, :, :total_generated_latent_frames, :, :] if history_pixels is None: history_pixels = vae_decode(real_history_latents, vae).cpu() else: section_latent_frames = (latent_window_size * 2 + 1) if is_last_section else (latent_window_size * 2) overlapped_frames = latent_window_size * 4 - 3 current_pixels = vae_decode(real_history_latents[:, :, :section_latent_frames], vae).cpu() history_pixels = soft_append_bcthw(current_pixels, history_pixels, overlapped_frames) # 各セクションの最終フレームを静止画として保存(セクション番号付き) if save_section_frames and history_pixels is not None: try: if i_section == 0 or current_pixels is None: # 最初のセクションは history_pixels の最後 last_frame = history_pixels[0, :, -1, :, :] else: # 2セクション目以降は current_pixels の最後 last_frame = current_pixels[0, :, -1, :, :] last_frame = einops.rearrange(last_frame, 'c h w -> h w c') last_frame = last_frame.cpu().numpy() last_frame = np.clip((last_frame * 127.5 + 127.5), 0, 255).astype(np.uint8) last_frame = resize_and_center_crop(last_frame, target_width=width, target_height=height) if is_first_section and end_frame is None: Image.fromarray(last_frame).save(os.path.join(outputs_folder, f'{job_id}_{i_section}_end.png')) else: Image.fromarray(last_frame).save(os.path.join(outputs_folder, f'{job_id}_{i_section}.png')) except Exception as e: print(f"[WARN] セクション{ i_section }最終フレーム画像保存時にエラー: {e}") if not high_vram: unload_complete_models() output_filename = os.path.join(outputs_folder, f'{job_id}_{total_generated_latent_frames}.mp4') save_bcthw_as_mp4(history_pixels, output_filename, fps=30) print(f'Decoded. Current latent shape {real_history_latents.shape}; pixel shape {history_pixels.shape}') print(f"\u25a0 セクション{i_section}の処理完了") print(f" - 現在の累計フレーム数: {int(max(0, total_generated_latent_frames * 4 - 3))}フレーム") print(f" - レンダリング時間: {max(0, (total_generated_latent_frames * 4 - 3) / 30) :.2f}秒") print(f" - 出力ファイル: {output_filename}") stream.output_queue.push(('file', output_filename)) if is_last_section: # 処理終了時に通知 if HAS_WINSOUND: winsound.PlaySound("SystemExclamation", winsound.SND_ALIAS) else: print("\n✓ 処理が完了しました!") # Linuxでの代替通知 # 全体の処理時間を計算 process_end_time = time.time() total_process_time = process_end_time - process_start_time hours, remainder = divmod(total_process_time, 3600) minutes, seconds = divmod(remainder, 60) time_str = "" if hours > 0: time_str = f"{int(hours)}時間 {int(minutes)}分 {seconds:.1f}秒" elif minutes > 0: time_str = f"{int(minutes)}分 {seconds:.1f}秒" else: time_str = f"{seconds:.1f}秒" print(f"\n全体の処理時間: {time_str}") completion_message = f"すべてのセクション({total_sections}/{total_sections})が完了しました。全体の処理時間: {time_str}" stream.output_queue.push(('progress', (None, completion_message, make_progress_bar_html(100, '処理完了')))) # 中間ファイルの削除処理 if not keep_section_videos: # 最終動画のフルパス final_video_path = output_filename final_video_name = os.path.basename(final_video_path) # job_id部分を取得(タイムスタンプ部分) job_id_part = job_id # ディレクトリ内のすべてのファイルを取得 files = os.listdir(outputs_folder) deleted_count = 0 for file in files: # 同じjob_idを持つMP4ファイルかチェック if file.startswith(job_id_part) and file.endswith('.mp4') and file != final_video_name: file_path = os.path.join(outputs_folder, file) try: os.remove(file_path) deleted_count += 1 print(f"[削除] 中間ファイル: {file}") except Exception as e: print(f"[エラー] ファイル削除時のエラー {file}: {e}") if deleted_count > 0: print(f"[済] {deleted_count}個の中間ファイルを削除しました。最終ファイルは保存されています: {final_video_name}") stream.output_queue.push(('progress', (None, f"{deleted_count}個の中間ファイルを削除しました。最終動画は保存されています。", make_progress_bar_html(100, '処理完了')))) break except: traceback.print_exc() if not high_vram: unload_complete_models( text_encoder, text_encoder_2, image_encoder, vae, transformer ) stream.output_queue.push(('end', None)) return def process(input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, use_random_seed, save_section_frames, keep_section_videos, section_settings): global stream assert input_image is not None, 'No input image!' # 動画生成の設定情報をログに出力 total_latent_sections = int(max(round((total_second_length * 30) / (latent_window_size * 4)), 1)) mode_name = "通常モード" if mode_radio.value == MODE_TYPE_NORMAL else "ループモード" print(f"\n==== 動画生成開始 =====") print(f"\u25c6 生成モード: {mode_name}") print(f"\u25c6 動画長: {total_second_length}秒") print(f"\u25c6 生成セクション数: {total_latent_sections}回") print(f"\u25c6 サンプリングステップ数: {steps}") print(f"\u25c6 TeaCache使用: {use_teacache}") # セクションごとのキーフレーム画像の使用状況をログに出力 valid_sections = [] if section_settings is not None: for i, sec_data in enumerate(section_settings): if sec_data and sec_data[1] is not None: # 画像が設定されている場合 valid_sections.append(sec_data[0]) if valid_sections: print(f"\u25c6 使用するキーフレーム画像: セクション{', '.join(map(str, valid_sections))}") else: print(f"\u25c6 キーフレーム画像: デフォルト設定のみ使用") print(f"=============================\n") if use_random_seed: seed = random.randint(0, 2**32 - 1) # UIのseed欄もランダム値で更新 yield None, None, '', '', gr.update(interactive=False), gr.update(interactive=True), gr.update(value=seed) else: yield None, None, '', '', gr.update(interactive=False), gr.update(interactive=True), gr.update() stream = AsyncStream() # GPUメモリの設定値をデバッグ出力し、正しい型に変換 gpu_memory_value = float(gpu_memory_preservation) if gpu_memory_preservation is not None else 6.0 print(f'Using GPU memory preservation setting: {gpu_memory_value} GB') async_run(worker, input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_value, use_teacache, save_section_frames, keep_section_videos, section_settings) output_filename = None while True: flag, data = stream.output_queue.next() if flag == 'file': output_filename = data yield output_filename, gr.update(), gr.update(), gr.update(), gr.update(interactive=False), gr.update(interactive=True), gr.update() if flag == 'progress': preview, desc, html = data yield gr.update(), gr.update(visible=True, value=preview), desc, html, gr.update(interactive=False), gr.update(interactive=True), gr.update() if flag == 'end': yield output_filename, gr.update(visible=False), gr.update(), '', gr.update(interactive=True), gr.update(interactive=False), gr.update() break def end_process(): stream.input_queue.push('end') # プリセット管理関連の関数 def initialize_presets(): """初期プリセットファイルがない場合に作成する関数""" preset_file = os.path.join(presets_folder, 'prompt_presets.json') # デフォルトのプロンプト default_prompts = [ 'A character doing some simple body movements.', 'A character uses expressive hand gestures and body language.', 'A character walks leisurely with relaxed movements.', 'A character performs dynamic movements with energy and flowing motion.', 'A character moves in unexpected ways, with surprising transitions poses.', ] # デフォルト起動時プロンプト default_startup_prompt = "A character doing some simple body movements." # 既存ファイルがあり、正常に読み込める場合は終了 if os.path.exists(preset_file): try: with open(preset_file, 'r', encoding='utf-8') as f: presets_data = json.load(f) # 起動時デフォルトがあるか確認 startup_default_exists = any(preset.get("is_startup_default", False) for preset in presets_data.get("presets", [])) # なければ追加 if not startup_default_exists: presets_data.setdefault("presets", []).append({ "name": "起動時デフォルト", "prompt": default_startup_prompt, "timestamp": datetime.now().isoformat(), "is_default": True, "is_startup_default": True }) presets_data["default_startup_prompt"] = default_startup_prompt with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) return except: # エラーが発生した場合は新規作成 pass # 新規作成 presets_data = { "presets": [], "default_startup_prompt": default_startup_prompt } # デフォルトのプリセットを追加 for i, prompt_text in enumerate(default_prompts): presets_data["presets"].append({ "name": f"デフォルト {i+1}: {prompt_text[:20]}...", "prompt": prompt_text, "timestamp": datetime.now().isoformat(), "is_default": True }) # 起動時デフォルトプリセットを追加 presets_data["presets"].append({ "name": "起動時デフォルト", "prompt": default_startup_prompt, "timestamp": datetime.now().isoformat(), "is_default": True, "is_startup_default": True }) # 保存 try: with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) except: # 保存に失敗してもエラーは出さない(次回起動時に再試行される) pass def load_presets(): """プリセットを読み込む関数""" preset_file = os.path.join(presets_folder, 'prompt_presets.json') # 初期化関数を呼び出し(初回実行時のみ作成される) initialize_presets() max_retries = 3 retry_count = 0 while retry_count < max_retries: try: with open(preset_file, 'r', encoding='utf-8') as f: file_contents = f.read() if not file_contents.strip(): print(f"読み込み時に空ファイルが検出されました: {preset_file}") # 空ファイルの場合は再初期化を試みる initialize_presets() retry_count += 1 continue data = json.loads(file_contents) print(f"プリセットファイル読み込み成功: {len(data.get('presets', []))}件") return data except (json.JSONDecodeError, UnicodeDecodeError) as e: # JSONパースエラーの場合はファイルが破損している可能性がある print(f"プリセットファイルの形式が不正です: {e}") # ファイルをバックアップ backup_file = f"{preset_file}.bak.{int(time.time())}" try: import shutil shutil.copy2(preset_file, backup_file) print(f"破損したファイルをバックアップしました: {backup_file}") except Exception as backup_error: print(f"バックアップ作成エラー: {backup_error}") # 再初期化 initialize_presets() retry_count += 1 except Exception as e: print(f"プリセット読み込みエラー: {e}") # エラー発生 retry_count += 1 # 再試行しても失敗した場合は空のデータを返す print("再試行しても読み込みに失敗しました。空のデータを返します。") return {"presets": []} def get_default_startup_prompt(): """起動時に表示するデフォルトプロンプトを取得する関数""" print("起動時デフォルトプロンプト読み込み開始") presets_data = load_presets() # プリセットからデフォルト起動時プロンプトを探す for preset in presets_data["presets"]: if preset.get("is_startup_default", False): startup_prompt = preset["prompt"] print(f"起動時デフォルトプロンプトを読み込み: '{startup_prompt[:30]}...' (長さ: {len(startup_prompt)}文字)") # 重複しているかチェック # 例えば「A character」が複数回出てくる場合は重複している可能性がある if "A character" in startup_prompt and startup_prompt.count("A character") > 1: print("プロンプトに重複が見つかりました。最初のセンテンスのみを使用します。") # 最初のセンテンスのみを使用 sentences = startup_prompt.split(".") if len(sentences) > 0: clean_prompt = sentences[0].strip() + "." print(f"正規化されたプロンプト: '{clean_prompt}'") return clean_prompt return startup_prompt # 見つからない場合はデフォルト設定を使用 if "default_startup_prompt" in presets_data: default_prompt = presets_data["default_startup_prompt"] print(f"デフォルト設定から読み込み: '{default_prompt[:30]}...' (長さ: {len(default_prompt)}文字)") # 同様に重複チェック if "A character" in default_prompt and default_prompt.count("A character") > 1: print("デフォルトプロンプトに重複が見つかりました。最初のセンテンスのみを使用します。") sentences = default_prompt.split(".") if len(sentences) > 0: clean_prompt = sentences[0].strip() + "." print(f"正規化されたデフォルトプロンプト: '{clean_prompt}'") return clean_prompt return default_prompt # フォールバックとしてプログラムのデフォルト値を返す fallback_prompt = "A character doing some simple body movements." print(f"プログラムのデフォルト値を使用: '{fallback_prompt}'") return fallback_prompt def save_preset(name, prompt_text): """プリセットを保存する関数""" presets_data = load_presets() if not name: # 名前が空の場合は起動時デフォルトとして保存 # 既存の起動時デフォルトを探す startup_default_exists = False for preset in presets_data["presets"]: if preset.get("is_startup_default", False): # 既存の起動時デフォルトを更新 preset["prompt"] = prompt_text preset["timestamp"] = datetime.now().isoformat() startup_default_exists = True # 起動時デフォルトを更新 break if not startup_default_exists: # 見つからない場合は新規作成 presets_data["presets"].append({ "name": "起動時デフォルト", "prompt": prompt_text, "timestamp": datetime.now().isoformat(), "is_default": True, "is_startup_default": True }) print(f"起動時デフォルトを新規作成: {prompt_text[:50]}...") # デフォルト設定も更新 presets_data["default_startup_prompt"] = prompt_text preset_file = os.path.join(presets_folder, 'prompt_presets.json') try: # JSON直接書き込み with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) # プロンプトの値を更新 if 'prompt' in globals(): prompt.value = prompt_text return "プリセット '起動時デフォルト' を保存しました" except Exception as e: print(f"プリセット保存エラー: {e}") traceback.print_exc() return f"保存エラー: {e}" # 通常のプリセット保存処理 # 同名のプリセットがあれば上書き、なければ追加 preset_exists = False for preset in presets_data["presets"]: if preset["name"] == name: preset["prompt"] = prompt_text preset["timestamp"] = datetime.now().isoformat() preset_exists = True # 既存のプリセットを更新 break if not preset_exists: presets_data["presets"].append({ "name": name, "prompt": prompt_text, "timestamp": datetime.now().isoformat(), "is_default": False }) # 新規プリセットを作成 preset_file = os.path.join(presets_folder, 'prompt_presets.json') try: # JSON直接書き込み with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) # ファイル保存成功 return f"プリセット '{name}' を保存しました" except Exception as e: print(f"プリセット保存エラー: {e}") # エラー発生 return f"保存エラー: {e}" def delete_preset(preset_name): """プリセットを削除する関数""" if not preset_name: return "プリセットを選択してください" presets_data = load_presets() # 削除対象のプリセットを確認 target_preset = None for preset in presets_data["presets"]: if preset["name"] == preset_name: target_preset = preset break if not target_preset: return f"プリセット '{preset_name}' が見つかりません" # デフォルトプリセットは削除できない if target_preset.get("is_default", False): return f"デフォルトプリセットは削除できません" # プリセットを削除 presets_data["presets"] = [p for p in presets_data["presets"] if p["name"] != preset_name] preset_file = os.path.join(presets_folder, 'prompt_presets.json') try: with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) return f"プリセット '{preset_name}' を削除しました" except Exception as e: return f"削除エラー: {e}" # 既存のQuick Prompts(初期化時にプリセットに変換されるので、互換性のために残す) quick_prompts = [ 'A character doing some simple body movements.', 'A character uses expressive hand gestures and body language.', 'A character walks leisurely with relaxed movements.', 'A character performs dynamic movements with energy and flowing motion.', 'A character moves in unexpected ways, with surprising transitions poses.', ] quick_prompts = [[x] for x in quick_prompts] css = make_progress_bar_css() + """ .title-suffix { color: currentColor; opacity: 0.05; } .highlighted-keyframe { border: 4px solid #ff3860 !important; box-shadow: 0 0 10px rgba(255, 56, 96, 0.5) !important; background-color: rgba(255, 56, 96, 0.05) !important; } /* セクション番号ラベルの強調表示 */ .highlighted-label label { color: #ff3860 !important; font-weight: bold !important; } """ block = gr.Blocks(css=css).queue() with block: gr.HTML('

FramePack-eichi

') # デバッグ情報の表示 # print_keyframe_debug_info() # モード選択用のラジオボタンと動画長選択用のラジオボタンを横並びに配置 with gr.Row(): with gr.Column(scale=1): mode_radio = gr.Radio(choices=[MODE_TYPE_NORMAL, MODE_TYPE_LOOP], value=MODE_TYPE_NORMAL, label="生成モード", info="通常:一般的な生成 / ループ:ループ動画用") with gr.Column(scale=1): # 設定から動的に選択肢を生成 length_radio = gr.Radio(choices=get_video_modes(), value="6秒", label="動画長", info="キーフレーム画像のコピー範囲と動画の長さを設定") with gr.Row(): with gr.Column(): input_image = gr.Image(sources='upload', type="numpy", label="Image", height=320) end_frame = gr.Image(sources='upload', type="numpy", label="Final Frame (Optional)", height=320) with gr.Row(): start_button = gr.Button(value="Start Generation") end_button = gr.Button(value="End Generation", interactive=False) prompt = gr.Textbox(label="Prompt", value=get_default_startup_prompt(), lines=6) with gr.Row(): gr.Markdown("※プリセット名を空にして「保存」すると起動時デフォルトになります") # 互換性のためにQuick Listも残しておくが、非表示にする with gr.Row(visible=False): example_quick_prompts = gr.Dataset(samples=quick_prompts, label='Quick List', samples_per_page=1000, components=[prompt]) example_quick_prompts.click(lambda x: x[0], inputs=[example_quick_prompts], outputs=prompt, show_progress=False, queue=False) with gr.Group(): use_teacache = gr.Checkbox(label='Use TeaCache', value=True, info='Faster speed, but often makes hands and fingers slightly worse.') # Use Random Seedの初期値 use_random_seed_default = True seed_default = random.randint(0, 2**32 - 1) if use_random_seed_default else 1 use_random_seed = gr.Checkbox(label="Use Random Seed", value=use_random_seed_default) n_prompt = gr.Textbox(label="Negative Prompt", value="", visible=False) # Not used seed = gr.Number(label="Seed", value=seed_default, precision=0) def set_random_seed(is_checked): if is_checked: return random.randint(0, 2**32 - 1) else: return gr.update() use_random_seed.change(fn=set_random_seed, inputs=use_random_seed, outputs=seed) total_second_length = gr.Slider(label="Total Video Length (Seconds)", minimum=1, maximum=120, value=6, step=1) latent_window_size = gr.Slider(label="Latent Window Size", minimum=1, maximum=33, value=9, step=1, visible=False) # Should not change steps = gr.Slider(label="Steps", minimum=1, maximum=100, value=25, step=1, info='Changing this value is not recommended.') cfg = gr.Slider(label="CFG Scale", minimum=1.0, maximum=32.0, value=1.0, step=0.01, visible=False) # Should not change gs = gr.Slider(label="Distilled CFG Scale", minimum=1.0, maximum=32.0, value=10.0, step=0.01, info='Changing this value is not recommended.') rs = gr.Slider(label="CFG Re-Scale", minimum=0.0, maximum=1.0, value=0.0, step=0.01, visible=False) # Should not change gpu_memory_preservation = gr.Slider(label="GPU Memory to Preserve (GB) (smaller = more VRAM usage)", minimum=6, maximum=128, value=9, step=0.1, info="空けておくGPUメモリ量を指定。小さい値=より多くのVRAMを使用可能=高速、大きい値=より少ないVRAMを使用=安全") # セクションごとの動画保存チェックボックスを追加(デフォルトOFF) keep_section_videos = gr.Checkbox(label="完了時にセクションごとの動画を残す", value=False, info="チェックがない場合は最終動画のみ保存されます(デフォルトOFF)") # セクションごとの静止画保存チェックボックスを追加(デフォルトOFF) save_section_frames = gr.Checkbox(label="セクションごとの静止画を保存", value=False, info="各セクションの最終フレームを静止画として保存します(デフォルトOFF)") # キーフレームコピー機能のオンオフ切り替え enable_keyframe_copy = gr.Checkbox(label="キーフレーム自動コピー機能を有効にする", value=True, info="オフにするとキーフレーム間の自動コピーが行われなくなります") # セクション設定(DataFrameをやめて個別入力欄に変更) # 設定から最大キーフレーム数を取得 max_keyframes = get_max_keyframes_count() # セクション設定の入力欄を動的に生成 section_number_inputs = [] section_image_inputs = [] section_prompt_inputs = [] # 空リストにしておく with gr.Group(): gr.Markdown("### セクション設定. セクション番号は動画の終わりからカウント.(任意。指定しない場合は通常のImage/プロンプトを使用)") for i in range(max_keyframes): with gr.Row(): section_number = gr.Number(label=f"セクション番号{i+1}", value=i, precision=0) section_image = gr.Image(label=f"キーフレーム画像{i+1}", sources="upload", type="numpy", height=200) section_number_inputs.append(section_number) section_image_inputs.append(section_image) # 重要なキーフレームの説明 with gr.Row(): with gr.Column(): # 設定から動的にHTML生成 note_html = gr.HTML(generate_keyframe_guide_html()) # section_settingsは9つの入力欄の値をまとめてリスト化 def collect_section_settings(*args): # args: [num1, img1, num2, img2, ...] return [[args[i], args[i+1], ""] for i in range(0, len(args), 2)] section_settings = gr.State([[None, None, ""] for _ in range(max_keyframes)]) section_inputs = [] for i in range(max_keyframes): section_inputs.extend([section_number_inputs[i], section_image_inputs[i]]) # section_inputsをまとめてsection_settings Stateに格納 def update_section_settings(*args): return collect_section_settings(*args) # section_inputsが変化したらsection_settings Stateを更新 for inp in section_inputs: inp.change(fn=update_section_settings, inputs=section_inputs, outputs=section_settings) # モード変更時の処理 mode_radio.change( fn=lambda mode, length: unified_mode_length_change_handler(mode, length, section_number_inputs), inputs=[mode_radio, length_radio], outputs=[input_image, end_frame] + section_image_inputs + [total_second_length] ) # 動画長変更時の処理 length_radio.change( fn=lambda mode, length: unified_mode_length_change_handler(mode, length, section_number_inputs), inputs=[mode_radio, length_radio], outputs=[input_image, end_frame] + section_image_inputs + [total_second_length] ) # 入力画像変更時の処理 input_image.change( fn=unified_input_image_change_handler, inputs=[input_image, mode_radio, length_radio, enable_keyframe_copy], outputs=[end_frame] + section_image_inputs ) # 各キーフレーム画像の変更イベントを個別に設定 # 一度に複数のコンポーネントを更新する代わりに、個別の更新関数を使用 def create_single_keyframe_handler(src_idx, target_idx): def handle_single_keyframe(img, mode, length, enable_copy): # コピー条件をチェック if img is None or not enable_copy: return gr.update() # コピー先のチェック targets = get_copy_targets(mode, length, src_idx) if target_idx in targets: return gr.update(value=img) return gr.update() return handle_single_keyframe # 各キーフレームについて、影響を受ける可能性のある後続のキーフレームごとに個別のイベントを設定 for i, src_image in enumerate(section_image_inputs): for j in range(i+1, len(section_image_inputs)): src_image.change( fn=create_single_keyframe_handler(i, j), inputs=[src_image, mode_radio, length_radio, enable_keyframe_copy], outputs=[section_image_inputs[j]] ) with gr.Column(): result_video = gr.Video(label="Finished Frames", autoplay=True, show_share_button=False, height=512, loop=True) progress_desc = gr.Markdown('', elem_classes='no-generating-animation') progress_bar = gr.HTML('', elem_classes='no-generating-animation') preview_image = gr.Image(label="Next Latents", height=200, visible=False) # プロンプト管理パネルの追加 with gr.Group(visible=True) as prompt_management: gr.Markdown("### プロンプト管理") # 編集画面を常時表示する with gr.Group(visible=True): # 起動時デフォルトの初期表示用に取得 default_prompt = "" default_name = "" for preset in load_presets()["presets"]: if preset.get("is_startup_default", False): default_prompt = preset["prompt"] default_name = preset["name"] break with gr.Row(): edit_name = gr.Textbox(label="プリセット名", placeholder="名前を入力...", value=default_name) edit_prompt = gr.Textbox(label="プロンプト", lines=5, value=default_prompt) with gr.Row(): # 起動時デフォルトをデフォルト選択に設定 default_preset = "起動時デフォルト" # プリセットデータから全プリセット名を取得 presets_data = load_presets() choices = [preset["name"] for preset in presets_data["presets"]] default_presets = [name for name in choices if any(p["name"] == name and p.get("is_default", False) for p in presets_data["presets"])] user_presets = [name for name in choices if name not in default_presets] sorted_choices = [(name, name) for name in sorted(default_presets) + sorted(user_presets)] preset_dropdown = gr.Dropdown(label="プリセット", choices=sorted_choices, value=default_preset, type="value") with gr.Row(): save_btn = gr.Button(value="保存", variant="primary") apply_preset_btn = gr.Button(value="反映", variant="primary") clear_btn = gr.Button(value="クリア") delete_preset_btn = gr.Button(value="削除") # メッセージ表示用 result_message = gr.Markdown("") # 実行ボタンのイベント ips = [input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, use_random_seed, save_section_frames, keep_section_videos, section_settings] start_button.click(fn=process, inputs=ips, outputs=[result_video, preview_image, progress_desc, progress_bar, start_button, end_button, seed]) end_button.click(fn=end_process) # プリセット保存ボタンのイベント def save_button_click_handler(name, prompt_text): """保存ボタンクリック時のハンドラ関数""" # 重複チェックと正規化 if "A character" in prompt_text and prompt_text.count("A character") > 1: sentences = prompt_text.split(".") if len(sentences) > 0: prompt_text = sentences[0].strip() + "." # 重複を検出したため正規化 # プリセット保存 result_msg = save_preset(name, prompt_text) # プリセットデータを取得してドロップダウンを更新 presets_data = load_presets() choices = [preset["name"] for preset in presets_data["presets"]] default_presets = [n for n in choices if any(p["name"] == n and p.get("is_default", False) for p in presets_data["presets"])] user_presets = [n for n in choices if n not in default_presets] sorted_choices = [(n, n) for n in sorted(default_presets) + sorted(user_presets)] # メインプロンプトは更新しない(保存のみを行う) return result_msg, gr.update(choices=sorted_choices), gr.update() # 保存ボタンのクリックイベントを接続 save_btn.click( fn=save_button_click_handler, inputs=[edit_name, edit_prompt], outputs=[result_message, preset_dropdown, prompt] ) # クリアボタン処理 def clear_fields(): return gr.update(value=""), gr.update(value="") clear_btn.click( fn=clear_fields, inputs=[], outputs=[edit_name, edit_prompt] ) # プリセット読込処理 def load_preset_handler(preset_name): # プリセット選択時に編集欄のみを更新 for preset in load_presets()["presets"]: if preset["name"] == preset_name: return gr.update(value=preset_name), gr.update(value=preset["prompt"]) return gr.update(), gr.update() # プリセット選択時に編集欄に反映 def load_preset_handler_wrapper(preset_name): # プリセット名がタプルの場合も処理する if isinstance(preset_name, tuple) and len(preset_name) == 2: preset_name = preset_name[1] # 値部分を取得 return load_preset_handler(preset_name) preset_dropdown.change( fn=load_preset_handler_wrapper, inputs=[preset_dropdown], outputs=[edit_name, edit_prompt] ) # 反映ボタン処理 - 編集画面の内容をメインプロンプトに反映 def apply_to_prompt(edit_text): """編集画面の内容をメインプロンプトに反映する関数""" # 編集画面のプロンプトをメインに適用 return gr.update(value=edit_text) # プリセット削除処理 def delete_preset_handler(preset_name): # プリセット名がタプルの場合も処理する if isinstance(preset_name, tuple) and len(preset_name) == 2: preset_name = preset_name[1] # 値部分を取得 result = delete_preset(preset_name) # プリセットデータを取得してドロップダウンを更新 presets_data = load_presets() choices = [preset["name"] for preset in presets_data["presets"]] default_presets = [name for name in choices if any(p["name"] == name and p.get("is_default", False) for p in presets_data["presets"])] user_presets = [name for name in choices if name not in default_presets] sorted_names = sorted(default_presets) + sorted(user_presets) updated_choices = [(name, name) for name in sorted_names] return result, gr.update(choices=updated_choices) apply_preset_btn.click( fn=apply_to_prompt, inputs=[edit_prompt], outputs=[prompt] ) delete_preset_btn.click( fn=delete_preset_handler, inputs=[preset_dropdown], outputs=[result_message, preset_dropdown] ) # 起動コード block.launch( server_name=args.server, server_port=args.port, share=args.share, inbrowser=args.inbrowser, ) ``` ## /version/v1.1/webui/video_mode_settings.py ```py path="/version/v1.1/webui/video_mode_settings.py" # -*- coding: utf-8 -*- """ FramePack-eichi 動画モード設定モジュール 柔軟なモード設定のための構造化データと関連ユーティリティ関数を提供します """ import math import gradio as gr # モードタイプの定数定義 MODE_TYPE_NORMAL = "通常" MODE_TYPE_LOOP = "ループ" # ビデオモード設定の定義 # このデータ構造がモード選択の中心となります VIDEO_MODE_SETTINGS = { "6秒": { "frames": 180, # 6秒×30FPS "sections": 6, # 必要セクション数(正確な計算に基づく) "display_seconds": 6.0, # UI表示用秒数 "important_keyframes": [0], # 重要なキーフレームのインデックス(0始まり) "copy_patterns": { "通常": { "0": [1, 2, 3], # キーフレーム0→1,2,3にコピー }, "ループ": { "0": [1, 2, 3], # キーフレーム0→1,2,3にコピー } } }, "8秒": { "frames": 240, # 8秒×30FPS "sections": 8, # 必要セクション数 "display_seconds": 8.0, # UI表示用秒数 "important_keyframes": [0], # 重要なキーフレームのインデックス(0始まり) "copy_patterns": { "通常": { "0": [1, 2, 3, 4, 5], # キーフレーム0→1,2,3,4,5にコピー }, "ループ": { "0": [1, 2, 3, 4, 5], # キーフレーム0→1,2,3,4,5にコピー } } }, "10(5x2)秒": { "frames": 324, # 10.8秒×30FPS "sections": 10, # 必要セクション数 "display_seconds": 10.8, # UI表示用秒数 "important_keyframes": [0, 4], # 重要なキーフレームのインデックス "copy_patterns": { "通常": { "0": [1, 2, 3], # キーフレーム0→1,2,3にコピー "4": [5, 6, 7], # キーフレーム4→5,6,7にコピー }, "ループ": { "0": [1, 2, 3], # キーフレーム0→1,2,3にコピー "4": [5, 6, 7], # キーフレーム4→5,6,7にコピー } } }, "12(4x3)秒": { "frames": 360, # 12秒×30FPS "sections": 12, # 必要セクション数 "display_seconds": 12.0, # UI表示用秒数 "important_keyframes": [0, 3, 6], # 重要なキーフレームのインデックス "copy_patterns": { "通常": { "0": [1, 2], # キーフレーム0→1,2にコピー "3": [4, 5], # キーフレーム3→4,5にコピー "6": [7, 8], # キーフレーム6→7,8にコピー }, "ループ": { "0": [1, 2], # キーフレーム0→1,2にコピー "3": [4, 5], # キーフレーム3→4,5にコピー "6": [7, 8], # キーフレーム6→7,8にコピー } } }, # 新しい16(4x4)秒モード "16(4x4)秒": { "frames": 480, # 16秒×30FPS "sections": 15, # 必要セクション数(計算に基づく) "display_seconds": 16.0, # UI表示用秒数 "important_keyframes": [0, 3, 6, 9], # 重要なキーフレームのインデックス "copy_patterns": { "通常": { "0": [1, 2], # キーフレーム0→1,2にコピー "3": [4, 5], # キーフレーム3→4,5にコピー "6": [7, 8], # キーフレーム6→7,8にコピー "9": [10, 11], # キーフレーム9→10,11にコピー }, "ループ": { "0": [1, 2], # キーフレーム0→1,2にコピー "3": [4, 5], # キーフレーム3→4,5にコピー "6": [7, 8], # キーフレーム6→7,8にコピー "9": [10, 11], # キーフレーム9→10,11にコピー } } } } # HTMLキャッシュ関連 _html_cache = {} def clear_html_cache(): """HTMLキャッシュをクリアする""" global _html_cache _html_cache = {} # ユーティリティ関数 def get_video_modes(): """利用可能なビデオモードのリストを取得""" return list(VIDEO_MODE_SETTINGS.keys()) def get_video_frames(mode_key): """モード名から総フレーム数を取得""" if mode_key not in VIDEO_MODE_SETTINGS: raise ValueError(f"Unknown video mode: {mode_key}") return VIDEO_MODE_SETTINGS[mode_key]["frames"] def get_video_seconds(mode_key): """モード名から表示用秒数を取得""" if mode_key not in VIDEO_MODE_SETTINGS: raise ValueError(f"Unknown video mode: {mode_key}") return VIDEO_MODE_SETTINGS[mode_key]["display_seconds"] def get_important_keyframes(mode_key): """重要なキーフレームのインデックスを取得""" if mode_key not in VIDEO_MODE_SETTINGS: raise ValueError(f"Unknown video mode: {mode_key}") return VIDEO_MODE_SETTINGS[mode_key]["important_keyframes"] def get_total_sections(mode_key): """モード名からセクション数を取得""" if mode_key not in VIDEO_MODE_SETTINGS: raise ValueError(f"Unknown video mode: {mode_key}") return VIDEO_MODE_SETTINGS[mode_key]["sections"] def get_copy_targets(mode, mode_key, keyframe_index): """指定キーフレームからのコピー先を取得""" if mode_key not in VIDEO_MODE_SETTINGS: raise ValueError(f"Unknown video mode: {mode_key}") if mode not in VIDEO_MODE_SETTINGS[mode_key]["copy_patterns"]: return [] str_keyframe_index = str(keyframe_index) if str_keyframe_index not in VIDEO_MODE_SETTINGS[mode_key]["copy_patterns"][mode]: return [] return VIDEO_MODE_SETTINGS[mode_key]["copy_patterns"][mode][str_keyframe_index] def get_max_keyframes_count(): """設定で使用されている最大キーフレーム数を取得""" max_kf = 0 for mode_key in VIDEO_MODE_SETTINGS: # まず重要なキーフレームの最大値をチェック if "important_keyframes" in VIDEO_MODE_SETTINGS[mode_key]: important_kfs = VIDEO_MODE_SETTINGS[mode_key]["important_keyframes"] if important_kfs and max(important_kfs) > max_kf: max_kf = max(important_kfs) # 次にコピーパターンの中の最大値をチェック for mode_type in ["通常", "ループ"]: if mode_type not in VIDEO_MODE_SETTINGS[mode_key]["copy_patterns"]: continue for src_kf_str in VIDEO_MODE_SETTINGS[mode_key]["copy_patterns"][mode_type]: src_kf = int(src_kf_str) if src_kf > max_kf: max_kf = src_kf targets = VIDEO_MODE_SETTINGS[mode_key]["copy_patterns"][mode_type][src_kf_str] if targets and max(targets) > max_kf: max_kf = max(targets) return max_kf + 1 # 0始まりなので+1 def generate_keyframe_guide_html(): """キーフレームガイドのHTML生成""" # キャッシュがあれば使用 global _html_cache if "keyframe_guide" in _html_cache: return _html_cache["keyframe_guide"] html = """
■ キーフレーム画像設定ガイド:
※ 重要なキーフレームは赤枠で強調表示されます
""" # キャッシュに保存 _html_cache["keyframe_guide"] = html return html # 動画モードの追加が容易になるようサポート関数を追加 def add_video_mode(mode_name, frames, sections, display_seconds, important_keyframes, copy_patterns): """ 新しい動画モードを設定に追加する関数 Args: mode_name: モード名(例: "6秒") frames: フレーム数 sections: セクション数 display_seconds: 表示用秒数 important_keyframes: 重要なキーフレームのインデックス(0始まり)のリスト copy_patterns: コピーパターン辞書 {"通常": {...}, "ループ": {...}} """ VIDEO_MODE_SETTINGS[mode_name] = { "frames": frames, "sections": sections, "display_seconds": display_seconds, "important_keyframes": important_keyframes, "copy_patterns": copy_patterns } # ガイドHTML等のキャッシュをクリア clear_html_cache() def handle_mode_length_change(mode, length, section_number_inputs): """モードと動画長の変更時のUI更新処理""" # 基本要素のクリア(Image, Final Frame) base_updates = [gr.update(value=None) for _ in range(2)] # キーフレーム画像の更新リスト生成 keyframe_updates = [] max_keyframes = get_max_keyframes_count() for i in range(max_keyframes): keyframe_updates.append(gr.update(value=None, elem_classes="")) # セクション番号ラベルのリセット for i in range(max_keyframes): if i < len(section_number_inputs): section_number_inputs[i].elem_classes = "" # 重要なキーフレームの強調表示 important_kfs = get_important_keyframes(length) for idx in important_kfs: if idx < len(keyframe_updates): keyframe_updates[idx] = gr.update(value=None, elem_classes="highlighted-keyframe") if idx < len(section_number_inputs): section_number_inputs[idx].elem_classes = "highlighted-label" # ループモードの場合はキーフレーム1も重要 if mode == MODE_TYPE_LOOP: keyframe_updates[0] = gr.update(value=None, elem_classes="highlighted-keyframe") if 0 < len(section_number_inputs): section_number_inputs[0].elem_classes = "highlighted-label" # 動画長の設定 video_length = get_video_seconds(length) # 結果を返す return base_updates + keyframe_updates + [gr.update(value=video_length)] def process_keyframe_change(keyframe_idx, img, mode, length, enable_copy=True): """キーフレーム画像変更時の処理(汎用版)""" if img is None or not enable_copy: # 更新なし max_keyframes = get_max_keyframes_count() return [gr.update() for _ in range(max_keyframes - keyframe_idx - 1)] # コピー先の取得 targets = get_copy_targets(mode, length, keyframe_idx) if not targets: max_keyframes = get_max_keyframes_count() return [gr.update() for _ in range(max_keyframes - keyframe_idx - 1)] # コピー先に対するアップデートを生成 max_keyframes = get_max_keyframes_count() updates = [] for i in range(keyframe_idx + 1, max_keyframes): # ターゲットインデックスへの相対位置を計算 if (i - keyframe_idx) in targets: updates.append(gr.update(value=img)) else: updates.append(gr.update()) return updates def print_settings_summary(enable_debug=False): """設定の概要をコンソールに出力(デバッグ用)""" if not enable_debug: return print("\n==== ビデオモード設定の概要 ====") for mode_key in VIDEO_MODE_SETTINGS: settings = VIDEO_MODE_SETTINGS[mode_key] print(f"\nモード: {mode_key}") print(f" フレーム数: {settings['frames']}") print(f" セクション数: {settings['sections']}") print(f" 表示秒数: {settings['display_seconds']}") print(f" 重要キーフレーム: {settings['important_keyframes']}") print(" コピーパターン:") for mode_type in settings["copy_patterns"]: print(f" {mode_type}:") for src, targets in settings["copy_patterns"][mode_type].items(): print(f" キーフレーム{src} → {targets}") max_kf = get_max_keyframes_count() print(f"\n最大キーフレーム数: {max_kf}") print("============================\n") # 将来の拡張用に保持 - 現在は未使用 """ def calculate_frames_per_section(latent_window_size=9): \"""1セクションあたりのフレーム数を計算\""" return latent_window_size * 4 - 3 def calculate_sections_from_frames(total_frames, latent_window_size=9): \"""フレーム数から必要なセクション数を計算\""" frames_per_section = calculate_frames_per_section(latent_window_size) return math.ceil(total_frames / frames_per_section) def calculate_total_frame_count(sections, latent_window_size=9): \"""セクション数から総フレーム数を計算\""" frames_per_section = calculate_frames_per_section(latent_window_size) return sections * frames_per_section def calculate_total_second_length(frames, fps=30): \"""フレーム数から秒数を計算\""" return frames / fps """ ``` ## /version/v1.2/run_endframe_ichi.bat ```bat path="/version/v1.2/run_endframe_ichi.bat" @echo off call environment.bat cd %~dp0webui "%DIR%\python\python.exe" endframe_ichi.py --server 127.0.0.1 --inbrowser :done pause ``` ## /version/v1.2/webui/endframe_ichi.py ```py path="/version/v1.2/webui/endframe_ichi.py" from diffusers_helper.hf_login import login import os import random import time import subprocess # クロスプラットフォーム対応のための条件付きインポート try: import winsound HAS_WINSOUND = True except ImportError: HAS_WINSOUND = False import json import traceback from datetime import datetime, timedelta os.environ['HF_HOME'] = os.path.abspath(os.path.realpath(os.path.join(os.path.dirname(__file__), './hf_download'))) # 設定モジュールをインポート(ローカルモジュール) import os.path from video_mode_settings import ( VIDEO_MODE_SETTINGS, get_video_modes, get_video_seconds, get_important_keyframes, get_copy_targets, get_max_keyframes_count, get_total_sections, generate_keyframe_guide_html, handle_mode_length_change, process_keyframe_change, MODE_TYPE_NORMAL, MODE_TYPE_LOOP ) # インデックス変換のユーティリティ関数追加 def ui_to_code_index(ui_index): """UI表示のキーフレーム番号(1始まり)をコード内インデックス(0始まり)に変換""" return ui_index - 1 def code_to_ui_index(code_index): """コード内インデックス(0始まり)をUI表示のキーフレーム番号(1始まり)に変換""" return code_index + 1 import gradio as gr import torch import einops import safetensors.torch as sf import numpy as np import argparse import math from PIL import Image from diffusers import AutoencoderKLHunyuanVideo from transformers import LlamaModel, CLIPTextModel, LlamaTokenizerFast, CLIPTokenizer from diffusers_helper.hunyuan import encode_prompt_conds, vae_decode, vae_encode, vae_decode_fake from diffusers_helper.utils import save_bcthw_as_mp4, crop_or_pad_yield_mask, soft_append_bcthw, resize_and_center_crop, state_dict_weighted_merge, state_dict_offset_merge, generate_timestamp from diffusers_helper.models.hunyuan_video_packed import HunyuanVideoTransformer3DModelPacked from diffusers_helper.pipelines.k_diffusion_hunyuan import sample_hunyuan from diffusers_helper.memory import cpu, gpu, get_cuda_free_memory_gb, move_model_to_device_with_memory_preservation, offload_model_from_device_for_memory_preservation, fake_diffusers_current_device, DynamicSwapInstaller, unload_complete_models, load_model_as_complete from diffusers_helper.thread_utils import AsyncStream, async_run from diffusers_helper.gradio.progress_bar import make_progress_bar_css, make_progress_bar_html from transformers import SiglipImageProcessor, SiglipVisionModel from diffusers_helper.clip_vision import hf_clip_vision_encode from diffusers_helper.bucket_tools import find_nearest_bucket parser = argparse.ArgumentParser() parser.add_argument('--share', action='store_true') parser.add_argument("--server", type=str, default='127.0.0.1') parser.add_argument("--port", type=int, default=8001) parser.add_argument("--inbrowser", action='store_true') args = parser.parse_args() print(args) free_mem_gb = get_cuda_free_memory_gb(gpu) high_vram = free_mem_gb > 60 print(f'Free VRAM {free_mem_gb} GB') print(f'High-VRAM Mode: {high_vram}') # 元のモデル読み込みコード try: text_encoder = LlamaModel.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='text_encoder', torch_dtype=torch.float16).cpu() text_encoder_2 = CLIPTextModel.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='text_encoder_2', torch_dtype=torch.float16).cpu() tokenizer = LlamaTokenizerFast.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='tokenizer') tokenizer_2 = CLIPTokenizer.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='tokenizer_2') vae = AutoencoderKLHunyuanVideo.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='vae', torch_dtype=torch.float16).cpu() except Exception as e: print(f"モデル読み込みエラー: {e}") print("プログラムを終了します...") import sys sys.exit(1) # 他のモデルも同様に例外処理 try: feature_extractor = SiglipImageProcessor.from_pretrained("lllyasviel/flux_redux_bfl", subfolder='feature_extractor') image_encoder = SiglipVisionModel.from_pretrained("lllyasviel/flux_redux_bfl", subfolder='image_encoder', torch_dtype=torch.float16).cpu() transformer = HunyuanVideoTransformer3DModelPacked.from_pretrained('lllyasviel/FramePackI2V_HY', torch_dtype=torch.bfloat16).cpu() except Exception as e: print(f"モデル読み込みエラー (追加モデル): {e}") print("プログラムを終了します...") import sys sys.exit(1) vae.eval() text_encoder.eval() text_encoder_2.eval() image_encoder.eval() transformer.eval() if not high_vram: vae.enable_slicing() vae.enable_tiling() transformer.high_quality_fp32_output_for_inference = True print('transformer.high_quality_fp32_output_for_inference = True') transformer.to(dtype=torch.bfloat16) vae.to(dtype=torch.float16) image_encoder.to(dtype=torch.float16) text_encoder.to(dtype=torch.float16) text_encoder_2.to(dtype=torch.float16) vae.requires_grad_(False) text_encoder.requires_grad_(False) text_encoder_2.requires_grad_(False) image_encoder.requires_grad_(False) transformer.requires_grad_(False) if not high_vram: # DynamicSwapInstaller is same as huggingface's enable_sequential_offload but 3x faster DynamicSwapInstaller.install_model(transformer, device=gpu) DynamicSwapInstaller.install_model(text_encoder, device=gpu) else: text_encoder.to(gpu) text_encoder_2.to(gpu) image_encoder.to(gpu) vae.to(gpu) transformer.to(gpu) stream = AsyncStream() # 設定ファイル関連処理のリファクタリング def get_settings_file_path(): """設定ファイルの絶対パスを取得する""" base_path = os.path.dirname(os.path.abspath(__file__)) settings_folder = os.path.join(base_path, 'settings') return os.path.join(settings_folder, 'app_settings.json') def get_output_folder_path(folder_name=None): """出力フォルダの絶対パスを取得する""" base_path = os.path.dirname(os.path.abspath(__file__)) if not folder_name or not folder_name.strip(): folder_name = "outputs" return os.path.join(base_path, folder_name) def initialize_settings(): """設定ファイルを初期化する(存在しない場合のみ)""" settings_file = get_settings_file_path() settings_dir = os.path.dirname(settings_file) if not os.path.exists(settings_file): # 初期デフォルト設定 default_settings = {'output_folder': 'outputs'} try: os.makedirs(settings_dir, exist_ok=True) with open(settings_file, 'w', encoding='utf-8') as f: json.dump(default_settings, f, ensure_ascii=False, indent=2) return True except Exception as e: print(f"設定ファイル初期化エラー: {e}") return False return True def load_settings(): """設定を読み込む関数""" settings_file = get_settings_file_path() default_settings = {'output_folder': 'outputs'} if os.path.exists(settings_file): try: with open(settings_file, 'r', encoding='utf-8') as f: file_content = f.read() if not file_content.strip(): return default_settings settings = json.loads(file_content) # デフォルト値とマージ for key, value in default_settings.items(): if key not in settings: settings[key] = value return settings except Exception as e: print(f"設定読み込みエラー: {e}") return default_settings def save_settings(settings): """設定を保存する関数""" settings_file = get_settings_file_path() try: # 保存前にディレクトリが存在するか確認 os.makedirs(os.path.dirname(settings_file), exist_ok=True) # JSON書き込み with open(settings_file, 'w', encoding='utf-8') as f: json.dump(settings, f, ensure_ascii=False, indent=2) return True except Exception as e: print(f"設定保存エラー: {e}") return False def open_output_folder(folder_path): """指定されたフォルダをOSに依存せず開く""" if not os.path.exists(folder_path): os.makedirs(folder_path, exist_ok=True) try: if os.name == 'nt': # Windows subprocess.Popen(['explorer', folder_path]) elif os.name == 'posix': # Linux/Mac try: subprocess.Popen(['xdg-open', folder_path]) except: subprocess.Popen(['open', folder_path]) print(f"フォルダを開きました: {folder_path}") return True except Exception as e: print(f"フォルダを開く際にエラーが発生しました: {e}") return False # フォルダ構造を先に定義 webui_folder = os.path.dirname(os.path.abspath(__file__)) presets_folder = os.path.join(webui_folder, 'presets') os.makedirs(presets_folder, exist_ok=True) # 設定保存用フォルダの設定 settings_folder = os.path.join(webui_folder, 'settings') os.makedirs(settings_folder, exist_ok=True) # 設定ファイル初期化 initialize_settings() # ベースパスを定義 base_path = os.path.dirname(os.path.abspath(__file__)) # 設定から出力フォルダを取得 app_settings = load_settings() output_folder_name = app_settings.get('output_folder', 'outputs') print(f"設定から出力フォルダを読み込み: {output_folder_name}") # 出力フォルダのフルパスを生成 outputs_folder = get_output_folder_path(output_folder_name) os.makedirs(outputs_folder, exist_ok=True) # 統一的なキーフレーム処理関数群 # 1. 統一的なキーフレーム変更ハンドラ def unified_keyframe_change_handler(keyframe_idx, img, mode, length, enable_copy=True): """すべてのキーフレーム処理を統一的に行う関数 Args: keyframe_idx: UIのキーフレーム番号-1 (0始まりのインデックス) img: 変更されたキーフレーム画像 mode: モード ("通常" or "ループ") length: 動画長 ("6秒", "8秒", "10(5x2)秒", "12(4x3)秒", "16(4x4)秒") enable_copy: コピー機能が有効かどうか Returns: 更新リスト: 変更するキーフレーム画像の更新情報のリスト """ if img is None or not enable_copy: # 画像が指定されていない、またはコピー機能が無効の場合は何もしない max_keyframes = get_max_keyframes_count() remaining = max(0, max_keyframes - keyframe_idx - 1) return [gr.update() for _ in range(remaining)] # video_mode_settings.pyから定義されたコピーターゲットを取得 targets = get_copy_targets(mode, length, keyframe_idx) # 結果の更新リスト作成 max_keyframes = get_max_keyframes_count() updates = [] # このキーフレーム以降のインデックスに対してのみ処理 for i in range(keyframe_idx + 1, max_keyframes): # コピーパターン定義では相対インデックスでなく絶対インデックスが使われているため、 # iがtargets内にあるかをチェック if i in targets: # コピー先リストに含まれている場合は画像をコピー updates.append(gr.update(value=img)) else: # 含まれていない場合は変更なし updates.append(gr.update()) return updates # 2. モード変更の統一ハンドラ def unified_mode_length_change_handler(mode, length, section_number_inputs): """モードと動画長の変更を統一的に処理する関数 Args: mode: モード ("通常" or "ループ") length: 動画長 ("6秒", "8秒", "10(5x2)秒", "12(4x3)秒", "16(4x4)秒") section_number_inputs: セクション番号入力欄のリスト Returns: 更新リスト: 各UI要素の更新情報のリスト """ # 基本要素のクリア(入力画像と終了フレーム) updates = [gr.update(value=None) for _ in range(2)] # すべてのキーフレーム画像をクリア section_image_count = get_max_keyframes_count() for _ in range(section_image_count): updates.append(gr.update(value=None, elem_classes="")) # セクション番号ラベルをリセット for i in range(len(section_number_inputs)): section_number_inputs[i].elem_classes = "" # 重要なキーフレームを強調表示 important_kfs = get_important_keyframes(length) for idx in important_kfs: ui_idx = code_to_ui_index(idx) update_idx = ui_idx + 1 # 入力画像と終了フレームの2つを考慮 if update_idx < len(updates): updates[update_idx] = gr.update(value=None, elem_classes="highlighted-keyframe") if idx < len(section_number_inputs): section_number_inputs[idx].elem_classes = "highlighted-label" # ループモードの場合はキーフレーム1も強調(まだ強調されていない場合) if mode == MODE_TYPE_LOOP and 0 not in important_kfs: updates[2] = gr.update(value=None, elem_classes="highlighted-keyframe") if 0 < len(section_number_inputs): section_number_inputs[0].elem_classes = "highlighted-label" # 動画長の設定 video_length = get_video_seconds(length) # 最終的な動画長設定を追加 updates.append(gr.update(value=video_length)) return updates # 3. 入力画像変更の統一ハンドラ def unified_input_image_change_handler(img, mode, length, enable_copy=True): """入力画像変更時の処理を統一的に行う関数 Args: img: 変更された入力画像 mode: モード ("通常" or "ループ") length: 動画長 ("6秒", "8秒", "10(5x2)秒", "12(4x3)秒", "16(4x4)秒") enable_copy: コピー機能が有効かどうか Returns: 更新リスト: 終了フレームとすべてのキーフレーム画像の更新情報のリスト """ if img is None or not enable_copy: # 画像が指定されていない、またはコピー機能が無効の場合は何もしない section_count = get_max_keyframes_count() return [gr.update() for _ in range(section_count + 1)] # +1 for end_frame # ループモードかどうかで処理を分岐 if mode == MODE_TYPE_LOOP: # ループモード: FinalFrameに入力画像をコピー updates = [gr.update(value=img)] # end_frame # キーフレーム画像は更新なし section_count = get_max_keyframes_count() updates.extend([gr.update() for _ in range(section_count)]) else: # 通常モード: FinalFrameは更新なし updates = [gr.update()] # end_frame # 動画長/モードに基づいてコピー先のキーフレームを取得 # これが設定ファイルに基づく方法 copy_targets = [] # 特殊処理のモードでは設定によって異なるキーフレームにコピー if length == "10(5x2)秒": # 10(5x2)秒の場合は5~8にコピー (インデックス4-7) copy_targets = [4, 5, 6, 7] elif length == "12(4x3)秒": # 12(4x3)秒の場合は7~9にコピー (インデックス6-8) copy_targets = [6, 7, 8] elif length == "16(4x4)秒": # 16(4x4)秒の場合は10~12にコピー (インデックス9-11) copy_targets = [9, 10, 11] elif length == "20(4x5)秒": # 20(4x5)秒の場合は13~15にコピー (インデックス12-14) copy_targets = [12, 13, 14] else: # 通常の動画長の場合は最初のいくつかのキーフレームにコピー if length == "6秒": copy_targets = [0, 1, 2, 3] # キーフレーム1-4 elif length == "8秒": copy_targets = [0, 1, 2, 3, 4, 5] # キーフレーム1-6 # キーフレーム画像の更新リスト作成 section_count = get_max_keyframes_count() for i in range(section_count): if i in copy_targets: updates.append(gr.update(value=img)) else: updates.append(gr.update()) return updates # 4. デバッグ情報表示関数 - コメントアウト部分を関数として維持 def print_keyframe_debug_info(): """キーフレーム設定の詳細情報を表示""" # print("\n[INFO] =========== キーフレーム設定デバッグ情報 ===========") # # # 設定内容の確認表示 # print("\n[INFO] 動画モード設定の確認:") # for mode_key in VIDEO_MODE_SETTINGS: # mode_info = VIDEO_MODE_SETTINGS[mode_key] # print(f" - {mode_key}: {mode_info['display_seconds']}秒, {mode_info['frames']}フレーム") # # # 重要キーフレームの表示(UIインデックスに変換) # important_kfs = mode_info['important_keyframes'] # important_kfs_ui = [code_to_ui_index(kf) for kf in important_kfs] # print(f" 重要キーフレーム: {important_kfs_ui}") # # # コピーパターンの表示 # for mode_type in ["通常", "ループ"]: # if mode_type in mode_info["copy_patterns"]: # print(f" {mode_type}モードのコピーパターン:") # for src, targets in mode_info["copy_patterns"][mode_type].items(): # src_ui = code_to_ui_index(int(src)) # targets_ui = [code_to_ui_index(t) for t in targets] # print(f" キーフレーム{src_ui} → {targets_ui}") # # print("[INFO] =================================================\n") pass @torch.no_grad() def worker(input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, save_section_frames, keep_section_videos, output_dir=None, section_settings=None): # 出力フォルダの設定 global outputs_folder global output_folder_name if output_dir and output_dir.strip(): # 出力フォルダパスを取得 outputs_folder = get_output_folder_path(output_dir) print(f"出力フォルダを設定: {outputs_folder}") # フォルダ名が現在の設定と異なる場合は設定ファイルを更新 if output_dir != output_folder_name: settings = load_settings() settings['output_folder'] = output_dir if save_settings(settings): output_folder_name = output_dir print(f"出力フォルダ設定を保存しました: {output_dir}") else: # デフォルト設定を使用 outputs_folder = get_output_folder_path(output_folder_name) print(f"デフォルト出力フォルダを使用: {outputs_folder}") # フォルダが存在しない場合は作成 os.makedirs(outputs_folder, exist_ok=True) # 処理時間計測の開始 process_start_time = time.time() # 既存の計算方法を保持しつつ、設定からセクション数も取得する total_latent_sections = (total_second_length * 30) / (latent_window_size * 4) total_latent_sections = int(max(round(total_latent_sections), 1)) # 現在のモードを取得(UIから渡された情報から) # セクション数を全セクション数として保存 total_sections = total_latent_sections job_id = generate_timestamp() # セクション处理の詳細ログを出力 latent_paddings = reversed(range(total_latent_sections)) if total_latent_sections > 4: latent_paddings = [3] + [2] * (total_latent_sections - 3) + [1, 0] # 全セクション数を事前に計算して保存(イテレータの消費を防ぐため) latent_paddings_list = list(latent_paddings) total_sections = len(latent_paddings_list) latent_paddings = latent_paddings_list # リストに変換したものを使用 print(f"\u25a0 セクション生成詳細:") print(f" - 生成予定セクション: {latent_paddings}") print(f" - 各セクションのフレーム数: 約{latent_window_size * 4 - 3}フレーム") print(f" - 合計セクション数: {total_sections}") stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Starting ...')))) try: # セクション設定の前処理 def get_section_settings_map(section_settings): """ section_settings: DataFrame形式のリスト [[番号, 画像, プロンプト], ...] → {セクション番号: (画像, プロンプト)} のdict """ result = {} if section_settings is not None: for row in section_settings: if row and row[0] is not None: sec_num = int(row[0]) img = row[1] prm = row[2] if len(row) > 2 else "" result[sec_num] = (img, prm) return result section_map = get_section_settings_map(section_settings) section_numbers_sorted = sorted(section_map.keys()) if section_map else [] def get_section_info(i_section): """ i_section: int section_map: {セクション番号: (画像, プロンプト)} 指定がなければ次のセクション、なければNone """ if not section_map: return None, None, None # i_section以降で最初に見つかる設定 for sec in range(i_section, max(section_numbers_sorted)+1): if sec in section_map: img, prm = section_map[sec] return sec, img, prm return None, None, None # セクション固有のプロンプト処理を行う関数 def process_section_prompt(i_section, section_map, llama_vec, clip_l_pooler, llama_attention_mask): """セクションに固有のプロンプトがあればエンコードして返す なければメインプロンプトのエンコード結果を返す 返り値: (llama_vec, clip_l_pooler, llama_attention_mask) """ if not isinstance(llama_vec, torch.Tensor) or not isinstance(llama_attention_mask, torch.Tensor): print("[ERROR] メインプロンプトのエンコード結果またはマスクが不正です") return llama_vec, clip_l_pooler, llama_attention_mask # セクション固有のプロンプトがあるか確認 section_info = None if section_map: valid_section_nums = [k for k in section_map.keys() if k >= i_section] if valid_section_nums: section_num = min(valid_section_nums) section_info = section_map[section_num] # セクション固有のプロンプトがあれば使用 if section_info and len(section_info) > 1: _, section_prompt = section_info if section_prompt and section_prompt.strip(): print(f"[section_prompt] セクション{i_section}の専用プロンプトを処理: {section_prompt[:30]}...") try: # プロンプト処理 section_llama_vec, section_clip_l_pooler = encode_prompt_conds( section_prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2 ) # マスクの作成 section_llama_vec, section_llama_attention_mask = crop_or_pad_yield_mask( section_llama_vec, length=512 ) # データ型を明示的にメインプロンプトと合わせる section_llama_vec = section_llama_vec.to( dtype=llama_vec.dtype, device=llama_vec.device ) section_clip_l_pooler = section_clip_l_pooler.to( dtype=clip_l_pooler.dtype, device=clip_l_pooler.device ) section_llama_attention_mask = section_llama_attention_mask.to( device=llama_attention_mask.device ) return section_llama_vec, section_clip_l_pooler, section_llama_attention_mask except Exception as e: print(f"[ERROR] セクションプロンプト処理エラー: {e}") # 共通プロンプトを使用 print(f"[section_prompt] セクション{i_section}は共通プロンプトを使用します") return llama_vec, clip_l_pooler, llama_attention_mask # Clean GPU if not high_vram: unload_complete_models( text_encoder, text_encoder_2, image_encoder, vae, transformer ) # Text encoding stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Text encoding ...')))) if not high_vram: fake_diffusers_current_device(text_encoder, gpu) # since we only encode one text - that is one model move and one encode, offload is same time consumption since it is also one load and one encode. load_model_as_complete(text_encoder_2, target_device=gpu) llama_vec, clip_l_pooler = encode_prompt_conds(prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2) if cfg == 1: llama_vec_n, clip_l_pooler_n = torch.zeros_like(llama_vec), torch.zeros_like(clip_l_pooler) else: llama_vec_n, clip_l_pooler_n = encode_prompt_conds(n_prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2) llama_vec, llama_attention_mask = crop_or_pad_yield_mask(llama_vec, length=512) llama_vec_n, llama_attention_mask_n = crop_or_pad_yield_mask(llama_vec_n, length=512) # Processing input image stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Image processing ...')))) def preprocess_image(img): H, W, C = img.shape height, width = find_nearest_bucket(H, W, resolution=640) img_np = resize_and_center_crop(img, target_width=width, target_height=height) img_pt = torch.from_numpy(img_np).float() / 127.5 - 1 img_pt = img_pt.permute(2, 0, 1)[None, :, None] return img_np, img_pt, height, width input_image_np, input_image_pt, height, width = preprocess_image(input_image) Image.fromarray(input_image_np).save(os.path.join(outputs_folder, f'{job_id}.png')) # VAE encoding stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'VAE encoding ...')))) if not high_vram: load_model_as_complete(vae, target_device=gpu) start_latent = vae_encode(input_image_pt, vae) # end_frameも同じタイミングでencode if end_frame is not None: end_frame_np, end_frame_pt, _, _ = preprocess_image(end_frame) end_frame_latent = vae_encode(end_frame_pt, vae) else: end_frame_latent = None # create section_latents here section_latents = None if section_map: section_latents = {} for sec_num, (img, prm) in section_map.items(): if img is not None: # 画像をVAE encode img_np, img_pt, _, _ = preprocess_image(img) section_latents[sec_num] = vae_encode(img_pt, vae) # CLIP Vision stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'CLIP Vision encoding ...')))) if not high_vram: load_model_as_complete(image_encoder, target_device=gpu) image_encoder_output = hf_clip_vision_encode(input_image_np, feature_extractor, image_encoder) image_encoder_last_hidden_state = image_encoder_output.last_hidden_state # Dtype llama_vec = llama_vec.to(transformer.dtype) llama_vec_n = llama_vec_n.to(transformer.dtype) clip_l_pooler = clip_l_pooler.to(transformer.dtype) clip_l_pooler_n = clip_l_pooler_n.to(transformer.dtype) image_encoder_last_hidden_state = image_encoder_last_hidden_state.to(transformer.dtype) # Sampling stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Start sampling ...')))) rnd = torch.Generator("cpu").manual_seed(seed) num_frames = latent_window_size * 4 - 3 history_latents = torch.zeros(size=(1, 16, 1 + 2 + 16, height // 8, width // 8), dtype=torch.float32).cpu() history_pixels = None total_generated_latent_frames = 0 latent_paddings = reversed(range(total_latent_sections)) if total_latent_sections > 4: # In theory the latent_paddings should follow the above sequence, but it seems that duplicating some # items looks better than expanding it when total_latent_sections > 4 # One can try to remove below trick and just # use `latent_paddings = list(reversed(range(total_latent_sections)))` to compare latent_paddings = [3] + [2] * (total_latent_sections - 3) + [1, 0] for i_section, latent_padding in enumerate(latent_paddings): # 先に変数を定義 is_first_section = i_section == 0 is_last_section = latent_padding == 0 use_end_latent = is_last_section and end_frame is not None latent_padding_size = latent_padding * latent_window_size # 定義後にログ出力 print(f"\n\u25a0 セクション{i_section}の処理開始 (パディング値: {latent_padding})") print(f" - 現在の生成フレーム数: {total_generated_latent_frames * 4 - 3}フレーム") print(f" - 生成予定フレーム数: {num_frames}フレーム") print(f" - 最初のセクション?: {is_first_section}") print(f" - 最後のセクション?: {is_last_section}") # set current_latent here # セクションごとのlatentを使う場合 if section_map and section_latents is not None and len(section_latents) > 0: # i_section以上で最小のsection_latentsキーを探す valid_keys = [k for k in section_latents.keys() if k >= i_section] if valid_keys: use_key = min(valid_keys) current_latent = section_latents[use_key] print(f"[section_latent] section {i_section}: use section {use_key} latent (section_map keys: {list(section_latents.keys())})") print(f"[section_latent] current_latent id: {id(current_latent)}, min: {current_latent.min().item():.4f}, max: {current_latent.max().item():.4f}, mean: {current_latent.mean().item():.4f}") else: current_latent = start_latent print(f"[section_latent] section {i_section}: use start_latent (no section_latent >= {i_section})") print(f"[section_latent] current_latent id: {id(current_latent)}, min: {current_latent.min().item():.4f}, max: {current_latent.max().item():.4f}, mean: {current_latent.mean().item():.4f}") else: current_latent = start_latent print(f"[section_latent] section {i_section}: use start_latent (no section_latents)") print(f"[section_latent] current_latent id: {id(current_latent)}, min: {current_latent.min().item():.4f}, max: {current_latent.max().item():.4f}, mean: {current_latent.mean().item():.4f}") if is_first_section and end_frame_latent is not None: history_latents[:, :, 0:1, :, :] = end_frame_latent if stream.input_queue.top() == 'end': stream.output_queue.push(('end', None)) return # セクション固有のプロンプトがあれば使用する current_llama_vec, current_clip_l_pooler, current_llama_attention_mask = process_section_prompt(i_section, section_map, llama_vec, clip_l_pooler, llama_attention_mask) print(f'latent_padding_size = {latent_padding_size}, is_last_section = {is_last_section}') indices = torch.arange(0, sum([1, latent_padding_size, latent_window_size, 1, 2, 16])).unsqueeze(0) clean_latent_indices_pre, blank_indices, latent_indices, clean_latent_indices_post, clean_latent_2x_indices, clean_latent_4x_indices = indices.split([1, latent_padding_size, latent_window_size, 1, 2, 16], dim=1) clean_latent_indices = torch.cat([clean_latent_indices_pre, clean_latent_indices_post], dim=1) clean_latents_pre = current_latent.to(history_latents) clean_latents_post, clean_latents_2x, clean_latents_4x = history_latents[:, :, :1 + 2 + 16, :, :].split([1, 2, 16], dim=2) clean_latents = torch.cat([clean_latents_pre, clean_latents_post], dim=2) if not high_vram: unload_complete_models() # GPUメモリ保存値を明示的に浮動小数点に変換 preserved_memory = float(gpu_memory_preservation) if gpu_memory_preservation is not None else 6.0 print(f'Setting transformer memory preservation to: {preserved_memory} GB') move_model_to_device_with_memory_preservation(transformer, target_device=gpu, preserved_memory_gb=preserved_memory) if use_teacache: transformer.initialize_teacache(enable_teacache=True, num_steps=steps) else: transformer.initialize_teacache(enable_teacache=False) def callback(d): preview = d['denoised'] preview = vae_decode_fake(preview) preview = (preview * 255.0).detach().cpu().numpy().clip(0, 255).astype(np.uint8) preview = einops.rearrange(preview, 'b c t h w -> (b h) (t w) c') if stream.input_queue.top() == 'end': stream.output_queue.push(('end', None)) raise KeyboardInterrupt('User ends the task.') current_step = d['i'] + 1 percentage = int(100.0 * current_step / steps) hint = f'Sampling {current_step}/{steps}' # セクション情報を追加(現在のセクション/全セクション) section_info = f'セクション: {i_section+1}/{total_sections}, ' desc = f'{section_info}Total generated frames: {int(max(0, total_generated_latent_frames * 4 - 3))}, Video length: {max(0, (total_generated_latent_frames * 4 - 3) / 30) :.2f} seconds (FPS-30). The video is being extended now ...' stream.output_queue.push(('progress', (preview, desc, make_progress_bar_html(percentage, hint)))) return generated_latents = sample_hunyuan( transformer=transformer, sampler='unipc', width=width, height=height, frames=num_frames, real_guidance_scale=cfg, distilled_guidance_scale=gs, guidance_rescale=rs, # shift=3.0, num_inference_steps=steps, generator=rnd, prompt_embeds=current_llama_vec, # セクションごとのプロンプトを使用 prompt_embeds_mask=current_llama_attention_mask, # セクションごとのマスクを使用 prompt_poolers=current_clip_l_pooler, # セクションごとのプロンプトを使用 negative_prompt_embeds=llama_vec_n, negative_prompt_embeds_mask=llama_attention_mask_n, negative_prompt_poolers=clip_l_pooler_n, device=gpu, dtype=torch.bfloat16, image_embeddings=image_encoder_last_hidden_state, latent_indices=latent_indices, clean_latents=clean_latents, clean_latent_indices=clean_latent_indices, clean_latents_2x=clean_latents_2x, clean_latent_2x_indices=clean_latent_2x_indices, clean_latents_4x=clean_latents_4x, clean_latent_4x_indices=clean_latent_4x_indices, callback=callback, ) if is_last_section: generated_latents = torch.cat([start_latent.to(generated_latents), generated_latents], dim=2) total_generated_latent_frames += int(generated_latents.shape[2]) history_latents = torch.cat([generated_latents.to(history_latents), history_latents], dim=2) if not high_vram: # 減圧時に使用するGPUメモリ値も明示的に浮動小数点に設定 preserved_memory_offload = 8.0 # こちらは固定値のまま print(f'Offloading transformer with memory preservation: {preserved_memory_offload} GB') offload_model_from_device_for_memory_preservation(transformer, target_device=gpu, preserved_memory_gb=preserved_memory_offload) load_model_as_complete(vae, target_device=gpu) real_history_latents = history_latents[:, :, :total_generated_latent_frames, :, :] if history_pixels is None: history_pixels = vae_decode(real_history_latents, vae).cpu() else: section_latent_frames = (latent_window_size * 2 + 1) if is_last_section else (latent_window_size * 2) overlapped_frames = latent_window_size * 4 - 3 current_pixels = vae_decode(real_history_latents[:, :, :section_latent_frames], vae).cpu() history_pixels = soft_append_bcthw(current_pixels, history_pixels, overlapped_frames) # 各セクションの最終フレームを静止画として保存(セクション番号付き) if save_section_frames and history_pixels is not None: try: if i_section == 0 or current_pixels is None: # 最初のセクションは history_pixels の最後 last_frame = history_pixels[0, :, -1, :, :] else: # 2セクション目以降は current_pixels の最後 last_frame = current_pixels[0, :, -1, :, :] last_frame = einops.rearrange(last_frame, 'c h w -> h w c') last_frame = last_frame.cpu().numpy() last_frame = np.clip((last_frame * 127.5 + 127.5), 0, 255).astype(np.uint8) last_frame = resize_and_center_crop(last_frame, target_width=width, target_height=height) if is_first_section and end_frame is None: Image.fromarray(last_frame).save(os.path.join(outputs_folder, f'{job_id}_{i_section}_end.png')) else: Image.fromarray(last_frame).save(os.path.join(outputs_folder, f'{job_id}_{i_section}.png')) except Exception as e: print(f"[WARN] セクション{ i_section }最終フレーム画像保存時にエラー: {e}") if not high_vram: unload_complete_models() output_filename = os.path.join(outputs_folder, f'{job_id}_{total_generated_latent_frames}.mp4') save_bcthw_as_mp4(history_pixels, output_filename, fps=30) print(f'Decoded. Current latent shape {real_history_latents.shape}; pixel shape {history_pixels.shape}') print(f"\u25a0 セクション{i_section}の処理完了") print(f" - 現在の累計フレーム数: {int(max(0, total_generated_latent_frames * 4 - 3))}フレーム") print(f" - レンダリング時間: {max(0, (total_generated_latent_frames * 4 - 3) / 30) :.2f}秒") print(f" - 出力ファイル: {output_filename}") stream.output_queue.push(('file', output_filename)) if is_last_section: # 処理終了時に通知 if HAS_WINSOUND: winsound.PlaySound("SystemExclamation", winsound.SND_ALIAS) else: print("\n✓ 処理が完了しました!") # Linuxでの代替通知 # 全体の処理時間を計算 process_end_time = time.time() total_process_time = process_end_time - process_start_time hours, remainder = divmod(total_process_time, 3600) minutes, seconds = divmod(remainder, 60) time_str = "" if hours > 0: time_str = f"{int(hours)}時間 {int(minutes)}分 {seconds:.1f}秒" elif minutes > 0: time_str = f"{int(minutes)}分 {seconds:.1f}秒" else: time_str = f"{seconds:.1f}秒" print(f"\n全体の処理時間: {time_str}") completion_message = f"すべてのセクション({total_sections}/{total_sections})が完了しました。全体の処理時間: {time_str}" stream.output_queue.push(('progress', (None, completion_message, make_progress_bar_html(100, '処理完了')))) # 中間ファイルの削除処理 if not keep_section_videos: # 最終動画のフルパス final_video_path = output_filename final_video_name = os.path.basename(final_video_path) # job_id部分を取得(タイムスタンプ部分) job_id_part = job_id # ディレクトリ内のすべてのファイルを取得 files = os.listdir(outputs_folder) deleted_count = 0 for file in files: # 同じjob_idを持つMP4ファイルかチェック if file.startswith(job_id_part) and file.endswith('.mp4') and file != final_video_name: file_path = os.path.join(outputs_folder, file) try: os.remove(file_path) deleted_count += 1 print(f"[削除] 中間ファイル: {file}") except Exception as e: print(f"[エラー] ファイル削除時のエラー {file}: {e}") if deleted_count > 0: print(f"[済] {deleted_count}個の中間ファイルを削除しました。最終ファイルは保存されています: {final_video_name}") stream.output_queue.push(('progress', (None, f"{deleted_count}個の中間ファイルを削除しました。最終動画は保存されています。", make_progress_bar_html(100, '処理完了')))) break except: traceback.print_exc() if not high_vram: unload_complete_models( text_encoder, text_encoder_2, image_encoder, vae, transformer ) stream.output_queue.push(('end', None)) return def process(input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, use_random_seed, save_section_frames, keep_section_videos, output_dir, section_settings): global stream assert input_image is not None, 'No input image!' # 動画生成の設定情報をログに出力 total_latent_sections = int(max(round((total_second_length * 30) / (latent_window_size * 4)), 1)) mode_name = "通常モード" if mode_radio.value == MODE_TYPE_NORMAL else "ループモード" print(f"\n==== 動画生成開始 =====") print(f"\u25c6 生成モード: {mode_name}") print(f"\u25c6 動画長: {total_second_length}秒") print(f"\u25c6 生成セクション数: {total_latent_sections}回") print(f"\u25c6 サンプリングステップ数: {steps}") print(f"\u25c6 TeaCache使用: {use_teacache}") # セクションごとのキーフレーム画像の使用状況をログに出力 valid_sections = [] if section_settings is not None: for i, sec_data in enumerate(section_settings): if sec_data and sec_data[1] is not None: # 画像が設定されている場合 valid_sections.append(sec_data[0]) if valid_sections: print(f"\u25c6 使用するキーフレーム画像: セクション{', '.join(map(str, valid_sections))}") else: print(f"\u25c6 キーフレーム画像: デフォルト設定のみ使用") print(f"=============================\n") if use_random_seed: seed = random.randint(0, 2**32 - 1) # UIのseed欄もランダム値で更新 yield None, None, '', '', gr.update(interactive=False), gr.update(interactive=True), gr.update(value=seed) else: yield None, None, '', '', gr.update(interactive=False), gr.update(interactive=True), gr.update() stream = AsyncStream() # GPUメモリの設定値をデバッグ出力し、正しい型に変換 gpu_memory_value = float(gpu_memory_preservation) if gpu_memory_preservation is not None else 6.0 print(f'Using GPU memory preservation setting: {gpu_memory_value} GB') # 出力フォルダが空の場合はデフォルト値を使用 if not output_dir or not output_dir.strip(): output_dir = "outputs" print(f'Output directory: {output_dir}') async_run(worker, input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_value, use_teacache, save_section_frames, keep_section_videos, output_dir, section_settings) output_filename = None while True: flag, data = stream.output_queue.next() if flag == 'file': output_filename = data yield output_filename, gr.update(), gr.update(), gr.update(), gr.update(interactive=False), gr.update(interactive=True), gr.update() if flag == 'progress': preview, desc, html = data yield gr.update(), gr.update(visible=True, value=preview), desc, html, gr.update(interactive=False), gr.update(interactive=True), gr.update() if flag == 'end': yield output_filename, gr.update(visible=False), gr.update(), '', gr.update(interactive=True), gr.update(interactive=False), gr.update() break def end_process(): stream.input_queue.push('end') # プリセット管理関連の関数 def initialize_presets(): """初期プリセットファイルがない場合に作成する関数""" preset_file = os.path.join(presets_folder, 'prompt_presets.json') # デフォルトのプロンプト default_prompts = [ 'A character doing some simple body movements.', 'A character uses expressive hand gestures and body language.', 'A character walks leisurely with relaxed movements.', 'A character performs dynamic movements with energy and flowing motion.', 'A character moves in unexpected ways, with surprising transitions poses.', ] # デフォルト起動時プロンプト default_startup_prompt = "A character doing some simple body movements." # 既存ファイルがあり、正常に読み込める場合は終了 if os.path.exists(preset_file): try: with open(preset_file, 'r', encoding='utf-8') as f: presets_data = json.load(f) # 起動時デフォルトがあるか確認 startup_default_exists = any(preset.get("is_startup_default", False) for preset in presets_data.get("presets", [])) # なければ追加 if not startup_default_exists: presets_data.setdefault("presets", []).append({ "name": "起動時デフォルト", "prompt": default_startup_prompt, "timestamp": datetime.now().isoformat(), "is_default": True, "is_startup_default": True }) presets_data["default_startup_prompt"] = default_startup_prompt with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) return except: # エラーが発生した場合は新規作成 pass # 新規作成 presets_data = { "presets": [], "default_startup_prompt": default_startup_prompt } # デフォルトのプリセットを追加 for i, prompt_text in enumerate(default_prompts): presets_data["presets"].append({ "name": f"デフォルト {i+1}: {prompt_text[:20]}...", "prompt": prompt_text, "timestamp": datetime.now().isoformat(), "is_default": True }) # 起動時デフォルトプリセットを追加 presets_data["presets"].append({ "name": "起動時デフォルト", "prompt": default_startup_prompt, "timestamp": datetime.now().isoformat(), "is_default": True, "is_startup_default": True }) # 保存 try: with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) except: # 保存に失敗してもエラーは出さない(次回起動時に再試行される) pass def load_presets(): """プリセットを読み込む関数""" preset_file = os.path.join(presets_folder, 'prompt_presets.json') # 初期化関数を呼び出し(初回実行時のみ作成される) initialize_presets() max_retries = 3 retry_count = 0 while retry_count < max_retries: try: with open(preset_file, 'r', encoding='utf-8') as f: file_contents = f.read() if not file_contents.strip(): print(f"読み込み時に空ファイルが検出されました: {preset_file}") # 空ファイルの場合は再初期化を試みる initialize_presets() retry_count += 1 continue data = json.loads(file_contents) print(f"プリセットファイル読み込み成功: {len(data.get('presets', []))}件") return data except (json.JSONDecodeError, UnicodeDecodeError) as e: # JSONパースエラーの場合はファイルが破損している可能性がある print(f"プリセットファイルの形式が不正です: {e}") # ファイルをバックアップ backup_file = f"{preset_file}.bak.{int(time.time())}" try: import shutil shutil.copy2(preset_file, backup_file) print(f"破損したファイルをバックアップしました: {backup_file}") except Exception as backup_error: print(f"バックアップ作成エラー: {backup_error}") # 再初期化 initialize_presets() retry_count += 1 except Exception as e: print(f"プリセット読み込みエラー: {e}") # エラー発生 retry_count += 1 # 再試行しても失敗した場合は空のデータを返す print("再試行しても読み込みに失敗しました。空のデータを返します。") return {"presets": []} def get_default_startup_prompt(): """起動時に表示するデフォルトプロンプトを取得する関数""" print("起動時デフォルトプロンプト読み込み開始") presets_data = load_presets() # プリセットからデフォルト起動時プロンプトを探す for preset in presets_data["presets"]: if preset.get("is_startup_default", False): startup_prompt = preset["prompt"] print(f"起動時デフォルトプロンプトを読み込み: '{startup_prompt[:30]}...' (長さ: {len(startup_prompt)}文字)") # 重複しているかチェック # 例えば「A character」が複数回出てくる場合は重複している可能性がある if "A character" in startup_prompt and startup_prompt.count("A character") > 1: print("プロンプトに重複が見つかりました。最初のセンテンスのみを使用します。") # 最初のセンテンスのみを使用 sentences = startup_prompt.split(".") if len(sentences) > 0: clean_prompt = sentences[0].strip() + "." print(f"正規化されたプロンプト: '{clean_prompt}'") return clean_prompt return startup_prompt # 見つからない場合はデフォルト設定を使用 if "default_startup_prompt" in presets_data: default_prompt = presets_data["default_startup_prompt"] print(f"デフォルト設定から読み込み: '{default_prompt[:30]}...' (長さ: {len(default_prompt)}文字)") # 同様に重複チェック if "A character" in default_prompt and default_prompt.count("A character") > 1: print("デフォルトプロンプトに重複が見つかりました。最初のセンテンスのみを使用します。") sentences = default_prompt.split(".") if len(sentences) > 0: clean_prompt = sentences[0].strip() + "." print(f"正規化されたデフォルトプロンプト: '{clean_prompt}'") return clean_prompt return default_prompt # フォールバックとしてプログラムのデフォルト値を返す fallback_prompt = "A character doing some simple body movements." print(f"プログラムのデフォルト値を使用: '{fallback_prompt}'") return fallback_prompt def save_preset(name, prompt_text): """プリセットを保存する関数""" presets_data = load_presets() if not name: # 名前が空の場合は起動時デフォルトとして保存 # 既存の起動時デフォルトを探す startup_default_exists = False for preset in presets_data["presets"]: if preset.get("is_startup_default", False): # 既存の起動時デフォルトを更新 preset["prompt"] = prompt_text preset["timestamp"] = datetime.now().isoformat() startup_default_exists = True # 起動時デフォルトを更新 break if not startup_default_exists: # 見つからない場合は新規作成 presets_data["presets"].append({ "name": "起動時デフォルト", "prompt": prompt_text, "timestamp": datetime.now().isoformat(), "is_default": True, "is_startup_default": True }) print(f"起動時デフォルトを新規作成: {prompt_text[:50]}...") # デフォルト設定も更新 presets_data["default_startup_prompt"] = prompt_text preset_file = os.path.join(presets_folder, 'prompt_presets.json') try: # JSON直接書き込み with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) # プロンプトの値を更新 if 'prompt' in globals(): prompt.value = prompt_text return "プリセット '起動時デフォルト' を保存しました" except Exception as e: print(f"プリセット保存エラー: {e}") traceback.print_exc() return f"保存エラー: {e}" # 通常のプリセット保存処理 # 同名のプリセットがあれば上書き、なければ追加 preset_exists = False for preset in presets_data["presets"]: if preset["name"] == name: preset["prompt"] = prompt_text preset["timestamp"] = datetime.now().isoformat() preset_exists = True # 既存のプリセットを更新 break if not preset_exists: presets_data["presets"].append({ "name": name, "prompt": prompt_text, "timestamp": datetime.now().isoformat(), "is_default": False }) # 新規プリセットを作成 preset_file = os.path.join(presets_folder, 'prompt_presets.json') try: # JSON直接書き込み with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) # ファイル保存成功 return f"プリセット '{name}' を保存しました" except Exception as e: print(f"プリセット保存エラー: {e}") # エラー発生 return f"保存エラー: {e}" def delete_preset(preset_name): """プリセットを削除する関数""" if not preset_name: return "プリセットを選択してください" presets_data = load_presets() # 削除対象のプリセットを確認 target_preset = None for preset in presets_data["presets"]: if preset["name"] == preset_name: target_preset = preset break if not target_preset: return f"プリセット '{preset_name}' が見つかりません" # デフォルトプリセットは削除できない if target_preset.get("is_default", False): return f"デフォルトプリセットは削除できません" # プリセットを削除 presets_data["presets"] = [p for p in presets_data["presets"] if p["name"] != preset_name] preset_file = os.path.join(presets_folder, 'prompt_presets.json') try: with open(preset_file, 'w', encoding='utf-8') as f: json.dump(presets_data, f, ensure_ascii=False, indent=2) return f"プリセット '{preset_name}' を削除しました" except Exception as e: return f"削除エラー: {e}" # 既存のQuick Prompts(初期化時にプリセットに変換されるので、互換性のために残す) quick_prompts = [ 'A character doing some simple body movements.', 'A character uses expressive hand gestures and body language.', 'A character walks leisurely with relaxed movements.', 'A character performs dynamic movements with energy and flowing motion.', 'A character moves in unexpected ways, with surprising transitions poses.', ] quick_prompts = [[x] for x in quick_prompts] css = make_progress_bar_css() + """ .title-suffix { color: currentColor; opacity: 0.05; } .highlighted-keyframe { border: 4px solid #ff3860 !important; box-shadow: 0 0 10px rgba(255, 56, 96, 0.5) !important; background-color: rgba(255, 56, 96, 0.05) !important; } /* セクション番号ラベルの強調表示 */ .highlighted-label label { color: #ff3860 !important; font-weight: bold !important; } """ block = gr.Blocks(css=css).queue() with block: gr.HTML('

FramePack-eichi

') # デバッグ情報の表示 # print_keyframe_debug_info() # モード選択用のラジオボタンと動画長選択用のラジオボタンを横並びに配置 with gr.Row(): with gr.Column(scale=1): mode_radio = gr.Radio(choices=[MODE_TYPE_NORMAL, MODE_TYPE_LOOP], value=MODE_TYPE_NORMAL, label="生成モード", info="通常:一般的な生成 / ループ:ループ動画用") with gr.Column(scale=1): # 設定から動的に選択肢を生成 length_radio = gr.Radio(choices=get_video_modes(), value="6秒", label="動画長", info="キーフレーム画像のコピー範囲と動画の長さを設定") with gr.Row(): with gr.Column(): input_image = gr.Image(sources='upload', type="numpy", label="Image", height=320) end_frame = gr.Image(sources='upload', type="numpy", label="Final Frame (Optional)", height=320) with gr.Row(): start_button = gr.Button(value="Start Generation") end_button = gr.Button(value="End Generation", interactive=False) prompt = gr.Textbox(label="Prompt", value=get_default_startup_prompt(), lines=6) with gr.Row(): gr.Markdown("※プリセット名を空にして「保存」すると起動時デフォルトになります") # 互換性のためにQuick Listも残しておくが、非表示にする with gr.Row(visible=False): example_quick_prompts = gr.Dataset(samples=quick_prompts, label='Quick List', samples_per_page=1000, components=[prompt]) example_quick_prompts.click(lambda x: x[0], inputs=[example_quick_prompts], outputs=prompt, show_progress=False, queue=False) with gr.Group(): use_teacache = gr.Checkbox(label='Use TeaCache', value=True, info='Faster speed, but often makes hands and fingers slightly worse.') # Use Random Seedの初期値 use_random_seed_default = True seed_default = random.randint(0, 2**32 - 1) if use_random_seed_default else 1 use_random_seed = gr.Checkbox(label="Use Random Seed", value=use_random_seed_default) n_prompt = gr.Textbox(label="Negative Prompt", value="", visible=False) # Not used seed = gr.Number(label="Seed", value=seed_default, precision=0) def set_random_seed(is_checked): if is_checked: return random.randint(0, 2**32 - 1) else: return gr.update() use_random_seed.change(fn=set_random_seed, inputs=use_random_seed, outputs=seed) total_second_length = gr.Slider(label="Total Video Length (Seconds)", minimum=1, maximum=120, value=6, step=1) latent_window_size = gr.Slider(label="Latent Window Size", minimum=1, maximum=33, value=9, step=1, visible=False) # Should not change steps = gr.Slider(label="Steps", minimum=1, maximum=100, value=25, step=1, info='Changing this value is not recommended.') cfg = gr.Slider(label="CFG Scale", minimum=1.0, maximum=32.0, value=1.0, step=0.01, visible=False) # Should not change gs = gr.Slider(label="Distilled CFG Scale", minimum=1.0, maximum=32.0, value=10.0, step=0.01, info='Changing this value is not recommended.') rs = gr.Slider(label="CFG Re-Scale", minimum=0.0, maximum=1.0, value=0.0, step=0.01, visible=False) # Should not change gpu_memory_preservation = gr.Slider(label="GPU Memory to Preserve (GB) (smaller = more VRAM usage)", minimum=6, maximum=128, value=9, step=0.1, info="空けておくGPUメモリ量を指定。小さい値=より多くのVRAMを使用可能=高速、大きい値=より少ないVRAMを使用=安全") # セクションごとの動画保存チェックボックスを追加(デフォルトOFF) keep_section_videos = gr.Checkbox(label="完了時にセクションごとの動画を残す", value=False, info="チェックがない場合は最終動画のみ保存されます(デフォルトOFF)") # セクションごとの静止画保存チェックボックスを追加(デフォルトOFF) save_section_frames = gr.Checkbox(label="セクションごとの静止画を保存", value=False, info="各セクションの最終フレームを静止画として保存します(デフォルトOFF)") # キーフレームコピー機能のオンオフ切り替え enable_keyframe_copy = gr.Checkbox(label="キーフレーム自動コピー機能を有効にする", value=True, info="オフにするとキーフレーム間の自動コピーが行われなくなります") # 出力フォルダ設定 gr.Markdown("※ 出力先は `webui` 配下に限定されます") with gr.Row(equal_height=True): with gr.Column(scale=4): # フォルダ名だけを入力欄に設定 output_dir = gr.Textbox( label="出力フォルダ名", value=output_folder_name, # 設定から読み込んだ値を使用 info="動画やキーフレーム画像の保存先フォルダ名", placeholder="outputs" ) with gr.Column(scale=1, min_width=100): open_folder_btn = gr.Button(value="📂 保存および出力フォルダを開く", size="sm") # 実際の出力パスを表示 with gr.Row(visible=False): path_display = gr.Textbox( label="出力フォルダの完全パス", value=os.path.join(base_path, output_folder_name), interactive=False ) # フォルダを開くボタンのイベント def handle_open_folder_btn(folder_name): """フォルダ名を保存し、そのフォルダを開く""" if not folder_name or not folder_name.strip(): folder_name = "outputs" # フォルダパスを取得 folder_path = get_output_folder_path(folder_name) # 設定を更新して保存 settings = load_settings() old_folder_name = settings.get('output_folder') if old_folder_name != folder_name: settings['output_folder'] = folder_name save_result = save_settings(settings) if save_result: # グローバル変数も更新 global output_folder_name, outputs_folder output_folder_name = folder_name outputs_folder = folder_path print(f"出力フォルダ設定を保存しました: {folder_name}") # フォルダを開く open_output_folder(folder_path) # 出力ディレクトリ入力欄とパス表示を更新 return gr.update(value=folder_name), gr.update(value=folder_path) open_folder_btn.click(fn=handle_open_folder_btn, inputs=[output_dir], outputs=[output_dir, path_display]) # セクション設定(DataFrameをやめて個別入力欄に変更) # 設定から最大キーフレーム数を取得 max_keyframes = get_max_keyframes_count() # セクション設定の入力欄を動的に生成 section_number_inputs = [] section_image_inputs = [] section_prompt_inputs = [] # プロンプト入力欄用のリスト with gr.Group(): gr.Markdown("### セクション設定. セクション番号は動画の終わりからカウント.(任意。指定しない場合は通常のImage/プロンプトを使用)") for i in range(max_keyframes): with gr.Row(): # 左側にセクション番号とプロンプトを配置 with gr.Column(scale=1): section_number = gr.Number(label=f"セクション番号{i+1}", value=i, precision=0) section_prompt = gr.Textbox(label=f"セクションプロンプト{i+1}", placeholder="セクション固有のプロンプト(空白の場合は共通プロンプトを使用)", lines=2) # 右側にキーフレーム画像のみ配置 with gr.Column(scale=2): section_image = gr.Image(label=f"キーフレーム画像{i+1}", sources="upload", type="numpy", height=200) section_number_inputs.append(section_number) section_image_inputs.append(section_image) section_prompt_inputs.append(section_prompt) # 重要なキーフレームの説明 with gr.Row(): with gr.Column(): # 設定から動的にHTML生成 note_html = gr.HTML(generate_keyframe_guide_html()) # section_settingsは入力欄の値をまとめてリスト化 def collect_section_settings(*args): # args: [num1, img1, prompt1, num2, img2, prompt2, ...] return [[args[i], args[i+1], args[i+2]] for i in range(0, len(args), 3)] section_settings = gr.State([[None, None, ""] for _ in range(max_keyframes)]) section_inputs = [] for i in range(max_keyframes): section_inputs.extend([section_number_inputs[i], section_image_inputs[i], section_prompt_inputs[i]]) # section_inputsをまとめてsection_settings Stateに格納 def update_section_settings(*args): return collect_section_settings(*args) # section_inputsが変化したらsection_settings Stateを更新 for inp in section_inputs: inp.change(fn=update_section_settings, inputs=section_inputs, outputs=section_settings) # モード変更時の処理 mode_radio.change( fn=lambda mode, length: unified_mode_length_change_handler(mode, length, section_number_inputs), inputs=[mode_radio, length_radio], outputs=[input_image, end_frame] + section_image_inputs + [total_second_length] ) # 動画長変更時の処理 length_radio.change( fn=lambda mode, length: unified_mode_length_change_handler(mode, length, section_number_inputs), inputs=[mode_radio, length_radio], outputs=[input_image, end_frame] + section_image_inputs + [total_second_length] ) # 入力画像変更時の処理 input_image.change( fn=unified_input_image_change_handler, inputs=[input_image, mode_radio, length_radio, enable_keyframe_copy], outputs=[end_frame] + section_image_inputs ) # 各キーフレーム画像の変更イベントを個別に設定 # 一度に複数のコンポーネントを更新する代わりに、個別の更新関数を使用 def create_single_keyframe_handler(src_idx, target_idx): def handle_single_keyframe(img, mode, length, enable_copy): # コピー条件をチェック if img is None or not enable_copy: return gr.update() # コピー先のチェック targets = get_copy_targets(mode, length, src_idx) if target_idx in targets: return gr.update(value=img) return gr.update() return handle_single_keyframe # 各キーフレームについて、影響を受ける可能性のある後続のキーフレームごとに個別のイベントを設定 for i, src_image in enumerate(section_image_inputs): for j in range(i+1, len(section_image_inputs)): src_image.change( fn=create_single_keyframe_handler(i, j), inputs=[src_image, mode_radio, length_radio, enable_keyframe_copy], outputs=[section_image_inputs[j]] ) with gr.Column(): result_video = gr.Video(label="Finished Frames", autoplay=True, show_share_button=False, height=512, loop=True) progress_desc = gr.Markdown('', elem_classes='no-generating-animation') progress_bar = gr.HTML('', elem_classes='no-generating-animation') preview_image = gr.Image(label="Next Latents", height=200, visible=False) # プロンプト管理パネルの追加 with gr.Group(visible=True) as prompt_management: gr.Markdown("### プロンプト管理") # 編集画面を常時表示する with gr.Group(visible=True): # 起動時デフォルトの初期表示用に取得 default_prompt = "" default_name = "" for preset in load_presets()["presets"]: if preset.get("is_startup_default", False): default_prompt = preset["prompt"] default_name = preset["name"] break with gr.Row(): edit_name = gr.Textbox(label="プリセット名", placeholder="名前を入力...", value=default_name) edit_prompt = gr.Textbox(label="プロンプト", lines=5, value=default_prompt) with gr.Row(): # 起動時デフォルトをデフォルト選択に設定 default_preset = "起動時デフォルト" # プリセットデータから全プリセット名を取得 presets_data = load_presets() choices = [preset["name"] for preset in presets_data["presets"]] default_presets = [name for name in choices if any(p["name"] == name and p.get("is_default", False) for p in presets_data["presets"])] user_presets = [name for name in choices if name not in default_presets] sorted_choices = [(name, name) for name in sorted(default_presets) + sorted(user_presets)] preset_dropdown = gr.Dropdown(label="プリセット", choices=sorted_choices, value=default_preset, type="value") with gr.Row(): save_btn = gr.Button(value="保存", variant="primary") apply_preset_btn = gr.Button(value="反映", variant="primary") clear_btn = gr.Button(value="クリア") delete_preset_btn = gr.Button(value="削除") # メッセージ表示用 result_message = gr.Markdown("") # 実行ボタンのイベント ips = [input_image, end_frame, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, use_random_seed, save_section_frames, keep_section_videos, output_dir, section_settings] start_button.click(fn=process, inputs=ips, outputs=[result_video, preview_image, progress_desc, progress_bar, start_button, end_button, seed]) end_button.click(fn=end_process) # プリセット保存ボタンのイベント def save_button_click_handler(name, prompt_text): """保存ボタンクリック時のハンドラ関数""" # 重複チェックと正規化 if "A character" in prompt_text and prompt_text.count("A character") > 1: sentences = prompt_text.split(".") if len(sentences) > 0: prompt_text = sentences[0].strip() + "." # 重複を検出したため正規化 # プリセット保存 result_msg = save_preset(name, prompt_text) # プリセットデータを取得してドロップダウンを更新 presets_data = load_presets() choices = [preset["name"] for preset in presets_data["presets"]] default_presets = [n for n in choices if any(p["name"] == n and p.get("is_default", False) for p in presets_data["presets"])] user_presets = [n for n in choices if n not in default_presets] sorted_choices = [(n, n) for n in sorted(default_presets) + sorted(user_presets)] # メインプロンプトは更新しない(保存のみを行う) return result_msg, gr.update(choices=sorted_choices), gr.update() # 保存ボタンのクリックイベントを接続 save_btn.click( fn=save_button_click_handler, inputs=[edit_name, edit_prompt], outputs=[result_message, preset_dropdown, prompt] ) # クリアボタン処理 def clear_fields(): return gr.update(value=""), gr.update(value="") clear_btn.click( fn=clear_fields, inputs=[], outputs=[edit_name, edit_prompt] ) # プリセット読込処理 def load_preset_handler(preset_name): # プリセット選択時に編集欄のみを更新 for preset in load_presets()["presets"]: if preset["name"] == preset_name: return gr.update(value=preset_name), gr.update(value=preset["prompt"]) return gr.update(), gr.update() # プリセット選択時に編集欄に反映 def load_preset_handler_wrapper(preset_name): # プリセット名がタプルの場合も処理する if isinstance(preset_name, tuple) and len(preset_name) == 2: preset_name = preset_name[1] # 値部分を取得 return load_preset_handler(preset_name) preset_dropdown.change( fn=load_preset_handler_wrapper, inputs=[preset_dropdown], outputs=[edit_name, edit_prompt] ) # 反映ボタン処理 - 編集画面の内容をメインプロンプトに反映 def apply_to_prompt(edit_text): """編集画面の内容をメインプロンプトに反映する関数""" # 編集画面のプロンプトをメインに適用 return gr.update(value=edit_text) # プリセット削除処理 def delete_preset_handler(preset_name): # プリセット名がタプルの場合も処理する if isinstance(preset_name, tuple) and len(preset_name) == 2: preset_name = preset_name[1] # 値部分を取得 result = delete_preset(preset_name) # プリセットデータを取得してドロップダウンを更新 presets_data = load_presets() choices = [preset["name"] for preset in presets_data["presets"]] default_presets = [name for name in choices if any(p["name"] == name and p.get("is_default", False) for p in presets_data["presets"])] user_presets = [name for name in choices if name not in default_presets] sorted_names = sorted(default_presets) + sorted(user_presets) updated_choices = [(name, name) for name in sorted_names] return result, gr.update(choices=updated_choices) apply_preset_btn.click( fn=apply_to_prompt, inputs=[edit_prompt], outputs=[prompt] ) delete_preset_btn.click( fn=delete_preset_handler, inputs=[preset_dropdown], outputs=[result_message, preset_dropdown] ) # 起動コード block.launch( server_name=args.server, server_port=args.port, share=args.share, inbrowser=args.inbrowser, ) ``` The content has been capped at 50000 tokens, and files over NaN bytes have been omitted. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.