Slicelayers commited on
Commit
ca28d65
·
verified ·
1 Parent(s): c7396ab

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -45
app.py CHANGED
@@ -9,7 +9,8 @@ import json
9
  import time
10
  import asyncio
11
  import os
12
- import cv2 # ★★★ 変更点1: OpenCVを追加
 
13
 
14
  # --- 定数とAPI設定 (Constants and API Configuration) ---
15
 
@@ -29,12 +30,14 @@ def pil_to_base64(img: Image.Image) -> str:
29
  img.save(buffered, format="PNG")
30
  return base64.b64encode(buffered.getvalue()).decode("utf-8")
31
 
 
32
  async def nano_banana_completion_api(base_image: Image.Image, prompt: str) -> Image.Image:
33
  """
34
- Nano Banana (gemini-2.5-flash-image-preview) APIを呼び出し、画像補完をシミュレートします。
35
- (この関数はモックのままです)
36
  """
37
-
 
 
38
  # Base64エンコード
39
  base64_image = pil_to_base64(base_image)
40
 
@@ -42,13 +45,17 @@ async def nano_banana_completion_api(base_image: Image.Image, prompt: str) -> Im
42
  payload = {
43
  "contents": [
44
  {
45
- "text": f"画像を編集・補完してください。この画像はLive2D素材の一部です。マスクされた(透明な)領域にプロンプトに従った内容を生成し、画像を自然に完成させてください。プロンプト: {prompt}"
46
- },
47
- {
48
- "inlineData": {
49
- "mimeType": "image/png",
50
- "data": base64_image
51
- }
 
 
 
 
52
  }
53
  ],
54
  "generationConfig": {
@@ -58,40 +65,49 @@ async def nano_banana_completion_api(base_image: Image.Image, prompt: str) -> Im
58
 
59
  print(f"--- API呼び出しペイロードを構築しました。プロンプト: {prompt} ---")
60
 
61
- # --- 実際のAPI呼び出しのシミュレーションと代替処理 ---
 
62
 
63
- # APIキーが設定されている場合は、ここでhttpxなどを使った非同期APIコールを行う
64
- # if API_KEY:
65
- # async with httpx.AsyncClient() as client:
66
- # response = await client.post(API_URL + API_KEY, json=payload, timeout=60)
67
- # # ... (レスポンス処理と画像デコード) ...
68
- # else:
69
 
70
- await asyncio.sleep(4) # 処理時間をシミュレート
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- # 補完処理が成功したと仮定し、モック画像を作成
73
- completed_image = base_image.copy()
74
- draw = ImageDraw.Draw(completed_image)
75
-
76
- try:
77
- # WindowsやLinux環境で一般的なフォントを指定
78
- font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if os.path.exists("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf") else "arial.ttf"
79
- font = ImageFont.truetype(font_path, 40)
80
- except IOError:
81
- font = ImageFont.load_default()
82
 
83
- text = "AI補完済み (MOCK)"
84
- text_color = (255, 0, 0, 200) # 半透明の赤
85
-
86
- w, h = completed_image.size
87
- # ImageDraw.textbbox を使用してテキストの境界ボックスを取得
88
- text_bbox = draw.textbbox((0, 0), text, font=font)
89
- tw, th = text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1]
 
 
90
 
91
- draw.text(((w - tw) / 2, (h - th) / 2), text, font=font, fill=text_color)
92
-
93
- print("--- AI補完処理をモック完了しました ---")
94
- return completed_image
95
 
96
 
97
  # --- メイン処理ロジック (Main Processing Logic) ---
@@ -128,9 +144,10 @@ async def segment_and_inpaint(original_image: Image.Image, inpaint_prompt: str):
128
  )
129
 
130
  # 境界を滑らかにするためのぼかし処理 (アンチエイリアス/フェザー処理のシミュレーション)
131
- hair_mask_np = cv2.GaussianBlur(hair_mask_np, (35, 35), 0)
132
-
133
- # 2値化は行わず、ぼかし後のグレースケール値をそのまま透明度として使用 (よりリアルなフェザー)
 
134
 
135
  # NumPy配列からPIL Imageに変換
136
  hair_mask = Image.fromarray(hair_mask_np, mode='L') # Lモード(グレースケール)
@@ -221,8 +238,8 @@ with gr.Blocks(theme=theme, title="Live2D素材自動分割・補完アプリ")
221
  """
222
  <div style='text-align: center; margin-bottom: 20px; padding: 10px; background: #E0F7FA; border-radius: 12px;'>
223
  <h1 style='color: #00796B; font-size: 2.5em; font-weight: 700;'>🎨 Live2D 素材 自動分割・補完アプリ 🤖</h1>
224
- <p style='color: #004D40; font-size: 1.1em;'>一枚絵をアップロードするだけで、AIによるパーツ分割と欠損部分の自動補完(Nano Banana API利用)をシミュレートします。</p>
225
- <p style='color: #004D40; font-size: 1.0em;'>💡 **CV2 (OpenCV) を使用し、楕円形のぼかしたマスクでより自然なパーツ分割をシミュレートしています。**</p>
226
  </div>
227
  """
228
  )
 
9
  import time
10
  import asyncio
11
  import os
