Slicelayers commited on
Commit
73a5aa8
·
verified ·
1 Parent(s): 83f12a5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +258 -20
app.py CHANGED
@@ -1,37 +1,275 @@
1
- import requests
 
 
 
 
2
  import base64
 
3
  import json
4
- import os
 
5
 
6
- API_KEY = os.getenv("NANO_NABABA_API_KEY") # Hugging Face Secrets に登録したキー
7
- API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent"
8
 
9
- def generate_completion(image_path, prompt="欠けている部分を補完してください。"):
10
- with open(image_path, "rb") as f:
11
- image_data = base64.b64encode(f.read()).decode("utf-8")
 
 
12
 
13
- headers = {
14
- "Content-Type": "application/json",
15
- "x-goog-api-key": API_KEY
16
- }
 
 
 
 
 
 
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  payload = {
19
  "contents": [
20
  {
21
  "parts": [
22
- {"text": prompt},
23
  {
24
- "inline_data": {
25
- "mime_type": "image/png",
26
- "data": image_data
 
 
 
27
  }
28
  }
29
  ]
30
  }
31
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- response = requests.post(API_URL, headers=headers, json=payload)
35
- print("Response status:", response.status_code)
36
- print(response.text)
37
- return response
 
 
1
+ # coding: utf-8
2
+ import gradio as gr
3
+ from PIL import Image, ImageDraw, ImageFont
4
+ import numpy as np
5
+ import io
6
  import base64
7
+ import zipfile
8
  import json
9
+ import time
10
+ import asyncio # asyncioをインポートしてfetchを非同期処理で代替する準備 (HuggingFace環境でのfetchはPythonの外部ライブラリが必要な場合があるため、ここではPythonのrequestsやhttpxの利用を検討すべきですが、構造維持のためasyncioを利用します)
11
 
12
+ # --- 定数とAPI設定 (Constants and API Configuration) ---
 
13
 
14
+ # Nano Banana API (gemini-2.5-flash-image-preview) の設定
15
+ # Canvas環境でAPIキーが自動注入されるため、ここでは空の文字列として保持します。
16
+ # 実際には、Hugging Face SpacesのシークレットとしてAPIキーを設定し、os.environから読み込むのが推奨されます。
17
+ API_KEY = ""
18
+ API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent?key="
19
 
20
+ # --- ヘルパー関数 (Helper Functions) ---
21
+
22
+ def pil_to_base64(img: Image.Image) -> str:
23
+ """PIL画像をBase64エンコードされたPNGデータに変換します。"""
24
+ buffered = io.BytesIO()
25
+ # アルファチャンネル(PNG)を維持
26
+ if img.mode != 'RGBA':
27
+ img = img.convert('RGBA')
28
+ img.save(buffered, format="PNG")
29
+ return base64.b64encode(buffered.getvalue()).decode("utf-8")
30
 
31
+ async def nano_banana_completion_api(base_image: Image.Image, prompt: str) -> Image.Image:
32
+ """
33
+ Nano Banana (gemini-2.5-flash-image-preview) APIを呼び出し、画像補完をシミュレートします。
34
+ ここでは、実際のAPI呼び出し構造を定義しつつ、画像への「AI補完済み」テキスト描画で処理をモックします。
35
+
36
+ NOTE: 実際のデプロイ環境では、この関数内でPythonの非同期HTTPクライアント(例: httpx)を使用して
37
+ API_URL + API_KEY への POSTリクエストを実行する必要があります。
38
+ """
39
+
40
+ # Base64エンコード
41
+ base64_image = pil_to_base64(base_image)
42
+
43
+ # プロンプトと画像を含むペイロードの構築(Image-to-Image用)
44
  payload = {
45
  "contents": [
46
  {
47
  "parts": [
 
48
  {
49
+ "text": f"画像を編集・補完してください。この画像はLive2D素材の一部です。マスクされた(透明な)領域にプロンプトに従った内容を生成し、画像を自然に完成させてください。プロンプト: {prompt}"
50
+ },
51
+ {
52
+ "inlineData": {
53
+ "mimeType": "image/png",
54
+ "data": base64_image
55
  }
56
  }
57
  ]
58
  }
59
+ ],
60
+ "generationConfig": {
61
+ "responseModalities": ["TEXT", "IMAGE"]
62
+ },
63
+ }
64
+
65
+ print(f"--- API呼び出しペイロードを構築しました。プロンプト: {prompt} ---")
66
+
67
+ # --- 実際のAPI呼び出しのシミュレーションと代替処理 ---
68
+
69
+ # 実際にはここにhttpxなどを使ったAPI呼び出しのロジックが入ります。
70
+ # API呼び出しには時間がかかるため、gr.sleepの代わりに実際のAPI呼び出し時間が必要です。
71
+ await asyncio.sleep(4) # 処理時間をシミュレート (gr.sleepをasyncio.sleepに置き換え)
72
+
73
+ # 補完処理が成功したと仮定し、モック画像を作成
74
+ completed_image = base_image.copy()
75
+ draw = ImageDraw.Draw(completed_image)
76
+
77
+ # 環境依存を避けるため、デフォルトフォントを使用
78
+ try:
79
+ font = ImageFont.truetype("arial.ttf", 40)
80
+ except IOError:
81
+ font = ImageFont.load_default()
82
+
83
+ # 画像の中央に「AI補完済み」のテキストを描画して補完を視覚的にモック
84
+ text = "AI補完済み (MOCK)"
85
+ text_color = (255, 0, 0, 200) # 半透明の赤
86
+
87
+ w, h = completed_image.size
88
+ # ImageDraw.textbbox() を使用してテキストの境界ボックスを取得
89
+ text_bbox = draw.textbbox((0, 0), text, font=font)
90
+ tw, th = text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1]
91
+
92
+ draw.text(((w - tw) / 2, (h - th) / 2), text, font=font, fill=text_color)
93
+
94
+ print("--- AI補完処理をモック完了しました ---")
95
+ return completed_image
96
+
97
+
98
+ # --- メイン処理ロジック (Main Processing Logic) ---
99
+
100
+ async def segment_and_inpaint(original_image: Image.Image, inpaint_prompt: str):
101
+ """
102
+ アップロードされた一枚絵を自動分割し、欠損部分をAI補完するメイン関数。
103
+ """
104
+ if original_image is None:
105
+ return None, None, None, "エラー: 画像がアップロードされていません。", None
106
+
107
+ # 1. 自動パーツ分割のシミュレーション (Mock Automatic Segmentation)
108
+ W, H = original_image.size
109
+
110
+ # --- Part A: 髪の毛 (Hair) を分離するマスクを作成 ---
111
+ hair_mask = Image.new('L', (W, H), 0)
112
+ draw_mask = ImageDraw.Draw(hair_mask)
113
+ draw_mask.rectangle([W * 0.1, 0, W * 0.5, H * 0.6], fill=255)
114
+
115
+ # 髪の毛パーツを作成 (マスク領域のみを抽出)
116
+ hair_part = Image.new('RGBA', (W, H), (0, 0, 0, 0))
117
+ hair_part.paste(original_image, (0, 0), hair_mask)
118
+
119
+ # --- Part B: 欠損穴のある体 (Body with Hole) を作成 ---
120
+ body_with_hole = original_image.copy().convert("RGBA")
121
+
122
+ # body_with_holeからhair_maskの領域を透明にする (ここに補完が必要)
123
+ body_with_hole_data = body_with_hole.getdata()
124
+ hair_mask_data = hair_mask.getdata()
125
+ new_data = []
126
+
127
+ for i in range(len(body_with_hole_data)):
128
+ r, g, b, a = body_with_hole_data[i]
129
+ mask_val = hair_mask_data[i]
130
+
131
+ # マスク値が高い(髪の毛領域)であれば、アルファチャンネルを0にする
132
+ if mask_val > 128:
133
+ new_data.append((r, g, b, 0))
134
+ else:
135
+ new_data.append((r, g, b, a))
136
+
137
+ body_with_hole.putdata(new_data)
138
+
139
+ print("--- 1. 自動パーツ分割 (モック) 完了: 髪の毛パーツと補完が必要な欠損体を作成 ---")
140
+
141
+ # 2. 欠損領域のAI補完 (AI Inpainting)
142
+ final_inpaint_prompt = inpaint_prompt or "マスクされた領域のキャラクターの顔と身体を自然に補完してください。"
143
+
144
+ # Nano Banana API (モック)を呼び出し
145
+ completed_body_part = await nano_banana_completion_api(body_with_hole, final_inpaint_prompt)
146
+
147
+ # 3. 出力ファイルの準備とZIPファイルの作成 (Prepare Output Files and Create ZIP)
148
+
149
+ output_parts = {
150
+ "body_completed": completed_body_part,
151
+ "hair_front": hair_part,
152
+ }
153
+
154
+ # JSON構造の作成
155
+ output_json = {
156
+ "parts": {
157
+ "body_completed": "body_completed.png",
158
+ "hair_front": "hair_front.png"
159
+ },
160
+ "inpaint_prompt_used": final_inpaint_prompt,
161
+ "timestamp": time.strftime("%Y%m%d_%H%M%S")
162
  }
163
+ json_data = json.dumps(output_json, indent=2, ensure_ascii=False)
164
+
165
+ zip_buffer = io.BytesIO()
166
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
167
+ # PNGパーツの追加
168
+ for name, img in output_parts.items():
169
+ img_buffer = io.BytesIO()
170
+ if img.mode != 'RGBA':
171
+ img = img.convert('RGBA')
172
+ img.save(img_buffer, format="PNG")
173
+ zipf.writestr(f"live2d_parts/{name}.png", img_buffer.getvalue())
174
+
175
+ # JSONファイルの追加
176
+ zipf.writestr("live2d_parts/parts_structure.json", json_data.encode('utf-8'))
177
+
178
+ zip_buffer.seek(0)
179
+ zip_file_path = f"live2d_parts_{output_json['timestamp']}.zip"
180
+
181
+ # Gradioに出力するためにファイルを一時的に保存
182
+ with open(zip_file_path, "wb") as f:
183
+ f.write(zip_buffer.read())
184
+
185
+ print(f"--- 4. 全パーツZIPファイル {zip_file_path} の作成完了 ---")
186
+
187
+ return (
188
+ completed_body_part, # 補完済みメインボディ
189
+ hair_part, # 分割された髪の毛
190
+ body_with_hole, # 補完前の欠損体 (デバッグ用)
191
+ json_data, # JSON構造
192
+ zip_file_path # ZIPファイル
193
+ )
194
+
195
+ # --- Gradioインターフェース定義 (Gradio Interface Definition) ---
196
+
197
+ # Gradioテーマ定義
198
+ theme = gr.themes.Soft(
199
+ primary_hue="blue",
200
+ secondary_hue="blue",
201
+ neutral_hue="gray",
202
+ ).set(
203
+ button_radius="xl",
204
+ input_radius="xl",
205
+ )
206
+
207
+
208
+ with gr.Blocks(theme=theme, title="Live2D素材自動分割・補完アプリ") as demo:
209
+ gr.Markdown(
210
+ """
211
+ <div style='text-align: center; margin-bottom: 20px; padding: 10px; background: #E0F7FA; border-radius: 12px;'>
212
+ <h1 style='color: #00796B; font-size: 2.5em; font-weight: 700;'>🎨 Live2D 素材 自動分割・補完アプリ 🤖</h1>
213
+ <p style='color: #004D40; font-size: 1.1em;'>一枚絵をアップロードするだけで、AIによるパーツ分割と欠損部分の自動補完(Nano Banana API利用)をシミュレートします。</p>
214
+ <p style='color: #004D40; font-size: 1.0em;'>💡 **Nano Banana (gemini-2.5-flash-image-preview) を使用した画像補完の動作構造を再現しています。**</p>
215
+ </div>
216
+ """
217
+ )
218
+
219
+ with gr.Row():
220
+ with gr.Column(scale=1):
221
+ # --- 入力エリア ---
222
+ input_image = gr.Image(type="pil", label="① 一枚絵イラストのアップロード (PNG/JPG)", height=400)
223
+ inpaint_prompt = gr.Textbox(
224
+ label="② AI補完プロンプト(オプション)",
225
+ value="マスクされた領域のキャラクターの顔と身体の肌、及び下に着ている服を、元のイラストのテイストに合わせて自然に補完してください。",
226
+ placeholder="例: マスクされた領域を元の絵柄で自然に描き足す"
227
+ )
228
+ process_button = gr.Button("③ 自動分割・補完を実行", variant="primary", scale=0)
229
+
230
+ gr.Markdown(
231
+ """
232
+ ### 🛠️ 手動再分割機能について
233
+ このデモでは未実装ですが、本番環境では、アップロード画像に対するキャンバス操作(ブラシや矩形ツール)を通じて、ユーザーが追加でパーツを指定し、再補完を行う機能がコア機能として実装されます。
234
+ """
235
+ )
236
+
237
+ with gr.Column(scale=2):
238
+ # --- 出力エリア ---
239
+ gr.Markdown("## 💡 処理結果 (自動分割・補完済パーツ)")
240
+
241
+ with gr.Tabs():
242
+ with gr.TabItem("メインパーツ (AI補完結果)"):
243
+ completed_body_output = gr.Image(label="補完済みメインボディパーツ (AI Inpainting)", height=300)
244
+
245
+ with gr.TabItem("分割パーツ例 (前髪)"):
246
+ hair_part_output = gr.Image(label="分割された髪の毛パーツ", height=300)
247
+
248
+ with gr.TabItem("欠損体 (補完AI入力)"):
249
+ body_with_hole_output = gr.Image(label="補完前の欠損体 (補完AIへの入力画像)", height=300)
250
+
251
+ with gr.Row():
252
+ download_zip = gr.File(label="全パーツZIPダウンロード", file_count="single")
253
+
254
+ gr.Markdown("## 📋 出力レイヤー構造 (JSON)")
255
+ output_json = gr.JSON(label="Live2Dパーツ構造JSON")
256
+
257
+
258
+ # --- イベントリスナー ---
259
+ process_button.click(
260
+ fn=segment_and_inpaint,
261
+ inputs=[input_image, inpaint_prompt],
262
+ outputs=[
263
+ completed_body_output,
264
+ hair_part_output,
265
+ body_with_hole_output,
266
+ output_json,
267
+ download_zip
268
+ ]
269
+ )
270
 
271
+ # デモの起動 (Hugging Face Spacesでの実行を想定)
272
+ if __name__ == "__main__":
273
+ # Gradioでは、非同期関数を直接.click()に渡すことができるため、asyncio.runは不要です。
274
+ # ただし、asyncio.sleepを使用したため、gr.Blocksの実行にはasyncioが必要です。
275
+ demo.launch(share=False)