Slicelayers commited on
Commit
bfea3f5
·
verified ·
1 Parent(s): ec6bace

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +53 -68
app.py CHANGED
@@ -1,6 +1,6 @@
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
@@ -131,55 +131,42 @@ async def nano_banana_completion_api(base_image: Image.Image, prompt: str) -> Im
131
 
132
  # --- メイン処理ロジック (Main Processing Logic) ---
133
 
134
- async def segment_and_inpaint(original_image_np: np.ndarray, sketchpad_filepath: str, inpaint_prompt: str):
135
  """
136
  アップロードされた一枚絵とブラシで描かれたマスクを受け取り、
137
  マスクされた領域を分割し、欠損部分をAI補完する関数。
 
138
  """
 
 
 
 
139
  if original_image_np is None:
140
- return None, None, None, "エラー: 元の画像がアップロードされていません。", None
141
 
142
- # --- 修正: Sketchpadの入力がファイルパスの場合の処理 ---
143
- if not sketchpad_filepath:
144
- # Sketchpadが空(何も描かれていない)場合はファイルパスがNoneまたは空文字列
145
- return None, None, None, "エラー: 分割したいパーツをブラシで描画してください。描画キャンバスが空です。", None
146
-
147
- try:
148
- # ファイルパスからPIL Imageとして読み込む
149
- sketchpad_image = Image.open(sketchpad_filepath).convert("RGBA")
150
- sketchpad_image_np = np.array(sketchpad_image)
151
- except Exception as e:
152
- print(f"Sketchpad画像読み込みエラー: {e}")
153
- return None, None, None, "エラー: 描画キャンバスの画像を読み込めませんでした。ファイルパスが不正です。", None
154
 
155
  # PIL Image (元の画像) を取得
156
  original_image = Image.fromarray(original_image_np).convert("RGBA")
157
 
158
- # Sketchpadからの入力は、元の画像の上にブラシで描画されたRGBA画像 (描画部分は不透明)
159
- # そのため、アルファチャンネルがマスクとして機能する
160
-
161
  try:
162
  # Sketchpadの画像からアルファチャンネルを抽出(これがユーザーの描いたマスク)
163
- # Sketchpadの出力はRGBAのNumPy配列であることを期待
164
  sketch_mask_alpha = sketchpad_image_np[:, :, 3]
165
  except IndexError:
166
- # 配列の次元が4未満(RGBAではない)場合に発生
167
- return None, None, None, "エラー: 描画データ形式が不正です。ブラシで描画が行われていないか、Gradioの出力形式に問題があります。", None
168
- except Exception as e:
169
- # その他のスライスエラーなど
170
- print(f"Sketchpad処理中の予期せぬエラー: {e}")
171
- return None, None, None, "エラー: 描画データの処理中に予期せぬエラーが発生しました。ブラシで適切に描画されているか確認してください。", None
172
-
173
- # ユーザーが何も描画しなかった場合のチェック(ファイルパスチェックで大方カバーされるが、念のため)
174
  if np.all(sketch_mask_alpha == 0):
175
- return None, None, None, "エラー: 分割したいパーツをブラシで描画してください。(アルファチャンネルが全てゼロです)", None
176
 
177
  # NumPy配列からPIL Imageのマスクに変換 (Lモード)
178
  hair_mask = Image.fromarray(sketch_mask_alpha, mode='L')
179
 
180
  # --- Part A: 分割パーツを作成 (ユーザーが描いた部分) ---
181
  hair_part = original_image.copy().convert('RGBA')
182
- # マスクをアルファチャンネルとして適用
183
  hair_part.putalpha(hair_mask)
184
 
185
  # --- Part B: 欠損穴のある体 (Body with Hole) を作成 (AI補完用入力) ---