12
+ import cv2
13
+ import httpx # ★★★ 変更点: httpx を追加
14
 
15
  # --- 定数とAPI設定 (Constants and API Configuration) ---
16
 
 
30
  img.save(buffered, format="PNG")
31
  return base64.b64encode(buffered.getvalue()).decode("utf-8")
32
 
33
+ # ★★★ 変更点: APIモックから実APIコールへ変更 ★★★
34
  async def nano_banana_completion_api(base_image: Image.Image, prompt: str) -> Image.Image:
35
  """
36
+ Nano Banana (gemini-2.5-flash-image-preview) APIを呼び出し、画像補完を実行します。
 
37
  """
38
+ if not API_KEY:
39
+ raise gr.Error("エラー: APIキーが設定されていません。Hugging Face Secretsで 'NANO_BANANA_API' を設定してください。")
40
+
41
  # Base64エンコード
42
  base64_image = pil_to_base64(base_image)
43
 
 
45
  payload = {
46
  "contents": [
47
  {
48
+ "parts": [
49
+ {
50
+ "text": f"画像を編集・補完してください。この画像はLive2D素材の一部です。マスクされた(透明な)領域にプロンプトに従った内容を生成し、画像を自然に完成させてください。プロンプト: {prompt}"
51
+ },
52
+ {
53
+ "inlineData": {
54
+ "mimeType": "image/png",
55
+ "data": base64_image
56
+ }
57
+ }
58
+ ]
59
  }
60
  ],
61
  "generationConfig": {
 
65
 
66
  print(f"--- API呼び出しペイロードを構築しました。プロンプト: {prompt} ---")
67
 
68
+ # --- 実際のAPI呼び出しロジック (httpxを使用) ---
69
+ final_api_url = API_URL + API_KEY
70
 
71
+ # 指数バックオフ付きリトライロジック
72
+ max_retries = 3
73
+ delay = 1
 
 
 
74
 
75
+ for attempt in range(max_retries):
76
+ try:
77
+ async with httpx.AsyncClient(timeout=30.0) as client:
78
+ response = await client.post(final_api_url, json=payload)
79
+ response.raise_for_status() # 200番台以外のステータスコードで例外を発生させる
80
+
81
+ result = response.json()
82
+ candidate = result.get('candidates', [{}])[0]
83
+
84
+ # Base64画像データの抽出
85
+ base64_data = None
86
+ for part in candidate.get('content', {}).get('parts', []):
87
+ if 'inlineData' in part and part['inlineData']['mimeType'].startswith('image/'):
88
+ base64_data = part['inlineData']['data']
89
+ break
90
 
91
+ if base64_data:
92
+ # Base64データをデコードしてPIL Imageに変換
93
+ image_data = base64.b64decode(base64_data)
94
+ completed_image = Image.open(io.BytesIO(image_data)).convert('RGBA')
95
+ print("--- AI補完処理を完了しました (実API) ---")
96
+ return completed_image
97
+ else:
98
+ raise ValueError("APIレスポンスから画像データが抽出できませんでした。")
 
 
99
 
100
+ except httpx.HTTPStatusError as e:
101
+ if e.response.status_code in [429, 503] and attempt < max_retries - 1:
102
+ print(f"APIレート制限/サーバーエラー (429/503)。{delay}秒待機後にリトライします...")
103
+ await asyncio.sleep(delay)
104
+ delay *= 2 # 指数バックオフ
105
+ else:
106
+ raise gr.Error(f"API呼び出しエラー: {e}")
107
+ except Exception as e:
108
+ raise gr.Error(f"予期せぬエラーが発生しました: {e}")
109
 
110
+ raise gr.Error("APIリクエストが最大リトライ回数を超えて失敗しました。")
 
 
 
111
 
112
 
113
  # --- メイン処理ロジック (Main Processing Logic) ---
 
144
  )
145
 
146
  # 境界を滑らかにするためのぼかし処理 (アンチエイリアス/フェザー処理のシミュレーション)
147
+ # ぼかしの強度を調整
148
+ kernel_size = min(W, H) // 30
149
+ kernel_size = kernel_size if kernel_size % 2 == 1 else kernel_size + 1 # カーネルサイズは奇数にする
150
+ hair_mask_np = cv2.GaussianBlur(hair_mask_np, (kernel_size, kernel_size), 0)
151
 
152
  # NumPy配列からPIL Imageに変換
153
  hair_mask = Image.fromarray(hair_mask_np, mode='L') # Lモード(グレースケール)
 
238
  """
239
  <div style='text-align: center; margin-bottom: 20px; padding: 10px; background: #E0F7FA; border-radius: 12px;'>
240
  <h1 style='color: #00796B; font-size: 2.5em; font-weight: 700;'>🎨 Live2D 素材 自動分割・補完アプリ 🤖</h1>
241
+ <p style='color: #004D40; font-size: 1.1em;'>一枚絵をアップロードするだけで、AIによるパーツ分割と欠損部分の自動補完(Nano Banana API利用)を**実稼働**させます。</p>
242
+ <p style='color: #004D40; font-size: 1.0em;'>💡 **注意:** AI補完機能を利用するには、Hugging Face Secretsに `NANO_BANANA_API` キーを設定する必要があります。</p>
243
  </div>
244
  """
245
  )