kechiro commited on
Commit
f8e0ff8
·
0 Parent(s):

Initial clean commit

Browse files
.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'>&#128188;</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
+ ![alt text](imgs/test2image.png)
9
+
10
+ - Image to Image
11
+ ![alt text](imgs/test2image.png)
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'>&#128188;</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