Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -9,7 +9,8 @@ import json
|
|
| 9 |
import time
|
| 10 |
import asyncio
|
| 11 |
import os
|
| 12 |
-
import cv2
|
|
|
|
| 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 |
-
"
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 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 |
-
#
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
# response = await client.post(API_URL + API_KEY, json=payload, timeout=60)
|
| 67 |
-
# # ... (レスポンス処理と画像デコード) ...
|
| 68 |
-
# else:
|
| 69 |
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
except IOError:
|
| 81 |
-
font = ImageFont.load_default()
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
|
| 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 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
| 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
|
| 225 |
-
<p style='color: #004D40; font-size: 1.0em;'>💡
|
| 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 |
)
|