@@ -187,24 +174,27 @@ async def segment_and_inpaint(original_image_np: np.ndarray, sketchpad_filepath:
187
 
188
  # マスクを反転 (描画した部分を透明な穴にするため)
189
  inverted_mask_np = cv2.bitwise_not(sketch_mask_alpha)
190
- # 反転マスクを体パーツのアルファチャンネルとして設定
191
  body_with_hole.putalpha(Image.fromarray(inverted_mask_np, mode='L'))
192
 
193
- print("--- 1. 手動パーツ分割 (Gradio Sketchpad利用) 完了: 分割パーツと補完が必要な欠損体を作成 ---")
194
 
195
  # 2. 欠損領域のAI補完 (AI Inpainting)
196
- completed_body_part = await nano_banana_completion_api(body_with_hole, inpaint_prompt or "マスクされた領域のキャラクターの顔と身体の肌、及び下に着ている服を、元のイラストのテイストに合わせて自然に補完してください。")
197
-
198
- # 3. 出力ファイルの準備とZIPファイルの作成 (Prepare Output Files and Create ZIP)
199
-
200
- # 補完後の体パーツと髪パーツがLive2Dのレイヤーとなる
 
 
 
 
201
  output_parts = {
202
  "body_completed": completed_body_part,
203
- "hair_front": hair_part, # ユーザーがブラシで指定したパーツ
204
  }
205
 
206
  # JSON構造の作成
207
- output_json = {
208
  "parts": {
209
  "body_completed": "body_completed.png",
210
  "hair_front": "hair_front.png"
@@ -213,11 +203,10 @@ async def segment_and_inpaint(original_image_np: np.ndarray, sketchpad_filepath:
213
  "note": "hair_frontはユーザーがブラシで描画したマスクに基づいて分割されました。",
214
  "timestamp": time.strftime("%Y%m%d_%H%M%S")
215
  }
216
- json_data = json.dumps(output_json, indent=2, ensure_ascii=False)
217
 
218
  zip_buffer = io.BytesIO()
219
  with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
220
- # PNGパーツの追加
221
  for name, img in output_parts.items():
222
  img_buffer = io.BytesIO()
223
  if img.mode != 'RGBA':
@@ -225,25 +214,24 @@ async def segment_and_inpaint(original_image_np: np.ndarray, sketchpad_filepath:
225
  img.save(img_buffer, format="PNG")
226
  zipf.writestr(f"live2d_parts/{name}.png", img_buffer.getvalue())
227
 
228
- # JSONファイルの追加
229
- zipf.writestr("live2d_parts/parts_structure.json", json_data.encode('utf-8'))
230
 
231
  zip_buffer.seek(0)
232
- zip_file_path = f"live2d_parts_{output_json['timestamp']}.zip"
233
 
234
- # Gradioに出力するためにファイルを一時的に保存
235
  with open(zip_file_path, "wb") as f:
236
  f.write(zip_buffer.read())
237
 
238
  print(f"--- 4. 全パーツZIPファイル {zip_file_path} の作成完了 ---")
239
 
240
- # デバッグ用に、補完前の欠損体 (body_with_hole) には元の画像を設定 (マスクはアルファチャンネルに含まれている)
241
  return (
242
- completed_body_part, # 補完済みメインボディ
243
- hair_part, # 分割された髪の毛 (ブラシ部分)
244
- body_with_hole, # 補完前の欠損体 (デバッグ用)
245
- json_data, # JSON構造
246
- zip_file_path # ZIPファイル
 
247
  )
248
 
249
  # --- Gradioインターフェース定義 (Gradio Interface Definition) ---
@@ -267,29 +255,23 @@ with gr.Blocks(theme=theme, title="Live2D素材自動分割・補完アプリ")
267
  """
268
  )
269
 
 
 
270
  with gr.Row():
271
  with gr.Column(scale=1):
272
- # 元画像をアップロードするためのコンポーネント (描画機能なし)
273
  original_image_upload = gr.Image(
274
  label="① 元画像のアップロード",
275
- type="numpy", # NumPy配列として受け取る
276
  height=200,
277
  )
278
- # 描画専用のSketchpadコンポーネント
279
  input_sketchpad = gr.Sketchpad(
280
  label="② 分割したいパーツをブラシで塗る (例: 前髪)",
281
  height=400,
282
- brush={"color": "#FF0000", "size": 20}, # ブラシ設定
283
- type="filepath" # ★★★ 修正: 出力タイプをファイルパスに変更し、より頑健にする ★★★
284
  )
285
-
286
- # ★★★ 削除: エラーの原因となっているSketchpadの背景更新ロジックを削除 ★★★
287
- # original_image_upload.change(
288
- # fn=lambda x: gr.update(image=x),
289
- # inputs=[original_image_upload],
290
- # outputs=[input_sketchpad],
291
- # queue=False
292
- # )
293
 
294
  inpaint_prompt = gr.Textbox(
295
  label="③ AI補完プロンプト(オプション)",
@@ -301,7 +283,8 @@ with gr.Blocks(theme=theme, title="Live2D素材自動分割・補完アプリ")
301
  gr.Markdown(
302
  """
303
  ### 📌 開発のポイント
304
- * **Gradio互換性対応:** 環境のGradioバージョンが低すぎる問題を回避するため、描画機能に特化した`gr.Sketchpad`を使用しました。
 
305
  """
306
  )
307
 
@@ -323,19 +306,21 @@ with gr.Blocks(theme=theme, title="Live2D素材自動分割・補完アプリ")
323
  download_zip = gr.File(label="全パーツZIPダウンロード", file_count="single")
324
 
325
  gr.Markdown("## 📋 出力レイヤー構造 (JSON)")
326
- output_json = gr.JSON(label="Live2Dパーツ構造JSON")
 
327
 
328
 
329
  # --- イベントリスナー ---
330
  process_button.click(
331
  fn=segment_and_inpaint,
332
- inputs=[original_image_upload, input_sketchpad, inpaint_prompt], # 入力コンポーネントの順序を修正
333
  outputs=[
334
  completed_body_output,
335
  hair_part_output,
336
  body_with_hole_output,
337
- output_json,
338
- download_zip
 
339
  ]
340
  )
341
 
 
1
  # coding: utf-8
2
  import gradio as gr
3
+ from PIL import Image
4
  import numpy as np
5
  import io
6
  import base64
 
131
 
132
  # --- メイン処理ロジック (Main Processing Logic) ---
133
 
134
+ async def segment_and_inpaint(original_image_np: np.ndarray, sketchpad_image_np: np.ndarray, inpaint_prompt: str):
135
  """
136
  アップロードされた一枚絵とブラシで描かれたマスクを受け取り、
137
  マスクされた領域を分割し、欠損部分をAI補完する関数。
138
+ (出力を6つにし、エラーメッセージを分離してorjsonエラーを回避)
139
  """
140
+ # 失敗時の統一リターン(6つの出力に対応)
141
+ def fail(message):
142
+ return None, None, None, None, None, f"❌ 処理失敗: {message}"
143
+
144
  if original_image_np is None:
145
+ return fail("元の画像がアップロードされていません。")
146
 
147
+ # Sketchpad入力の型チェックを強化(TypeError対策)
148
+ if not isinstance(sketchpad_image_np, np.ndarray) or sketchpad_image_np.ndim != 4:
149
+ return fail("分割したいパーツをブラシで描画してください。描画キャンバスが空または不正な形式です。")
 
 
 
 
 
 
 
 
 
150
 
151
  # PIL Image (元の画像) を取得
152
  original_image = Image.fromarray(original_image_np).convert("RGBA")
153
 
154
+ # Sketchpadからの入力は、元の画像の上にブラシで描画されたRGBA画像
 
 
155
  try:
156
  # Sketchpadの画像からアルファチャンネルを抽出(これがユーザーの描いたマスク)
 
157
  sketch_mask_alpha = sketchpad_image_np[:, :, 3]
158
  except IndexError:
159
+ return fail("描画データ形式が不正です。RGBA (4チャンネル) の画像として処理できませんでした。")
160
+
161
+ # ユーザーが何も描画しなかった場合のチェック
 
 
 
 
 
162
  if np.all(sketch_mask_alpha == 0):
163
+ return fail("分割したいパーツをブラシで描画してください。(描画が検出されません)")
164
 
165
  # NumPy配列からPIL Imageのマスクに変換 (Lモード)
166
  hair_mask = Image.fromarray(sketch_mask_alpha, mode='L')
167
 
168
  # --- Part A: 分割パーツを作成 (ユーザーが描いた部分) ---
169
  hair_part = original_image.copy().convert('RGBA')
 
170
  hair_part.putalpha(hair_mask)
171
 
172
  # --- Part B: 欠損穴のある体 (Body with Hole) を作成 (AI補完用入力) ---
 
174
 
175
  # マスクを反転 (描画した部分を透明な穴にするため)
176
  inverted_mask_np = cv2.bitwise_not(sketch_mask_alpha)
 
177
  body_with_hole.putalpha(Image.fromarray(inverted_mask_np, mode='L'))
178
 
179
+ print("--- 1. 手動パーツ分割 完了 ---")
180
 
181
  # 2. 欠損領域のAI補完 (AI Inpainting)
182
+ try:
183
+ completed_body_part = await nano_banana_completion_api(body_with_hole, inpaint_prompt or "マスクされた領域のキャラクターの顔と身体の肌、及び下に着ている服を、元のイラストのテイストに合わせて自然に補完してください。")
184
+ except gr.Error as e:
185
+ # APIエラーはここで捕まえる
186
+ return fail(f"AI補完APIエラー: {e}")
187
+ except Exception as e:
188
+ return fail(f"AI補完処理中に予期せぬエラー: {e}")
189
+
190
+ # 3. 出力ファイルの準備とZIPファイルの作成
191
  output_parts = {
192
  "body_completed": completed_body_part,
193
+ "hair_front": hair_part,
194
  }
195
 
196
  # JSON構造の作成
197
+ output_json_data = {
198
  "parts": {
199
  "body_completed": "body_completed.png",
200
  "hair_front": "hair_front.png"
 
203
  "note": "hair_frontはユーザーがブラシで描画したマスクに基づいて分割されました。",
204
  "timestamp": time.strftime("%Y%m%d_%H%M%S")
205
  }
206
+ json_data_str = json.dumps(output_json_data, indent=2, ensure_ascii=False)
207
 
208
  zip_buffer = io.BytesIO()
209
  with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
 
210
  for name, img in output_parts.items():
211
  img_buffer = io.BytesIO()
212
  if img.mode != 'RGBA':
 
214
  img.save(img_buffer, format="PNG")
215
  zipf.writestr(f"live2d_parts/{name}.png", img_buffer.getvalue())
216
 
217
+ zipf.writestr("live2d_parts/parts_structure.json", json_data_str.encode('utf-8'))
 
218
 
219
  zip_buffer.seek(0)
220
+ zip_file_path = f"live2d_parts_{output_json_data['timestamp']}.zip"
221
 
 
222
  with open(zip_file_path, "wb") as f:
223
  f.write(zip_buffer.read())
224
 
225
  print(f"--- 4. 全パーツZIPファイル {zip_file_path} の作成完了 ---")
226
 
227
+ # 成功時のリターン (6つの出力)
228
  return (
229
+ completed_body_part,
230
+ hair_part,
231
+ body_with_hole,
232
+ json_data_str, # JSON文字列をそのまま渡す
233
+ zip_file_path,
234
+ "✅ 処理完了!結果を確認し、ZIPファイルをダウンロードしてください。" # ステータス
235
  )
236
 
237
  # --- Gradioインターフェース定義 (Gradio Interface Definition) ---
 
255
  """
256
  )
257
 
258
+ status_message = gr.Textbox(label="処理ステータス", interactive=False, value="処理を開始するには、画像アップロードと描画を行い、実行ボタンを押してください。", elem_classes="status-box")
259
+
260
  with gr.Row():
261
  with gr.Column(scale=1):
262
+ # 元画像をアップロードするためのコンポーネント
263
  original_image_upload = gr.Image(
264
  label="① 元画像のアップロード",
265
+ type="numpy",
266
  height=200,
267
  )
268
+ # 描画専用のSketchpadコンポーネント (type="numpy"に戻し、Python側で堅牢にチェック)
269
  input_sketchpad = gr.Sketchpad(
270
  label="② 分割したいパーツをブラシで塗る (例: 前髪)",
271
  height=400,
272
+ brush={"color": "#FF0000", "size": 20},
273
+ type="numpy" # ★★★ 修正: numpyに戻す ★★★
274
  )
 
 
 
 
 
 
 
 
275
 
276
  inpaint_prompt = gr.Textbox(
277
  label="③ AI補完プロンプト(オプション)",
 
283
  gr.Markdown(
284
  """
285
  ### 📌 開発のポイント
286
+ * **安定性の向上:** 発生していた型の不一致エラー(`slice`や`read`)を回避するため、Gradioの出力をNumPy配列に統一し、入力チェックを強化しました。
287
+ * **エラー表示の改善:** 処理ステータスを専用のテキストボックスに出力するように変更し、JSONデコードエラーを防いでいます。
288
  """
289
  )
290
 
 
306
  download_zip = gr.File(label="全パーツZIPダウンロード", file_count="single")
307
 
308
  gr.Markdown("## 📋 出力レイヤー構造 (JSON)")
309
+ # JSONDecodeErrorを避けるため、JSONコンポーネントではなくテキストボックスを使用
310
+ output_json_text = gr.Textbox(label="Live2Dパーツ構造 JSON (確認用)", lines=10, interactive=False)
311
 
312
 
313
  # --- イベントリスナー ---
314
  process_button.click(
315
  fn=segment_and_inpaint,
316
+ inputs=[original_image_upload, input_sketchpad, inpaint_prompt],
317
  outputs=[
318
  completed_body_output,
319
  hair_part_output,
320
  body_with_hole_output,
321
+ output_json_text, # JSON出力をテキストボックスに変更
322
+ download_zip,
323
+ status_message # ステータスメッセージを追加
324
  ]
325
  )
326