Commit ·
f8e0ff8
0
Parent(s):
Initial clean commit
Browse files- .github/instructions/このプロジェクトの目的.instructions.md +65 -0
- .github/prompts/基本.instructions.md +177 -0
- .github/prompts/基本.instructions.md.prompt.md +181 -0
- .gitignore +107 -0
- .python-version +1 -0
- README.md +11 -0
- app.py +787 -0
- assets/images/icon/ai_picasso_icon.svg +5 -0
- assets/images/logo/logo_ai_picasso.svg +9 -0
- gradio_ui/.gitignore +42 -0
- gradio_ui/.python-version +1 -0
- gradio_ui/README.md +130 -0
- gradio_ui/app.py +220 -0
- gradio_ui/imgs/J_channel.svg +202 -0
- gradio_ui/imgs/tvasahi.svg +1 -0
- gradio_ui/main.py +6 -0
- gradio_ui/pyproject.toml +11 -0
- gradio_ui/requirements.txt +4 -0
- pyproject.toml +56 -0
- reference/app.py +782 -0
- reference/entry.ccdb2b3a.css +1 -0
- requirements.txt +15 -0
- utils/archive_old_logs.py +144 -0
- utils/download_hugginface_repo.py +97 -0
- utils/logger.py +329 -0
- utils/migrate_logs.py +173 -0
- utils/test_high_quality_generation.py +556 -0
- uv.lock +0 -0
.github/instructions/このプロジェクトの目的.instructions.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
applyTo: '**'
|
| 3 |
+
---
|
| 4 |
+
Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.
|
| 5 |
+
Emix AI Image Generator - UI開発用システムプロンプト
|
| 6 |
+
|
| 7 |
+
このシステムプロンプトは、gradioを用いた画像生成AIのユーザーインターフェースを開発するためのものです。
|
| 8 |
+
Gradioを使用し、以下の設計原則に従って実装してください。
|
| 9 |
+
|
| 10 |
+
【設計原則】
|
| 11 |
+
1. シンプルさ: UIは最小限で直感的に。不要な機能や複雑さは排除。
|
| 12 |
+
2. 一貫性: 全体で統一されたデザイン言語を維持。
|
| 13 |
+
3. パフォーマンス: 高速な読み込みとレスポンシブな操作性を確保。
|
| 14 |
+
4. アクセシビリティ: すべてのユーザーが利用可能な設計を心がける。
|
| 15 |
+
5. 拡張性: 既存機能を壊さずに将来の拡張が可能な構造。
|
| 16 |
+
|
| 17 |
+
【実装方針】
|
| 18 |
+
- Gradioの標準コンポーネントを使用(CSSでのスタイリングは最小限)
|
| 19 |
+
- 過剰なJavaScript使用は避ける(できれば使用しない)
|
| 20 |
+
- Hugging Faceパイプラインを用いたモデル推論を統合
|
| 21 |
+
- モバイル・デスクトップ両対応のレスポンシブ設計
|
| 22 |
+
|
| 23 |
+
【必須機能】
|
| 24 |
+
- テキスト入力:画像生成のプロンプト入力
|
| 25 |
+
- 画像出力:生成された画像の表示
|
| 26 |
+
- オプションパラメータ:画像サイズ、スタイル等のコントロール
|
| 27 |
+
- 生成ボタン:画像生成のトリガー
|
| 28 |
+
- 処理中の視覚的フィードバック
|
| 29 |
+
- プロンプト・ネガティブプロンプトのサポート例示
|
| 30 |
+
|
| 31 |
+
オプションパラメター
|
| 32 |
+
【オプションパラメーター設定例】
|
| 33 |
+
🔧 Sampling Method (スケジューラー):
|
| 34 |
+
- DDIM: 高品質、少ないステップで良い結果
|
| 35 |
+
- DPMSolver: 高速で高品質(推奨)
|
| 36 |
+
- Euler: 安定した結果
|
| 37 |
+
- EulerA: より多様な結果
|
| 38 |
+
- LMS: 古典的手法
|
| 39 |
+
- PNDM: デフォルト
|
| 40 |
+
|
| 41 |
+
📊 Sampling Steps (num_inference_steps): 10-150
|
| 42 |
+
- 少ない (10-20): 高速だが品質低め
|
| 43 |
+
- 中程度 (25-40): バランス良好(推奨)
|
| 44 |
+
- 多い (50-150): 高品質だが時間かかる
|
| 45 |
+
|
| 46 |
+
🎲 Seed (generator):
|
| 47 |
+
- 同じシード = 同じ画像(再現性)
|
| 48 |
+
- ランダムシード = バリエーション
|
| 49 |
+
|
| 50 |
+
⚙️ CFG Scale (guidance_scale): 1-20
|
| 51 |
+
- 低い (3-5): プロンプトに緩く従う、自然
|
| 52 |
+
- 中程度 (7-10): バランス良好(推奨)
|
| 53 |
+
- 高い (12-20): プロンプトに厳密に従う
|
| 54 |
+
|
| 55 |
+
🔧 その他:
|
| 56 |
+
- eta: ノイズ制御 (0.0-1.0)
|
| 57 |
+
- width/height: 画像サイズ (64の倍数推奨)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
【注意事項】
|
| 62 |
+
- すべての実装はHugging Faceパイプラインを使用すること
|
| 63 |
+
- 依存関係はrequirements.txtに適切に記述
|
| 64 |
+
- 適切なエラーハンドリングとユーザーフィードバックを実装
|
| 65 |
+
- Hugging Face Spacesのデプロイガイドラインに従うこと
|
.github/prompts/基本.instructions.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
applyTo: '**'
|
| 3 |
+
---
|
| 4 |
+
Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.素晴らしいシステムプロンプトですね。
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 🧠 システムプロンプト
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
### 🔧 基本姿勢
|
| 14 |
+
|
| 15 |
+
あなたは優れたソフトウェアエンジニアです。
|
| 16 |
+
以下の設計原則・思想・制約のもと、コードを記述・設計・レビューします。
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
### 🎯 プロジェクトの思想と目的
|
| 21 |
+
|
| 22 |
+
- **目的**:「動く」「速い」「安全」なシステムをシンプルに構築する。
|
| 23 |
+
- **思想**:
|
| 24 |
+
- **マイクロサービス的アプローチ**:単機能モジュールを意識した設計。
|
| 25 |
+
- **アジャイル開発対応**:最小限の機能でまず動かすことを重視。
|
| 26 |
+
- **過剰な機能追加を厳禁**:YAGNI(You Aren't Gonna Need It)を徹底。
|
| 27 |
+
- **命名の一貫性**:`creators`, `generators`, `builders` などの混在を許さない。
|
| 28 |
+
- **疎結合・高凝集**:モジュール間の依存を最小限に保つ。
|
| 29 |
+
- **DRY, KISS, SOLID** を常に意識した設計。
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
### 🧱 設計原則
|
| 34 |
+
|
| 35 |
+
1. **動くこと**:最小限の機能で動作確認を行う。
|
| 36 |
+
2. **速いこと**:パフォーマンスを意識した実装。
|
| 37 |
+
3. **安全なこと**:型安全性、エラー処理、セキュリティを考慮。
|
| 38 |
+
4. **可読性の高さ**:コードは誰が読んでも理解できるように。
|
| 39 |
+
5. **保守性の高さ**:変更が容易で拡張可能な設計。
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
### 🚫 禁止事項と制約
|
| 44 |
+
|
| 45 |
+
- ❌ `creators`, `generators`, `builders` などの **役割が曖昧な命名の混在を禁止**。
|
| 46 |
+
- 例:`UserCreator`, `UserGenerator`, `UserBuilder` は役割が曖昧で混在してはならない。
|
| 47 |
+
- 代替案:`UserService`, `UserFactory`, `UserHandler` など、**一貫性のある命名**を使用。
|
| 48 |
+
- ❌ **過剰な機能追加**(機能蔓延)をしない。
|
| 49 |
+
- 必要な機能だけを実装し、**YAGNI** を徹底。
|
| 50 |
+
- ❌ **ハードコード禁止**:設定値やマジックナンバーは定数化・設定ファイル化。
|
| 51 |
+
- ❌ **コメントの省略禁止**:コードには日本語でわかりやすく説明を記述。
|
| 52 |
+
- ❌ **無意味な抽象化禁止**:過度なデザインパターンの適用を避ける。
|
| 53 |
+
- ❌ **正常に動いている環境の変更禁止**:動作確認済みの環境設定を不用意に変更しない。
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
### 🧪 コーディング規約
|
| 58 |
+
|
| 59 |
+
- ✅ **型ヒント**(type hints)を必須とする。
|
| 60 |
+
- ✅ **docstring** を記述し、関数・クラスの目的を明確にする。
|
| 61 |
+
- ✅ **単体テスト**(unittest / pytest)を必ず記述。
|
| 62 |
+
- ✅ **エラー処理**は以下の流れで実装:
|
| 63 |
+
1. エラーの分析
|
| 64 |
+
2. 対処方法の検討
|
| 65 |
+
3. エラー処理の実装(try-except, logging, fallback など)
|
| 66 |
+
- ✅ **仮想環境**を使用して実行・テストすること。
|
| 67 |
+
|
| 68 |
+
---
|
| 69 |
+
|
| 70 |
+
### 🧭 モジュール設計の指針
|
| 71 |
+
|
| 72 |
+
- 各モジュールは **1つの責務のみを持つ**(Single Responsibility Principle)。
|
| 73 |
+
- モジュール名は **明確で一貫性のある命名**(例:`user_service.py`, `auth_handler.py`)。
|
| 74 |
+
- モジュール間は **依存を最小限に保ち、疎結合**にする。
|
| 75 |
+
- 共通処理は **utils や shared モジュール**に切り出す。
|
| 76 |
+
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
### 🧠 開発プロセス
|
| 80 |
+
|
| 81 |
+
1. **最小限の機能で動かす**(MVP)
|
| 82 |
+
2. **動作確認 → テスト追加 → リファクタリング**
|
| 83 |
+
3. **必要に応じて機能追加**(ただしYAGNIを意識)
|
| 84 |
+
4. **命名・構造の整合性を常にチェック**
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
### 🧾 エラー対応のスタイル
|
| 89 |
+
|
| 90 |
+
エラー発生時は以下の形式で報告してください:
|
| 91 |
+
|
| 92 |
+
```
|
| 93 |
+
【エラー内容】
|
| 94 |
+
TypeError: 'NoneType' object is not subscriptable
|
| 95 |
+
|
| 96 |
+
【原因分析】
|
| 97 |
+
user_data が None の場合にアクセスしようとしている。
|
| 98 |
+
|
| 99 |
+
【対処方法】
|
| 100 |
+
user_data が None でないことを確認してからアクセスする。
|
| 101 |
+
例:
|
| 102 |
+
if user_data:
|
| 103 |
+
return user_data['id']
|
| 104 |
+
else:
|
| 105 |
+
return None
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
### 🧩 まとめ:理想とするコードの特徴
|
| 111 |
+
|
| 112 |
+
| 特徴 | 説明 |
|
| 113 |
+
|--------------|------|
|
| 114 |
+
| **シンプル** | 不要な機能や複雑さを排除 |
|
| 115 |
+
| **明確** | 命名・構造が一貫しており、意図が伝わる |
|
| 116 |
+
| **安全** | 型安全・エラー処理が徹底されている |
|
| 117 |
+
| **保守性** | 変更・拡張が容易な設計 |
|
| 118 |
+
| **テスト可能** | 単体テストが容易に記述可能 |
|
| 119 |
+
|
| 120 |
+
---
|
| 121 |
+
|
| 122 |
+
### 📋 メタデータJSONスキーマ設計原則
|
| 123 |
+
|
| 124 |
+
本プロジェクトでは、画像パーツのレイアウト情報を管理するためにJSONメタデータを使用します。
|
| 125 |
+
|
| 126 |
+
#### メタデータの役割
|
| 127 |
+
|
| 128 |
+
- ✅ **レイアウト情報のみ管理**: bbox(位置・サイズ)、z_order(重なり順)、opacity(透明度)
|
| 129 |
+
- ✅ **DRY原則の徹底**: カラー情報��RGB, fill, stroke)は画像ファイル内部に埋め込み、JSONには記載しない
|
| 130 |
+
- ✅ **後方互換性の確保**: オプションフィールドは`.get()`でデフォルト値を使用
|
| 131 |
+
|
| 132 |
+
#### 必須フィールド
|
| 133 |
+
|
| 134 |
+
```json
|
| 135 |
+
{
|
| 136 |
+
"part_name": "background",
|
| 137 |
+
"bbox": {"x": 0, "y": 0, "width": 1024, "height": 1024},
|
| 138 |
+
"canvas_size": {"width": 1024, "height": 1024},
|
| 139 |
+
"z_order": 0,
|
| 140 |
+
"has_alpha": true,
|
| 141 |
+
"is_vector": false,
|
| 142 |
+
"data_type": "bitmap"
|
| 143 |
+
}
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
#### オプションフィールド(推奨)
|
| 147 |
+
|
| 148 |
+
```json
|
| 149 |
+
{
|
| 150 |
+
"opacity": 1.0,
|
| 151 |
+
"blend_mode": "normal",
|
| 152 |
+
"color_profile": "sRGB IEC61966-2.1",
|
| 153 |
+
"is_global_lineart": false,
|
| 154 |
+
"parent_part": null,
|
| 155 |
+
"z_order_mode": "auto",
|
| 156 |
+
"vector_type": "text",
|
| 157 |
+
"text_content": "LOGO",
|
| 158 |
+
"fill_color": {"r": 255, "g": 0, "b": 0, "a": 255},
|
| 159 |
+
"stroke_color": {"r": 0, "g": 0, "b": 0, "a": 255},
|
| 160 |
+
"background_color": {"r": 255, "g": 255, "b": 255, "a": 0}
|
| 161 |
+
}
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
#### 設計上の注意点
|
| 165 |
+
|
| 166 |
+
- ✅ **カラー情報のメタデータ化**: fill_color, stroke_color, background_colorをRGBA形式で保存
|
| 167 |
+
- **用途**: PDFフォールバックカラー、プレビュー生成、レイヤー識別用
|
| 168 |
+
- **⚠️ 必須要件**: メタデータに色情報が存在する場合、**必ず使用すること**(瑕疵案件対策)
|
| 169 |
+
- **優先順位**: メタデータのカラー情報 > 画像ファイル内の実RGBA値(顧客指定色を最優先)
|
| 170 |
+
- **欠損時の挙動**: メタデータに色情報がない場合のみ、画像ファイルから自動抽出
|
| 171 |
+
- ✅ **z_order自動計算**: 面積降順 → bbox.y降順 → bbox.x昇順でソート(一意性確保)
|
| 172 |
+
- ✅ **ベクター固有情報**: `vector_type`, `text_content`を保存(将来的な拡張性)
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
ご希望であれば、このプロンプトを `.md` や `.txt` ファイル形式で出力することも可能です。
|
| 177 |
+
また、チームで共有するための簡潔なバージョンも作成できます。お気軽にどうぞ。
|
.github/prompts/基本.instructions.md.prompt.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
mode: agent
|
| 3 |
+
---
|
| 4 |
+
Define the task to achieve, including specific requirements, constraints, and success criteria.
|
| 5 |
+
---
|
| 6 |
+
applyTo: '**'
|
| 7 |
+
---
|
| 8 |
+
Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.素晴らしいシステムプロンプトですね。
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## 🧠 システムプロンプト
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
### 🔧 基本姿勢
|
| 18 |
+
|
| 19 |
+
あなたは優れたソフトウェアエンジニアです。
|
| 20 |
+
以下の設計原則・思想・制約のもと、コードを記述・設計・レビューします。
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
### 🎯 プロジェクトの思想と目的
|
| 25 |
+
|
| 26 |
+
- **目的**:「動く」「速い」「安全」なシステムをシンプルに構築する。
|
| 27 |
+
- **思想**:
|
| 28 |
+
- **マイクロサービス的アプローチ**:単機能モジュールを意識した設計。
|
| 29 |
+
- **アジャイル開発対応**:最小限の機能でまず動かすことを重視。
|
| 30 |
+
- **過剰な機能追加を厳禁**:YAGNI(You Aren't Gonna Need It)を徹底。
|
| 31 |
+
- **命名の一貫性**:`creators`, `generators`, `builders` などの混在を許さない。
|
| 32 |
+
- **疎結合・高凝集**:モジュール間の依存を最小限に保つ。
|
| 33 |
+
- **DRY, KISS, SOLID** を常に意識した設計。
|
| 34 |
+
|
| 35 |
+
---
|
| 36 |
+
|
| 37 |
+
### 🧱 設計原則
|
| 38 |
+
|
| 39 |
+
1. **動くこと**:最小限の機能で動作確認を行う。
|
| 40 |
+
2. **速いこと**:パフォーマンスを意識した実装。
|
| 41 |
+
3. **安全なこと**:型安全性、エラー処理、セキュリティを考慮。
|
| 42 |
+
4. **可読性の高さ**:コードは誰が読んでも理解できるように。
|
| 43 |
+
5. **保守性の高さ**:変更が容易で拡張可能な設計。
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
### 🚫 禁止事項と制約
|
| 48 |
+
|
| 49 |
+
- ❌ `creators`, `generators`, `builders` などの **役割が曖昧な命名の混在を禁止**。
|
| 50 |
+
- 例:`UserCreator`, `UserGenerator`, `UserBuilder` は役割が曖昧で混在してはならない。
|
| 51 |
+
- 代替案:`UserService`, `UserFactory`, `UserHandler` など、**一貫性のある命名**を使用。
|
| 52 |
+
- ❌ **過剰な機能追加**(機能蔓延)をしない。
|
| 53 |
+
- 必要な機能だけを実装し、**YAGNI** を徹底。
|
| 54 |
+
- ❌ **ハードコード禁止**:設定値やマジックナンバーは定数化・設定ファイル化。
|
| 55 |
+
- ❌ **コメントの省略禁止**:コードには日本語でわかりやすく説明を記述。
|
| 56 |
+
- ❌ **無意味な抽象化禁止**:過度なデザインパターンの適用を避ける。
|
| 57 |
+
- ❌ **正常に動いている環境の変更禁止**:動作確認済みの環境設定を不用意に変更しない。
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
### 🧪 コーディング規約
|
| 62 |
+
|
| 63 |
+
- ✅ **型ヒント**(type hints)を必須とする。
|
| 64 |
+
- ✅ **docstring** を記述し、関数・クラスの目的を明確にする。
|
| 65 |
+
- ✅ **単体テスト**(unittest / pytest)を必ず記述。
|
| 66 |
+
- ✅ **エラー処理**は以下の流れで実装:
|
| 67 |
+
1. エラーの分析
|
| 68 |
+
2. 対処方法の検討
|
| 69 |
+
3. エラー処理の実装(try-except, logging, fallback など)
|
| 70 |
+
- ✅ **仮想環境**を使用して実行・テストすること。
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
### 🧭 モジュール設計の指針
|
| 75 |
+
|
| 76 |
+
- 各モジュールは **1つの責務のみを持つ**(Single Responsibility Principle)。
|
| 77 |
+
- モジュール名は **明確で一貫性のある命名**(例:`user_service.py`, `auth_handler.py`)。
|
| 78 |
+
- モジュール間は **依存を最小限に保ち、疎結合**にする。
|
| 79 |
+
- 共通処理は **utils や shared モジュール**に切り出す。
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
### 🧠 開発プロセス
|
| 84 |
+
|
| 85 |
+
1. **最小限の機能で動かす**(MVP)
|
| 86 |
+
2. **動作確認 → テスト追加 → リファクタリング**
|
| 87 |
+
3. **必要に応じて機能追加**(ただしYAGNIを意識)
|
| 88 |
+
4. **命名・構造の整合性を常にチェック**
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
### 🧾 エラー対応のスタイル
|
| 93 |
+
|
| 94 |
+
エラー発生時は以下の形式で報告してください:
|
| 95 |
+
|
| 96 |
+
```
|
| 97 |
+
【エラー内容】
|
| 98 |
+
TypeError: 'NoneType' object is not subscriptable
|
| 99 |
+
|
| 100 |
+
【原因分析】
|
| 101 |
+
user_data が None の場合にアクセスしようとしている。
|
| 102 |
+
|
| 103 |
+
【対処方法】
|
| 104 |
+
user_data が None でないことを確認してからアクセスする。
|
| 105 |
+
例:
|
| 106 |
+
if user_data:
|
| 107 |
+
return user_data['id']
|
| 108 |
+
else:
|
| 109 |
+
return None
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
### 🧩 まとめ:理想とするコードの特徴
|
| 115 |
+
|
| 116 |
+
| 特徴 | 説明 |
|
| 117 |
+
|--------------|------|
|
| 118 |
+
| **シンプル** | 不要な機能や複雑さを排除 |
|
| 119 |
+
| **明確** | 命名・構造が一貫しており、意図が伝わる |
|
| 120 |
+
| **安全** | 型安全・エラー処理が徹底されている |
|
| 121 |
+
| **保守性** | 変更・拡張が容易な設計 |
|
| 122 |
+
| **テスト可能** | 単体テストが容易に記述可能 |
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
### 📋 メタデータJSONスキーマ設計原則
|
| 127 |
+
|
| 128 |
+
本プロジェクトでは、画像パーツのレイアウト情報を管理するためにJSONメタデータを使用します。
|
| 129 |
+
|
| 130 |
+
#### メタデータの役割
|
| 131 |
+
|
| 132 |
+
- ✅ **レイアウト情報のみ管理**: bbox(位置・サイズ)、z_order(重なり順)、opacity(透明度)
|
| 133 |
+
- ✅ **DRY原則の徹底**: カラー情報(RGB, fill, stroke)は画像ファイル内部に埋め込み、JSONには記載しない
|
| 134 |
+
- ✅ **後方互換性の確保**: オプションフィールドは`.get()`でデフォルト値を使用
|
| 135 |
+
|
| 136 |
+
#### 必須フィールド
|
| 137 |
+
|
| 138 |
+
```json
|
| 139 |
+
{
|
| 140 |
+
"part_name": "background",
|
| 141 |
+
"bbox": {"x": 0, "y": 0, "width": 1024, "height": 1024},
|
| 142 |
+
"canvas_size": {"width": 1024, "height": 1024},
|
| 143 |
+
"z_order": 0,
|
| 144 |
+
"has_alpha": true,
|
| 145 |
+
"is_vector": false,
|
| 146 |
+
"data_type": "bitmap"
|
| 147 |
+
}
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
#### オプションフィールド(推奨)
|
| 151 |
+
|
| 152 |
+
```json
|
| 153 |
+
{
|
| 154 |
+
"opacity": 1.0,
|
| 155 |
+
"blend_mode": "normal",
|
| 156 |
+
"color_profile": "sRGB IEC61966-2.1",
|
| 157 |
+
"is_global_lineart": false,
|
| 158 |
+
"parent_part": null,
|
| 159 |
+
"z_order_mode": "auto",
|
| 160 |
+
"vector_type": "text",
|
| 161 |
+
"text_content": "LOGO",
|
| 162 |
+
"fill_color": {"r": 255, "g": 0, "b": 0, "a": 255},
|
| 163 |
+
"stroke_color": {"r": 0, "g": 0, "b": 0, "a": 255},
|
| 164 |
+
"background_color": {"r": 255, "g": 255, "b": 255, "a": 0}
|
| 165 |
+
}
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
#### 設計上の注意点
|
| 169 |
+
|
| 170 |
+
- ✅ **カラー情報のメタデータ化**: fill_color, stroke_color, background_colorをRGBA形式で保存
|
| 171 |
+
- **用途**: PDFフォールバックカラー、プレビュー生成、レイヤー識別用
|
| 172 |
+
- **⚠️ 必須要件**: メタデータに色情報が存在する場合、**必ず使用すること**(瑕疵案件対策)
|
| 173 |
+
- **優先順位**: メタデータのカラー情報 > 画像ファイル内の実RGBA値(顧客指定色を最優先)
|
| 174 |
+
- **欠損時の挙動**: メタデータに色情報がない場合のみ、画像ファイルから自動抽出
|
| 175 |
+
- ✅ **z_order自動計算**: 面積降順 → bbox.y降順 → bbox.x昇順でソート(一意性確保)
|
| 176 |
+
- ✅ **ベクター固有情報**: `vector_type`, `text_content`を保存(将来的な拡張性)
|
| 177 |
+
|
| 178 |
+
---
|
| 179 |
+
|
| 180 |
+
ご希望であれば、このプロンプトを `.md` や `.txt` ファイル形式で出力することも可能です。
|
| 181 |
+
また、チームで共有するための簡潔なバージョンも作成できます。お気軽にどうぞ。
|
.gitignore
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python-generated files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
dist/
|
| 9 |
+
wheels/
|
| 10 |
+
*.egg-info
|
| 11 |
+
.eggs/
|
| 12 |
+
*.egg
|
| 13 |
+
MANIFEST
|
| 14 |
+
develop-eggs/
|
| 15 |
+
lib/
|
| 16 |
+
lib64/
|
| 17 |
+
parts/
|
| 18 |
+
sdist/
|
| 19 |
+
var/
|
| 20 |
+
downloads/
|
| 21 |
+
eggs/
|
| 22 |
+
.installed.cfg
|
| 23 |
+
pip-wheel-metadata/
|
| 24 |
+
share/python-wheels/
|
| 25 |
+
|
| 26 |
+
# Virtual environments
|
| 27 |
+
.venv/
|
| 28 |
+
venv/
|
| 29 |
+
ENV/
|
| 30 |
+
env/
|
| 31 |
+
|
| 32 |
+
# IDE
|
| 33 |
+
.vscode/
|
| 34 |
+
.idea/
|
| 35 |
+
*.swp
|
| 36 |
+
*.swo
|
| 37 |
+
*~
|
| 38 |
+
.vs/
|
| 39 |
+
|
| 40 |
+
# OS
|
| 41 |
+
.DS_Store
|
| 42 |
+
Thumbs.db
|
| 43 |
+
desktop.ini
|
| 44 |
+
|
| 45 |
+
# 出力ファイル(生成された画像)
|
| 46 |
+
# これらのファイルはgit追跡外とし、app.pyでbase64埋め込みを使用
|
| 47 |
+
outputs/
|
| 48 |
+
*.png
|
| 49 |
+
*.jpg
|
| 50 |
+
*.jpeg
|
| 51 |
+
*.webp
|
| 52 |
+
|
| 53 |
+
# ログファイル
|
| 54 |
+
logs/
|
| 55 |
+
*.log
|
| 56 |
+
|
| 57 |
+
# 作業用ファイル(README.mdと.github以下のmdは除外しない)
|
| 58 |
+
*.md
|
| 59 |
+
!README.md
|
| 60 |
+
!.github/**/*.md
|
| 61 |
+
*.txt
|
| 62 |
+
!requirements.txt
|
| 63 |
+
|
| 64 |
+
# テンプレート・テスト・コピーファイル(作業用)
|
| 65 |
+
仮想環境への入り方.txt
|
| 66 |
+
prompt_base.txt
|
| 67 |
+
app copy 2.py
|
| 68 |
+
背景再現用のテスト.html
|
| 69 |
+
背景再現用のテスト copy.html
|
| 70 |
+
背景再現用のテスト copy 2.html
|
| 71 |
+
|
| 72 |
+
# ダウンロード用スクリプト(トークン露出防止のため追跡外)
|
| 73 |
+
utils/test_download_hugginface_repo.py
|
| 74 |
+
|
| 75 |
+
# テストディレクトリ
|
| 76 |
+
test/
|
| 77 |
+
|
| 78 |
+
# 不要なフォルダ(削除予定または作業用)
|
| 79 |
+
Miragic-AI-Image-Generator/
|
| 80 |
+
gradio_UI_Asahi-main/
|
| 81 |
+
|
| 82 |
+
# Hugging Face Cache
|
| 83 |
+
.cache/
|
| 84 |
+
huggingface/
|
| 85 |
+
|
| 86 |
+
# PyTorchモデルファイル
|
| 87 |
+
*.pth
|
| 88 |
+
*.pt
|
| 89 |
+
*.ckpt
|
| 90 |
+
*.safetensors
|
| 91 |
+
*.safetensors
|
| 92 |
+
|
| 93 |
+
# Jupyter Notebook
|
| 94 |
+
.ipynb_checkpoints/
|
| 95 |
+
*.ipynb
|
| 96 |
+
|
| 97 |
+
# 環境設定
|
| 98 |
+
.env
|
| 99 |
+
.env.local
|
| 100 |
+
.env.*
|
| 101 |
+
|
| 102 |
+
# Gradio
|
| 103 |
+
flagged/
|
| 104 |
+
gradio_cached_examples/
|
| 105 |
+
|
| 106 |
+
# Git
|
| 107 |
+
*.orig
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.11
|
README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Emix 0 5
|
| 3 |
+
emoji: 🐨
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.49.1
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
short_description: emix-0-5 demo app
|
| 11 |
+
---
|
app.py
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Emix Image Generator - Main Application
|
| 3 |
+
|
| 4 |
+
統合機能:(TV Asahi J Channel デザイン)
|
| 5 |
+
- aipicasso/emix-0-5 モデルによる高品質画像生成
|
| 6 |
+
- 詳細パラメータ制御とログ機能
|
| 7 |
+
- Text-to-Image 対応
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import gradio as gr
|
| 11 |
+
import torch
|
| 12 |
+
from diffusers import (
|
| 13 |
+
StableDiffusionXLPipeline,
|
| 14 |
+
DDIMScheduler,
|
| 15 |
+
DPMSolverMultistepScheduler,
|
| 16 |
+
EulerDiscreteScheduler,
|
| 17 |
+
EulerAncestralDiscreteScheduler,
|
| 18 |
+
PNDMScheduler,
|
| 19 |
+
LMSDiscreteScheduler
|
| 20 |
+
)
|
| 21 |
+
from huggingface_hub import login
|
| 22 |
+
import os
|
| 23 |
+
import base64
|
| 24 |
+
from datetime import datetime
|
| 25 |
+
|
| 26 |
+
# 環境変数の読み込み(dotenvがあれば使用)
|
| 27 |
+
try:
|
| 28 |
+
from dotenv import load_dotenv
|
| 29 |
+
load_dotenv()
|
| 30 |
+
except ImportError:
|
| 31 |
+
pass # dotenvがない場合はスキップ
|
| 32 |
+
import random
|
| 33 |
+
import json
|
| 34 |
+
import logging
|
| 35 |
+
from pathlib import Path
|
| 36 |
+
import traceback
|
| 37 |
+
from PIL import Image
|
| 38 |
+
import time
|
| 39 |
+
|
| 40 |
+
# 統合ロガーのインポート
|
| 41 |
+
import sys
|
| 42 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), 'utils'))
|
| 43 |
+
from logger import get_logger, log_generation
|
| 44 |
+
|
| 45 |
+
# 標準ロガー設定
|
| 46 |
+
logging.basicConfig(level=logging.INFO)
|
| 47 |
+
logger = logging.getLogger(__name__)
|
| 48 |
+
|
| 49 |
+
# 定数定義
|
| 50 |
+
HISTORY_FILE = "logs/generation_history.json"
|
| 51 |
+
OUTPUT_DIR = "outputs"
|
| 52 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "aipicasso/emix-0-5") # 環境変数で変更可能
|
| 53 |
+
|
| 54 |
+
# 統合ロガーインスタンス
|
| 55 |
+
unified_logger = get_logger("logs")
|
| 56 |
+
|
| 57 |
+
# グローバル変数でパイプラインを管理
|
| 58 |
+
txt2img_pipe = None
|
| 59 |
+
model_loaded = False
|
| 60 |
+
|
| 61 |
+
def setup_scheduler(pipe, scheduler_type="default"):
|
| 62 |
+
"""
|
| 63 |
+
スケジューラーの設定
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
pipe: StableDiffusionXLPipeline
|
| 67 |
+
scheduler_type: スケジューラータイプ
|
| 68 |
+
- "default": デフォルト
|
| 69 |
+
- "DDIM": 高品質、少ないステップ
|
| 70 |
+
- "DPMSolver": 高速で高品質(推奨)
|
| 71 |
+
- "Euler": 安定した結果
|
| 72 |
+
- "EulerA": より多様な結果
|
| 73 |
+
- "LMS": 古典的手法
|
| 74 |
+
- "PNDM": デフォルト
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
設定されたscheduler
|
| 78 |
+
"""
|
| 79 |
+
schedulers = {
|
| 80 |
+
"DDIM": DDIMScheduler,
|
| 81 |
+
"DPMSolver": DPMSolverMultistepScheduler,
|
| 82 |
+
"Euler": EulerDiscreteScheduler,
|
| 83 |
+
"EulerA": EulerAncestralDiscreteScheduler,
|
| 84 |
+
"PNDM": PNDMScheduler
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
# LMSはscipyが必要なため、利用可能な場合のみ追加
|
| 88 |
+
try:
|
| 89 |
+
schedulers["LMS"] = LMSDiscreteScheduler
|
| 90 |
+
except:
|
| 91 |
+
logger.warning("⚠️ LMSスケジューラーは利用できません (scipyが必要)")
|
| 92 |
+
|
| 93 |
+
if scheduler_type != "default" and scheduler_type in schedulers:
|
| 94 |
+
try:
|
| 95 |
+
return schedulers[scheduler_type].from_config(pipe.scheduler.config)
|
| 96 |
+
except ImportError as e:
|
| 97 |
+
logger.warning(f"⚠️ {scheduler_type}スケジューラーが利用できません: {e}")
|
| 98 |
+
return pipe.scheduler
|
| 99 |
+
return pipe.scheduler
|
| 100 |
+
|
| 101 |
+
def setup_model():
|
| 102 |
+
"""モデルのセットアップと最適化"""
|
| 103 |
+
global txt2img_pipe, model_loaded
|
| 104 |
+
|
| 105 |
+
if model_loaded:
|
| 106 |
+
return True
|
| 107 |
+
|
| 108 |
+
try:
|
| 109 |
+
logger.info("🔧 モデルをセットアップ中...")
|
| 110 |
+
|
| 111 |
+
# GPU確認
|
| 112 |
+
if not torch.cuda.is_available():
|
| 113 |
+
logger.error("❌ CUDA が利用できません。GPUを確認してください。")
|
| 114 |
+
return False
|
| 115 |
+
|
| 116 |
+
device = "cuda"
|
| 117 |
+
logger.info(f"✅ デバイス: {device}")
|
| 118 |
+
|
| 119 |
+
# Text-to-Image パイプライン
|
| 120 |
+
logger.info(f"📦 Text-to-Image パイプライン読み込み中: {MODEL_NAME}")
|
| 121 |
+
txt2img_pipe = StableDiffusionXLPipeline.from_pretrained(
|
| 122 |
+
MODEL_NAME,
|
| 123 |
+
torch_dtype=torch.float16,
|
| 124 |
+
use_safetensors=True
|
| 125 |
+
).to(device)
|
| 126 |
+
|
| 127 |
+
# GPU移動後にFP16に変換
|
| 128 |
+
try:
|
| 129 |
+
txt2img_pipe = txt2img_pipe.to(dtype=torch.float16)
|
| 130 |
+
logger.info("✅ FP16モードに変換")
|
| 131 |
+
except:
|
| 132 |
+
logger.warning("⚠️ FP16変換をスキップ、FP32で継続")
|
| 133 |
+
|
| 134 |
+
# メモリ効率化 (xformersは使用しない - CPU版PyTorchのため)
|
| 135 |
+
try:
|
| 136 |
+
txt2img_pipe.enable_xformers_memory_efficient_attention()
|
| 137 |
+
logger.info("✅ xFormers メモリ効率化を有効化")
|
| 138 |
+
except Exception as e:
|
| 139 |
+
logger.warning(f"⚠️ xFormers無効 (CPU版PyTorch使用中): {e}")
|
| 140 |
+
|
| 141 |
+
# CPU Offloadは無効化(全てGPUで処理)
|
| 142 |
+
logger.info("🎯 GPU専用モードで動作")
|
| 143 |
+
|
| 144 |
+
logger.info("✅ モデルセットアップ完了")
|
| 145 |
+
model_loaded = True
|
| 146 |
+
return True
|
| 147 |
+
|
| 148 |
+
except Exception as e:
|
| 149 |
+
logger.error(f"❌ モデルセットアップ失敗: {e}")
|
| 150 |
+
return False
|
| 151 |
+
|
| 152 |
+
def log_generation_details(prompt, negative_prompt, params, output_filepath, execution_time):
|
| 153 |
+
"""
|
| 154 |
+
生成詳細のログ記録(統合ロガー使用)
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
prompt: メインプロンプト
|
| 158 |
+
negative_prompt: ネガティブプロンプト
|
| 159 |
+
params: 生成パラメータ辞書
|
| 160 |
+
output_filepath: 生成画像のファイルパス
|
| 161 |
+
execution_time: 実行時間(秒)
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
generation_id: 生成記録のユニークID
|
| 165 |
+
"""
|
| 166 |
+
try:
|
| 167 |
+
generation_id = unified_logger.log_generation(
|
| 168 |
+
prompt=prompt,
|
| 169 |
+
negative_prompt=negative_prompt,
|
| 170 |
+
parameters=params,
|
| 171 |
+
output_filepath=output_filepath,
|
| 172 |
+
execution_time=execution_time
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
logger.info(f"📝 生成ログを記録: {generation_id}")
|
| 176 |
+
return generation_id
|
| 177 |
+
|
| 178 |
+
except Exception as e:
|
| 179 |
+
logger.error(f"❌ ログ記録失敗: {e}")
|
| 180 |
+
traceback.print_exc()
|
| 181 |
+
return None
|
| 182 |
+
|
| 183 |
+
def load_generation_history():
|
| 184 |
+
"""生成履歴を読み込む(統合ロガー形式)"""
|
| 185 |
+
try:
|
| 186 |
+
if os.path.exists(HISTORY_FILE):
|
| 187 |
+
with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
|
| 188 |
+
data = json.load(f)
|
| 189 |
+
# 統合ロガーの形式: {"generations": [...]}
|
| 190 |
+
if isinstance(data, dict) and 'generations' in data:
|
| 191 |
+
generations = data['generations']
|
| 192 |
+
# 最新10件を返す
|
| 193 |
+
return generations[-10:] if len(generations) > 10 else generations
|
| 194 |
+
# 古い形式(リスト)の場合
|
| 195 |
+
elif isinstance(data, list):
|
| 196 |
+
return data[-10:]
|
| 197 |
+
else:
|
| 198 |
+
return []
|
| 199 |
+
return []
|
| 200 |
+
except Exception as e:
|
| 201 |
+
logger.error(f"履歴読み込み失敗: {e}")
|
| 202 |
+
return []
|
| 203 |
+
|
| 204 |
+
def format_history_display():
|
| 205 |
+
"""履歴表示用のフォーマット(統合ロガー形式対応)"""
|
| 206 |
+
history = load_generation_history()
|
| 207 |
+
if not history:
|
| 208 |
+
return "📝 生成履歴がありません"
|
| 209 |
+
|
| 210 |
+
display_text = "## 📋 Recent Generation History (最新10件)\n\n"
|
| 211 |
+
|
| 212 |
+
for i, entry in enumerate(reversed(history), 1):
|
| 213 |
+
# 統合ロガー形式のフィールド
|
| 214 |
+
gen_id = entry.get('generation_id', 'Unknown')
|
| 215 |
+
timestamp = entry.get('timestamp', 'Unknown')
|
| 216 |
+
prompt = entry.get('prompt', 'No prompt')
|
| 217 |
+
# プロンプトが長い場合は省略
|
| 218 |
+
prompt_display = prompt[:50] + "..." if len(prompt) > 50 else prompt
|
| 219 |
+
|
| 220 |
+
# パラメータから情報取得
|
| 221 |
+
params = entry.get('parameters', {})
|
| 222 |
+
seed = params.get('seed', 'N/A')
|
| 223 |
+
steps = params.get('num_inference_steps', 'N/A')
|
| 224 |
+
|
| 225 |
+
# 結果情報
|
| 226 |
+
result = entry.get('result', {})
|
| 227 |
+
success = result.get('success', False)
|
| 228 |
+
exec_time = result.get('execution_time_seconds', 0)
|
| 229 |
+
|
| 230 |
+
status = "✅ Success" if success else "❌ Failed"
|
| 231 |
+
|
| 232 |
+
display_text += f"### {i}. {status}\n"
|
| 233 |
+
display_text += f"**ID:** {gen_id}\n"
|
| 234 |
+
display_text += f"**Time:** {timestamp}\n"
|
| 235 |
+
display_text += f"**Prompt:** {prompt_display}\n"
|
| 236 |
+
display_text += f"**Seed:** {seed} | **Steps:** {steps}\n"
|
| 237 |
+
display_text += f"**Execution:** {exec_time:.1f}s\n"
|
| 238 |
+
display_text += "---\n"
|
| 239 |
+
|
| 240 |
+
return display_text
|
| 241 |
+
|
| 242 |
+
def refresh_history():
|
| 243 |
+
"""履歴更新関数"""
|
| 244 |
+
return format_history_display()
|
| 245 |
+
|
| 246 |
+
def generate_txt2img(prompt, negative_prompt="", num_images=1, steps=25, guidance=7.5, size=1024, seed=None, scheduler="default"):
|
| 247 |
+
"""
|
| 248 |
+
テキストから画像生成(完全なパラメータ対応)
|
| 249 |
+
|
| 250 |
+
Args:
|
| 251 |
+
prompt: メインプロンプト
|
| 252 |
+
negative_prompt: ネガティブプロンプト
|
| 253 |
+
num_images: 生成画像数
|
| 254 |
+
steps: サンプリングステップ数 (10-150)
|
| 255 |
+
guidance: CFG Scale/ガイダンス強度 (1-20)
|
| 256 |
+
size: 画像サイズ (512, 768, 1024)
|
| 257 |
+
seed: シード値 (Noneでランダム)
|
| 258 |
+
scheduler: スケジューラータイプ
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
生成された画像のリスト
|
| 262 |
+
"""
|
| 263 |
+
global txt2img_pipe
|
| 264 |
+
|
| 265 |
+
if not prompt.strip():
|
| 266 |
+
return []
|
| 267 |
+
|
| 268 |
+
if not model_loaded:
|
| 269 |
+
if not setup_model():
|
| 270 |
+
return []
|
| 271 |
+
|
| 272 |
+
try:
|
| 273 |
+
logger.info(f"🎨 画像生成開始: {prompt[:50]}...")
|
| 274 |
+
|
| 275 |
+
start_time = time.time()
|
| 276 |
+
|
| 277 |
+
# シード設定(0またはNoneの場合はランダム)
|
| 278 |
+
if seed is None or seed == 0:
|
| 279 |
+
seed = random.randint(1, 2**32-1)
|
| 280 |
+
|
| 281 |
+
generator = torch.Generator(device="cuda").manual_seed(seed)
|
| 282 |
+
|
| 283 |
+
# スケジューラー設定
|
| 284 |
+
original_scheduler = txt2img_pipe.scheduler
|
| 285 |
+
if scheduler != "default":
|
| 286 |
+
txt2img_pipe.scheduler = setup_scheduler(txt2img_pipe, scheduler)
|
| 287 |
+
|
| 288 |
+
# パラメータ設定
|
| 289 |
+
params = {
|
| 290 |
+
"prompt": prompt,
|
| 291 |
+
"negative_prompt": negative_prompt,
|
| 292 |
+
"num_inference_steps": int(steps),
|
| 293 |
+
"guidance_scale": float(guidance),
|
| 294 |
+
"width": int(size),
|
| 295 |
+
"height": int(size),
|
| 296 |
+
"num_images_per_prompt": 1,
|
| 297 |
+
"generator": generator
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
# 画像生成(autocastを使用しない - test_high_quality_generation.pyと同じ)
|
| 301 |
+
result = txt2img_pipe(**params)
|
| 302 |
+
|
| 303 |
+
# スケジューラーを元に戻す
|
| 304 |
+
if scheduler != "default":
|
| 305 |
+
txt2img_pipe.scheduler = original_scheduler
|
| 306 |
+
|
| 307 |
+
execution_time = time.time() - start_time
|
| 308 |
+
|
| 309 |
+
# 画像保存
|
| 310 |
+
outputs_dir = Path("outputs")
|
| 311 |
+
outputs_dir.mkdir(exist_ok=True)
|
| 312 |
+
|
| 313 |
+
saved_paths = []
|
| 314 |
+
for i, image in enumerate(result.images):
|
| 315 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 316 |
+
filename = f"txt2img_{timestamp}_seed{seed}_{i+1}.png"
|
| 317 |
+
filepath = outputs_dir / filename
|
| 318 |
+
image.save(filepath, quality=95)
|
| 319 |
+
saved_paths.append(str(filepath))
|
| 320 |
+
logger.info(f"💾 画像保存: {filepath}")
|
| 321 |
+
|
| 322 |
+
# ログ記録(統合ロガー使用)
|
| 323 |
+
log_params = {
|
| 324 |
+
"num_inference_steps": int(steps),
|
| 325 |
+
"guidance_scale": float(guidance),
|
| 326 |
+
"width": int(size),
|
| 327 |
+
"height": int(size),
|
| 328 |
+
"seed": seed,
|
| 329 |
+
"scheduler_type": scheduler,
|
| 330 |
+
"num_images": num_images,
|
| 331 |
+
"torch_dtype": "float16",
|
| 332 |
+
"mode": "txt2img"
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
log_generation_details(
|
| 336 |
+
prompt=prompt,
|
| 337 |
+
negative_prompt=negative_prompt,
|
| 338 |
+
params=log_params,
|
| 339 |
+
output_filepath=saved_paths[0] if saved_paths else "",
|
| 340 |
+
execution_time=execution_time
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
logger.info(f"✅ 生成完了: {execution_time:.2f}秒, {len(result.images)}枚")
|
| 344 |
+
|
| 345 |
+
return result.images
|
| 346 |
+
|
| 347 |
+
except Exception as e:
|
| 348 |
+
logger.error(f"❌ 画像生成失敗: {e}")
|
| 349 |
+
logger.error(traceback.format_exc())
|
| 350 |
+
return []
|
| 351 |
+
|
| 352 |
+
def create_gradio_app():
|
| 353 |
+
"""Gradio アプリケーションの作成"""
|
| 354 |
+
|
| 355 |
+
# カスタムカラーオブジェクトを作成(TV Asahi Blue)
|
| 356 |
+
# custom_blue = gr.themes.Color(
|
| 357 |
+
# c50="#f0f4ff",
|
| 358 |
+
# c100="#dbeafe",
|
| 359 |
+
# c200="#bfdbfe",
|
| 360 |
+
# c300="#93c5fd",
|
| 361 |
+
# c400="#60a5fa",
|
| 362 |
+
# c500="#284baf", # メインの色
|
| 363 |
+
# c600="#1e40af",
|
| 364 |
+
# c700="#1d4ed8",
|
| 365 |
+
# c800="#1e3a8a",
|
| 366 |
+
# c900="#1e3a8a",
|
| 367 |
+
# c950="#172554"
|
| 368 |
+
# )
|
| 369 |
+
|
| 370 |
+
custom_css = """
|
| 371 |
+
body,
|
| 372 |
+
.gradio-container {
|
| 373 |
+
--range-color: #f97316;
|
| 374 |
+
background-color: #f6f8ff;
|
| 375 |
+
background-image:
|
| 376 |
+
url("data:image/svg+xml,%3Csvg%20width%3D%22160%22%20height%3D%22160%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22160%22%20height%3D%22160%22%20fill%3D%22transparent%22%2F%3E%3Cline%20x1%3D%2278%22%20y1%3D%2280%22%20x2%3D%2282%22%20y2%3D%2280%22%20stroke%3D%22rgba%2842%2C42%2C42%2C0.5%29%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%2F%3E%3Cline%20x1%3D%2280%22%20y1%3D%2278%22%20x2%3D%2280%22%20y2%3D%2282%22%20stroke%3D%22rgba%2842%2C42%2C42%2C0.5%29%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fsvg%3E"),
|
| 377 |
+
linear-gradient(90deg, rgba(42, 42, 42, 0.1) 0px, rgba(42, 42, 42, 0.1) 1px, transparent 1px, transparent 40px),
|
| 378 |
+
linear-gradient(0deg, rgba(42, 42, 42, 0.1) 0px, rgba(42, 42, 42, 0.1) 1px, transparent 1px, transparent 40px),
|
| 379 |
+
radial-gradient(circle at 10% 10%, rgba(11, 213, 126, 0.2) 0%, rgba(11, 213, 126, 0) 20%),
|
| 380 |
+
linear-gradient(135deg,
|
| 381 |
+
rgba(240, 244, 255, 0.4) 0%,
|
| 382 |
+
rgba(230, 240, 255, 0.2) 25%,
|
| 383 |
+
rgba(220, 235, 255, 0.1) 50%,
|
| 384 |
+
rgba(200, 220, 255, 0.15) 75%,
|
| 385 |
+
rgba(220, 200, 255, 0.3) 100%
|
| 386 |
+
);
|
| 387 |
+
background-size:
|
| 388 |
+
160px 160px,
|
| 389 |
+
40px 40px,
|
| 390 |
+
40px 40px,
|
| 391 |
+
100% 100%,
|
| 392 |
+
100% 100%;
|
| 393 |
+
background-position: 0 0, 0 0, 0 0, 0 0, 0 0;
|
| 394 |
+
background-repeat: repeat, repeat, repeat, no-repeat, no-repeat;
|
| 395 |
+
background-attachment: fixed;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
/* 主要パネルの透過感を維持 */
|
| 399 |
+
.gradio-container .gradio-block {
|
| 400 |
+
backdrop-filter: blur(4px);
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.logo-banner {
|
| 404 |
+
position: fixed;
|
| 405 |
+
top: 16px;
|
| 406 |
+
left: 16px;
|
| 407 |
+
z-index: 5;
|
| 408 |
+
margin: 0;
|
| 409 |
+
padding: 0;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.logo-banner svg,
|
| 413 |
+
.logo-banner img {
|
| 414 |
+
display: block;
|
| 415 |
+
width: auto;
|
| 416 |
+
height: auto;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.gradio-container input[type="range"] {
|
| 420 |
+
accent-color: #f97316;
|
| 421 |
+
}
|
| 422 |
+
.gradio-container input[type="range"]::-webkit-slider-thumb {
|
| 423 |
+
background-color: #f97316;
|
| 424 |
+
}
|
| 425 |
+
.gradio-container input[type="range"]::-moz-range-thumb {
|
| 426 |
+
background-color: #f97316;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.card-panel {
|
| 430 |
+
border-radius: 20px;
|
| 431 |
+
background: rgba(255, 255, 255, 0.82);
|
| 432 |
+
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.12);
|
| 433 |
+
padding: 24px;
|
| 434 |
+
border: 1px solid rgba(255, 255, 255, 0.65);
|
| 435 |
+
backdrop-filter: blur(10px);
|
| 436 |
+
overflow: hidden;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.card-panel > * {
|
| 440 |
+
width: 100%;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.card-panel details {
|
| 444 |
+
background: transparent;
|
| 445 |
+
border: none;
|
| 446 |
+
box-shadow: none;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.card-panel details > summary {
|
| 450 |
+
font-weight: 600;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.card-panel .gradio-image {
|
| 454 |
+
background: transparent;
|
| 455 |
+
border: none;
|
| 456 |
+
box-shadow: none;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.card-panel .gradio-image img {
|
| 460 |
+
border-radius: 16px;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.sample-thumb-row {
|
| 464 |
+
display: flex;
|
| 465 |
+
gap: 16px;
|
| 466 |
+
width: 100%;
|
| 467 |
+
flex-wrap: wrap;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.sample-thumb {
|
| 471 |
+
border-radius: 16px;
|
| 472 |
+
overflow: hidden;
|
| 473 |
+
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.12);
|
| 474 |
+
border: 1px solid rgba(148, 163, 184, 0.55);
|
| 475 |
+
background: rgba(255, 255, 255, 0.92);
|
| 476 |
+
padding: 0 !important;
|
| 477 |
+
position: relative;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.sample-thumb img {
|
| 481 |
+
width: 100%;
|
| 482 |
+
height: 100%;
|
| 483 |
+
object-fit: cover;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
.sample-thumb button[aria-label="Fullscreen"] {
|
| 487 |
+
position: absolute;
|
| 488 |
+
top: 12px;
|
| 489 |
+
right: 16px;
|
| 490 |
+
z-index: 5;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
.generate-btn button {
|
| 494 |
+
display: inline-flex;
|
| 495 |
+
align-items: center;
|
| 496 |
+
justify-content: center;
|
| 497 |
+
gap: 8px;
|
| 498 |
+
min-height: 62px; /* 約1.2倍の縦幅 */
|
| 499 |
+
padding: 18px 36px;
|
| 500 |
+
border-radius: 12px;
|
| 501 |
+
background: linear-gradient(135deg, #fb923c 0%, #f97316 45%, #ea580c 100%);
|
| 502 |
+
color: #ffffff;
|
| 503 |
+
font-size: 1.05rem;
|
| 504 |
+
font-weight: 600;
|
| 505 |
+
letter-spacing: 0.02em;
|
| 506 |
+
border: 1px solid rgba(249, 115, 22, 0.5);
|
| 507 |
+
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.12);
|
| 508 |
+
transition: transform 0.25s ease, box-shadow 0.25s ease, letter-spacing 0.25s ease, background 0.25s ease;
|
| 509 |
+
transform: scale(1);
|
| 510 |
+
cursor: pointer;
|
| 511 |
+
will-change: transform;
|
| 512 |
+
background-size: 120% 120%;
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
.generate-btn button:hover,
|
| 516 |
+
.generate-btn:hover button {
|
| 517 |
+
transform: scale(1.08) !important;
|
| 518 |
+
letter-spacing: 0.08em;
|
| 519 |
+
box-shadow: 0 24px 50px rgba(15, 23, 42, 0.16);
|
| 520 |
+
background-position: 100% 0;
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
.generate-btn button:active,
|
| 524 |
+
.generate-btn:active button {
|
| 525 |
+
transform: scale(0.96) !important;
|
| 526 |
+
letter-spacing: 0.03em;
|
| 527 |
+
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.14);
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
.generate-btn button:focus-visible {
|
| 531 |
+
outline: 2px solid rgba(249, 115, 22, 0.65);
|
| 532 |
+
outline-offset: 3px;
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
.dark .generate-btn button {
|
| 536 |
+
color: #1b1b1f;
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
.contain-fullscreen button[aria-label*="Close"],
|
| 540 |
+
.contain-fullscreen button[aria-label*="close"],
|
| 541 |
+
.contain-fullscreen button[aria-label*="Exit"],
|
| 542 |
+
.contain-fullscreen button[aria-label*="exit"],
|
| 543 |
+
.contain-fullscreen button[aria-label*="閉じる"] {
|
| 544 |
+
margin-right: 18px;
|
| 545 |
+
margin-top: 10px;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.model-title {
|
| 549 |
+
display: inline-flex;
|
| 550 |
+
align-items: center;
|
| 551 |
+
gap: 12px;
|
| 552 |
+
margin: 32px 0 16px;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.model-icon {
|
| 556 |
+
width: 88px;
|
| 557 |
+
height: 88px;
|
| 558 |
+
border-radius: 12px;
|
| 559 |
+
object-fit: cover;
|
| 560 |
+
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.12);
|
| 561 |
+
background: rgba(255, 255, 255, 0.85);
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.model-title-text {
|
| 565 |
+
display: flex;
|
| 566 |
+
flex-direction: column;
|
| 567 |
+
align-items: flex-start;
|
| 568 |
+
gap: 6px;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.model-name {
|
| 572 |
+
font-size: 2.4rem;
|
| 573 |
+
font-weight: 600;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
.model-link {
|
| 577 |
+
display: inline-flex;
|
| 578 |
+
align-items: center;
|
| 579 |
+
gap: 4px;
|
| 580 |
+
color: #1f2937;
|
| 581 |
+
font-weight: 500;
|
| 582 |
+
text-decoration: none;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.model-link .link-icon {
|
| 586 |
+
font-size: 1.2rem;
|
| 587 |
+
opacity: 0.75;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
.model-link:hover {
|
| 591 |
+
text-decoration: underline;
|
| 592 |
+
}
|
| 593 |
+
"""
|
| 594 |
+
|
| 595 |
+
# ロゴSVGの読み込み
|
| 596 |
+
logo_svg_html = ""
|
| 597 |
+
logo_svg_path = Path(__file__).parent / "assets"/ "images" / "logo" / "logo_ai_picasso.svg"
|
| 598 |
+
try:
|
| 599 |
+
logo_svg_html = logo_svg_path.read_text(encoding="utf-8")
|
| 600 |
+
except FileNotFoundError:
|
| 601 |
+
logger.warning("⚠️ ロゴSVGが見つかりません: %s", logo_svg_path)
|
| 602 |
+
except Exception as exc:
|
| 603 |
+
logger.warning("⚠️ ロゴSVG読み込みに失敗しました: %s", exc)
|
| 604 |
+
|
| 605 |
+
sample_image_names = ["ComfyUI_04014_.png", "ComfyUI_04069_.png"]
|
| 606 |
+
sample_images = []
|
| 607 |
+
sample_dir = Path(__file__).parent / "assets" / "images" / "samples"
|
| 608 |
+
for name in sample_image_names:
|
| 609 |
+
sample_path = sample_dir / name
|
| 610 |
+
if sample_path.exists():
|
| 611 |
+
try:
|
| 612 |
+
with Image.open(sample_path) as img:
|
| 613 |
+
width, height = img.size
|
| 614 |
+
# PNG画像をBase64に埋め込み
|
| 615 |
+
image_bytes = sample_path.read_bytes()
|
| 616 |
+
image_b64 = base64.b64encode(image_bytes).decode("ascii")
|
| 617 |
+
image_data_uri = f"data:image/png;base64,{image_b64}"
|
| 618 |
+
except Exception as exc:
|
| 619 |
+
logger.warning("⚠️ サンプル画像の読み込みに失敗しました: %s (%s)", sample_path, exc)
|
| 620 |
+
width, height = (240, 240)
|
| 621 |
+
image_data_uri = None
|
| 622 |
+
target_height = 240
|
| 623 |
+
scaled_width = max(1, int(round((target_height / height) * width))) if height else 240
|
| 624 |
+
sample_images.append({
|
| 625 |
+
"data_uri": image_data_uri,
|
| 626 |
+
"width": scaled_width,
|
| 627 |
+
"height": target_height
|
| 628 |
+
})
|
| 629 |
+
else:
|
| 630 |
+
logger.warning("⚠️ サンプル画像が見つかりません: %s", sample_path)
|
| 631 |
+
|
| 632 |
+
# メインUI構築
|
| 633 |
+
with gr.Blocks(
|
| 634 |
+
title="Emix",
|
| 635 |
+
theme=gr.themes.Default(),
|
| 636 |
+
css=custom_css
|
| 637 |
+
) as demo:
|
| 638 |
+
|
| 639 |
+
if logo_svg_html:
|
| 640 |
+
gr.HTML(f"<div class='logo-banner'>{logo_svg_html}</div>")
|
| 641 |
+
|
| 642 |
+
icon_path = Path(__file__).parent / "assets" / "images" / "icon" / "ai_picasso_icon.svg"
|
| 643 |
+
icon_html = ""
|
| 644 |
+
try:
|
| 645 |
+
icon_bytes = icon_path.read_bytes()
|
| 646 |
+
icon_b64 = base64.b64encode(icon_bytes).decode("ascii")
|
| 647 |
+
icon_html = f"<img src='data:image/svg+xml;base64,{icon_b64}' alt='Emix Icon' class='model-icon' />"
|
| 648 |
+
except FileNotFoundError:
|
| 649 |
+
logger.warning("⚠️ モデルアイコンが見つかりません: %s", icon_path)
|
| 650 |
+
except Exception as exc:
|
| 651 |
+
logger.warning("⚠️ モデルアイコン読み込みに失敗しました: %s", exc)
|
| 652 |
+
|
| 653 |
+
title_text_html = (
|
| 654 |
+
"<div class='model-title-text'>"
|
| 655 |
+
"<span class='model-name'>Emix-0-5</span>"
|
| 656 |
+
"<a class='model-link' href='https://aipicasso.co.jp/' target='_blank' rel='noopener noreferrer'>"
|
| 657 |
+
"<span class='link-icon'>💼</span><span>https://aipicasso.co.jp/</span>"
|
| 658 |
+
"</a>"
|
| 659 |
+
"</div>"
|
| 660 |
+
)
|
| 661 |
+
|
| 662 |
+
gr.HTML(
|
| 663 |
+
f"<div class='model-title'>{icon_html}{title_text_html}</div>"
|
| 664 |
+
)
|
| 665 |
+
|
| 666 |
+
with gr.Row():
|
| 667 |
+
with gr.Column(scale=2):
|
| 668 |
+
with gr.Group(elem_classes=["card-panel"]):
|
| 669 |
+
txt_prompt = gr.Textbox(
|
| 670 |
+
label="Prompt / プロンプト",
|
| 671 |
+
placeholder="Enter your prompt | 高品質なアニメ風の美しい女性の画像を生成するプロンプトを入力 | Example : anime girl, white hair, golden eyes, Santa outfit, Santa hat, blush, surprised expression, hands on cheeks, detailed face, clean white background, festive, Christmas theme",
|
| 672 |
+
lines=3,
|
| 673 |
+
max_lines=5
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
txt_negative_prompt = gr.Textbox(
|
| 677 |
+
label="Negative Prompt / ネガティブプロンプト",
|
| 678 |
+
# イラスト/キャラクター生成向けに調整したネガティブプロンプト
|
| 679 |
+
value=(
|
| 680 |
+
"lowres, bad anatomy, bad hands, missing fingers, extra fingers, mutated hands,"
|
| 681 |
+
" poorly drawn face, poorly drawn hands, deformed, mutated, watermark, signature,"
|
| 682 |
+
" text, logo, duplicate, cropped, jpeg artifacts, blurry, out of focus, oversaturated,"
|
| 683 |
+
" unnatural colors, sticker, mosaic, artifacts, ugly, nsfw, low quality"
|
| 684 |
+
),
|
| 685 |
+
lines=3,
|
| 686 |
+
max_lines=5
|
| 687 |
+
)
|
| 688 |
+
|
| 689 |
+
with gr.Group(elem_classes=["card-panel"]):
|
| 690 |
+
with gr.Accordion("Advanced Settings / 詳細設定", open=True):
|
| 691 |
+
txt_step = gr.Slider(
|
| 692 |
+
minimum=10, maximum=150, value=25, step=5,
|
| 693 |
+
label="Sampling Steps / サンプリングステップ数 (推奨: 20-40)"
|
| 694 |
+
)
|
| 695 |
+
txt_guidance = gr.Slider(
|
| 696 |
+
minimum=3.0, maximum=15.0, value=7.5, step=0.5,
|
| 697 |
+
label="CFG Scale / ガイダンス強度 (推奨: 7-10)"
|
| 698 |
+
)
|
| 699 |
+
# 画像サイズは1024x1024固定 (UIには表示しない)
|
| 700 |
+
# サポート解像度例: 512x512, 768x768, 1024x1024, 1280x1280, 1536x1536
|
| 701 |
+
txt_size = 1024 # 固定値
|
| 702 |
+
txt_seed = gr.Number(
|
| 703 |
+
label="Seed (空欄でランダム)",
|
| 704 |
+
value=-1,
|
| 705 |
+
precision=0
|
| 706 |
+
)
|
| 707 |
+
txt_scheduler = gr.Dropdown(
|
| 708 |
+
choices=["default", "DDIM", "DPMSolver", "Euler", "EulerA", "LMS", "PNDM"],
|
| 709 |
+
value="default",
|
| 710 |
+
label="Scheduler / スケジューラー (推奨: DPMSolver)"
|
| 711 |
+
)
|
| 712 |
+
|
| 713 |
+
txt_generate_btn = gr.Button(
|
| 714 |
+
"🎨 画像生成開始",
|
| 715 |
+
variant="primary",
|
| 716 |
+
size="lg",
|
| 717 |
+
elem_classes=["generate-btn"]
|
| 718 |
+
)
|
| 719 |
+
|
| 720 |
+
with gr.Column(scale=3):
|
| 721 |
+
with gr.Group(elem_classes=["card-panel"]):
|
| 722 |
+
txt_gallery = gr.Image(
|
| 723 |
+
label="Generated Image / 生成された画像",
|
| 724 |
+
type="pil",
|
| 725 |
+
interactive=False,
|
| 726 |
+
show_label=True,
|
| 727 |
+
show_download_button=True,
|
| 728 |
+
container=True,
|
| 729 |
+
height=None,
|
| 730 |
+
width=None
|
| 731 |
+
)
|
| 732 |
+
|
| 733 |
+
if sample_images:
|
| 734 |
+
gr.Markdown("## Samples")
|
| 735 |
+
with gr.Row(elem_classes=["sample-thumb-row"]):
|
| 736 |
+
for info in sample_images:
|
| 737 |
+
gr.Image(
|
| 738 |
+
value=info["data_uri"],
|
| 739 |
+
interactive=False,
|
| 740 |
+
type="pil",
|
| 741 |
+
show_label=False,
|
| 742 |
+
show_download_button=False,
|
| 743 |
+
show_fullscreen_button=True,
|
| 744 |
+
elem_classes=["sample-thumb"],
|
| 745 |
+
height=info["height"],
|
| 746 |
+
width=info["width"]
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
# 画像生成用のラッパー関数(2重呼び出し防止)
|
| 750 |
+
def generate_single_image(prompt, neg_prompt, step, guidance, seed, scheduler):
|
| 751 |
+
result = generate_txt2img(prompt, neg_prompt, 1, step, guidance, txt_size, seed, scheduler)
|
| 752 |
+
return result[0] if result else None
|
| 753 |
+
|
| 754 |
+
# イベントバインディング
|
| 755 |
+
txt_generate_btn.click(
|
| 756 |
+
fn=generate_single_image,
|
| 757 |
+
inputs=[txt_prompt, txt_negative_prompt, txt_step, txt_guidance, txt_seed, txt_scheduler],
|
| 758 |
+
outputs=txt_gallery,
|
| 759 |
+
show_progress=True
|
| 760 |
+
)
|
| 761 |
+
|
| 762 |
+
return demo
|
| 763 |
+
|
| 764 |
+
def main():
|
| 765 |
+
"""メインアプリケーション"""
|
| 766 |
+
logger.info("🚀 Emix Image Generator 起動中...")
|
| 767 |
+
|
| 768 |
+
# 必要なディレクトリを作成
|
| 769 |
+
Path("outputs").mkdir(exist_ok=True)
|
| 770 |
+
Path("logs").mkdir(exist_ok=True)
|
| 771 |
+
|
| 772 |
+
# Gradio アプリケーション作成
|
| 773 |
+
demo = create_gradio_app()
|
| 774 |
+
|
| 775 |
+
# アプリケーション起動
|
| 776 |
+
logger.info("🌐 Webアプリケーションを起動...")
|
| 777 |
+
demo.launch(
|
| 778 |
+
server_name="127.0.0.1",
|
| 779 |
+
server_port=7860,
|
| 780 |
+
share=False,
|
| 781 |
+
show_error=True,
|
| 782 |
+
quiet=False,
|
| 783 |
+
inbrowser=True # ブラウザを自動で開く
|
| 784 |
+
)
|
| 785 |
+
|
| 786 |
+
if __name__ == "__main__":
|
| 787 |
+
main()
|
assets/images/icon/ai_picasso_icon.svg
ADDED
|
|
assets/images/logo/logo_ai_picasso.svg
ADDED
|
|
gradio_ui/.gitignore
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python-generated files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[oc]
|
| 4 |
+
build/
|
| 5 |
+
dist/
|
| 6 |
+
wheels/
|
| 7 |
+
*.egg-info
|
| 8 |
+
|
| 9 |
+
# Virtual environments
|
| 10 |
+
.venv
|
| 11 |
+
|
| 12 |
+
# UV lock file
|
| 13 |
+
uv.lock
|
| 14 |
+
|
| 15 |
+
# Project-specific files
|
| 16 |
+
reference/
|
| 17 |
+
|
| 18 |
+
# IDE and development tools
|
| 19 |
+
.serena/
|
| 20 |
+
.github/
|
| 21 |
+
.github/prompts/
|
| 22 |
+
|
| 23 |
+
# Unused image files (keep only: j_channel.svg, J_channel.svg, tvasahi.svg)
|
| 24 |
+
imgs/j_channel.eps
|
| 25 |
+
imgs/j_channel.png
|
| 26 |
+
imgs/J_channel.psd
|
| 27 |
+
imgs/J_channel_large.png
|
| 28 |
+
imgs/pc_main.jpg
|
| 29 |
+
imgs/pc_main.psd
|
| 30 |
+
|
| 31 |
+
# Other unused image formats
|
| 32 |
+
*.webp
|
| 33 |
+
*.bmp
|
| 34 |
+
*.tiff
|
| 35 |
+
*.ico
|
| 36 |
+
|
| 37 |
+
# Unused Python files
|
| 38 |
+
# (Add specific .py files here as they become unused)
|
| 39 |
+
|
| 40 |
+
# documentation files
|
| 41 |
+
依頼内容.md
|
| 42 |
+
仕様書.md
|
gradio_ui/.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.11.12
|
gradio_ui/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# テレビ朝日 スーパーJチャンネル - AI画像生成システムUI
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
Gradioを使用したテレビ朝日 スーパーJチャンネル向けのAI画像生成UI。
|
| 5 |
+
|
| 6 |
+
### スクリーンショット
|
| 7 |
+
- Text to Image
|
| 8 |
+

|
| 9 |
+
|
| 10 |
+
- Image to Image
|
| 11 |
+

|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
## 起動方法
|
| 16 |
+
|
| 17 |
+
### UV使用
|
| 18 |
+
```bash
|
| 19 |
+
# UVでの起動
|
| 20 |
+
uv run python app.py
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
### Python環境
|
| 24 |
+
```bash
|
| 25 |
+
# 依存関係インストール
|
| 26 |
+
pip install -r requirements.txt
|
| 27 |
+
|
| 28 |
+
# アプリケーション起動
|
| 29 |
+
python app.py
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
起動後、ブラウザで `http://localhost:7860` にアクセス
|
| 33 |
+
|
| 34 |
+
## 機能
|
| 35 |
+
|
| 36 |
+
### Text-to-Image タブ
|
| 37 |
+
|
| 38 |
+
- テキストプロンプトから最大4枚の画像生成
|
| 39 |
+
- Advanced Settings(Step、Guidance Scale、Size等)
|
| 40 |
+
- 2x2グリッドでの結果表示
|
| 41 |
+
- ダウンロード機能付きギャラリー
|
| 42 |
+
|
| 43 |
+
### Image-to-Image タブ
|
| 44 |
+
|
| 45 |
+
- 参考画像 + テキストプロンプトで画像生成
|
| 46 |
+
- Prompt Strength(参考画像の影響度)調整
|
| 47 |
+
- アップロード画像のプレビュー
|
| 48 |
+
- 同じAdvanced Settings対応
|
| 49 |
+
|
| 50 |
+
### 共通機能
|
| 51 |
+
|
| 52 |
+
- テレビ朝日・Jチャンネルのロゴ表示(#284baf カラー)
|
| 53 |
+
- 最小限のデザイン(シンプルさ最優先)
|
| 54 |
+
- Base64インライン画像埋め込み
|
| 55 |
+
- 日本語・英語併記のUI
|
| 56 |
+
|
| 57 |
+
## 技術仕様
|
| 58 |
+
|
| 59 |
+
- **フレームワーク**: Gradio >= 5.48.0
|
| 60 |
+
- **環境管理**: UV(Python 3.11.12)
|
| 61 |
+
- **レイアウト**: gr.Blocks + カスタムテーマ
|
| 62 |
+
- **色設計**: #284baf統一カラー(ボタン・タブ)
|
| 63 |
+
- **依存関係**: gradio, numpy, pillow
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
### 画像生成関数の実装
|
| 67 |
+
|
| 68 |
+
現在はダミー処理。実際のAIモデルに置き換える場合:
|
| 69 |
+
|
| 70 |
+
```python
|
| 71 |
+
def generate_txt2img(prompt, num_images=4):
|
| 72 |
+
"""
|
| 73 |
+
テキストから画像生成
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
prompt (str): 生成プロンプト
|
| 77 |
+
num_images (int): 生成枚数(1-4枚)
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
list: 生成された画像のリスト
|
| 81 |
+
|
| 82 |
+
Note:
|
| 83 |
+
実際のAI画像生成モデル(Stable Diffusion, DALL-E等)に置き換える際は、
|
| 84 |
+
Advanced Settingsのパラメータ(step, guidance, size等)も
|
| 85 |
+
引数として追加し、モデルに渡してください。
|
| 86 |
+
"""
|
| 87 |
+
# 例: モデルにパラメータを渡す場合
|
| 88 |
+
# return model.generate(prompt, num_images=num_images, steps=step, guidance_scale=guidance, size=size)
|
| 89 |
+
pass
|
| 90 |
+
|
| 91 |
+
def generate_img2img(prompt, reference_image, num_images=4):
|
| 92 |
+
"""
|
| 93 |
+
参考画像+テキストから画像生成
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
prompt (str): 生成プロンプト
|
| 97 |
+
reference_image: 参考画像(PIL Image)
|
| 98 |
+
num_images (int): 生成枚数(1-4枚)
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
list: 生成された画像のリスト
|
| 102 |
+
"""
|
| 103 |
+
pass
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
### ログ機能
|
| 107 |
+
|
| 108 |
+
`logs/` フォルダを活用してユーザー操作をトラッキング
|
| 109 |
+
|
| 110 |
+
## 📁 ファイル構成
|
| 111 |
+
|
| 112 |
+
```text
|
| 113 |
+
gradio_ui_asahi/
|
| 114 |
+
├── app.py # メインアプリケーション
|
| 115 |
+
├── requirements.txt # 依存関係
|
| 116 |
+
├── pyproject.toml # UV設定
|
| 117 |
+
├── imgs/ # ロゴ画像
|
| 118 |
+
│ ├── tvasahi.svg # TVasahiロゴ(フッター用)
|
| 119 |
+
│ └── j_channel.svg # Jチャンネルロゴ(ヘッダー用)
|
| 120 |
+
├── logs/ # ログファイル(将来使用)
|
| 121 |
+
└── README.md # このファイル
|
| 122 |
+
```
|
| 123 |
+
## 設計原則
|
| 124 |
+
- **統一感**: #284baf カラーでブランディング統一
|
| 125 |
+
|
| 126 |
+
## 更新履歴
|
| 127 |
+
|
| 128 |
+
- **v1.0**: 初期UI実装(txt2img/img2img)
|
| 129 |
+
- **v1.1**: Advanced Settings 追加
|
| 130 |
+
- **v1.2**: カラーテーマ統一(#284baf)
|
gradio_ui/app.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
+
import base64
|
| 4 |
+
|
| 5 |
+
# ダミー画像生成関数(実際のモデルは未実装)
|
| 6 |
+
def generate_txt2img(prompt, num_images=4):
|
| 7 |
+
"""テキストから画像生成(ダミー処理)"""
|
| 8 |
+
if not prompt.strip():
|
| 9 |
+
return []
|
| 10 |
+
|
| 11 |
+
# 実際の実装では、ここでAI画像生成モデルを呼び出し
|
| 12 |
+
# 現在はダミー画像を返す(グレーのデフォルト)
|
| 13 |
+
dummy_images = []
|
| 14 |
+
for i in range(num_images):
|
| 15 |
+
# プレースホルダー画像のパスを生成
|
| 16 |
+
dummy_path = f"https://via.placeholder.com/512x512?text=Generated+Image+{i+1}"
|
| 17 |
+
dummy_images.append(dummy_path)
|
| 18 |
+
|
| 19 |
+
return dummy_images
|
| 20 |
+
|
| 21 |
+
def generate_img2img(prompt, reference_image, num_images=4):
|
| 22 |
+
"""参考画像+テキストから画像生成(ダミー処理)"""
|
| 23 |
+
if not prompt.strip():
|
| 24 |
+
return []
|
| 25 |
+
|
| 26 |
+
if reference_image is None:
|
| 27 |
+
return generate_txt2img(prompt, num_images)
|
| 28 |
+
|
| 29 |
+
# 実際の実装では、参考画像とプロンプトを使用してAI生成
|
| 30 |
+
dummy_images = []
|
| 31 |
+
for i in range(num_images):
|
| 32 |
+
dummy_path = f"https://via.placeholder.com/512x512?text=Img2Img+Result+{i+1}"
|
| 33 |
+
dummy_images.append(dummy_path)
|
| 34 |
+
|
| 35 |
+
return dummy_images
|
| 36 |
+
|
| 37 |
+
# Gradioテーマシステムを使用してボタンとタブの色を統一
|
| 38 |
+
|
| 39 |
+
# カスタムカラーオブジェクトを作成
|
| 40 |
+
custom_blue = gr.themes.Color(
|
| 41 |
+
c50="#f0f4ff",
|
| 42 |
+
c100="#dbeafe",
|
| 43 |
+
c200="#bfdbfe",
|
| 44 |
+
c300="#93c5fd",
|
| 45 |
+
c400="#60a5fa",
|
| 46 |
+
c500="#284baf", # メインの色
|
| 47 |
+
c600="#1e40af",
|
| 48 |
+
c700="#1d4ed8",
|
| 49 |
+
c800="#1e3a8a",
|
| 50 |
+
c900="#1e3a8a",
|
| 51 |
+
c950="#172554"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# メインUI構築
|
| 55 |
+
with gr.Blocks(
|
| 56 |
+
title="TV Asahi J Channel Image UI",
|
| 57 |
+
theme=gr.themes.Default(primary_hue=custom_blue),
|
| 58 |
+
css=".header-bg { background-color: #284baf; } .footer-bg { background-color: white; justify-content: center; display: flex; align-items: center; }"
|
| 59 |
+
) as demo:
|
| 60 |
+
|
| 61 |
+
# ヘッダー部分(j_channel.pngを中央配置)
|
| 62 |
+
with gr.Row(elem_classes=["header-bg"], equal_height=True):
|
| 63 |
+
try:
|
| 64 |
+
with open("imgs/j_channel.svg", 'rb') as f:
|
| 65 |
+
b64 = base64.b64encode(f.read()).decode()
|
| 66 |
+
gr.HTML(
|
| 67 |
+
'<div style="width:100%;display:flex;justify-content:center;align-items:center;">'
|
| 68 |
+
f'<img src="data:image/svg+xml;base64,{b64}" style="height:150px;aspect-ratio:auto;display:block;" alt="J Channel logo" />'
|
| 69 |
+
'</div>'
|
| 70 |
+
)
|
| 71 |
+
except:
|
| 72 |
+
gr.HTML('<span>Missing J Channel logo</span>')
|
| 73 |
+
|
| 74 |
+
# メインタブ
|
| 75 |
+
with gr.Tabs() as tabs:
|
| 76 |
+
|
| 77 |
+
# Text-to-Image タブ
|
| 78 |
+
with gr.TabItem("Text to Image", id="txt2img"):
|
| 79 |
+
|
| 80 |
+
with gr.Row():
|
| 81 |
+
with gr.Column(scale=2):
|
| 82 |
+
txt_prompt = gr.Textbox(
|
| 83 |
+
label="Prompt / プロンプト",
|
| 84 |
+
placeholder="Enter your prompt | モデルに入力するプロンプトをここに入力",
|
| 85 |
+
lines=1,
|
| 86 |
+
max_lines=1
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
with gr.Accordion("Advanced Settings", open=False):
|
| 90 |
+
txt_num_images = gr.Slider(
|
| 91 |
+
minimum=1, maximum=4, value=4, step=1,
|
| 92 |
+
label="Number of Images / 生成枚数"
|
| 93 |
+
)
|
| 94 |
+
txt_step = gr.Slider(
|
| 95 |
+
minimum=2, maximum=50, value=12, step=1,
|
| 96 |
+
label="Step"
|
| 97 |
+
)
|
| 98 |
+
txt_guidance = gr.Slider(
|
| 99 |
+
minimum=0, maximum=20, value=7.5, step=0.5,
|
| 100 |
+
label="Guidance Scale (プロンプトの反映力)"
|
| 101 |
+
)
|
| 102 |
+
txt_prompt_strength = gr.Slider(
|
| 103 |
+
minimum=0, maximum=1, value=0.8, step=0.05,
|
| 104 |
+
label="Prompt Strength (画像入力時のみ)",
|
| 105 |
+
interactive=False
|
| 106 |
+
)
|
| 107 |
+
txt_size = gr.Slider(
|
| 108 |
+
minimum=512, maximum=2048, value=1024, step=64,
|
| 109 |
+
label="Size (px)"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
txt_generate_btn = gr.Button(
|
| 113 |
+
"画像生成開始",
|
| 114 |
+
variant="primary"
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
with gr.Column(scale=3):
|
| 118 |
+
txt_gallery = gr.Gallery(
|
| 119 |
+
label="生成された画像",
|
| 120 |
+
columns=2,
|
| 121 |
+
rows=2,
|
| 122 |
+
height="auto",
|
| 123 |
+
show_download_button=True,
|
| 124 |
+
object_fit="contain"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
# Image-to-Image タブ
|
| 128 |
+
with gr.TabItem("Image to Image", id="img2img"):
|
| 129 |
+
|
| 130 |
+
with gr.Row():
|
| 131 |
+
with gr.Column(scale=2):
|
| 132 |
+
# 参考画像入力
|
| 133 |
+
img_reference = gr.Image(
|
| 134 |
+
label="参考画像",
|
| 135 |
+
type="pil"
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
img_prompt = gr.Textbox(
|
| 139 |
+
label="Prompt / プロンプト",
|
| 140 |
+
placeholder="Enter your prompt | モデルに入力するプロンプトをここに入力",
|
| 141 |
+
lines=1,
|
| 142 |
+
max_lines=1
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
with gr.Accordion("Advanced Settings", open=False):
|
| 146 |
+
img_num_images = gr.Slider(
|
| 147 |
+
minimum=1, maximum=4, value=4, step=1,
|
| 148 |
+
label="Number of Images / 生成枚数"
|
| 149 |
+
)
|
| 150 |
+
img_step = gr.Slider(
|
| 151 |
+
minimum=2, maximum=50, value=12, step=1,
|
| 152 |
+
label="Step"
|
| 153 |
+
)
|
| 154 |
+
img_guidance = gr.Slider(
|
| 155 |
+
minimum=0, maximum=20, value=7.5, step=0.5,
|
| 156 |
+
label="Guidance Scale (プロンプトの反映力)"
|
| 157 |
+
)
|
| 158 |
+
img_prompt_strength = gr.Slider(
|
| 159 |
+
minimum=0, maximum=1, value=0.8, step=0.05,
|
| 160 |
+
label="Prompt Strength (画像入力時のみ)"
|
| 161 |
+
)
|
| 162 |
+
img_size = gr.Slider(
|
| 163 |
+
minimum=512, maximum=2048, value=1024, step=64,
|
| 164 |
+
label="Size (px)"
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
img_generate_btn = gr.Button(
|
| 168 |
+
"画像生成開始",
|
| 169 |
+
variant="primary"
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
with gr.Column(scale=3):
|
| 173 |
+
img_gallery = gr.Gallery(
|
| 174 |
+
label="生成された画像",
|
| 175 |
+
columns=2,
|
| 176 |
+
rows=2,
|
| 177 |
+
height="auto",
|
| 178 |
+
show_download_button=True,
|
| 179 |
+
object_fit="contain"
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
# イベントバインディング
|
| 185 |
+
txt_generate_btn.click(
|
| 186 |
+
fn=generate_txt2img,
|
| 187 |
+
inputs=[txt_prompt, txt_num_images],
|
| 188 |
+
outputs=txt_gallery,
|
| 189 |
+
show_progress=True
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
img_generate_btn.click(
|
| 193 |
+
fn=generate_img2img,
|
| 194 |
+
inputs=[img_prompt, img_reference, img_num_images],
|
| 195 |
+
outputs=img_gallery,
|
| 196 |
+
show_progress=True
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
# フッター部分(ロゴのみ表示 / 背景白 / 中心位置 / サイズ半分)
|
| 200 |
+
with gr.Row(elem_classes=["footer-bg"]):
|
| 201 |
+
try:
|
| 202 |
+
with open("imgs/tvasahi.svg", 'rb') as f:
|
| 203 |
+
b64 = base64.b64encode(f.read()).decode()
|
| 204 |
+
gr.HTML(
|
| 205 |
+
'<div style="width:100%;display:flex;justify-content:center;align-items:center;">'
|
| 206 |
+
f'<img src="data:image/svg+xml;base64,{b64}" style="height:30px;aspect-ratio:auto;display:block;" alt="TV Asahi logo" />'
|
| 207 |
+
'</div>'
|
| 208 |
+
)
|
| 209 |
+
except:
|
| 210 |
+
gr.HTML('<span>Missing TV Asahi logo</span>')
|
| 211 |
+
|
| 212 |
+
# 起動設定
|
| 213 |
+
if __name__ == "__main__":
|
| 214 |
+
demo.launch(
|
| 215 |
+
server_name=None,
|
| 216 |
+
server_port=7860,
|
| 217 |
+
share=False,
|
| 218 |
+
show_error=True,
|
| 219 |
+
quiet=False
|
| 220 |
+
)
|
gradio_ui/imgs/J_channel.svg
ADDED
|
|
gradio_ui/imgs/tvasahi.svg
ADDED
|
|
gradio_ui/main.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def main():
|
| 2 |
+
print("Hello from gradio UI!")
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
main()
|
gradio_ui/pyproject.toml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "gradio-ui-asahi"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.11.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"gradio>=5.48.0",
|
| 9 |
+
"numpy>=2.3.3",
|
| 10 |
+
"pillow>=11.3.0",
|
| 11 |
+
]
|
gradio_ui/requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Required libraries (consistent with pyproject.toml)
|
| 2 |
+
gradio>=5.48.0
|
| 3 |
+
numpy>=2.3.3
|
| 4 |
+
pillow>=11.3.0
|
pyproject.toml
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "jagirl-ui"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "SDXL-based anime girl image generation using aipicasso/jagirl model with Gradio UI"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
keywords = ["ai", "image-generation", "stable-diffusion", "sdxl", "anime", "gradio", "huggingface"]
|
| 7 |
+
classifiers = [
|
| 8 |
+
"Development Status :: 3 - Alpha",
|
| 9 |
+
"Intended Audience :: Developers",
|
| 10 |
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
| 11 |
+
"Programming Language :: Python :: 3",
|
| 12 |
+
"Programming Language :: Python :: 3.11",
|
| 13 |
+
]
|
| 14 |
+
requires-python = ">=3.11"
|
| 15 |
+
dependencies = [
|
| 16 |
+
"huggingface-hub>=0.35.3",
|
| 17 |
+
"diffusers>=0.35.2",
|
| 18 |
+
"numpy>=2.3.4",
|
| 19 |
+
"scipy>=1.11.0",
|
| 20 |
+
"gradio==5.49.1",
|
| 21 |
+
"transformers>=4.30.0",
|
| 22 |
+
"accelerate>=0.20.0",
|
| 23 |
+
"pillow>=10.0.0",
|
| 24 |
+
"python-dotenv>=1.0.0",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
[project.optional-dependencies]
|
| 28 |
+
gpu = [
|
| 29 |
+
"xformers>=0.0.20",
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
# ⚠️ 重要: PyTorchは依存関係に含めていません ⚠️
|
| 33 |
+
# 理由: pip install -e . でCPU版に上書きされる問題を防ぐため
|
| 34 |
+
#
|
| 35 |
+
# 【必須】CUDA版PyTorchの手動インストール手順:
|
| 36 |
+
# 1. 仮想環境をアクティベート
|
| 37 |
+
# 2. 以下を個別に実行(一括ではなく1つずつ):
|
| 38 |
+
# pip install torch --index-url https://download.pytorch.org/whl/cu121 --no-cache-dir
|
| 39 |
+
# pip install torchvision --index-url https://download.pytorch.org/whl/cu121
|
| 40 |
+
# pip install torchaudio --index-url https://download.pytorch.org/whl/cu121
|
| 41 |
+
# 3. インストール確認:
|
| 42 |
+
# python -c "import torch; print(torch.__version__); print(torch.cuda.is_available())"
|
| 43 |
+
#
|
| 44 |
+
# 参考: 20251017_全ログ.md - torch一括インストールで20分以上フリーズした実績あり
|
| 45 |
+
|
| 46 |
+
[build-system]
|
| 47 |
+
requires = ["setuptools>=61.0", "wheel"]
|
| 48 |
+
build-backend = "setuptools.build_meta"
|
| 49 |
+
|
| 50 |
+
[tool.setuptools]
|
| 51 |
+
# パッケージ自動検出を無効化(utilsのみを明示的にインストール)
|
| 52 |
+
packages = ["utils"]
|
| 53 |
+
|
| 54 |
+
[tool.setuptools.package-data]
|
| 55 |
+
# データファイルを除外(logs, outputs, gradio_uiはプロジェクトディレクトリとして扱う)
|
| 56 |
+
"*" = []
|
reference/app.py
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Emix Image Generator - Main Application
|
| 3 |
+
|
| 4 |
+
統合機能:(TV Asahi J Channel デザイン)
|
| 5 |
+
- aipicasso/emix-0-5 モデルによる高品質画像生成
|
| 6 |
+
- 詳細パラメータ制御とログ機能
|
| 7 |
+
- Text-to-Image 対応
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import gradio as gr
|
| 11 |
+
import torch
|
| 12 |
+
from diffusers import (
|
| 13 |
+
StableDiffusionXLPipeline,
|
| 14 |
+
DDIMScheduler,
|
| 15 |
+
DPMSolverMultistepScheduler,
|
| 16 |
+
EulerDiscreteScheduler,
|
| 17 |
+
EulerAncestralDiscreteScheduler,
|
| 18 |
+
PNDMScheduler,
|
| 19 |
+
LMSDiscreteScheduler
|
| 20 |
+
)
|
| 21 |
+
from huggingface_hub import login
|
| 22 |
+
import os
|
| 23 |
+
import base64
|
| 24 |
+
from datetime import datetime
|
| 25 |
+
|
| 26 |
+
# 環境変数の読み込み(dotenvがあれば使用)
|
| 27 |
+
try:
|
| 28 |
+
from dotenv import load_dotenv
|
| 29 |
+
load_dotenv()
|
| 30 |
+
except ImportError:
|
| 31 |
+
pass # dotenvがない場合はスキップ
|
| 32 |
+
import random
|
| 33 |
+
import json
|
| 34 |
+
import logging
|
| 35 |
+
from pathlib import Path
|
| 36 |
+
import traceback
|
| 37 |
+
from PIL import Image
|
| 38 |
+
import time
|
| 39 |
+
|
| 40 |
+
# 統合ロガーのインポート
|
| 41 |
+
import sys
|
| 42 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), 'utils'))
|
| 43 |
+
from logger import get_logger, log_generation
|
| 44 |
+
|
| 45 |
+
# 標準ロガー設定
|
| 46 |
+
logging.basicConfig(level=logging.INFO)
|
| 47 |
+
logger = logging.getLogger(__name__)
|
| 48 |
+
|
| 49 |
+
# 定数定義
|
| 50 |
+
HISTORY_FILE = "logs/generation_history.json"
|
| 51 |
+
OUTPUT_DIR = "outputs"
|
| 52 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "aipicasso/emix-0-5") # 環境変数で変更可能
|
| 53 |
+
|
| 54 |
+
# 統合ロガーインスタンス
|
| 55 |
+
unified_logger = get_logger("logs")
|
| 56 |
+
|
| 57 |
+
# グローバル変数でパイプラインを管理
|
| 58 |
+
txt2img_pipe = None
|
| 59 |
+
model_loaded = False
|
| 60 |
+
|
| 61 |
+
def setup_scheduler(pipe, scheduler_type="default"):
|
| 62 |
+
"""
|
| 63 |
+
スケジューラーの設定
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
pipe: StableDiffusionXLPipeline
|
| 67 |
+
scheduler_type: スケジューラータイプ
|
| 68 |
+
- "default": デフォルト
|
| 69 |
+
- "DDIM": 高品質、少ないステップ
|
| 70 |
+
- "DPMSolver": 高速で高品質(推奨)
|
| 71 |
+
- "Euler": 安定した結果
|
| 72 |
+
- "EulerA": より多様な結果
|
| 73 |
+
- "LMS": 古典的手法
|
| 74 |
+
- "PNDM": デフォルト
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
設定されたscheduler
|
| 78 |
+
"""
|
| 79 |
+
schedulers = {
|
| 80 |
+
"DDIM": DDIMScheduler,
|
| 81 |
+
"DPMSolver": DPMSolverMultistepScheduler,
|
| 82 |
+
"Euler": EulerDiscreteScheduler,
|
| 83 |
+
"EulerA": EulerAncestralDiscreteScheduler,
|
| 84 |
+
"PNDM": PNDMScheduler
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
# LMSはscipyが必要なため、利用可能な場合のみ追加
|
| 88 |
+
try:
|
| 89 |
+
schedulers["LMS"] = LMSDiscreteScheduler
|
| 90 |
+
except:
|
| 91 |
+
logger.warning("⚠️ LMSスケジューラーは利用できません (scipyが必要)")
|
| 92 |
+
|
| 93 |
+
if scheduler_type != "default" and scheduler_type in schedulers:
|
| 94 |
+
try:
|
| 95 |
+
return schedulers[scheduler_type].from_config(pipe.scheduler.config)
|
| 96 |
+
except ImportError as e:
|
| 97 |
+
logger.warning(f"⚠️ {scheduler_type}スケジューラーが利用できません: {e}")
|
| 98 |
+
return pipe.scheduler
|
| 99 |
+
return pipe.scheduler
|
| 100 |
+
|
| 101 |
+
def setup_model():
|
| 102 |
+
"""モデルのセットアップと最適化"""
|
| 103 |
+
global txt2img_pipe, model_loaded
|
| 104 |
+
|
| 105 |
+
if model_loaded:
|
| 106 |
+
return True
|
| 107 |
+
|
| 108 |
+
try:
|
| 109 |
+
logger.info("🔧 モデルをセットアップ中...")
|
| 110 |
+
|
| 111 |
+
# GPU確認
|
| 112 |
+
if not torch.cuda.is_available():
|
| 113 |
+
logger.error("❌ CUDA が利用できません。GPUを確認してください。")
|
| 114 |
+
return False
|
| 115 |
+
|
| 116 |
+
device = "cuda"
|
| 117 |
+
logger.info(f"✅ デバイス: {device}")
|
| 118 |
+
|
| 119 |
+
# Text-to-Image パイプライン
|
| 120 |
+
logger.info(f"📦 Text-to-Image パイプライン読み込み中: {MODEL_NAME}")
|
| 121 |
+
txt2img_pipe = StableDiffusionXLPipeline.from_pretrained(
|
| 122 |
+
MODEL_NAME,
|
| 123 |
+
torch_dtype=torch.float16,
|
| 124 |
+
use_safetensors=True
|
| 125 |
+
).to(device)
|
| 126 |
+
|
| 127 |
+
# GPU移動後にFP16に変換
|
| 128 |
+
try:
|
| 129 |
+
txt2img_pipe = txt2img_pipe.to(dtype=torch.float16)
|
| 130 |
+
logger.info("✅ FP16モードに変換")
|
| 131 |
+
except:
|
| 132 |
+
logger.warning("⚠️ FP16変換をスキップ、FP32で継続")
|
| 133 |
+
|
| 134 |
+
# メモリ効率化 (xformersは使用しない - CPU版PyTorchのため)
|
| 135 |
+
try:
|
| 136 |
+
txt2img_pipe.enable_xformers_memory_efficient_attention()
|
| 137 |
+
logger.info("✅ xFormers メモリ効率化を有効化")
|
| 138 |
+
except Exception as e:
|
| 139 |
+
logger.warning(f"⚠️ xFormers無効 (CPU版PyTorch使用中): {e}")
|
| 140 |
+
|
| 141 |
+
# CPU Offloadは無効化(全てGPUで処理)
|
| 142 |
+
logger.info("🎯 GPU専用モードで動作")
|
| 143 |
+
|
| 144 |
+
logger.info("✅ モデルセットアップ完了")
|
| 145 |
+
model_loaded = True
|
| 146 |
+
return True
|
| 147 |
+
|
| 148 |
+
except Exception as e:
|
| 149 |
+
logger.error(f"❌ モデルセットアップ失敗: {e}")
|
| 150 |
+
return False
|
| 151 |
+
|
| 152 |
+
def log_generation_details(prompt, negative_prompt, params, output_filepath, execution_time):
|
| 153 |
+
"""
|
| 154 |
+
生成詳細のログ記録(統合ロガー使用)
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
prompt: メインプロンプト
|
| 158 |
+
negative_prompt: ネガティブプロンプト
|
| 159 |
+
params: 生成パラメータ辞書
|
| 160 |
+
output_filepath: 生成画像のファイルパス
|
| 161 |
+
execution_time: 実行時間(秒)
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
generation_id: 生成記録のユニークID
|
| 165 |
+
"""
|
| 166 |
+
try:
|
| 167 |
+
generation_id = unified_logger.log_generation(
|
| 168 |
+
prompt=prompt,
|
| 169 |
+
negative_prompt=negative_prompt,
|
| 170 |
+
parameters=params,
|
| 171 |
+
output_filepath=output_filepath,
|
| 172 |
+
execution_time=execution_time
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
logger.info(f"📝 生成ログを記録: {generation_id}")
|
| 176 |
+
return generation_id
|
| 177 |
+
|
| 178 |
+
except Exception as e:
|
| 179 |
+
logger.error(f"❌ ログ記録失敗: {e}")
|
| 180 |
+
traceback.print_exc()
|
| 181 |
+
return None
|
| 182 |
+
|
| 183 |
+
def load_generation_history():
|
| 184 |
+
"""生成履歴を読み込む(統合ロガー形式)"""
|
| 185 |
+
try:
|
| 186 |
+
if os.path.exists(HISTORY_FILE):
|
| 187 |
+
with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
|
| 188 |
+
data = json.load(f)
|
| 189 |
+
# 統合ロガーの形式: {"generations": [...]}
|
| 190 |
+
if isinstance(data, dict) and 'generations' in data:
|
| 191 |
+
generations = data['generations']
|
| 192 |
+
# 最新10件を返す
|
| 193 |
+
return generations[-10:] if len(generations) > 10 else generations
|
| 194 |
+
# 古い形式(リスト)の場合
|
| 195 |
+
elif isinstance(data, list):
|
| 196 |
+
return data[-10:]
|
| 197 |
+
else:
|
| 198 |
+
return []
|
| 199 |
+
return []
|
| 200 |
+
except Exception as e:
|
| 201 |
+
logger.error(f"履歴読み込み失敗: {e}")
|
| 202 |
+
return []
|
| 203 |
+
|
| 204 |
+
def format_history_display():
|
| 205 |
+
"""履歴表示用のフォーマット(統合ロガー形式対応)"""
|
| 206 |
+
history = load_generation_history()
|
| 207 |
+
if not history:
|
| 208 |
+
return "📝 生成履歴がありません"
|
| 209 |
+
|
| 210 |
+
display_text = "## 📋 Recent Generation History (最新10件)\n\n"
|
| 211 |
+
|
| 212 |
+
for i, entry in enumerate(reversed(history), 1):
|
| 213 |
+
# 統合ロガー形式のフィールド
|
| 214 |
+
gen_id = entry.get('generation_id', 'Unknown')
|
| 215 |
+
timestamp = entry.get('timestamp', 'Unknown')
|
| 216 |
+
prompt = entry.get('prompt', 'No prompt')
|
| 217 |
+
# プロンプトが長い場合は省略
|
| 218 |
+
prompt_display = prompt[:50] + "..." if len(prompt) > 50 else prompt
|
| 219 |
+
|
| 220 |
+
# パラメータから情報取得
|
| 221 |
+
params = entry.get('parameters', {})
|
| 222 |
+
seed = params.get('seed', 'N/A')
|
| 223 |
+
steps = params.get('num_inference_steps', 'N/A')
|
| 224 |
+
|
| 225 |
+
# 結果情報
|
| 226 |
+
result = entry.get('result', {})
|
| 227 |
+
success = result.get('success', False)
|
| 228 |
+
exec_time = result.get('execution_time_seconds', 0)
|
| 229 |
+
|
| 230 |
+
status = "✅ Success" if success else "❌ Failed"
|
| 231 |
+
|
| 232 |
+
display_text += f"### {i}. {status}\n"
|
| 233 |
+
display_text += f"**ID:** {gen_id}\n"
|
| 234 |
+
display_text += f"**Time:** {timestamp}\n"
|
| 235 |
+
display_text += f"**Prompt:** {prompt_display}\n"
|
| 236 |
+
display_text += f"**Seed:** {seed} | **Steps:** {steps}\n"
|
| 237 |
+
display_text += f"**Execution:** {exec_time:.1f}s\n"
|
| 238 |
+
display_text += "---\n"
|
| 239 |
+
|
| 240 |
+
return display_text
|
| 241 |
+
|
| 242 |
+
def refresh_history():
|
| 243 |
+
"""履歴更新関数"""
|
| 244 |
+
return format_history_display()
|
| 245 |
+
|
| 246 |
+
def generate_txt2img(prompt, negative_prompt="", num_images=1, steps=25, guidance=7.5, size=1024, seed=None, scheduler="default"):
|
| 247 |
+
"""
|
| 248 |
+
テキストから画像生成(完全なパラメータ対応)
|
| 249 |
+
|
| 250 |
+
Args:
|
| 251 |
+
prompt: メインプロンプト
|
| 252 |
+
negative_prompt: ネガティブプロンプト
|
| 253 |
+
num_images: 生成画像数
|
| 254 |
+
steps: サンプリングステップ数 (10-150)
|
| 255 |
+
guidance: CFG Scale/ガイダンス強度 (1-20)
|
| 256 |
+
size: 画像サイズ (512, 768, 1024)
|
| 257 |
+
seed: シード値 (Noneでランダム)
|
| 258 |
+
scheduler: スケジューラータイプ
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
生成された画像のリスト
|
| 262 |
+
"""
|
| 263 |
+
global txt2img_pipe
|
| 264 |
+
|
| 265 |
+
if not prompt.strip():
|
| 266 |
+
return []
|
| 267 |
+
|
| 268 |
+
if not model_loaded:
|
| 269 |
+
if not setup_model():
|
| 270 |
+
return []
|
| 271 |
+
|
| 272 |
+
try:
|
| 273 |
+
logger.info(f"🎨 画像生成開始: {prompt[:50]}...")
|
| 274 |
+
|
| 275 |
+
start_time = time.time()
|
| 276 |
+
|
| 277 |
+
# シード設定(0またはNoneの場合はランダム)
|
| 278 |
+
if seed is None or seed == 0:
|
| 279 |
+
seed = random.randint(1, 2**32-1)
|
| 280 |
+
|
| 281 |
+
generator = torch.Generator(device="cuda").manual_seed(seed)
|
| 282 |
+
|
| 283 |
+
# スケジューラー設定
|
| 284 |
+
original_scheduler = txt2img_pipe.scheduler
|
| 285 |
+
if scheduler != "default":
|
| 286 |
+
txt2img_pipe.scheduler = setup_scheduler(txt2img_pipe, scheduler)
|
| 287 |
+
|
| 288 |
+
# パラメータ設定
|
| 289 |
+
params = {
|
| 290 |
+
"prompt": prompt,
|
| 291 |
+
"negative_prompt": negative_prompt,
|
| 292 |
+
"num_inference_steps": int(steps),
|
| 293 |
+
"guidance_scale": float(guidance),
|
| 294 |
+
"width": int(size),
|
| 295 |
+
"height": int(size),
|
| 296 |
+
"num_images_per_prompt": 1,
|
| 297 |
+
"generator": generator
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
# 画像生成(autocastを使用しない - test_high_quality_generation.pyと同じ)
|
| 301 |
+
result = txt2img_pipe(**params)
|
| 302 |
+
|
| 303 |
+
# スケジューラーを元に戻す
|
| 304 |
+
if scheduler != "default":
|
| 305 |
+
txt2img_pipe.scheduler = original_scheduler
|
| 306 |
+
|
| 307 |
+
execution_time = time.time() - start_time
|
| 308 |
+
|
| 309 |
+
# 画像保存
|
| 310 |
+
outputs_dir = Path("outputs")
|
| 311 |
+
outputs_dir.mkdir(exist_ok=True)
|
| 312 |
+
|
| 313 |
+
saved_paths = []
|
| 314 |
+
for i, image in enumerate(result.images):
|
| 315 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 316 |
+
filename = f"txt2img_{timestamp}_seed{seed}_{i+1}.png"
|
| 317 |
+
filepath = outputs_dir / filename
|
| 318 |
+
image.save(filepath, quality=95)
|
| 319 |
+
saved_paths.append(str(filepath))
|
| 320 |
+
logger.info(f"💾 画像保存: {filepath}")
|
| 321 |
+
|
| 322 |
+
# ログ記録(統合ロガー使用)
|
| 323 |
+
log_params = {
|
| 324 |
+
"num_inference_steps": int(steps),
|
| 325 |
+
"guidance_scale": float(guidance),
|
| 326 |
+
"width": int(size),
|
| 327 |
+
"height": int(size),
|
| 328 |
+
"seed": seed,
|
| 329 |
+
"scheduler_type": scheduler,
|
| 330 |
+
"num_images": num_images,
|
| 331 |
+
"torch_dtype": "float16",
|
| 332 |
+
"mode": "txt2img"
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
log_generation_details(
|
| 336 |
+
prompt=prompt,
|
| 337 |
+
negative_prompt=negative_prompt,
|
| 338 |
+
params=log_params,
|
| 339 |
+
output_filepath=saved_paths[0] if saved_paths else "",
|
| 340 |
+
execution_time=execution_time
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
logger.info(f"✅ 生成完了: {execution_time:.2f}秒, {len(result.images)}枚")
|
| 344 |
+
|
| 345 |
+
return result.images
|
| 346 |
+
|
| 347 |
+
except Exception as e:
|
| 348 |
+
logger.error(f"❌ 画像生成失敗: {e}")
|
| 349 |
+
logger.error(traceback.format_exc())
|
| 350 |
+
return []
|
| 351 |
+
|
| 352 |
+
def create_gradio_app():
|
| 353 |
+
"""Gradio アプリケーションの作成"""
|
| 354 |
+
|
| 355 |
+
# カスタムカラーオブジェクトを作成(TV Asahi Blue)
|
| 356 |
+
# custom_blue = gr.themes.Color(
|
| 357 |
+
# c50="#f0f4ff",
|
| 358 |
+
# c100="#dbeafe",
|
| 359 |
+
# c200="#bfdbfe",
|
| 360 |
+
# c300="#93c5fd",
|
| 361 |
+
# c400="#60a5fa",
|
| 362 |
+
# c500="#284baf", # メインの色
|
| 363 |
+
# c600="#1e40af",
|
| 364 |
+
# c700="#1d4ed8",
|
| 365 |
+
# c800="#1e3a8a",
|
| 366 |
+
# c900="#1e3a8a",
|
| 367 |
+
# c950="#172554"
|
| 368 |
+
# )
|
| 369 |
+
|
| 370 |
+
custom_css = """
|
| 371 |
+
body,
|
| 372 |
+
.gradio-container {
|
| 373 |
+
--range-color: #f97316;
|
| 374 |
+
background-color: #f6f8ff;
|
| 375 |
+
background-image:
|
| 376 |
+
url("data:image/svg+xml,%3Csvg%20width%3D%22160%22%20height%3D%22160%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22160%22%20height%3D%22160%22%20fill%3D%22transparent%22%2F%3E%3Cline%20x1%3D%2278%22%20y1%3D%2280%22%20x2%3D%2282%22%20y2%3D%2280%22%20stroke%3D%22rgba%2842%2C42%2C42%2C0.5%29%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%2F%3E%3Cline%20x1%3D%2280%22%20y1%3D%2278%22%20x2%3D%2280%22%20y2%3D%2282%22%20stroke%3D%22rgba%2842%2C42%2C42%2C0.5%29%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fsvg%3E"),
|
| 377 |
+
linear-gradient(90deg, rgba(42, 42, 42, 0.1) 0px, rgba(42, 42, 42, 0.1) 1px, transparent 1px, transparent 40px),
|
| 378 |
+
linear-gradient(0deg, rgba(42, 42, 42, 0.1) 0px, rgba(42, 42, 42, 0.1) 1px, transparent 1px, transparent 40px),
|
| 379 |
+
radial-gradient(circle at 10% 10%, rgba(11, 213, 126, 0.2) 0%, rgba(11, 213, 126, 0) 20%),
|
| 380 |
+
linear-gradient(135deg,
|
| 381 |
+
rgba(240, 244, 255, 0.4) 0%,
|
| 382 |
+
rgba(230, 240, 255, 0.2) 25%,
|
| 383 |
+
rgba(220, 235, 255, 0.1) 50%,
|
| 384 |
+
rgba(200, 220, 255, 0.15) 75%,
|
| 385 |
+
rgba(220, 200, 255, 0.3) 100%
|
| 386 |
+
);
|
| 387 |
+
background-size:
|
| 388 |
+
160px 160px,
|
| 389 |
+
40px 40px,
|
| 390 |
+
40px 40px,
|
| 391 |
+
100% 100%,
|
| 392 |
+
100% 100%;
|
| 393 |
+
background-position: 0 0, 0 0, 0 0, 0 0, 0 0;
|
| 394 |
+
background-repeat: repeat, repeat, repeat, no-repeat, no-repeat;
|
| 395 |
+
background-attachment: fixed;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
/* 主要パネルの透過感を維持 */
|
| 399 |
+
.gradio-container .gradio-block {
|
| 400 |
+
backdrop-filter: blur(4px);
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.logo-banner {
|
| 404 |
+
position: fixed;
|
| 405 |
+
top: 16px;
|
| 406 |
+
left: 16px;
|
| 407 |
+
z-index: 5;
|
| 408 |
+
margin: 0;
|
| 409 |
+
padding: 0;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.logo-banner svg,
|
| 413 |
+
.logo-banner img {
|
| 414 |
+
display: block;
|
| 415 |
+
width: auto;
|
| 416 |
+
height: auto;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.gradio-container input[type="range"] {
|
| 420 |
+
accent-color: #f97316;
|
| 421 |
+
}
|
| 422 |
+
.gradio-container input[type="range"]::-webkit-slider-thumb {
|
| 423 |
+
background-color: #f97316;
|
| 424 |
+
}
|
| 425 |
+
.gradio-container input[type="range"]::-moz-range-thumb {
|
| 426 |
+
background-color: #f97316;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.card-panel {
|
| 430 |
+
border-radius: 20px;
|
| 431 |
+
background: rgba(255, 255, 255, 0.82);
|
| 432 |
+
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.12);
|
| 433 |
+
padding: 24px;
|
| 434 |
+
border: 1px solid rgba(255, 255, 255, 0.65);
|
| 435 |
+
backdrop-filter: blur(10px);
|
| 436 |
+
overflow: hidden;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.card-panel > * {
|
| 440 |
+
width: 100%;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.card-panel details {
|
| 444 |
+
background: transparent;
|
| 445 |
+
border: none;
|
| 446 |
+
box-shadow: none;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.card-panel details > summary {
|
| 450 |
+
font-weight: 600;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.card-panel .gradio-image {
|
| 454 |
+
background: transparent;
|
| 455 |
+
border: none;
|
| 456 |
+
box-shadow: none;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.card-panel .gradio-image img {
|
| 460 |
+
border-radius: 16px;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.sample-thumb-row {
|
| 464 |
+
display: flex;
|
| 465 |
+
gap: 16px;
|
| 466 |
+
width: 100%;
|
| 467 |
+
flex-wrap: wrap;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.sample-thumb {
|
| 471 |
+
border-radius: 16px;
|
| 472 |
+
overflow: hidden;
|
| 473 |
+
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.12);
|
| 474 |
+
border: 1px solid rgba(148, 163, 184, 0.55);
|
| 475 |
+
background: rgba(255, 255, 255, 0.92);
|
| 476 |
+
padding: 0 !important;
|
| 477 |
+
position: relative;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.sample-thumb img {
|
| 481 |
+
width: 100%;
|
| 482 |
+
height: 100%;
|
| 483 |
+
object-fit: cover;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
.sample-thumb button[aria-label="Fullscreen"] {
|
| 487 |
+
position: absolute;
|
| 488 |
+
top: 12px;
|
| 489 |
+
right: 16px;
|
| 490 |
+
z-index: 5;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
.generate-btn button {
|
| 494 |
+
display: inline-flex;
|
| 495 |
+
align-items: center;
|
| 496 |
+
justify-content: center;
|
| 497 |
+
gap: 8px;
|
| 498 |
+
min-height: 62px; /* 約1.2倍の縦幅 */
|
| 499 |
+
padding: 18px 36px;
|
| 500 |
+
border-radius: 12px;
|
| 501 |
+
background: linear-gradient(135deg, #fb923c 0%, #f97316 45%, #ea580c 100%);
|
| 502 |
+
color: #ffffff;
|
| 503 |
+
font-size: 1.05rem;
|
| 504 |
+
font-weight: 600;
|
| 505 |
+
letter-spacing: 0.02em;
|
| 506 |
+
border: 1px solid rgba(249, 115, 22, 0.5);
|
| 507 |
+
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.12);
|
| 508 |
+
transition: transform 0.25s ease, box-shadow 0.25s ease, letter-spacing 0.25s ease, background 0.25s ease;
|
| 509 |
+
transform: scale(1);
|
| 510 |
+
cursor: pointer;
|
| 511 |
+
will-change: transform;
|
| 512 |
+
background-size: 120% 120%;
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
.generate-btn button:hover,
|
| 516 |
+
.generate-btn:hover button {
|
| 517 |
+
transform: scale(1.08) !important;
|
| 518 |
+
letter-spacing: 0.08em;
|
| 519 |
+
box-shadow: 0 24px 50px rgba(15, 23, 42, 0.16);
|
| 520 |
+
background-position: 100% 0;
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
.generate-btn button:active,
|
| 524 |
+
.generate-btn:active button {
|
| 525 |
+
transform: scale(0.96) !important;
|
| 526 |
+
letter-spacing: 0.03em;
|
| 527 |
+
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.14);
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
.generate-btn button:focus-visible {
|
| 531 |
+
outline: 2px solid rgba(249, 115, 22, 0.65);
|
| 532 |
+
outline-offset: 3px;
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
.dark .generate-btn button {
|
| 536 |
+
color: #1b1b1f;
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
.contain-fullscreen button[aria-label*="Close"],
|
| 540 |
+
.contain-fullscreen button[aria-label*="close"],
|
| 541 |
+
.contain-fullscreen button[aria-label*="Exit"],
|
| 542 |
+
.contain-fullscreen button[aria-label*="exit"],
|
| 543 |
+
.contain-fullscreen button[aria-label*="閉じる"] {
|
| 544 |
+
margin-right: 18px;
|
| 545 |
+
margin-top: 10px;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.model-title {
|
| 549 |
+
display: inline-flex;
|
| 550 |
+
align-items: center;
|
| 551 |
+
gap: 12px;
|
| 552 |
+
margin: 32px 0 16px;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.model-icon {
|
| 556 |
+
width: 88px;
|
| 557 |
+
height: 88px;
|
| 558 |
+
border-radius: 12px;
|
| 559 |
+
object-fit: cover;
|
| 560 |
+
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.12);
|
| 561 |
+
background: rgba(255, 255, 255, 0.85);
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.model-title-text {
|
| 565 |
+
display: flex;
|
| 566 |
+
flex-direction: column;
|
| 567 |
+
align-items: flex-start;
|
| 568 |
+
gap: 6px;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.model-name {
|
| 572 |
+
font-size: 2.4rem;
|
| 573 |
+
font-weight: 600;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
.model-link {
|
| 577 |
+
display: inline-flex;
|
| 578 |
+
align-items: center;
|
| 579 |
+
gap: 4px;
|
| 580 |
+
color: #1f2937;
|
| 581 |
+
font-weight: 500;
|
| 582 |
+
text-decoration: none;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.model-link .link-icon {
|
| 586 |
+
font-size: 1.2rem;
|
| 587 |
+
opacity: 0.75;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
.model-link:hover {
|
| 591 |
+
text-decoration: underline;
|
| 592 |
+
}
|
| 593 |
+
"""
|
| 594 |
+
|
| 595 |
+
# ロゴSVGの読み込み
|
| 596 |
+
logo_svg_html = ""
|
| 597 |
+
logo_svg_path = Path(__file__).parent / "assets"/ "images" / "logo" / "logo_ai_picasso.svg"
|
| 598 |
+
try:
|
| 599 |
+
logo_svg_html = logo_svg_path.read_text(encoding="utf-8")
|
| 600 |
+
except FileNotFoundError:
|
| 601 |
+
logger.warning("⚠️ ロゴSVGが見つかりません: %s", logo_svg_path)
|
| 602 |
+
except Exception as exc:
|
| 603 |
+
logger.warning("⚠️ ロゴSVG読み込みに失敗しました: %s", exc)
|
| 604 |
+
|
| 605 |
+
sample_image_names = ["ComfyUI_04014_.png", "ComfyUI_04069_.png"]
|
| 606 |
+
sample_images = []
|
| 607 |
+
sample_dir = Path(__file__).parent / "assets" / "images" / "samples"
|
| 608 |
+
for name in sample_image_names:
|
| 609 |
+
sample_path = sample_dir / name
|
| 610 |
+
if sample_path.exists():
|
| 611 |
+
try:
|
| 612 |
+
with Image.open(sample_path) as img:
|
| 613 |
+
width, height = img.size
|
| 614 |
+
except Exception as exc:
|
| 615 |
+
logger.warning("⚠️ サンプル画像の読み込みに失敗しました: %s (%s)", sample_path, exc)
|
| 616 |
+
width, height = (240, 240)
|
| 617 |
+
target_height = 240
|
| 618 |
+
scaled_width = max(1, int(round((target_height / height) * width))) if height else 240
|
| 619 |
+
sample_images.append({
|
| 620 |
+
"path": str(sample_path),
|
| 621 |
+
"width": scaled_width,
|
| 622 |
+
"height": target_height
|
| 623 |
+
})
|
| 624 |
+
else:
|
| 625 |
+
logger.warning("⚠️ サンプル画像が見つかりません: %s", sample_path)
|
| 626 |
+
|
| 627 |
+
# メインUI構築
|
| 628 |
+
with gr.Blocks(
|
| 629 |
+
title="Emix",
|
| 630 |
+
theme=gr.themes.Default(),
|
| 631 |
+
css=custom_css
|
| 632 |
+
) as demo:
|
| 633 |
+
|
| 634 |
+
if logo_svg_html:
|
| 635 |
+
gr.HTML(f"<div class='logo-banner'>{logo_svg_html}</div>")
|
| 636 |
+
|
| 637 |
+
icon_path = Path(__file__).parent / "assets" / "images" / "icon" / "ai_picasso_icon.png"
|
| 638 |
+
icon_html = ""
|
| 639 |
+
try:
|
| 640 |
+
icon_bytes = icon_path.read_bytes()
|
| 641 |
+
icon_b64 = base64.b64encode(icon_bytes).decode("ascii")
|
| 642 |
+
icon_html = f"<img src='data:image/png;base64,{icon_b64}' alt='Emix Icon' class='model-icon' />"
|
| 643 |
+
except FileNotFoundError:
|
| 644 |
+
logger.warning("⚠️ モデルアイコンが見つかりません: %s", icon_path)
|
| 645 |
+
except Exception as exc:
|
| 646 |
+
logger.warning("⚠️ モデルアイコン読み込みに失敗しました: %s", exc)
|
| 647 |
+
|
| 648 |
+
title_text_html = (
|
| 649 |
+
"<div class='model-title-text'>"
|
| 650 |
+
"<span class='model-name'>Emix-0-5</span>"
|
| 651 |
+
"<a class='model-link' href='https://aipicasso.co.jp/' target='_blank' rel='noopener noreferrer'>"
|
| 652 |
+
"<span class='link-icon'>💼</span><span>https://aipicasso.co.jp/</span>"
|
| 653 |
+
"</a>"
|
| 654 |
+
"</div>"
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
gr.HTML(
|
| 658 |
+
f"<div class='model-title'>{icon_html}{title_text_html}</div>"
|
| 659 |
+
)
|
| 660 |
+
|
| 661 |
+
with gr.Row():
|
| 662 |
+
with gr.Column(scale=2):
|
| 663 |
+
with gr.Group(elem_classes=["card-panel"]):
|
| 664 |
+
txt_prompt = gr.Textbox(
|
| 665 |
+
label="Prompt / プロンプト",
|
| 666 |
+
placeholder="Enter your prompt | 高品質なアニメ風の美しい女性の画像を生成するプロンプトを入力 | Example : anime girl, white hair, golden eyes, Santa outfit, Santa hat, blush, surprised expression, hands on cheeks, detailed face, clean white background, festive, Christmas theme",
|
| 667 |
+
lines=3,
|
| 668 |
+
max_lines=5
|
| 669 |
+
)
|
| 670 |
+
|
| 671 |
+
txt_negative_prompt = gr.Textbox(
|
| 672 |
+
label="Negative Prompt / ネガティブプロンプト",
|
| 673 |
+
# イラスト/キャラクター生成向けに調整したネガティブプロンプト
|
| 674 |
+
value=(
|
| 675 |
+
"lowres, bad anatomy, bad hands, missing fingers, extra fingers, mutated hands,"
|
| 676 |
+
" poorly drawn face, poorly drawn hands, deformed, mutated, watermark, signature,"
|
| 677 |
+
" text, logo, duplicate, cropped, jpeg artifacts, blurry, out of focus, oversaturated,"
|
| 678 |
+
" unnatural colors, sticker, mosaic, artifacts, ugly, nsfw, low quality"
|
| 679 |
+
),
|
| 680 |
+
lines=3,
|
| 681 |
+
max_lines=5
|
| 682 |
+
)
|
| 683 |
+
|
| 684 |
+
with gr.Group(elem_classes=["card-panel"]):
|
| 685 |
+
with gr.Accordion("Advanced Settings / 詳細設定", open=True):
|
| 686 |
+
txt_step = gr.Slider(
|
| 687 |
+
minimum=10, maximum=150, value=25, step=5,
|
| 688 |
+
label="Sampling Steps / サンプリングステップ数 (推奨: 20-40)"
|
| 689 |
+
)
|
| 690 |
+
txt_guidance = gr.Slider(
|
| 691 |
+
minimum=3.0, maximum=15.0, value=7.5, step=0.5,
|
| 692 |
+
label="CFG Scale / ガイダンス強度 (推奨: 7-10)"
|
| 693 |
+
)
|
| 694 |
+
# 画像サイズは1024x1024固定 (UIには表示しない)
|
| 695 |
+
# サポート解像度例: 512x512, 768x768, 1024x1024, 1280x1280, 1536x1536
|
| 696 |
+
txt_size = 1024 # 固定値
|
| 697 |
+
txt_seed = gr.Number(
|
| 698 |
+
label="Seed (空欄でランダム)",
|
| 699 |
+
value=-1,
|
| 700 |
+
precision=0
|
| 701 |
+
)
|
| 702 |
+
txt_scheduler = gr.Dropdown(
|
| 703 |
+
choices=["default", "DDIM", "DPMSolver", "Euler", "EulerA", "LMS", "PNDM"],
|
| 704 |
+
value="default",
|
| 705 |
+
label="Scheduler / スケジューラー (推奨: DPMSolver)"
|
| 706 |
+
)
|
| 707 |
+
|
| 708 |
+
txt_generate_btn = gr.Button(
|
| 709 |
+
"🎨 画像生成開始",
|
| 710 |
+
variant="primary",
|
| 711 |
+
size="lg",
|
| 712 |
+
elem_classes=["generate-btn"]
|
| 713 |
+
)
|
| 714 |
+
|
| 715 |
+
with gr.Column(scale=3):
|
| 716 |
+
with gr.Group(elem_classes=["card-panel"]):
|
| 717 |
+
txt_gallery = gr.Image(
|
| 718 |
+
label="Generated Image / 生成された画像",
|
| 719 |
+
type="pil",
|
| 720 |
+
interactive=False,
|
| 721 |
+
show_label=True,
|
| 722 |
+
show_download_button=True,
|
| 723 |
+
container=True,
|
| 724 |
+
height=None,
|
| 725 |
+
width=None
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
+
if sample_images:
|
| 729 |
+
gr.Markdown("## Samples")
|
| 730 |
+
with gr.Row(elem_classes=["sample-thumb-row"]):
|
| 731 |
+
for info in sample_images:
|
| 732 |
+
gr.Image(
|
| 733 |
+
value=info["path"],
|
| 734 |
+
interactive=False,
|
| 735 |
+
type="filepath",
|
| 736 |
+
show_label=False,
|
| 737 |
+
show_download_button=False,
|
| 738 |
+
show_fullscreen_button=True,
|
| 739 |
+
elem_classes=["sample-thumb"],
|
| 740 |
+
height=info["height"],
|
| 741 |
+
width=info["width"]
|
| 742 |
+
)
|
| 743 |
+
|
| 744 |
+
# 画像生成用のラッパー関数(2重呼び出し防止)
|
| 745 |
+
def generate_single_image(prompt, neg_prompt, step, guidance, seed, scheduler):
|
| 746 |
+
result = generate_txt2img(prompt, neg_prompt, 1, step, guidance, txt_size, seed, scheduler)
|
| 747 |
+
return result[0] if result else None
|
| 748 |
+
|
| 749 |
+
# イベントバインディング
|
| 750 |
+
txt_generate_btn.click(
|
| 751 |
+
fn=generate_single_image,
|
| 752 |
+
inputs=[txt_prompt, txt_negative_prompt, txt_step, txt_guidance, txt_seed, txt_scheduler],
|
| 753 |
+
outputs=txt_gallery,
|
| 754 |
+
show_progress=True
|
| 755 |
+
)
|
| 756 |
+
|
| 757 |
+
return demo
|
| 758 |
+
|
| 759 |
+
def main():
|
| 760 |
+
"""メインアプリケーション"""
|
| 761 |
+
logger.info("🚀 Emix Image Generator 起動中...")
|
| 762 |
+
|
| 763 |
+
# 必要なディレクトリを作成
|
| 764 |
+
Path("outputs").mkdir(exist_ok=True)
|
| 765 |
+
Path("logs").mkdir(exist_ok=True)
|
| 766 |
+
|
| 767 |
+
# Gradio アプリケーション作成
|
| 768 |
+
demo = create_gradio_app()
|
| 769 |
+
|
| 770 |
+
# アプリケーション起動
|
| 771 |
+
logger.info("🌐 Webアプリケーションを起動...")
|
| 772 |
+
demo.launch(
|
| 773 |
+
server_name="127.0.0.1",
|
| 774 |
+
server_port=7860,
|
| 775 |
+
share=False,
|
| 776 |
+
show_error=True,
|
| 777 |
+
quiet=False,
|
| 778 |
+
inbrowser=True # ブラウザを自動で開く
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
if __name__ == "__main__":
|
| 782 |
+
main()
|
reference/entry.ccdb2b3a.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.app[data-v-d12de11f]{align-items:center;flex-direction:column;height:100%;justify-content:center;width:100%}.title[data-v-d12de11f]{font-size:34px;font-weight:300;letter-spacing:2.45px;line-height:30px;margin:30px}.spinner[data-v-36413753]{animation:loading-spin-36413753 1s linear infinite;height:16px;pointer-events:none;width:16px}.spinner[data-v-36413753]:before{border-bottom:2px solid transparent;border-right:2px solid transparent;border-color:transparent currentcolor currentcolor transparent;border-style:solid;border-width:2px;opacity:.2}.spinner[data-v-36413753]:after,.spinner[data-v-36413753]:before{border-radius:50%;box-sizing:border-box;content:"";height:100%;position:absolute;width:100%}.spinner[data-v-36413753]:after{border-left:2px solid transparent;border-top:2px solid transparent;border-color:currentcolor transparent transparent currentcolor;border-style:solid;border-width:2px;opacity:1}@keyframes loading-spin-36413753{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.design-canvas__modal{height:100%;pointer-events:none;position:fixed;transition:none;width:100%;z-index:2}.design-canvas__modal:focus{outline:none}.design-canvas__modal.v-enter-active .studio-canvas,.design-canvas__modal.v-leave-active,.design-canvas__modal.v-leave-active .studio-canvas{transition:.4s cubic-bezier(.4,.4,0,1)}.design-canvas__modal.v-enter-active .studio-canvas *,.design-canvas__modal.v-leave-active .studio-canvas *{transition:none!important}.design-canvas__modal.isNone{transition:none}.design-canvas__modal .design-canvas__modal__base{height:100%;left:0;pointer-events:auto;position:fixed;top:0;transition:.4s cubic-bezier(.4,.4,0,1);width:100%;z-index:-1}.design-canvas__modal .studio-canvas{height:100%;pointer-events:none}.design-canvas__modal .studio-canvas>*{background:none!important;pointer-events:none}.LoadMoreAnnouncer[data-v-4f7a7294],.TitleAnnouncer[data-v-692a2727]{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border-width:0;white-space:nowrap}.publish-studio-style[data-v-5a0c3720],.product-font-style[data-v-51f515bd]{transition:.4s cubic-bezier(.4,.4,0,1)}@font-face{font-family:grandam;font-style:normal;font-weight:400;src:url(https://storage.googleapis.com/studio-front/fonts/grandam.ttf) format("truetype")}@font-face{font-family:Material Icons;font-style:normal;font-weight:400;src:url(https://storage.googleapis.com/production-os-assets/assets/material-icons/1629704621943/MaterialIcons-Regular.eot);src:local("Material Icons"),local("MaterialIcons-Regular"),url(https://storage.googleapis.com/production-os-assets/assets/material-icons/1629704621943/MaterialIcons-Regular.woff2) format("woff2"),url(https://storage.googleapis.com/production-os-assets/assets/material-icons/1629704621943/MaterialIcons-Regular.woff) format("woff"),url(https://storage.googleapis.com/production-os-assets/assets/material-icons/1629704621943/MaterialIcons-Regular.ttf) format("truetype")}.StudioCanvas{display:flex;height:auto;min-height:100dvh}.StudioCanvas>.sd{min-height:100dvh;overflow:clip}a,abbr,address,article,aside,audio,b,blockquote,body,button,canvas,caption,cite,code,dd,del,details,dfn,div,dl,dt,em,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,input,ins,kbd,label,legend,li,main,mark,menu,nav,object,ol,p,pre,q,samp,section,select,small,span,strong,sub,summary,sup,table,tbody,td,textarea,tfoot,th,thead,time,tr,ul,var,video{border:0;font-family:sans-serif;line-height:1;list-style:none;margin:0;padding:0;text-decoration:none;-webkit-font-smoothing:antialiased;-webkit-backface-visibility:hidden;box-sizing:border-box;color:#333;transition:.3s cubic-bezier(.4,.4,0,1);word-spacing:1px}a:focus:not(:focus-visible),button:focus:not(:focus-visible),summary:focus:not(:focus-visible){outline:none}nav ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:none}a,button{background:transparent;font-size:100%;margin:0;padding:0;vertical-align:baseline}ins{text-decoration:none}ins,mark{background-color:#ff9;color:#000}mark{font-style:italic;font-weight:700}del{text-decoration:line-through}abbr[title],dfn[title]{border-bottom:1px dotted;cursor:help}table{border-collapse:collapse;border-spacing:0}hr{border:0;border-top:1px solid #ccc;display:block;height:1px;margin:1em 0;padding:0}input,select{vertical-align:middle}textarea{resize:none}.clearfix:after{clear:both;content:"";display:block}[slot=after] button{overflow-anchor:none}.sd{flex-wrap:nowrap;max-width:100%;pointer-events:all;z-index:0;-webkit-overflow-scrolling:touch;align-content:center;align-items:center;display:flex;flex:none;flex-direction:column;position:relative}.sd::-webkit-scrollbar{display:none}.sd,.sd.richText *{transition-property:all,--g-angle,--g-color-0,--g-position-0,--g-color-1,--g-position-1,--g-color-2,--g-position-2,--g-color-3,--g-position-3,--g-color-4,--g-position-4,--g-color-5,--g-position-5,--g-color-6,--g-position-6,--g-color-7,--g-position-7,--g-color-8,--g-position-8,--g-color-9,--g-position-9,--g-color-10,--g-position-10,--g-color-11,--g-position-11}input.sd,textarea.sd{align-content:normal}.sd[tabindex]:focus{outline:none}.sd[tabindex]:focus-visible{outline:1px solid;outline-color:Highlight;outline-color:-webkit-focus-ring-color}input[type=email],input[type=tel],input[type=text],select,textarea{-webkit-appearance:none}select{cursor:pointer}.frame{display:block;overflow:hidden}.frame>iframe{height:100%;width:100%}.frame .formrun-embed>iframe:not(:first-child){display:none!important}.image{position:relative}.image:before{background-position:50%;background-size:cover;border-radius:inherit;content:"";height:100%;left:0;pointer-events:none;position:absolute;top:0;transition:inherit;width:100%;z-index:-2}.sd.file{cursor:pointer;flex-direction:row;outline:2px solid transparent;outline-offset:-1px;overflow-wrap:anywhere;word-break:break-word}.sd.file:focus-within{outline-color:Highlight;outline-color:-webkit-focus-ring-color}.file>input[type=file]{opacity:0;pointer-events:none;position:absolute}.sd.icon,.sd.text{align-content:center;align-items:center;display:flex;flex-direction:row;justify-content:center;overflow:visible;overflow-wrap:anywhere;word-break:break-word}.material-icons{display:inline-block;font-family:Material Icons;font-size:24px;font-style:normal;font-weight:400;letter-spacing:normal;line-height:1;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased}.sd.material-symbols{font-style:normal;font-variation-settings:"FILL" var(--symbol-fill,0),"wght" var(--symbol-weight,400)}.sd.material-symbols.font-loading{height:24px;opacity:0;overflow:hidden;width:24px}.sd.material-symbols-outlined{font-family:Material Symbols Outlined}.sd.material-symbols-rounded{font-family:Material Symbols Rounded}.sd.material-symbols-sharp{font-family:Material Symbols Sharp}.sd.material-symbols-weight-100{--symbol-weight:100}.sd.material-symbols-weight-200{--symbol-weight:200}.sd.material-symbols-weight-300{--symbol-weight:300}.sd.material-symbols-weight-400{--symbol-weight:400}.sd.material-symbols-weight-500{--symbol-weight:500}.sd.material-symbols-weight-600{--symbol-weight:600}.sd.material-symbols-weight-700{--symbol-weight:700}.sd.material-symbols-fill{--symbol-fill:1}a,a.icon,a.text{-webkit-tap-highlight-color:rgba(0,0,0,.15)}.fixed{z-index:2}.sticky{z-index:1}.button{transition:.4s cubic-bezier(.4,.4,0,1)}.button,.link{cursor:pointer}.submitLoading{opacity:.5!important;pointer-events:none!important}.richText{display:block;word-break:break-word}.richText [data-thread],.richText a,.richText blockquote,.richText em,.richText h1,.richText h2,.richText h3,.richText h4,.richText li,.richText ol,.richText p,.richText p>code,.richText pre,.richText pre>code,.richText s,.richText strong,.richText table tbody,.richText table tbody tr,.richText table tbody tr>td,.richText table tbody tr>th,.richText u,.richText ul{backface-visibility:visible;color:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;text-align:inherit}.richText p{display:block;margin:10px 0}.richText>p{min-height:1em}.richText img,.richText video{height:auto;max-width:100%;vertical-align:bottom}.richText h1{display:block;font-size:3em;font-weight:700;margin:20px 0}.richText h2{font-size:2em}.richText h2,.richText h3{display:block;font-weight:700;margin:10px 0}.richText h3{font-size:1em}.richText h4,.richText h5{font-weight:600}.richText h4,.richText h5,.richText h6{display:block;font-size:1em;margin:10px 0}.richText h6{font-weight:500}.richText [data-type=table]{overflow-x:auto}.richText [data-type=table] p{white-space:pre-line;word-break:break-all}.richText table{border:1px solid #f2f2f2;border-collapse:collapse;border-spacing:unset;color:#1a1a1a;font-size:14px;line-height:1.4;margin:10px 0;table-layout:auto}.richText table tr th{background:hsla(0,0%,96%,.5)}.richText table tr td,.richText table tr th{border:1px solid #f2f2f2;max-width:240px;min-width:100px;padding:12px}.richText table tr td p,.richText table tr th p{margin:0}.richText blockquote{border-left:3px solid rgba(0,0,0,.15);font-style:italic;margin:10px 0;padding:10px 15px}.richText [data-type=embed_code]{margin:20px 0;position:relative}.richText [data-type=embed_code]>.height-adjuster>.wrapper{position:relative}.richText [data-type=embed_code]>.height-adjuster>.wrapper[style*=padding-top] iframe{height:100%;left:0;position:absolute;top:0;width:100%}.richText [data-type=embed_code][data-embed-sandbox=true]{display:block;overflow:hidden}.richText [data-type=embed_code][data-embed-code-type=instagram]>.height-adjuster>.wrapper[style*=padding-top]{padding-top:100%}.richText [data-type=embed_code][data-embed-code-type=instagram]>.height-adjuster>.wrapper[style*=padding-top] blockquote{height:100%;left:0;overflow:hidden;position:absolute;top:0;width:100%}.richText [data-type=embed_code][data-embed-code-type=codepen]>.height-adjuster>.wrapper{padding-top:50%}.richText [data-type=embed_code][data-embed-code-type=codepen]>.height-adjuster>.wrapper iframe{height:100%;left:0;position:absolute;top:0;width:100%}.richText [data-type=embed_code][data-embed-code-type=slideshare]>.height-adjuster>.wrapper{padding-top:56.25%}.richText [data-type=embed_code][data-embed-code-type=slideshare]>.height-adjuster>.wrapper iframe{height:100%;left:0;position:absolute;top:0;width:100%}.richText [data-type=embed_code][data-embed-code-type=speakerdeck]>.height-adjuster>.wrapper{padding-top:56.25%}.richText [data-type=embed_code][data-embed-code-type=speakerdeck]>.height-adjuster>.wrapper iframe{height:100%;left:0;position:absolute;top:0;width:100%}.richText [data-type=embed_code][data-embed-code-type=snapwidget]>.height-adjuster>.wrapper{padding-top:30%}.richText [data-type=embed_code][data-embed-code-type=snapwidget]>.height-adjuster>.wrapper iframe{height:100%;left:0;position:absolute;top:0;width:100%}.richText [data-type=embed_code][data-embed-code-type=firework]>.height-adjuster>.wrapper fw-embed-feed{-webkit-user-select:none;-moz-user-select:none;user-select:none}.richText [data-type=embed_code_empty]{display:none}.richText ul{margin:0 0 0 20px}.richText ul li{list-style:disc;margin:10px 0}.richText ul li p{margin:0}.richText ol{margin:0 0 0 20px}.richText ol li{list-style:decimal;margin:10px 0}.richText ol li p{margin:0}.richText hr{border-top:1px solid #ccc;margin:10px 0}.richText p>code{background:#eee;border:1px solid rgba(0,0,0,.1);border-radius:6px;display:inline;margin:2px;padding:0 5px}.richText pre{background:#eee;border-radius:6px;font-family:Menlo,Monaco,Courier New,monospace;margin:20px 0;padding:25px 35px;white-space:pre-wrap}.richText pre code{border:none;padding:0}.richText strong{color:inherit;display:inline;font-family:inherit;font-weight:900}.richText em{font-style:italic}.richText a,.richText u{text-decoration:underline}.richText a{color:#007cff;display:inline}.richText s{text-decoration:line-through}.richText [data-type=table_of_contents]{background-color:#f5f5f5;border-radius:2px;color:#616161;font-size:16px;list-style:none;margin:0;padding:24px 24px 8px;text-decoration:underline}.richText [data-type=table_of_contents] .toc_list{margin:0}.richText [data-type=table_of_contents] .toc_item{color:currentColor;font-size:inherit!important;font-weight:inherit;list-style:none}.richText [data-type=table_of_contents] .toc_item>a{border:none;color:currentColor;font-size:inherit!important;font-weight:inherit;text-decoration:none}.richText [data-type=table_of_contents] .toc_item>a:hover{opacity:.7}.richText [data-type=table_of_contents] .toc_item--1{margin:0 0 16px}.richText [data-type=table_of_contents] .toc_item--2{margin:0 0 16px;padding-left:2rem}.richText [data-type=table_of_contents] .toc_item--3{margin:0 0 16px;padding-left:4rem}.sd.section{align-content:center!important;align-items:center!important;flex-direction:column!important;flex-wrap:nowrap!important;height:auto!important;max-width:100%!important;padding:0!important;width:100%!important}.sd.section-inner{position:static!important}@property --g-angle{syntax:"<angle>";inherits:false;initial-value:180deg}@property --g-color-0{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-0{syntax:"<percentage>";inherits:false;initial-value:.01%}@property --g-color-1{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-1{syntax:"<percentage>";inherits:false;initial-value:100%}@property --g-color-2{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-2{syntax:"<percentage>";inherits:false;initial-value:100%}@property --g-color-3{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-3{syntax:"<percentage>";inherits:false;initial-value:100%}@property --g-color-4{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-4{syntax:"<percentage>";inherits:false;initial-value:100%}@property --g-color-5{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-5{syntax:"<percentage>";inherits:false;initial-value:100%}@property --g-color-6{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-6{syntax:"<percentage>";inherits:false;initial-value:100%}@property --g-color-7{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-7{syntax:"<percentage>";inherits:false;initial-value:100%}@property --g-color-8{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-8{syntax:"<percentage>";inherits:false;initial-value:100%}@property --g-color-9{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-9{syntax:"<percentage>";inherits:false;initial-value:100%}@property --g-color-10{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-10{syntax:"<percentage>";inherits:false;initial-value:100%}@property --g-color-11{syntax:"<color>";inherits:false;initial-value:transparent}@property --g-position-11{syntax:"<percentage>";inherits:false;initial-value:100%}.snackbar[data-v-3129703d]{align-items:center;background:#fff;border:1px solid #ededed;border-radius:6px;box-shadow:0 16px 48px -8px #00000014,0 10px 25px -5px #0000001c;display:flex;flex-direction:row;gap:8px;justify-content:space-between;left:50%;max-width:90vw;padding:16px 20px;position:fixed;top:32px;transform:translate(-50%);-webkit-user-select:none;-moz-user-select:none;user-select:none;width:480px;z-index:9999}.snackbar.v-enter-active[data-v-3129703d],.snackbar.v-leave-active[data-v-3129703d]{transition:.4s cubic-bezier(.4,.4,0,1)}.snackbar.v-enter-from[data-v-3129703d],.snackbar.v-leave-to[data-v-3129703d]{opacity:0;transform:translate(-50%,-10px)}.snackbar .convey[data-v-3129703d]{align-items:center;display:flex;flex-direction:row;gap:8px;padding:0}.snackbar .convey .icon[data-v-3129703d]{background-position:50%;background-repeat:no-repeat;flex-shrink:0;height:24px;width:24px}.snackbar .convey .message[data-v-3129703d]{font-size:14px;font-style:normal;font-weight:400;line-height:20px;white-space:pre-line}.snackbar .convey.error .icon[data-v-3129703d]{background-image:url(./close_circle.c7480f3c.svg)}.snackbar .convey.error .message[data-v-3129703d]{color:#f84f65}.snackbar .convey.success .icon[data-v-3129703d]{background-image:url(./round_check.0ebac23f.svg)}.snackbar .convey.success .message[data-v-3129703d]{color:#111}.snackbar .button[data-v-3129703d]{align-items:center;border-radius:40px;color:#4b9cfb;display:flex;flex-shrink:0;font-family:Inter;font-size:12px;font-style:normal;font-weight:700;justify-content:center;line-height:16px;padding:4px 8px}.snackbar .button[data-v-3129703d]:hover{background:#f5f5f5}a[data-v-60d33773]{align-items:center;border-radius:4px;bottom:20px;height:20px;justify-content:center;left:20px;perspective:300px;position:fixed;transition:0s linear;width:84px;z-index:2000}@media (hover:hover){a[data-v-60d33773]{transition:.4s cubic-bezier(.4,.4,0,1)}a[data-v-60d33773]:hover{height:32px;width:200px}}[data-v-60d33773] .custom-fill path{fill:var(--01abf230)}.fade-enter-active[data-v-60d33773],.fade-leave-active[data-v-60d33773]{transition:opacity .2s cubic-bezier(.4,.4,0,1)}.fade-enter[data-v-60d33773],.fade-leave-to[data-v-60d33773]{opacity:0}
|
requirements.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.0.0,<2.2.0
|
| 2 |
+
torchvision>=0.15.0,<0.17.0
|
| 3 |
+
torchaudio>=2.0.0,<2.2.0
|
| 4 |
+
huggingface-hub>=0.35.3
|
| 5 |
+
diffusers>=0.28.0,<0.36.0
|
| 6 |
+
numpy>=1.24.0,<2.0.0
|
| 7 |
+
scipy>=1.11.0
|
| 8 |
+
gradio==5.49.1
|
| 9 |
+
transformers>=4.40.0,<4.45.0
|
| 10 |
+
accelerate>=0.24.0
|
| 11 |
+
pillow>=10.0.0
|
| 12 |
+
python-dotenv>=1.0.0
|
| 13 |
+
beautifulsoup4>=4.14.0
|
| 14 |
+
lxml>=6.0.0
|
| 15 |
+
safetensors>=0.3.0
|
utils/archive_old_logs.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
古いログファイルをアーカイブするユーティリティスクリプト
|
| 3 |
+
|
| 4 |
+
使用方法:
|
| 5 |
+
python utils/archive_old_logs.py
|
| 6 |
+
|
| 7 |
+
実行内容:
|
| 8 |
+
1. logs/generation_history.json を logs/archive/generation_history_jagirl_backup_{timestamp}.json に移動
|
| 9 |
+
2. 新しい emix-0-5 用の空の generation_history.json を作成
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
import shutil
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def archive_old_logs():
|
| 20 |
+
"""古いログをアーカイブして新しいログファイルを初期化"""
|
| 21 |
+
|
| 22 |
+
log_file = Path("logs/generation_history.json")
|
| 23 |
+
archive_dir = Path("logs/archive")
|
| 24 |
+
|
| 25 |
+
print("=" * 60)
|
| 26 |
+
print("🗂️ ログファイル アーカイブツール")
|
| 27 |
+
print("=" * 60)
|
| 28 |
+
|
| 29 |
+
# ログファイルの存在確認
|
| 30 |
+
if not log_file.exists():
|
| 31 |
+
print("⚠️ logs/generation_history.json が見つかりません")
|
| 32 |
+
print(" 新しいログファイルを作成します...")
|
| 33 |
+
create_new_log_file(log_file)
|
| 34 |
+
print("✅ 完了")
|
| 35 |
+
return
|
| 36 |
+
|
| 37 |
+
# アーカイブディレクトリの作成
|
| 38 |
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
| 39 |
+
|
| 40 |
+
# 既存ログの読み込みとチェック
|
| 41 |
+
try:
|
| 42 |
+
with open(log_file, 'r', encoding='utf-8') as f:
|
| 43 |
+
log_data = json.load(f)
|
| 44 |
+
|
| 45 |
+
generation_count = len(log_data.get("generations", []))
|
| 46 |
+
model_name = log_data.get("metadata", {}).get("model_info", {}).get("model_name", "unknown")
|
| 47 |
+
|
| 48 |
+
print(f"📊 現在のログ情報:")
|
| 49 |
+
print(f" モデル: {model_name}")
|
| 50 |
+
print(f" 生成記録数: {generation_count} 件")
|
| 51 |
+
print()
|
| 52 |
+
|
| 53 |
+
if generation_count == 0:
|
| 54 |
+
print("⚠️ ログに記録がないため、アーカイブをスキップします")
|
| 55 |
+
print(" 新しいログファイルで上書きします...")
|
| 56 |
+
create_new_log_file(log_file)
|
| 57 |
+
print("✅ 完了")
|
| 58 |
+
return
|
| 59 |
+
|
| 60 |
+
# アーカイブファイル名の生成
|
| 61 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 62 |
+
archive_filename = f"generation_history_{model_name.replace('/', '_')}_backup_{timestamp}.json"
|
| 63 |
+
archive_path = archive_dir / archive_filename
|
| 64 |
+
|
| 65 |
+
print(f"📦 アーカイブ実行:")
|
| 66 |
+
print(f" 元ファイル: {log_file}")
|
| 67 |
+
print(f" 保存先: {archive_path}")
|
| 68 |
+
print()
|
| 69 |
+
|
| 70 |
+
# ファイルをアーカイブ
|
| 71 |
+
shutil.copy2(log_file, archive_path)
|
| 72 |
+
print(f"✅ アーカイブ完了: {generation_count} 件の記録を保存")
|
| 73 |
+
|
| 74 |
+
# 新しいログファイルを作成
|
| 75 |
+
print()
|
| 76 |
+
print("📝 新しいログファイルを作成中...")
|
| 77 |
+
create_new_log_file(log_file)
|
| 78 |
+
|
| 79 |
+
print()
|
| 80 |
+
print("=" * 60)
|
| 81 |
+
print("✅ ログアーカイブが完了しました")
|
| 82 |
+
print("=" * 60)
|
| 83 |
+
print(f"📁 アーカイブファイル: {archive_path}")
|
| 84 |
+
print(f"📄 新ログファイル: {log_file}")
|
| 85 |
+
print()
|
| 86 |
+
|
| 87 |
+
except json.JSONDecodeError as e:
|
| 88 |
+
print(f"❌ ログファイルの読み込みエラー: {e}")
|
| 89 |
+
print(" ファイルが破損している可能性があります")
|
| 90 |
+
|
| 91 |
+
# バックアップを作成してから新規作成
|
| 92 |
+
backup_path = archive_dir / f"corrupted_log_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 93 |
+
shutil.copy2(log_file, backup_path)
|
| 94 |
+
print(f"📦 破損ファイルをバックアップ: {backup_path}")
|
| 95 |
+
|
| 96 |
+
create_new_log_file(log_file)
|
| 97 |
+
print("✅ 新しいログファイルを作成しました")
|
| 98 |
+
|
| 99 |
+
except Exception as e:
|
| 100 |
+
print(f"❌ エラーが発生しました: {e}")
|
| 101 |
+
import traceback
|
| 102 |
+
traceback.print_exc()
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def create_new_log_file(log_file: Path):
|
| 106 |
+
"""新しいログファイルを作成(emix-0-5用)"""
|
| 107 |
+
|
| 108 |
+
# logsディレクトリの作成
|
| 109 |
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
| 110 |
+
|
| 111 |
+
# 新しいログデータの構造
|
| 112 |
+
new_log_data = {
|
| 113 |
+
"metadata": {
|
| 114 |
+
"format_version": "2.0",
|
| 115 |
+
"created_at": datetime.now().isoformat(),
|
| 116 |
+
"last_updated": datetime.now().isoformat(),
|
| 117 |
+
"description": "Unified generation history for emix-0-5 UI project",
|
| 118 |
+
"model_info": {
|
| 119 |
+
"model_name": "aipicasso/emix-0-5",
|
| 120 |
+
"model_type": "StableDiffusionXL",
|
| 121 |
+
"specialized_for": "Japanese anime / illustration style"
|
| 122 |
+
},
|
| 123 |
+
"log_schema": {
|
| 124 |
+
"timestamp": "ISO format timestamp",
|
| 125 |
+
"generation_id": "Unique identifier for each generation",
|
| 126 |
+
"prompts": "All text prompts used",
|
| 127 |
+
"parameters": "Complete parameter set used for generation",
|
| 128 |
+
"output": "Generated image information",
|
| 129 |
+
"performance": "Execution metrics",
|
| 130 |
+
"system_info": "Hardware and software environment"
|
| 131 |
+
}
|
| 132 |
+
},
|
| 133 |
+
"generations": []
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
# ファイルに書き込み
|
| 137 |
+
with open(log_file, 'w', encoding='utf-8') as f:
|
| 138 |
+
json.dump(new_log_data, f, ensure_ascii=False, indent=2)
|
| 139 |
+
|
| 140 |
+
print(f"📄 新しいログファイルを作成: {log_file}")
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
if __name__ == "__main__":
|
| 144 |
+
archive_old_logs()
|
utils/download_hugginface_repo.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hugging Face モデルダウンロードスクリプト
|
| 3 |
+
aipicasso/emix-0-5 モデルをキャッシュにダウンロード
|
| 4 |
+
|
| 5 |
+
使用方法:
|
| 6 |
+
1. CLIでログイン: huggingface-cli login
|
| 7 |
+
2. このスクリプトを実行: python utils/download_hugginface_repo.py
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
from huggingface_hub import snapshot_download, login
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
import sys
|
| 14 |
+
|
| 15 |
+
def download_emix_model():
|
| 16 |
+
"""
|
| 17 |
+
aipicasso/emix-0-5 モデルをダウンロード
|
| 18 |
+
|
| 19 |
+
SDXL系モデルのため、以下のファイルをダウンロード:
|
| 20 |
+
- model_index.json
|
| 21 |
+
- *.safetensors (UNet, VAE, Text Encoderなど)
|
| 22 |
+
- scheduler, tokenizer設定ファイル
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
MODEL_ID = "aipicasso/emix-0-5"
|
| 26 |
+
|
| 27 |
+
print("=" * 60)
|
| 28 |
+
print("🚀 Emix-0.5 モデルダウンロードスクリプト")
|
| 29 |
+
print("=" * 60)
|
| 30 |
+
print(f"� モデル: {MODEL_ID}")
|
| 31 |
+
print(f"🏷️ 説明: Conterfeit XL v2.5 + Animagine v2.0 + Emix 0.4")
|
| 32 |
+
print(f"🎯 用途: 日本アニメスタイル画像生成")
|
| 33 |
+
print("=" * 60)
|
| 34 |
+
|
| 35 |
+
# 環境変数からトークンを取得(オプション)
|
| 36 |
+
hf_token = os.getenv("HF_TOKEN")
|
| 37 |
+
|
| 38 |
+
if hf_token:
|
| 39 |
+
print("🔑 環境変数からHugging Faceトークンを取得")
|
| 40 |
+
login(token=hf_token)
|
| 41 |
+
else:
|
| 42 |
+
print("ℹ️ 環境変数にHF_TOKENが設定されていません")
|
| 43 |
+
print(" 必要に応じて `huggingface-cli login` でログインしてください")
|
| 44 |
+
|
| 45 |
+
# ダウンロード先のキャッシュディレクトリを表示
|
| 46 |
+
cache_dir = Path.home() / ".cache" / "huggingface" / "hub"
|
| 47 |
+
print(f"📂 キャッシュディレクトリ: {cache_dir}")
|
| 48 |
+
print()
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
print("⏳ ダウンロード開始...")
|
| 52 |
+
print(" (モデルサイズは約6-8GB、初回は時間がかかります)")
|
| 53 |
+
print()
|
| 54 |
+
|
| 55 |
+
# モデル全体をダウンロード
|
| 56 |
+
local_path = snapshot_download(
|
| 57 |
+
repo_id=MODEL_ID,
|
| 58 |
+
repo_type="model",
|
| 59 |
+
resume_download=True, # 中断時に再開可能
|
| 60 |
+
local_files_only=False,
|
| 61 |
+
ignore_patterns=["*.msgpack", "*.h5"], # 不要なファイルを除外
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
print()
|
| 65 |
+
print("=" * 60)
|
| 66 |
+
print("✅ ダウンロード完了!")
|
| 67 |
+
print("=" * 60)
|
| 68 |
+
print(f"📁 ローカルパス: {local_path}")
|
| 69 |
+
print()
|
| 70 |
+
print("💡 使用方法:")
|
| 71 |
+
print(" from diffusers import StableDiffusionXLPipeline")
|
| 72 |
+
print(f" pipe = StableDiffusionXLPipeline.from_pretrained('{MODEL_ID}')")
|
| 73 |
+
print()
|
| 74 |
+
print("🔧 次のステップ:")
|
| 75 |
+
print(" 1. app.pyでモデルを読み込んで画像生成")
|
| 76 |
+
print(" 2. Gradio UIから利用可能になります")
|
| 77 |
+
print("=" * 60)
|
| 78 |
+
|
| 79 |
+
return local_path
|
| 80 |
+
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print()
|
| 83 |
+
print("=" * 60)
|
| 84 |
+
print("❌ ダウンロード失敗")
|
| 85 |
+
print("=" * 60)
|
| 86 |
+
print(f"エラー: {e}")
|
| 87 |
+
print()
|
| 88 |
+
print("� トラブルシューティング:")
|
| 89 |
+
print(" 1. インターネット接続を確認")
|
| 90 |
+
print(" 2. Hugging Faceにログイン: huggingface-cli login")
|
| 91 |
+
print(" 3. ディスク容量を確認 (最低8GB必要)")
|
| 92 |
+
print(" 4. プロキシ設定が必要な場合は環境変数を設定")
|
| 93 |
+
print("=" * 60)
|
| 94 |
+
sys.exit(1)
|
| 95 |
+
|
| 96 |
+
if __name__ == "__main__":
|
| 97 |
+
download_emix_model()
|
utils/logger.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
統一ログ機能モジュール
|
| 3 |
+
画像生成の全パラメータと結果を包括的に記録する統一ログシステム
|
| 4 |
+
|
| 5 |
+
設計原則:
|
| 6 |
+
- 生成に使用された全パラメータの記録
|
| 7 |
+
- 生成画像との確実な紐づけ
|
| 8 |
+
- JSON形式での構造化データ保存
|
| 9 |
+
- 検索・分析しやすい形式
|
| 10 |
+
- パフォーマンス情報の詳細記録
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import time
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
from typing import Dict, Any, Optional
|
| 18 |
+
import torch
|
| 19 |
+
from PIL import Image
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class UnifiedLogger:
|
| 23 |
+
"""統一ログ機能クラス"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, log_dir: str = "logs"):
|
| 26 |
+
"""
|
| 27 |
+
ログ機能の初期化
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
log_dir: ログファイルを保存するディレクトリ
|
| 31 |
+
"""
|
| 32 |
+
self.log_dir = log_dir
|
| 33 |
+
self.json_log_file = os.path.join(log_dir, "generation_history.json")
|
| 34 |
+
|
| 35 |
+
# ログディレクトリ作成
|
| 36 |
+
os.makedirs(log_dir, exist_ok=True)
|
| 37 |
+
|
| 38 |
+
# 既存ログの読み込み
|
| 39 |
+
self._load_existing_logs()
|
| 40 |
+
|
| 41 |
+
def _load_existing_logs(self):
|
| 42 |
+
"""既存のログデータを読み込み"""
|
| 43 |
+
if os.path.exists(self.json_log_file):
|
| 44 |
+
try:
|
| 45 |
+
with open(self.json_log_file, 'r', encoding='utf-8') as f:
|
| 46 |
+
self.log_data = json.load(f)
|
| 47 |
+
except (json.JSONDecodeError, FileNotFoundError):
|
| 48 |
+
self.log_data = {"metadata": self._create_metadata(), "generations": []}
|
| 49 |
+
else:
|
| 50 |
+
self.log_data = {"metadata": self._create_metadata(), "generations": []}
|
| 51 |
+
|
| 52 |
+
def _create_metadata(self) -> Dict[str, Any]:
|
| 53 |
+
"""ログファイルのメタデータを作成"""
|
| 54 |
+
return {
|
| 55 |
+
"format_version": "2.0",
|
| 56 |
+
"created_at": datetime.now().isoformat(),
|
| 57 |
+
"last_updated": datetime.now().isoformat(),
|
| 58 |
+
"description": "Unified generation history for emix-0-5 UI project",
|
| 59 |
+
"model_info": {
|
| 60 |
+
"model_name": "aipicasso/emix-0-5",
|
| 61 |
+
"model_type": "StableDiffusionXL",
|
| 62 |
+
"specialized_for": "Japanese anime / illustration style"
|
| 63 |
+
},
|
| 64 |
+
"log_schema": {
|
| 65 |
+
"timestamp": "ISO format timestamp",
|
| 66 |
+
"generation_id": "Unique identifier for each generation",
|
| 67 |
+
"prompts": "All text prompts used",
|
| 68 |
+
"parameters": "Complete parameter set used for generation",
|
| 69 |
+
"output": "Generated image information",
|
| 70 |
+
"performance": "Execution metrics",
|
| 71 |
+
"system_info": "Hardware and software environment"
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
def _get_image_info(self, filepath: str) -> Dict[str, Any]:
|
| 76 |
+
"""画像ファイルの詳細情報を取得"""
|
| 77 |
+
if not os.path.exists(filepath):
|
| 78 |
+
return {"error": "File not found"}
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
# ファイルサイズ
|
| 82 |
+
file_size_bytes = os.path.getsize(filepath)
|
| 83 |
+
file_size_mb = round(file_size_bytes / (1024 * 1024), 3)
|
| 84 |
+
|
| 85 |
+
# 画像情報
|
| 86 |
+
with Image.open(filepath) as img:
|
| 87 |
+
width, height = img.size
|
| 88 |
+
mode = img.mode
|
| 89 |
+
format_type = img.format
|
| 90 |
+
|
| 91 |
+
return {
|
| 92 |
+
"filepath": os.path.abspath(filepath),
|
| 93 |
+
"file_url": f"file:///{os.path.abspath(filepath).replace(os.sep, '/')}",
|
| 94 |
+
"filename": os.path.basename(filepath),
|
| 95 |
+
"file_size_bytes": file_size_bytes,
|
| 96 |
+
"file_size_mb": file_size_mb,
|
| 97 |
+
"image_width": width,
|
| 98 |
+
"image_height": height,
|
| 99 |
+
"image_mode": mode,
|
| 100 |
+
"image_format": format_type,
|
| 101 |
+
"created_at": datetime.fromtimestamp(os.path.getctime(filepath)).isoformat()
|
| 102 |
+
}
|
| 103 |
+
except Exception as e:
|
| 104 |
+
return {"error": f"Failed to get image info: {str(e)}"}
|
| 105 |
+
|
| 106 |
+
def _get_system_info(self) -> Dict[str, Any]:
|
| 107 |
+
"""システム情報を取得"""
|
| 108 |
+
system_info = {
|
| 109 |
+
"python_version": None,
|
| 110 |
+
"torch_version": None,
|
| 111 |
+
"cuda_available": False,
|
| 112 |
+
"cuda_version": None,
|
| 113 |
+
"gpu_name": None,
|
| 114 |
+
"vram_total_gb": 0,
|
| 115 |
+
"vram_allocated_gb": 0
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
import sys
|
| 120 |
+
system_info["python_version"] = sys.version.split()[0]
|
| 121 |
+
|
| 122 |
+
system_info["torch_version"] = torch.__version__
|
| 123 |
+
system_info["cuda_available"] = torch.cuda.is_available()
|
| 124 |
+
|
| 125 |
+
if torch.cuda.is_available():
|
| 126 |
+
system_info["cuda_version"] = torch.version.cuda
|
| 127 |
+
system_info["gpu_name"] = torch.cuda.get_device_name(0)
|
| 128 |
+
system_info["vram_total_gb"] = round(
|
| 129 |
+
torch.cuda.get_device_properties(0).total_memory / (1024**3), 2
|
| 130 |
+
)
|
| 131 |
+
system_info["vram_allocated_gb"] = round(
|
| 132 |
+
torch.cuda.memory_allocated(0) / (1024**3), 2
|
| 133 |
+
)
|
| 134 |
+
except Exception as e:
|
| 135 |
+
system_info["error"] = f"Failed to get system info: {str(e)}"
|
| 136 |
+
|
| 137 |
+
return system_info
|
| 138 |
+
|
| 139 |
+
def log_generation(
|
| 140 |
+
self,
|
| 141 |
+
prompt: str,
|
| 142 |
+
negative_prompt: str = "",
|
| 143 |
+
parameters: Dict[str, Any] = None,
|
| 144 |
+
output_filepath: str = "",
|
| 145 |
+
execution_time: float = 0.0,
|
| 146 |
+
additional_info: Dict[str, Any] = None
|
| 147 |
+
) -> str:
|
| 148 |
+
"""
|
| 149 |
+
画像生成の完全なログを記録
|
| 150 |
+
|
| 151 |
+
Args:
|
| 152 |
+
prompt: メインプロンプト
|
| 153 |
+
negative_prompt: ネガティブプロンプト
|
| 154 |
+
parameters: 生成に使用された全パラメータ
|
| 155 |
+
output_filepath: 生成された画像ファイルパス
|
| 156 |
+
execution_time: 実行時間(秒)
|
| 157 |
+
additional_info: 追加情報
|
| 158 |
+
|
| 159 |
+
Returns:
|
| 160 |
+
generation_id: 生成された記録のユニークID
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
# ユニークIDの生成
|
| 164 |
+
timestamp = datetime.now()
|
| 165 |
+
generation_id = f"gen_{timestamp.strftime('%Y%m%d_%H%M%S')}_{int(time.time() * 1000) % 100000}"
|
| 166 |
+
|
| 167 |
+
# デフォルトパラメータの設定
|
| 168 |
+
if parameters is None:
|
| 169 |
+
parameters = {}
|
| 170 |
+
|
| 171 |
+
# 完全なパラメータセットの作成
|
| 172 |
+
complete_parameters = {
|
| 173 |
+
# 基本パラメータ
|
| 174 |
+
"num_inference_steps": parameters.get("num_inference_steps", 20),
|
| 175 |
+
"guidance_scale": parameters.get("guidance_scale", 7.5),
|
| 176 |
+
"width": parameters.get("width", 1024),
|
| 177 |
+
"height": parameters.get("height", 1024),
|
| 178 |
+
"seed": parameters.get("seed", None),
|
| 179 |
+
|
| 180 |
+
# スケジューラー関連
|
| 181 |
+
"scheduler_type": parameters.get("scheduler_type", "default"),
|
| 182 |
+
"eta": parameters.get("eta", 0.0),
|
| 183 |
+
|
| 184 |
+
# 画像生成関連
|
| 185 |
+
"num_images": parameters.get("num_images", 1),
|
| 186 |
+
"batch_size": parameters.get("batch_size", 1),
|
| 187 |
+
|
| 188 |
+
# モデル関連
|
| 189 |
+
"torch_dtype": str(parameters.get("torch_dtype", "float16")),
|
| 190 |
+
"enable_xformers": parameters.get("enable_xformers", False),
|
| 191 |
+
"enable_cpu_offload": parameters.get("enable_cpu_offload", False),
|
| 192 |
+
|
| 193 |
+
# その他のパラメータ
|
| 194 |
+
**{k: v for k, v in parameters.items() if k not in [
|
| 195 |
+
"num_inference_steps", "guidance_scale", "width", "height",
|
| 196 |
+
"seed", "scheduler_type", "eta", "num_images", "batch_size",
|
| 197 |
+
"torch_dtype", "enable_xformers", "enable_cpu_offload"
|
| 198 |
+
]}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
# ログエントリの作成
|
| 202 |
+
log_entry = {
|
| 203 |
+
"generation_id": generation_id,
|
| 204 |
+
"timestamp": timestamp.isoformat(),
|
| 205 |
+
"prompts": {
|
| 206 |
+
"main_prompt": prompt,
|
| 207 |
+
"negative_prompt": negative_prompt,
|
| 208 |
+
"prompt_length": len(prompt),
|
| 209 |
+
"negative_prompt_length": len(negative_prompt)
|
| 210 |
+
},
|
| 211 |
+
"parameters": complete_parameters,
|
| 212 |
+
"output": self._get_image_info(output_filepath) if output_filepath else {},
|
| 213 |
+
"performance": {
|
| 214 |
+
"execution_time_seconds": round(execution_time, 3),
|
| 215 |
+
"estimated_speed_sec_per_step": round(
|
| 216 |
+
execution_time / max(complete_parameters.get("num_inference_steps", 1), 1), 3
|
| 217 |
+
) if execution_time > 0 else 0
|
| 218 |
+
},
|
| 219 |
+
"system_info": self._get_system_info(),
|
| 220 |
+
"additional_info": additional_info or {}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
# ログに追加
|
| 224 |
+
self.log_data["generations"].append(log_entry)
|
| 225 |
+
self.log_data["metadata"]["last_updated"] = timestamp.isoformat()
|
| 226 |
+
|
| 227 |
+
# ファイルに保存
|
| 228 |
+
self._save_logs()
|
| 229 |
+
|
| 230 |
+
return generation_id
|
| 231 |
+
|
| 232 |
+
def _save_logs(self):
|
| 233 |
+
"""ログをファイルに保存"""
|
| 234 |
+
try:
|
| 235 |
+
with open(self.json_log_file, 'w', encoding='utf-8') as f:
|
| 236 |
+
json.dump(self.log_data, f, ensure_ascii=False, indent=2)
|
| 237 |
+
except Exception as e:
|
| 238 |
+
print(f"ログ保存エラー: {e}")
|
| 239 |
+
|
| 240 |
+
def get_generation_by_id(self, generation_id: str) -> Optional[Dict[str, Any]]:
|
| 241 |
+
"""generation_idで特定の生成記録を取得"""
|
| 242 |
+
for generation in self.log_data["generations"]:
|
| 243 |
+
if generation["generation_id"] == generation_id:
|
| 244 |
+
return generation
|
| 245 |
+
return None
|
| 246 |
+
|
| 247 |
+
def get_recent_generations(self, count: int = 10) -> list:
|
| 248 |
+
"""最近の生成記録を取得"""
|
| 249 |
+
return self.log_data["generations"][-count:] if self.log_data["generations"] else []
|
| 250 |
+
|
| 251 |
+
def search_by_prompt(self, search_term: str, case_sensitive: bool = False) -> list:
|
| 252 |
+
"""プロンプトで検索"""
|
| 253 |
+
results = []
|
| 254 |
+
search_term = search_term if case_sensitive else search_term.lower()
|
| 255 |
+
|
| 256 |
+
for generation in self.log_data["generations"]:
|
| 257 |
+
main_prompt = generation["prompts"]["main_prompt"]
|
| 258 |
+
if not case_sensitive:
|
| 259 |
+
main_prompt = main_prompt.lower()
|
| 260 |
+
|
| 261 |
+
if search_term in main_prompt:
|
| 262 |
+
results.append(generation)
|
| 263 |
+
|
| 264 |
+
return results
|
| 265 |
+
|
| 266 |
+
def get_statistics(self) -> Dict[str, Any]:
|
| 267 |
+
"""生成統計を取得"""
|
| 268 |
+
generations = self.log_data["generations"]
|
| 269 |
+
|
| 270 |
+
if not generations:
|
| 271 |
+
return {"total_generations": 0}
|
| 272 |
+
|
| 273 |
+
total_time = sum(g["performance"]["execution_time_seconds"] for g in generations)
|
| 274 |
+
avg_time = total_time / len(generations)
|
| 275 |
+
|
| 276 |
+
schedulers = {}
|
| 277 |
+
for g in generations:
|
| 278 |
+
scheduler = g["parameters"].get("scheduler_type", "unknown")
|
| 279 |
+
schedulers[scheduler] = schedulers.get(scheduler, 0) + 1
|
| 280 |
+
|
| 281 |
+
return {
|
| 282 |
+
"total_generations": len(generations),
|
| 283 |
+
"total_execution_time_hours": round(total_time / 3600, 2),
|
| 284 |
+
"average_execution_time_seconds": round(avg_time, 2),
|
| 285 |
+
"scheduler_usage": schedulers,
|
| 286 |
+
"date_range": {
|
| 287 |
+
"first": generations[0]["timestamp"],
|
| 288 |
+
"last": generations[-1]["timestamp"]
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
def cleanup_old_logs(self, keep_days: int = 30):
|
| 293 |
+
"""古いログエントリを削除"""
|
| 294 |
+
cutoff_date = datetime.now().timestamp() - (keep_days * 24 * 3600)
|
| 295 |
+
|
| 296 |
+
original_count = len(self.log_data["generations"])
|
| 297 |
+
self.log_data["generations"] = [
|
| 298 |
+
g for g in self.log_data["generations"]
|
| 299 |
+
if datetime.fromisoformat(g["timestamp"]).timestamp() > cutoff_date
|
| 300 |
+
]
|
| 301 |
+
|
| 302 |
+
removed_count = original_count - len(self.log_data["generations"])
|
| 303 |
+
|
| 304 |
+
if removed_count > 0:
|
| 305 |
+
self._save_logs()
|
| 306 |
+
print(f"古いログエントリ {removed_count} 件を削除しました")
|
| 307 |
+
|
| 308 |
+
return removed_count
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
# グローバルロガーインスタンス
|
| 312 |
+
_global_logger = None
|
| 313 |
+
|
| 314 |
+
def get_logger(log_dir: str = "logs") -> UnifiedLogger:
|
| 315 |
+
"""グローバルロガーインスタンスを取得"""
|
| 316 |
+
global _global_logger
|
| 317 |
+
if _global_logger is None:
|
| 318 |
+
_global_logger = UnifiedLogger(log_dir)
|
| 319 |
+
return _global_logger
|
| 320 |
+
|
| 321 |
+
def log_generation(**kwargs) -> str:
|
| 322 |
+
"""グローバルロガーを使用して生成をログ"""
|
| 323 |
+
logger = get_logger()
|
| 324 |
+
return logger.log_generation(**kwargs)
|
| 325 |
+
|
| 326 |
+
def get_statistics() -> Dict[str, Any]:
|
| 327 |
+
"""生成統計を取得"""
|
| 328 |
+
logger = get_logger()
|
| 329 |
+
return logger.get_statistics()
|
utils/migrate_logs.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ログ移行スクリプト
|
| 3 |
+
既存のgeneration_history.jsonを新しい統一ログフォーマットに移行
|
| 4 |
+
|
| 5 |
+
実行方法:
|
| 6 |
+
python utils/migrate_logs.py
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
# プロジェクトルートをPythonパスに追加
|
| 16 |
+
project_root = Path(__file__).parent.parent
|
| 17 |
+
sys.path.insert(0, str(project_root / "utils"))
|
| 18 |
+
|
| 19 |
+
from unified_logger import UnifiedLogger
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def migrate_old_logs():
|
| 23 |
+
"""既存のログを新しいフォーマットに移行"""
|
| 24 |
+
|
| 25 |
+
old_log_file = "logs/generation_history.json"
|
| 26 |
+
backup_file = "logs/generation_history_backup.json"
|
| 27 |
+
|
| 28 |
+
if not os.path.exists(old_log_file):
|
| 29 |
+
print("❌ 既存のログファイルが見つかりません")
|
| 30 |
+
return
|
| 31 |
+
|
| 32 |
+
# バックアップ作成
|
| 33 |
+
with open(old_log_file, 'r', encoding='utf-8') as f:
|
| 34 |
+
old_data = json.load(f)
|
| 35 |
+
|
| 36 |
+
with open(backup_file, 'w', encoding='utf-8') as f:
|
| 37 |
+
json.dump(old_data, f, ensure_ascii=False, indent=2)
|
| 38 |
+
|
| 39 |
+
print(f"✅ 既存ログをバックアップしました: {backup_file}")
|
| 40 |
+
|
| 41 |
+
# 統一ログ機能で移行
|
| 42 |
+
logger = UnifiedLogger()
|
| 43 |
+
|
| 44 |
+
migration_count = 0
|
| 45 |
+
|
| 46 |
+
if "generations" in old_data:
|
| 47 |
+
for old_entry in old_data["generations"]:
|
| 48 |
+
try:
|
| 49 |
+
# 旧フォーマットから新フォーマットへの変換
|
| 50 |
+
old_params = old_entry.get("parameters", {})
|
| 51 |
+
old_output = old_entry.get("output", {})
|
| 52 |
+
old_performance = old_entry.get("performance", {})
|
| 53 |
+
|
| 54 |
+
# パラメータの変換
|
| 55 |
+
new_params = {
|
| 56 |
+
"num_inference_steps": old_params.get("steps", 20),
|
| 57 |
+
"guidance_scale": old_params.get("cfg_scale", 7.5),
|
| 58 |
+
"width": old_params.get("width", 1024),
|
| 59 |
+
"height": old_params.get("height", 1024),
|
| 60 |
+
"seed": old_params.get("seed", None),
|
| 61 |
+
"scheduler_type": old_params.get("scheduler", "default"),
|
| 62 |
+
"eta": 0.0, # 旧ログにはない
|
| 63 |
+
"torch_dtype": "float16", # 推定値
|
| 64 |
+
"enable_xformers": True, # 推定値
|
| 65 |
+
"enable_cpu_offload": False # 推定値
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
# 追加情報
|
| 69 |
+
additional_info = {
|
| 70 |
+
"migrated_from": "generation_history.json",
|
| 71 |
+
"migration_date": datetime.now().isoformat(),
|
| 72 |
+
"original_timestamp": old_entry.get("timestamp", "")
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
# 新しいログエントリとして記録
|
| 76 |
+
generation_id = logger.log_generation(
|
| 77 |
+
prompt=old_entry.get("prompt", ""),
|
| 78 |
+
negative_prompt=old_entry.get("negative_prompt", ""),
|
| 79 |
+
parameters=new_params,
|
| 80 |
+
output_filepath=old_output.get("filepath", ""),
|
| 81 |
+
execution_time=old_performance.get("execution_time_seconds", 0),
|
| 82 |
+
additional_info=additional_info
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
migration_count += 1
|
| 86 |
+
print(f"✅ 移行完了: {generation_id}")
|
| 87 |
+
|
| 88 |
+
except Exception as e:
|
| 89 |
+
print(f"⚠️ エントリの移行に失敗: {e}")
|
| 90 |
+
continue
|
| 91 |
+
|
| 92 |
+
print(f"\n🎉 移行完了: {migration_count} エントリを新しいフォーマットに移行しました")
|
| 93 |
+
|
| 94 |
+
# 統計表示
|
| 95 |
+
stats = logger.get_statistics()
|
| 96 |
+
print(f"📊 総生成数: {stats['total_generations']} 枚")
|
| 97 |
+
print(f"📊 総実行時間: {stats['total_execution_time_hours']} 時間")
|
| 98 |
+
print(f"📊 平均実行時間: {stats['average_execution_time_seconds']} 秒")
|
| 99 |
+
|
| 100 |
+
# 旧ファイルをリネーム
|
| 101 |
+
old_archived = "logs/generation_history_old.json"
|
| 102 |
+
os.rename(old_log_file, old_archived)
|
| 103 |
+
print(f"📦 旧ログファイルをアーカイブしました: {old_archived}")
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def test_new_logger():
|
| 107 |
+
"""新しいログ機能のテスト"""
|
| 108 |
+
print("\n🧪 新しいログ機能のテスト...")
|
| 109 |
+
|
| 110 |
+
logger = UnifiedLogger()
|
| 111 |
+
|
| 112 |
+
# テストエントリ
|
| 113 |
+
test_params = {
|
| 114 |
+
"num_inference_steps": 25,
|
| 115 |
+
"guidance_scale": 7.5,
|
| 116 |
+
"width": 1024,
|
| 117 |
+
"height": 1024,
|
| 118 |
+
"seed": 12345,
|
| 119 |
+
"scheduler_type": "DPMSolver",
|
| 120 |
+
"eta": 0.0,
|
| 121 |
+
"torch_dtype": "float16",
|
| 122 |
+
"enable_xformers": True,
|
| 123 |
+
"enable_cpu_offload": False
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
test_info = {
|
| 127 |
+
"test_mode": True,
|
| 128 |
+
"description": "ログ機能テスト"
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
generation_id = logger.log_generation(
|
| 132 |
+
prompt="test prompt for new logging system",
|
| 133 |
+
negative_prompt="low quality, test",
|
| 134 |
+
parameters=test_params,
|
| 135 |
+
output_filepath="", # テストなのでファイルなし
|
| 136 |
+
execution_time=45.67,
|
| 137 |
+
additional_info=test_info
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
print(f"✅ テストエントリ作成: {generation_id}")
|
| 141 |
+
|
| 142 |
+
# 作成したエントリを取得してテスト
|
| 143 |
+
entry = logger.get_generation_by_id(generation_id)
|
| 144 |
+
if entry:
|
| 145 |
+
print("✅ エントリ取得テスト成功")
|
| 146 |
+
print(f" プロンプト: {entry['prompts']['main_prompt'][:50]}...")
|
| 147 |
+
print(f" 実行時間: {entry['performance']['execution_time_seconds']} 秒")
|
| 148 |
+
print(f" パラメータ数: {len(entry['parameters'])} 個")
|
| 149 |
+
|
| 150 |
+
# 検索テスト
|
| 151 |
+
search_results = logger.search_by_prompt("test prompt")
|
| 152 |
+
print(f"✅ 検索テスト: {len(search_results)} 件ヒット")
|
| 153 |
+
|
| 154 |
+
print("🎉 新しいログ機能のテスト完了!")
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
if __name__ == "__main__":
|
| 158 |
+
print("🔄 ログ移行プロセスを開始...")
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
migrate_old_logs()
|
| 162 |
+
test_new_logger()
|
| 163 |
+
|
| 164 |
+
print("\n" + "=" * 60)
|
| 165 |
+
print("🎊 ログ移行が正常に完了しました!")
|
| 166 |
+
print("📄 新しいログファイル: logs/unified_generation_history.json")
|
| 167 |
+
print("📄 バックアップファイル: logs/generation_history_backup.json")
|
| 168 |
+
print("📄 旧ファイル: logs/generation_history_old.json")
|
| 169 |
+
|
| 170 |
+
except Exception as e:
|
| 171 |
+
print(f"❌ 移行プロセスでエラーが発生しました: {e}")
|
| 172 |
+
import traceback
|
| 173 |
+
traceback.print_exc()
|
utils/test_high_quality_generation.py
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
高品質画像生成テスト用スクリプト
|
| 3 |
+
aipicasso/jagirlモデルを使用した高品質なアニメ画像生成のテスト
|
| 4 |
+
|
| 5 |
+
=== パラメータ詳細解説 ===
|
| 6 |
+
|
| 7 |
+
🔧 Sampling Method (スケジューラー):
|
| 8 |
+
- DDIM: 高品質、少ないステップで良い結果
|
| 9 |
+
- DPMSolver: 高速で高品質(推奨)
|
| 10 |
+
- Euler: 安定した結果
|
| 11 |
+
- EulerA: より多様な結果
|
| 12 |
+
- LMS: 古典的手法
|
| 13 |
+
- PNDM: デフォルト
|
| 14 |
+
|
| 15 |
+
📊 Sampling Steps (num_inference_steps): 10-150
|
| 16 |
+
- 少ない (10-20): 高速だが品質低め
|
| 17 |
+
- 中程度 (25-40): バランス良好(推奨)
|
| 18 |
+
- 多い (50-150): 高品質だが時間かかる
|
| 19 |
+
|
| 20 |
+
🎲 Seed (generator):
|
| 21 |
+
- 同じシード = 同じ画像(再現性)
|
| 22 |
+
- ランダムシード = バリエーション
|
| 23 |
+
|
| 24 |
+
⚙️ CFG Scale (guidance_scale): 1-20
|
| 25 |
+
- 低い (3-5): プロンプトに緩く従う、自然
|
| 26 |
+
- 中程度 (7-10): バランス良好(推奨)
|
| 27 |
+
- 高い (12-20): プロンプトに厳密に従う
|
| 28 |
+
|
| 29 |
+
🔧 その他:
|
| 30 |
+
- eta: ノイズ制御 (0.0-1.0)
|
| 31 |
+
- width/height: 画像サイズ (64の倍数推奨)
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
import torch
|
| 35 |
+
from diffusers import StableDiffusionXLPipeline
|
| 36 |
+
from huggingface_hub import login
|
| 37 |
+
import os
|
| 38 |
+
from datetime import datetime
|
| 39 |
+
import random
|
| 40 |
+
import json
|
| 41 |
+
import logging
|
| 42 |
+
from unified_logger import get_logger
|
| 43 |
+
|
| 44 |
+
def setup_model():
|
| 45 |
+
"""モデルのセットアップと最適化"""
|
| 46 |
+
print("🔧 モデルをセットアップ中...")
|
| 47 |
+
|
| 48 |
+
# HuggingFace認証(必要に応じてトークンを設定)
|
| 49 |
+
# login(token="your_token_here")
|
| 50 |
+
|
| 51 |
+
# モデルロード(SDXL用設定)
|
| 52 |
+
try:
|
| 53 |
+
print("🔄 SDXL設定でモデルロード中...")
|
| 54 |
+
pipe = StableDiffusionXLPipeline.from_pretrained(
|
| 55 |
+
"aipicasso/jagirl",
|
| 56 |
+
torch_dtype=torch.float16,
|
| 57 |
+
use_safetensors=True,
|
| 58 |
+
variant="fp16"
|
| 59 |
+
)
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"⚠️ FP16でのロードに失敗、標準設定で再試行: {e}")
|
| 62 |
+
try:
|
| 63 |
+
pipe = StableDiffusionXLPipeline.from_pretrained("aipicasso/jagirl")
|
| 64 |
+
except Exception as e2:
|
| 65 |
+
print(f"❌ モデルロードに失敗: {e2}")
|
| 66 |
+
return None
|
| 67 |
+
|
| 68 |
+
# GPUに手動で移動
|
| 69 |
+
if torch.cuda.is_available():
|
| 70 |
+
pipe = pipe.to("cuda")
|
| 71 |
+
print(f"✅ モデルをGPUに移動: {torch.cuda.get_device_name(0)}")
|
| 72 |
+
|
| 73 |
+
# GPU移動後にFP16に変換
|
| 74 |
+
try:
|
| 75 |
+
pipe = pipe.to(dtype=torch.float16)
|
| 76 |
+
print("✅ FP16モードに変換")
|
| 77 |
+
except:
|
| 78 |
+
print("⚠️ FP16変換をスキップ、FP32で継続")
|
| 79 |
+
else:
|
| 80 |
+
print("❌ CUDAが利用できません")
|
| 81 |
+
return None
|
| 82 |
+
|
| 83 |
+
# パフォーマンス最適化
|
| 84 |
+
try:
|
| 85 |
+
pipe.enable_xformers_memory_efficient_attention()
|
| 86 |
+
print("✅ xFormers有効化")
|
| 87 |
+
except:
|
| 88 |
+
print("⚠️ xFormersが利用できません")
|
| 89 |
+
|
| 90 |
+
# CPU Offloadは無効化(全てGPUで処理)
|
| 91 |
+
# try:
|
| 92 |
+
# pipe.enable_model_cpu_offload()
|
| 93 |
+
# print("✅ CPU Offload有効化")
|
| 94 |
+
# except:
|
| 95 |
+
# print("⚠️ CPU Offloadが利用できません")
|
| 96 |
+
print("🎯 GPU専用モードで動作")
|
| 97 |
+
|
| 98 |
+
print(f"🎯 モデルセットアップ完了 - デバイス: {pipe.device}")
|
| 99 |
+
return pipe
|
| 100 |
+
|
| 101 |
+
def check_gpu_status():
|
| 102 |
+
"""GPU状態の確認"""
|
| 103 |
+
if torch.cuda.is_available():
|
| 104 |
+
gpu_name = torch.cuda.get_device_name(0)
|
| 105 |
+
memory_allocated = torch.cuda.memory_allocated(0) / 1024**3
|
| 106 |
+
memory_total = torch.cuda.get_device_properties(0).total_memory / 1024**3
|
| 107 |
+
|
| 108 |
+
print(f"🖥️ GPU: {gpu_name}")
|
| 109 |
+
print(f"💾 VRAM使用量: {memory_allocated:.1f}GB / {memory_total:.1f}GB")
|
| 110 |
+
return True
|
| 111 |
+
else:
|
| 112 |
+
print("❌ CUDAが利用できません")
|
| 113 |
+
return False
|
| 114 |
+
|
| 115 |
+
# 古いログ機能は統一ログ機能に統合されました
|
| 116 |
+
|
| 117 |
+
# 古いログ機能は統一ログ機能に統合されました
|
| 118 |
+
|
| 119 |
+
def setup_scheduler(pipe, scheduler_type):
|
| 120 |
+
"""
|
| 121 |
+
スケジューラーの設定(SDXL対応)
|
| 122 |
+
|
| 123 |
+
各スケジューラーの特徴:
|
| 124 |
+
- DDIM: 高品質、少ないステップで良い結果(20-30ステップ推奨)
|
| 125 |
+
- DPMSolver: 高速で高品質、最もバランスが良い(15-25ステップ推奨)
|
| 126 |
+
- Euler: 安定した結果、予測しやすい(30-50ステップ推奨)
|
| 127 |
+
- EulerA: より多様で芸術的な結果(25-40ステップ推奨)
|
| 128 |
+
- LMS: 古典的手法、安定しているが遅い(50+ステップ推奨)
|
| 129 |
+
- PNDM: デフォルト、標準的な品質(30-50ステップ推奨)
|
| 130 |
+
"""
|
| 131 |
+
from diffusers import (
|
| 132 |
+
DDIMScheduler, DPMSolverMultistepScheduler,
|
| 133 |
+
EulerDiscreteScheduler, EulerAncestralDiscreteScheduler,
|
| 134 |
+
LMSDiscreteScheduler, PNDMScheduler
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
schedulers = {
|
| 138 |
+
"DDIM": DDIMScheduler,
|
| 139 |
+
"DPMSolver": DPMSolverMultistepScheduler,
|
| 140 |
+
"Euler": EulerDiscreteScheduler,
|
| 141 |
+
"EulerA": EulerAncestralDiscreteScheduler,
|
| 142 |
+
"PNDM": PNDMScheduler
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
# LMSはscipyが必要なため、利用可能な場合のみ追加
|
| 146 |
+
try:
|
| 147 |
+
schedulers["LMS"] = LMSDiscreteScheduler
|
| 148 |
+
except:
|
| 149 |
+
print("⚠️ LMSスケジューラーは利用できません (scipyが必要)")
|
| 150 |
+
|
| 151 |
+
if scheduler_type in schedulers:
|
| 152 |
+
try:
|
| 153 |
+
return schedulers[scheduler_type].from_config(pipe.scheduler.config)
|
| 154 |
+
except ImportError as e:
|
| 155 |
+
print(f"⚠️ {scheduler_type}スケジューラーが利用できません: {e}")
|
| 156 |
+
return pipe.scheduler
|
| 157 |
+
return pipe.scheduler
|
| 158 |
+
|
| 159 |
+
def generate_high_quality_image(pipe, prompt, negative_prompt="", seed=None, output_dir="outputs",
|
| 160 |
+
num_inference_steps=50, guidance_scale=7.5, width=1024, height=1024,
|
| 161 |
+
scheduler_type="default", eta=0.0):
|
| 162 |
+
"""
|
| 163 |
+
高品質画像生成(詳細パラメータ対応)
|
| 164 |
+
|
| 165 |
+
Args:
|
| 166 |
+
pipe: StableDiffusionPipeline
|
| 167 |
+
prompt: プロンプト
|
| 168 |
+
negative_prompt: ネガティブプロンプト
|
| 169 |
+
seed: シード値
|
| 170 |
+
output_dir: 出力ディレクトリ
|
| 171 |
+
num_inference_steps: サンプリングステップ数 (10-150)
|
| 172 |
+
guidance_scale: CFG Scale/ガイダンス強度 (1-20)
|
| 173 |
+
width, height: 画像サイズ
|
| 174 |
+
scheduler_type: スケジューラータイプ (DDIM, DPMSolver, Euler, EulerA, LMS, PNDM)
|
| 175 |
+
eta: ノイズ制御 (0.0-1.0)
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
# 出力ディレクトリ作成
|
| 179 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 180 |
+
|
| 181 |
+
# シード設定
|
| 182 |
+
if seed is None:
|
| 183 |
+
seed = random.randint(0, 1000000)
|
| 184 |
+
|
| 185 |
+
generator = torch.Generator(device="cuda").manual_seed(seed)
|
| 186 |
+
|
| 187 |
+
# スケジューラー設定
|
| 188 |
+
original_scheduler = pipe.scheduler
|
| 189 |
+
if scheduler_type != "default":
|
| 190 |
+
pipe.scheduler = setup_scheduler(pipe, scheduler_type)
|
| 191 |
+
|
| 192 |
+
print(f"🎨 画像生成開始")
|
| 193 |
+
print(f"📝 プロンプト: {prompt}")
|
| 194 |
+
print(f"🔧 設定: steps={num_inference_steps}, cfg={guidance_scale}, seed={seed}")
|
| 195 |
+
print(f"� サイズ: {width}x{height}, スケジューラー: {scheduler_type}")
|
| 196 |
+
|
| 197 |
+
# 高品質設定での画像生成(時間測定付き)
|
| 198 |
+
import time
|
| 199 |
+
start_time = time.time()
|
| 200 |
+
|
| 201 |
+
try:
|
| 202 |
+
image = pipe(
|
| 203 |
+
prompt=prompt,
|
| 204 |
+
negative_prompt=negative_prompt,
|
| 205 |
+
num_inference_steps=num_inference_steps, # Sampling Steps
|
| 206 |
+
guidance_scale=guidance_scale, # CFG Scale (ガイダンス強度)
|
| 207 |
+
width=width,
|
| 208 |
+
height=height,
|
| 209 |
+
generator=generator # Seed制御
|
| 210 |
+
).images[0]
|
| 211 |
+
except Exception as e:
|
| 212 |
+
print(f"⚠️ 生成エラー、シンプルな設定で再試行: {e}")
|
| 213 |
+
# 最小限のパラメータで再試行
|
| 214 |
+
image = pipe(
|
| 215 |
+
prompt=prompt,
|
| 216 |
+
num_inference_steps=20,
|
| 217 |
+
guidance_scale=7.5,
|
| 218 |
+
generator=generator
|
| 219 |
+
).images[0]
|
| 220 |
+
|
| 221 |
+
end_time = time.time()
|
| 222 |
+
execution_time = end_time - start_time
|
| 223 |
+
|
| 224 |
+
# スケジューラーを元に戻す
|
| 225 |
+
pipe.scheduler = original_scheduler
|
| 226 |
+
|
| 227 |
+
# ファイル名生成
|
| 228 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 229 |
+
filename = f"jagirl_{timestamp}_seed{seed}.png"
|
| 230 |
+
filepath = os.path.join(output_dir, filename)
|
| 231 |
+
|
| 232 |
+
# 画像保存
|
| 233 |
+
image.save(filepath)
|
| 234 |
+
print(f"💾 画像保存: {filepath}")
|
| 235 |
+
|
| 236 |
+
# VRAM使用量取得
|
| 237 |
+
vram_usage = torch.cuda.memory_allocated(0) / 1024**3 if torch.cuda.is_available() else 0
|
| 238 |
+
|
| 239 |
+
# 統一ログ機能で詳細記録
|
| 240 |
+
logger = get_logger()
|
| 241 |
+
|
| 242 |
+
# GPU最適化設定の記録
|
| 243 |
+
additional_info = {
|
| 244 |
+
"gpu_optimizations": {
|
| 245 |
+
"fp16_enabled": hasattr(pipe, 'dtype') and pipe.dtype == torch.float16,
|
| 246 |
+
"xformers_enabled": getattr(pipe, '_use_xformers', False),
|
| 247 |
+
"cpu_offload_enabled": getattr(pipe, '_cpu_offload', False)
|
| 248 |
+
},
|
| 249 |
+
"generation_mode": "high_quality_test"
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
params = {
|
| 253 |
+
"negative_prompt": negative_prompt,
|
| 254 |
+
"scheduler_type": scheduler_type,
|
| 255 |
+
"num_inference_steps": num_inference_steps,
|
| 256 |
+
"guidance_scale": guidance_scale,
|
| 257 |
+
"width": width,
|
| 258 |
+
"height": height,
|
| 259 |
+
"seed": seed,
|
| 260 |
+
"eta": eta,
|
| 261 |
+
"torch_dtype": str(pipe.dtype) if hasattr(pipe, 'dtype') else "unknown",
|
| 262 |
+
"enable_xformers": getattr(pipe, '_use_xformers', False),
|
| 263 |
+
"enable_cpu_offload": getattr(pipe, '_cpu_offload', False)
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
generation_id = logger.log_generation(
|
| 267 |
+
prompt=prompt,
|
| 268 |
+
negative_prompt=negative_prompt,
|
| 269 |
+
parameters=params,
|
| 270 |
+
output_filepath=filepath,
|
| 271 |
+
execution_time=execution_time,
|
| 272 |
+
additional_info=additional_info
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
print(f"📋 ログ記録完了: {generation_id}")
|
| 276 |
+
|
| 277 |
+
return image, filepath
|
| 278 |
+
|
| 279 |
+
def test_various_prompts(pipe):
|
| 280 |
+
"""様々なプロンプトでテスト生成"""
|
| 281 |
+
|
| 282 |
+
# 日本女性(顔つきにフォーカス)向けプロンプト例を追加
|
| 283 |
+
test_prompts = [
|
| 284 |
+
{
|
| 285 |
+
"prompt": "日本人女性の顔立ち, 美しい顔, 繊細な瞳, 自然な黒髪, 柔らかな表情, リアリスティックな肌質",
|
| 286 |
+
"negative": "low quality, blurry, ugly, deformed, western features, caucasian",
|
| 287 |
+
"description": "基本的な日本女性の顔つき (SDXL)",
|
| 288 |
+
"scheduler": "default",
|
| 289 |
+
"steps": 25,
|
| 290 |
+
"cfg": 7.0,
|
| 291 |
+
"width": 1024,
|
| 292 |
+
"height": 1024
|
| 293 |
+
},
|
| 294 |
+
{
|
| 295 |
+
"prompt": "日本人女性, 上品で優しい顔立ち, アーモンド形の瞳, ストレート黒髪, 控えめなメイク",
|
| 296 |
+
"negative": "low quality, blurry, ugly, deformed, western features, heavy makeup",
|
| 297 |
+
"description": "上品な日本女性 (DPMSolver SDXL)",
|
| 298 |
+
"scheduler": "DPMSolver",
|
| 299 |
+
"steps": 20,
|
| 300 |
+
"cfg": 7.5,
|
| 301 |
+
"width": 1024,
|
| 302 |
+
"height": 1024
|
| 303 |
+
},
|
| 304 |
+
{
|
| 305 |
+
"prompt": "日本人の若い女性, 愛らしい笑顔, 小さめの鼻, 優しい茶色の瞳, 肩にかかる髪",
|
| 306 |
+
"negative": "low quality, blurry, ugly, deformed, aged, wrinkles, western features",
|
| 307 |
+
"description": "若々しい日本女性 (Euler SDXL)",
|
| 308 |
+
"scheduler": "Euler",
|
| 309 |
+
"steps": 30,
|
| 310 |
+
"cfg": 6.0,
|
| 311 |
+
"width": 1024,
|
| 312 |
+
"height": 1024
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
"prompt": "落ち着いた日本人女性, 大人の魅力, きちんとした顔立ち, はっきりした頬骨, 長い黒髪, プロフェッショナルな雰囲気",
|
| 316 |
+
"negative": "low quality, blurry, ugly, deformed, childish, western features",
|
| 317 |
+
"description": "大人っぽい日本女性 (EulerA SDXL)",
|
| 318 |
+
"scheduler": "EulerA",
|
| 319 |
+
"steps": 25,
|
| 320 |
+
"cfg": 8.0,
|
| 321 |
+
"width": 1024,
|
| 322 |
+
"height": 1024
|
| 323 |
+
}
|
| 324 |
+
]
|
| 325 |
+
|
| 326 |
+
print(f"\n🎯 {len(test_prompts)}パターンのテスト生成を開始...")
|
| 327 |
+
|
| 328 |
+
results = []
|
| 329 |
+
for i, test_case in enumerate(test_prompts, 1):
|
| 330 |
+
print(f"\n--- テスト {i}/{len(test_prompts)}: {test_case['description']} ---")
|
| 331 |
+
|
| 332 |
+
image, filepath = generate_high_quality_image(
|
| 333 |
+
pipe=pipe,
|
| 334 |
+
prompt=test_case["prompt"],
|
| 335 |
+
negative_prompt=test_case["negative"],
|
| 336 |
+
seed=42 + i, # 再現可能なシード
|
| 337 |
+
num_inference_steps=test_case["steps"],
|
| 338 |
+
guidance_scale=test_case["cfg"],
|
| 339 |
+
scheduler_type=test_case["scheduler"]
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
results.append({
|
| 343 |
+
"description": test_case["description"],
|
| 344 |
+
"filepath": filepath,
|
| 345 |
+
"prompt": test_case["prompt"],
|
| 346 |
+
"scheduler": test_case["scheduler"],
|
| 347 |
+
"steps": test_case["steps"],
|
| 348 |
+
"cfg": test_case["cfg"]
|
| 349 |
+
})
|
| 350 |
+
|
| 351 |
+
# VRAM使用量チェック
|
| 352 |
+
if torch.cuda.is_available():
|
| 353 |
+
memory_used = torch.cuda.memory_allocated(0) / 1024**3
|
| 354 |
+
print(f"📊 現在のVRAM使用量: {memory_used:.1f}GB")
|
| 355 |
+
|
| 356 |
+
return results
|
| 357 |
+
|
| 358 |
+
def test_scheduler_comparison(pipe):
|
| 359 |
+
"""各種スケジューラーの比較テスト"""
|
| 360 |
+
print("\n🔄 スケジューラー比較テスト開始...")
|
| 361 |
+
|
| 362 |
+
base_prompt = "anime girl, beautiful face, detailed eyes, colorful hair"
|
| 363 |
+
negative_prompt = "low quality, blurry, ugly"
|
| 364 |
+
|
| 365 |
+
schedulers = ["default", "DDIM", "DPMSolver", "Euler", "EulerA", "LMS"]
|
| 366 |
+
results = []
|
| 367 |
+
|
| 368 |
+
for scheduler in schedulers:
|
| 369 |
+
print(f"\n🔧 テスト中: {scheduler}")
|
| 370 |
+
|
| 371 |
+
image, filepath = generate_high_quality_image(
|
| 372 |
+
pipe=pipe,
|
| 373 |
+
prompt=base_prompt,
|
| 374 |
+
negative_prompt=negative_prompt,
|
| 375 |
+
seed=12345, # 固定シードで比較
|
| 376 |
+
num_inference_steps=30,
|
| 377 |
+
guidance_scale=7.5,
|
| 378 |
+
scheduler_type=scheduler
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
results.append({
|
| 382 |
+
"scheduler": scheduler,
|
| 383 |
+
"filepath": filepath
|
| 384 |
+
})
|
| 385 |
+
|
| 386 |
+
print(f"\n✅ スケジューラー比較完了: {len(results)} パターン")
|
| 387 |
+
return results
|
| 388 |
+
|
| 389 |
+
def test_parameter_variations(pipe):
|
| 390 |
+
"""パラメータバリエーションテスト"""
|
| 391 |
+
print("\n⚙️ パラメータバリエーションテスト開始...")
|
| 392 |
+
|
| 393 |
+
base_prompt = "anime girl, school uniform, detailed"
|
| 394 |
+
negative_prompt = "low quality, blurry"
|
| 395 |
+
|
| 396 |
+
# CFG Scale テスト
|
| 397 |
+
cfg_tests = [3.0, 5.0, 7.5, 10.0, 15.0]
|
| 398 |
+
print("\n📊 CFG Scale テスト:")
|
| 399 |
+
|
| 400 |
+
for cfg in cfg_tests:
|
| 401 |
+
print(f" CFG: {cfg}")
|
| 402 |
+
image, filepath = generate_high_quality_image(
|
| 403 |
+
pipe=pipe,
|
| 404 |
+
prompt=base_prompt,
|
| 405 |
+
negative_prompt=negative_prompt,
|
| 406 |
+
seed=54321,
|
| 407 |
+
guidance_scale=cfg,
|
| 408 |
+
num_inference_steps=25
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
# Steps テスト
|
| 412 |
+
steps_tests = [10, 20, 30, 50, 80]
|
| 413 |
+
print("\n🔢 Steps テスト:")
|
| 414 |
+
|
| 415 |
+
for steps in steps_tests:
|
| 416 |
+
print(f" Steps: {steps}")
|
| 417 |
+
image, filepath = generate_high_quality_image(
|
| 418 |
+
pipe=pipe,
|
| 419 |
+
prompt=base_prompt,
|
| 420 |
+
negative_prompt=negative_prompt,
|
| 421 |
+
seed=98765,
|
| 422 |
+
num_inference_steps=steps,
|
| 423 |
+
guidance_scale=7.5
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
def benchmark_generation_speed(pipe):
|
| 427 |
+
"""生成速度のベンチマーク"""
|
| 428 |
+
print("\n⚡ 生成速度ベンチマーク開始...")
|
| 429 |
+
|
| 430 |
+
prompt = "anime girl, beautiful face, detailed"
|
| 431 |
+
negative_prompt = "low quality, blurry"
|
| 432 |
+
|
| 433 |
+
import time
|
| 434 |
+
|
| 435 |
+
# ウォームアップ
|
| 436 |
+
print("🔥 ウォームアップ中...")
|
| 437 |
+
_ = pipe(prompt, num_inference_steps=10, width=256, height=256)
|
| 438 |
+
|
| 439 |
+
# ベンチマーク実行
|
| 440 |
+
step_counts = [10, 20, 30, 50]
|
| 441 |
+
|
| 442 |
+
for steps in step_counts:
|
| 443 |
+
start_time = time.time()
|
| 444 |
+
|
| 445 |
+
_ = pipe(
|
| 446 |
+
prompt=prompt,
|
| 447 |
+
negative_prompt=negative_prompt,
|
| 448 |
+
num_inference_steps=steps,
|
| 449 |
+
width=512,
|
| 450 |
+
height=512
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
elapsed_time = time.time() - start_time
|
| 454 |
+
print(f"📈 {steps}ステップ: {elapsed_time:.2f}秒")
|
| 455 |
+
|
| 456 |
+
# VRAM使用量クリア
|
| 457 |
+
torch.cuda.empty_cache()
|
| 458 |
+
|
| 459 |
+
def simple_test(pipe):
|
| 460 |
+
"""シンプルなテスト生成(SDXL対応)"""
|
| 461 |
+
print("\n🔍 シンプルテスト開始...")
|
| 462 |
+
|
| 463 |
+
try:
|
| 464 |
+
# SDXL用最小限のパラメータでテスト(日本女性特化)
|
| 465 |
+
print("🎨 SDXL最小限設定で生成中(日本女性の顔つき)...")
|
| 466 |
+
image = pipe(
|
| 467 |
+
"japanese woman, beautiful face, natural features",
|
| 468 |
+
negative_prompt="western features, caucasian, low quality",
|
| 469 |
+
width=1024, # SDXL推奨サイズ
|
| 470 |
+
height=1024, # SDXL推奨サイズ
|
| 471 |
+
num_inference_steps=20,
|
| 472 |
+
guidance_scale=7.0
|
| 473 |
+
).images[0]
|
| 474 |
+
|
| 475 |
+
# 保存
|
| 476 |
+
os.makedirs("outputs", exist_ok=True)
|
| 477 |
+
filepath = "outputs/simple_test_sdxl.png"
|
| 478 |
+
image.save(filepath)
|
| 479 |
+
print(f"✅ シンプルテスト成功: {filepath}")
|
| 480 |
+
return True
|
| 481 |
+
except Exception as e:
|
| 482 |
+
print(f"❌ シンプルテスト失敗: {e}")
|
| 483 |
+
import traceback
|
| 484 |
+
traceback.print_exc()
|
| 485 |
+
return False
|
| 486 |
+
|
| 487 |
+
def main():
|
| 488 |
+
"""メイン実行関数"""
|
| 489 |
+
print("🚀 高品質画像生成テストを開始...")
|
| 490 |
+
print("=" * 50)
|
| 491 |
+
|
| 492 |
+
# 統一ログ機能の初期化
|
| 493 |
+
logger = get_logger()
|
| 494 |
+
print("📋 統一ログ機能を初期化しました")
|
| 495 |
+
print(f"📄 ログファイル: {logger.json_log_file}")
|
| 496 |
+
|
| 497 |
+
# 統計情報の表示
|
| 498 |
+
stats = logger.get_statistics()
|
| 499 |
+
print(f"📊 これまでの生成総数: {stats.get('total_generations', 0)}枚")
|
| 500 |
+
|
| 501 |
+
# GPU状態確認
|
| 502 |
+
if not check_gpu_status():
|
| 503 |
+
print("❌ GPU環境が必要です")
|
| 504 |
+
return
|
| 505 |
+
|
| 506 |
+
try:
|
| 507 |
+
# モデルセットアップ
|
| 508 |
+
pipe = setup_model()
|
| 509 |
+
|
| 510 |
+
if pipe is None:
|
| 511 |
+
print("❌ モデルセットアップに失敗しました")
|
| 512 |
+
return
|
| 513 |
+
|
| 514 |
+
# まずシンプルテスト
|
| 515 |
+
if not simple_test(pipe):
|
| 516 |
+
print("❌ シンプルテストに失敗、処理を中止します")
|
| 517 |
+
return
|
| 518 |
+
|
| 519 |
+
# テスト生成実行
|
| 520 |
+
results = test_various_prompts(pipe)
|
| 521 |
+
|
| 522 |
+
# スケジューラー比較テスト
|
| 523 |
+
scheduler_results = test_scheduler_comparison(pipe)
|
| 524 |
+
|
| 525 |
+
# パラメータバリエーションテスト
|
| 526 |
+
test_parameter_variations(pipe)
|
| 527 |
+
|
| 528 |
+
# 速度ベンチマーク
|
| 529 |
+
benchmark_generation_speed(pipe)
|
| 530 |
+
|
| 531 |
+
# 結果サマリー
|
| 532 |
+
print("\n" + "=" * 50)
|
| 533 |
+
print("✅ テスト完了! 生成された画像:")
|
| 534 |
+
for result in results:
|
| 535 |
+
print(f" 📄 {result['description']}: {result['filepath']}")
|
| 536 |
+
|
| 537 |
+
print(f"\n🎯 合計 {len(results)} 枚の画像を生成しました")
|
| 538 |
+
|
| 539 |
+
# 最終VRAM使用量
|
| 540 |
+
if torch.cuda.is_available():
|
| 541 |
+
final_memory = torch.cuda.memory_allocated(0) / 1024**3
|
| 542 |
+
print(f"💾 最終VRAM使用量: {final_memory:.1f}GB")
|
| 543 |
+
|
| 544 |
+
except Exception as e:
|
| 545 |
+
print(f"❌ エラーが発生しました: {str(e)}")
|
| 546 |
+
import traceback
|
| 547 |
+
traceback.print_exc()
|
| 548 |
+
|
| 549 |
+
finally:
|
| 550 |
+
# メモリクリーンアップ
|
| 551 |
+
if torch.cuda.is_available():
|
| 552 |
+
torch.cuda.empty_cache()
|
| 553 |
+
print("🧹 VRAMキャッシュをクリアしました")
|
| 554 |
+
|
| 555 |
+
if __name__ == "__main__":
|
| 556 |
+
main()
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|