import gradio as gr import os from PIL import Image from utils.pose_utils import initialize_dwpose, safe_detect_pose from utils.notifications import notify_success, notify_error, NotificationMessages from utils.coordinate_system import update_coordinate_system from utils.image_processing import process_uploaded_image from utils.export_utils import export_pose_as_image, export_pose_as_json, get_timestamp_filename from utils.model_setup import download_models as _download_models_setup import json import tempfile import base64 import io import time # グローバル変数(refs互換)- 編集中のポーズデータを保持 _current_poses = None # refsと同じマルチフレーム管理 _current_frame_index = 0 # 現在編集中のフレーム _current_pose_data = None # レガシー互換性のため _is_updating = False # Issue 038: データ同期処理中フラグ(refs issue043準拠) def load_javascript(): """JavaScriptファイルを読み込む""" try: js_path = os.path.join(os.path.dirname(__file__), "static", "pose_editor.js") with open(js_path, "r", encoding="utf-8") as f: js_content = f.read() return f"" except FileNotFoundError: print(f"[ERROR] JavaScript file not found: {js_path}") return "" def image_to_base64(image): """PIL画像をBase64データURLに変換""" if image is None: return None # 画像をリサイズ(Canvas表示用に640x640以下に) max_size = 640 image.thumbnail((max_size, max_size), Image.Resampling.LANCZOS) # Base64エンコード buffer = io.BytesIO() image.save(buffer, format='PNG') img_str = base64.b64encode(buffer.getvalue()).decode() return f"data:image/png;base64,{img_str}" def main(): # DWPoseモデル初期化 success, message = initialize_dwpose() if not success: print(f"警告: {message}") with gr.Blocks(title="Character OpenPose Editor", head=load_javascript()) as demo: gr.Markdown("# Character OpenPose Editor") gr.Markdown("2頭身・3頭身キャラクター向けのOpenPose・DWPoseのポーズ編集ツール") with gr.Row(): # 左側:入力部 with gr.Column(scale=1): gr.Markdown("### 入力") # 参考画像アップロード input_image = gr.Image( label="参考画像", type="pil", elem_id="input_image" ) # テンプレートポーズ選択 template_dropdown = gr.Dropdown( label="テンプレート", choices=["2頭身立ちポーズ", "3頭身立ちポーズ", "4頭身立ちポーズ", "5頭身立ちポーズ", "6頭身立ちポーズ", "7頭身立ちポーズ"], value="3頭身立ちポーズ" ) # テンプレート適用ボタン template_update_btn = gr.Button( "🔄 テンプレートに更新", variant="secondary" ) # JSONファイルアップロード json_upload = gr.File( label="JSONファイルをロード", file_types=[".json"], elem_id="json_upload" ) # 中央:エディット部 with gr.Column(scale=2): gr.Markdown("### ポーズエディター") # 表示設定(超コンパクトに1行配置) with gr.Row(equal_height=True): with gr.Column(scale=0, min_width=240): gr.Markdown("**表示設定**") with gr.Row(): draw_hand = gr.Checkbox(label="手を描画", value=True, container=False, scale=0, min_width=90) draw_face = gr.Checkbox(label="顔を描画", value=False, container=False, scale=0, min_width=90) with gr.Column(scale=1, min_width=160): gr.Markdown("**編集モード**") edit_mode = gr.Radio( choices=["簡易モード", "詳細モード"], value="簡易モード", container=False ) # ポーズ描画キャンバス pose_canvas = gr.HTML( elem_id="pose_canvas_container", value='' ) # 非表示のデータ保持用コンポーネント pose_data = gr.JSON(visible=False, value={}) # JavaScript→Python データ転送用の隠しテキストボックス js_pose_update = gr.Textbox(visible=False, elem_id="js_pose_update") # 右側:出力部 with gr.Column(scale=1): gr.Markdown("### 出力") # 出力サイズ設定 with gr.Row(): canvas_width = gr.Number( label="幅", value=512, minimum=64, maximum=2048, step=64, scale=1, min_width=100 ) canvas_height = gr.Number( label="高さ", value=512, minimum=64, maximum=2048, step=64, scale=1, min_width=100 ) # サイズ適用ボタン(手動更新トリガー) canvas_update_btn = gr.Button( "画像サイズのUpdate", variant="secondary" ) # ポーズ画像出力(非表示) output_image = gr.Image( label="ポーズ画像", type="pil", elem_id="output_image", visible=False ) # ポーズ画像ダウンロード(2クリック方式) download_image_btn = gr.Button( "📥 画像をダウンロード", variant="secondary" ) download_image_file = gr.File( label="画像ファイル", visible=False ) # JSONデータ表示(非表示) output_json = gr.JSON( label="ポーズデータ (JSON)", elem_id="output_json", visible=False ) # JSONダウンロード(2クリック方式) download_json_btn = gr.Button( "📥 JSONをダウンロード", variant="secondary" ) download_json_file = gr.File( label="JSONファイル", visible=False ) # イベントハンドラー def on_image_upload(image): """画像アップロード時のポーズ検出(refs互換・マルチフレーム管理)""" global _current_poses, _current_frame_index if image is None: # 入力クリア時は出力数に合わせて安全に戻す(UIを壊さない) # output_json(None), pose_data({}), js_executor(no-op), canvas_width(no change), canvas_height(no change) return None, {}, gr.update(), gr.update(), gr.update() print(f"[DEBUG] 🖼️ Image upload detected: {type(image)}") # 画像処理 processed_image, original_size, scale_info = process_uploaded_image(image) print(f"[DEBUG] 📐 Image processed: original_size={original_size}, scale_info={scale_info}") # ポーズ検出実行 pose_result = safe_detect_pose(image) print(f"[DEBUG] 🤖 Pose detection result type: {type(pose_result)}") if pose_result is not None: print(f"[DEBUG] 📊 Pose result keys: {list(pose_result.keys()) if isinstance(pose_result, dict) else 'Not a dict'}") if isinstance(pose_result, dict) and 'bodies' in pose_result: bodies = pose_result['bodies'] if 'candidate' in bodies: candidates = bodies['candidate'] print(f"[DEBUG] 🎯 Candidates count: {len(candidates)}") print(f"[DEBUG] 📍 First 3 candidates: {candidates[:3] if len(candidates) >= 3 else candidates}") valid_count = len([c for c in candidates if c and len(c) >= 2 and c[0] > 0 and c[1] > 0]) zero_count = len([c for c in candidates if c and len(c) >= 2 and (c[0] == 0 or c[1] == 0)]) print(f"[DEBUG] ✅ Valid candidates: {valid_count}, 🚫 Zero coordinates: {zero_count}") # refsと同じ_current_poses形式で保存 if pose_result: # people形式に変換 person_data = { "pose_keypoints_2d": [], "hand_left_keypoints_2d": pose_result.get('hands', [[], []])[0] if pose_result.get('hands') else [], "hand_right_keypoints_2d": pose_result.get('hands', [[], []])[1] if pose_result.get('hands') and len(pose_result['hands']) > 1 else [], "face_keypoints_2d": pose_result.get('faces', [[]])[0] if pose_result.get('faces') else [] } # bodies.candidateからpose_keypoints_2d変換 if 'bodies' in pose_result and 'candidate' in pose_result['bodies']: candidates = pose_result['bodies']['candidate'] for candidate in candidates: if candidate and len(candidate) >= 2: person_data["pose_keypoints_2d"].extend([candidate[0], candidate[1], candidate[2] if len(candidate) > 2 else 1.0]) _current_poses = [{ 'people': [person_data], 'metadata': { 'resolution': pose_result.get('resolution', [512, 512]) } }] _current_frame_index = 0 print(f"[DEBUG] ✅ グローバル変数更新完了(画像アップロード・refs互換)") # 🎨 背景画像をポーズデータに含める(元画像をそのまま) image_base64 = image_to_base64(image) pose_result['background_image'] = image_base64 # 🔧 Issue045: 参考画像サイズに合わせてCanvas/出力サイズを更新 try: # original_size: (width, height) data_w = int(original_size[0]) if original_size else 512 data_h = int(original_size[1]) if original_size else 512 # Canvas表示サイズ(希望の出力サイズ) new_w = max(64, min(2048, data_w)) new_h = max(64, min(2048, data_h)) # 重要: pose_result['resolution'] は検出結果そのまま(座標系)を保持し、JS側でスケールする # Python側のメタは検出のまま(_current_poses 作成時に設定済み) except Exception: data_w, data_h = 512, 512 new_w = 512 new_h = 512 # JSでCanvasとデータを新サイズへスケール(左寄り防止のため) js_code = f"setTimeout(() => updateCanvasResolution({new_w}, {new_h}), 100);" return ( pose_result, # output_json pose_result, # pose_data gr.update(value=f""), # js_executor (canvas表示は正方形) gr.update(value=new_w), # 出力幅(データ解像度) gr.update(value=new_h) # 出力高さ(データ解像度) ) else: print(f"[DEBUG] ❌ Pose detection failed") return None, {}, gr.update(), gr.update(), gr.update() def on_canvas_size_update(width, height): """Canvas解像度更新""" global _current_poses, _current_frame_index try: width = int(width) if width else 512 height = int(height) if height else 512 # 解像度制限 width = max(64, min(2048, width)) height = max(64, min(2048, height)) # 座標系更新 update_coordinate_system((width, height), (640, 640)) # Python側のメタ解像度も更新(エクスポート整合性) try: if _current_poses and 0 <= _current_frame_index < len(_current_poses): _current_poses[_current_frame_index]['metadata']['resolution'] = [width, height] except Exception: pass # JavaScript側でCanvas更新 js_code = f"updateCanvasResolution({width}, {height});" notify_success(f"Canvas解像度を{width}x{height}に更新しました") return gr.update(value=f"") except Exception as e: notify_error(f"Canvas解像度更新に失敗しました: {str(e)}") return gr.update() def on_display_settings_change(draw_hand, draw_face, edit_mode): """表示設定変更時(JavaScript側で直接監視するため、現在は使用されていません)""" # 🔧 念のため残しておくが、JavaScript側の直接監視により不要になった js_code = f""" if (window.updateDisplaySettings) {{ window.updateDisplaySettings({str(draw_hand).lower()}, {str(draw_face).lower()}, '{edit_mode}'); }} """ return gr.update(value=f"") def load_template_pose(template_name): """テンプレートポーズを読み込み(refs互換・マルチフレーム管理)""" global _current_poses, _current_frame_index try: # 各頭身テンプレートファイルから読み込み template_file_map = { "2頭身立ちポーズ": "2heads.json", "3頭身立ちポーズ": "3heads.json", "4頭身立ちポーズ": "4heads.json", "5頭身立ちポーズ": "5heads.json", "6頭身立ちポーズ": "6heads.json", "7頭身立ちポーズ": "7heads.json" } if template_name in template_file_map: templates_path = os.path.join(os.path.dirname(__file__), "poses", template_file_map[template_name]) with open(templates_path, "r", encoding="utf-8") as f: pose_data = json.load(f) else: # 従来のテンプレート処理 templates_path = os.path.join(os.path.dirname(__file__), "templates", "poses.json") with open(templates_path, "r", encoding="utf-8") as f: templates = json.load(f) # テンプレート名をキーに変換 template_key_map = { "2頭身立ちポーズ": "2_head_standing", "2頭身座りポーズ": "2_head_sitting" } template_key = template_key_map.get(template_name) if template_key and template_key in templates["poses"]: pose_data = templates["poses"][template_key]["data"] else: notify_error("テンプレートが見つかりません") return None, {} if pose_data: # 頭身テンプレートファイルの場合は直接フォーマット、templates/poses.jsonの場合は.dataアクセス if template_name in template_file_map: # 各頭身jsonファイルは直接DWPoseフォーマット actual_pose_data = pose_data else: # templates/poses.jsonの場合は.dataアクセス actual_pose_data = pose_data # people形式に変換してから保存 person_data = { "pose_keypoints_2d": [], "hand_left_keypoints_2d": actual_pose_data.get('hands', [[], []])[0] if actual_pose_data.get('hands') else [], "hand_right_keypoints_2d": actual_pose_data.get('hands', [[], []])[1] if actual_pose_data.get('hands') and len(actual_pose_data['hands']) > 1 else [], "face_keypoints_2d": actual_pose_data.get('faces', [[]])[0] if actual_pose_data.get('faces') else [] } # bodies.candidateからpose_keypoints_2d変換 if 'bodies' in actual_pose_data and 'candidate' in actual_pose_data['bodies']: candidates = actual_pose_data['bodies']['candidate'] for candidate in candidates: if candidate and len(candidate) >= 2: person_data["pose_keypoints_2d"].extend([candidate[0], candidate[1], candidate[2] if len(candidate) > 2 else 1.0]) _current_poses = [{ 'people': [person_data], 'metadata': { 'resolution': actual_pose_data.get('resolution', [512, 512]) } }] _current_frame_index = 0 # people形式でデータを構築(元のbodies/hands/facesも保持) people_format_data = { 'people': [person_data], 'bodies': actual_pose_data.get('bodies', {}), # 元のbodiesデータも保持 'hands': actual_pose_data.get('hands', []), # 元のhandsデータも保持 'faces': actual_pose_data.get('faces', []), # 元のfacesデータも保持 'metadata': { 'resolution': actual_pose_data.get('resolution', [512, 512]) }, 'is_template_load': True # テンプレート読み込みフラグ } # 🔧 Issue045: テンプレートは常に512x512へ target_w, target_h = 512, 512 # Python側メタも512固定に try: if _current_poses and 0 <= _current_frame_index < len(_current_poses): _current_poses[_current_frame_index]['metadata']['resolution'] = [target_w, target_h] except Exception: pass # JS実行とUIサイズ反映 js_code = f"setTimeout(() => updateCanvasResolution({target_w}, {target_h}), 100);" notify_success(f"{template_name}を読み込みました") return ( people_format_data, # output_json people_format_data, # pose_data gr.update(value=f""), # js_executor gr.update(value=target_w), # canvas_width gr.update(value=target_h) # canvas_height ) else: notify_error("テンプレートが見つかりません") return None, {}, gr.update(), gr.update(), gr.update() except Exception as e: notify_error(f"テンプレート読み込みに失敗しました: {str(e)}") return None, {}, gr.update(), gr.update(), gr.update() def export_image(pose_data, draw_hand, draw_face, width, height): """ポーズ画像をエクスポート(Button + File方式)(refs互換・マルチフレーム管理)""" global _current_poses, _current_frame_index # refsと同じ_current_posesから最新データを取得 export_data = None if _current_poses and 0 <= _current_frame_index < len(_current_poses): current_frame = _current_poses[_current_frame_index] if current_frame['people'] and current_frame['people'][0]: person = current_frame['people'][0] # people形式のまま直接エクスポート(refs互換) export_data = { 'people': [person], 'resolution': current_frame['metadata'].get('resolution', [512, 512]) } # フォールバック:引数のpose_dataを使用 if export_data is None: export_data = pose_data print(f"[DEBUG] 🖼️ 画像ダウンロードボタンクリック") print(f"[DEBUG] 📊 グローバルデータ: {bool(_current_poses)}, 引数データ: {bool(pose_data)}") print(f"[DEBUG] 📊 使用データ: {bool(export_data)}, 手: {draw_hand}, 顔: {draw_face}") if export_data: print(f"[DEBUG] 📊 エクスポートデータキー: {list(export_data.keys()) if isinstance(export_data, dict) else 'not a dict'}") if isinstance(export_data, dict): for key, value in export_data.items(): print(f"[DEBUG] 📊 {key}: {type(value)} - {len(value) if isinstance(value, (list, dict)) else value}") # 🚨 グローバルデータの詳細チェック if _current_poses: print(f"[DEBUG] 🔍 グローバルデータ詳細:") print(f"[DEBUG] 🔍 - Type: {type(_current_poses)}") print(f"[DEBUG] 🔍 - Frames: {len(_current_poses) if _current_poses else 0}") if _current_poses and 0 <= _current_frame_index < len(_current_poses): current_frame = _current_poses[_current_frame_index] print(f"[DEBUG] 🔍 - Current frame: {bool(current_frame['people']) if 'people' in current_frame else False}") if current_frame.get('people'): person = current_frame['people'][0] print(f"[DEBUG] 🔍 - Person keys: {list(person.keys()) if person else 'None'}") else: print(f"[DEBUG] 🔍 グローバルデータがNone") if not export_data: notify_error("エクスポートするポーズデータがありません") print(f"[DEBUG] ❌ データなし") return gr.update(visible=False) try: # 🎨 表示設定を反映した画像エクスポート(UIで指定されたサイズを使用) canvas_width = int(width) if width else 512 canvas_height = int(height) if height else 512 # 解像度制限 canvas_width = max(64, min(2048, canvas_width)) canvas_height = max(64, min(2048, canvas_height)) # 🚨 画像生成前のデータ検証 print(f"[DEBUG] 🎨 画像生成開始: canvas_size=({canvas_width}, {canvas_height})") if 'bodies' in export_data and 'candidate' in export_data['bodies']: candidates = export_data['bodies']['candidate'] print(f"[DEBUG] 🎨 Candidates for image: {len(candidates) if candidates else 0}") if candidates: valid_candidates = [c for c in candidates if c and len(c) >= 2 and c[0] > 0 and c[1] > 0] print(f"[DEBUG] 🎨 Valid candidates: {len(valid_candidates)}") image = export_pose_as_image( export_data, canvas_size=(canvas_width, canvas_height), enable_hands=draw_hand, enable_face=draw_face ) if image is None: print(f"[DEBUG] ❌ 画像生成失敗 - export_pose_as_image returned None") return gr.update(visible=False) # タイムスタンプ付きファイル名でテンポラリファイルに保存 filename = get_timestamp_filename("dwpose_edit", "png") temp_path = os.path.join(tempfile.gettempdir(), filename) image.save(temp_path) print(f"[DEBUG] ✅ ファイル準備完了: {temp_path}") notify_success(f"画像をエクスポートしました: {filename}") # 🎯 Button + File方式でファイルを表示 return gr.update(value=temp_path, visible=True) except Exception as e: print(f"[DEBUG] ❌ エラー: {e}") notify_error(f"画像エクスポートエラー: {str(e)}") return gr.update(visible=False) def export_json(pose_data): """ポーズJSONをエクスポート(people形式・refs互換・グローバル管理)""" global _current_poses, _current_frame_index # refsと同じ_current_posesから最新データを取得(簡素化) export_data = None if _current_poses and 0 <= _current_frame_index < len(_current_poses): current_frame = _current_poses[_current_frame_index] if current_frame['people'] and current_frame['people'][0]: # 💖 people形式で直接エクスポート(変換不要) export_data = { 'people': current_frame['people'], 'resolution': current_frame['metadata'].get('resolution', [512, 512]) } # フォールバック:引数のpose_dataを使用 if export_data is None: export_data = pose_data print(f"[DEBUG] 📥 JSONダウンロードボタンクリック(people形式)") print(f"[DEBUG] 📊 グローバルデータ: {bool(_current_poses)}, 引数データ: {bool(pose_data)}") print(f"[DEBUG] 📊 使用データ: {bool(export_data)}") if not export_data: notify_error("エクスポートするポーズデータがありません") print(f"[DEBUG] ❌ データなし") return gr.update(visible=False) try: # export_pose_as_json()が新しいpeople形式で出力 json_str = export_pose_as_json(export_data) if not json_str: print(f"[DEBUG] ❌ JSON生成失敗") return gr.update(visible=False) # タイムスタンプ付きファイル名でテンポラリファイルに保存 filename = get_timestamp_filename("dwpose_data", "json") temp_path = os.path.join(tempfile.gettempdir(), filename) with open(temp_path, 'w', encoding='utf-8') as f: f.write(json_str) print(f"[DEBUG] ✅ ファイル準備完了: {temp_path}") notify_success(f"people形式JSONをエクスポートしました: {filename}") # 🎯 Button + File方式でファイルを表示 return gr.update(value=temp_path, visible=True) except Exception as e: print(f"[DEBUG] ❌ エラー: {e}") notify_error(f"JSONエクスポートエラー: {str(e)}") return gr.update(visible=False) def on_js_pose_update(js_pose_str): """JavaScript側からのポーズデータ更新(refs互換・マルチフレーム管理)""" global _current_poses, _current_frame_index, _is_updating update_timestamp = int(time.time() * 1000) print(f"[DEBUG] 🔄 JavaScript→Python データ転送開始: {len(js_pose_str) if js_pose_str else 0}文字, timestamp={update_timestamp}") print(f"[DEBUG] 🔍 更新フラグ状態: _is_updating={_is_updating}") print(f"[DEBUG] 🔍 グローバル状態: _current_poses={bool(_current_poses)}, _current_frame_index={_current_frame_index}") # データの中身をちょっと確認 if js_pose_str: try: data_preview = json.loads(js_pose_str) print(f"[DEBUG] 🔍 受信データ構造:", { 'hasPeople': 'people' in data_preview, 'peopleCount': len(data_preview.get('people', [])), 'hasHandLeft': bool(data_preview.get('people', [{}])[0].get('hand_left_keypoints_2d')) if data_preview.get('people') else False, 'hasHandRight': bool(data_preview.get('people', [{}])[0].get('hand_right_keypoints_2d')) if data_preview.get('people') else False, 'hasFace': bool(data_preview.get('people', [{}])[0].get('face_keypoints_2d')) if data_preview.get('people') else False }) except: print(f"[DEBUG] 🔍 受信データパース失敗") # Issue 038: 処理中フラグチェック(refs issue043準拠) if _is_updating: print(f"[DEBUG] ⚠️ データ更新処理中のため、新しい要求をスキップ (timestamp={update_timestamp})") return gr.update(), "" # 現在の状態を維持 if not js_pose_str or js_pose_str.strip() == "": return gr.update(), "" # pose_data更新なし、テキストボックスクリア # 処理開始フラグを立てる _is_updating = True print(f"[DEBUG] 🔒 データ更新フラグ設定: _is_updating={_is_updating} (timestamp={update_timestamp})") try: # JSON文字列をパース canvas_data = json.loads(js_pose_str) print(f"[DEBUG] 🎨 Canvas JSON解析成功: keys={list(canvas_data.keys())}") # refs互換:タイムスタンプを除去 if '_t' in canvas_data: del canvas_data['_t'] print(f"[DEBUG] 🔄 タイムスタンプ除去完了") # peopleフォーマットからpose_dataを抽出(refs互換方式) if 'people' in canvas_data and canvas_data['people']: pose_data = canvas_data['people'][0] print(f"[DEBUG] 🎯 People形式データ抽出成功") # _current_posesの初期化(初回のみ) if _current_poses is None: _current_poses = [{ 'people': [pose_data], 'metadata': { 'resolution': canvas_data.get('resolution', [512, 512]) } }] print(f"[DEBUG] 🚀 _current_poses初期化完了") else: # 現在のフレームのpeopleデータを更新(refs互換) if 0 <= _current_frame_index < len(_current_poses): _current_poses[_current_frame_index]['people'] = [pose_data] # 🔧 JS側で解像度が更新されている場合はmetadataにも反映 try: if 'resolution' in canvas_data and isinstance(canvas_data['resolution'], list): _current_poses[_current_frame_index]['metadata']['resolution'] = canvas_data['resolution'] except Exception: pass print(f"[DEBUG] 🎯 フレーム{_current_frame_index}のpeopleデータ更新完了") else: # フレームが範囲外の場合は追加 _current_poses.append({ 'people': [pose_data], 'metadata': { 'resolution': canvas_data.get('resolution', [512, 512]) } }) print(f"[DEBUG] 🎯 新フレーム追加完了") # 表示用に現在のフレームデータを構築(既存UI互換) current_frame_data = _current_poses[_current_frame_index] display_data = { 'bodies': {'candidate': [], 'subset': []}, 'hands': [], 'faces': [], 'resolution': current_frame_data['metadata'].get('resolution', [512, 512]) } # people形式からbodies.candidate形式に変換(表示用) if current_frame_data['people'] and current_frame_data['people'][0]: person = current_frame_data['people'][0] # pose_keypoints_2d をcandidate形式に変換 if 'pose_keypoints_2d' in person: pose_keypoints = person['pose_keypoints_2d'] print(f"[DEBUG] 🔄 pose_keypoints length: {len(pose_keypoints)}") if len(pose_keypoints) >= 3: for i in range(0, len(pose_keypoints), 3): if i + 2 < len(pose_keypoints): x = pose_keypoints[i] y = pose_keypoints[i + 1] conf = pose_keypoints[i + 2] display_data['bodies']['candidate'].append([x, y, conf, 0]) # 手と顔データ(表示用) left_hand = person.get('hand_left_keypoints_2d', []) right_hand = person.get('hand_right_keypoints_2d', []) face_data = person.get('face_keypoints_2d', []) display_data['hands'] = [left_hand, right_hand] display_data['faces'] = [face_data] if face_data else [] print(f"[DEBUG] 🫳 Hand data: left={len(left_hand)}, right={len(right_hand)}") print(f"[DEBUG] 😊 Face data: {len(face_data) if face_data else 0}") print(f"[DEBUG] ✅ refs互換データ更新完了: frames={len(_current_poses)}") print(f"[DEBUG] 📊 表示用データ: candidates={len(display_data['bodies']['candidate'])}") # pose_dataコンポーネントを更新、テキストボックスをクリア return display_data, "" except json.JSONDecodeError as e: print(f"[DEBUG] ❌ JavaScript→Python JSONパースエラー: {e}") return gr.update(), "" # エラー時は更新せず、テキストボックスクリア except Exception as e: print(f"[DEBUG] ❌ JavaScript→Python エラー: {e}") return gr.update(), "" finally: # Issue 038: 処理完了後は必ずフラグを解除(refs issue043準拠) old_flag = _is_updating _is_updating = False print(f"[DEBUG] 🔓 データ更新処理完了 - フラグ解除: {old_flag} → {_is_updating} (timestamp={update_timestamp})") def on_json_upload(file): """JSONファイルアップロード時の処理(people形式対応・refs互換)""" global _current_poses, _current_frame_index if file is None: return None, {}, gr.update(), gr.update(), gr.update() try: # ファイルを読み込む with open(file.name, 'r', encoding='utf-8') as f: json_content = f.read() # JSONをパース loaded_data = json.loads(json_content) # people形式(配列)かどうかチェック if isinstance(loaded_data, list) and len(loaded_data) > 0: # 新しいpeople形式 frame_data = loaded_data[0] # 最初のフレームを使用 if 'people' in frame_data and frame_data['people']: person_data = frame_data['people'][0] canvas_width = frame_data.get('canvas_width', 512) canvas_height = frame_data.get('canvas_height', 512) # グローバル変数に保存(refs互換) _current_poses = [{ 'people': [person_data], 'metadata': { 'resolution': [canvas_width, canvas_height] } }] _current_frame_index = 0 # 🔧 Issue #043: ハイブリッドデータ形式で返す(people形式 + bodies互換) bodies_data = convert_people_to_bodies_format(person_data, [canvas_width, canvas_height]) display_data = { 'people': [person_data], # JavaScript側で手・顔データ取得用 'bodies': bodies_data['bodies'], # 互換性維持 'hands': bodies_data['hands'], # 古いコードとの互換性 'faces': bodies_data['faces'], # 古いコードとの互換性 'resolution': [canvas_width, canvas_height], 'metadata': {'resolution': [canvas_width, canvas_height]} } print(f"[DEBUG] ✅ people形式JSON読み込み完了(ハイブリッド形式)") notify_success("people形式JSONファイルを読み込みました") # CanvasとデータをJSON内の解像度に合わせる js_code = f"setTimeout(() => updateCanvasResolution({int(canvas_width)}, {int(canvas_height)}), 100);" return ( display_data, # output_json display_data, # pose_data gr.update(value=f""), # js_executor gr.update(value=int(canvas_width)), # 出力幅 gr.update(value=int(canvas_height)) # 出力高さ ) else: notify_error("無効なpeople形式データです") return None, {}, gr.update(), gr.update(), gr.update() else: # 従来のbodies形式(互換性維持) if not validate_pose_json(loaded_data): notify_error("無効なポーズデータフォーマットです") return None, {} # メタデータを除去(存在する場合) if 'metadata' in loaded_data: del loaded_data['metadata'] # bodies形式からpeople形式に変換してグローバル保存 person_data = { "pose_keypoints_2d": [], "hand_left_keypoints_2d": loaded_data.get('hands', [[], []])[0] if loaded_data.get('hands') else [], "hand_right_keypoints_2d": loaded_data.get('hands', [[], []])[1] if loaded_data.get('hands') and len(loaded_data['hands']) > 1 else [], "face_keypoints_2d": loaded_data.get('faces', [[]])[0] if loaded_data.get('faces') else [] } # bodies.candidateからpose_keypoints_2d変換 if 'bodies' in loaded_data and 'candidate' in loaded_data['bodies']: candidates = loaded_data['bodies']['candidate'] for candidate in candidates: if candidate and len(candidate) >= 2: person_data["pose_keypoints_2d"].extend([candidate[0], candidate[1], candidate[2] if len(candidate) > 2 else 1.0]) _current_poses = [{ 'people': [person_data], 'metadata': { 'resolution': loaded_data.get('resolution', [512, 512]) } }] _current_frame_index = 0 # 🔧 Issue #043: ハイブリッドデータ形式で返す(people形式 + bodies互換) hybrid_data = loaded_data.copy() # 元のbodies形式を保持 hybrid_data['people'] = [person_data] # JavaScript側で手・顔データ取得用 print(f"[DEBUG] ✅ bodies形式JSON読み込み・変換完了(ハイブリッド形式)") notify_success("bodies形式JSONファイルを読み込みました(people形式に変換)") # 幅・高さを決定 res = loaded_data.get('resolution', [512, 512]) try: w = int(res[0]); h = int(res[1]) except Exception: w, h = 512, 512 js_code = f"setTimeout(() => updateCanvasResolution({w}, {h}), 100);" return ( hybrid_data, # output_json hybrid_data, # pose_data gr.update(value=f""), gr.update(value=w), gr.update(value=h) ) except json.JSONDecodeError as e: notify_error(f"JSONパースエラー: {str(e)}") return None, {}, gr.update(), gr.update(), gr.update() except Exception as e: notify_error(f"ファイル読み込みエラー: {str(e)}") return None, {}, gr.update(), gr.update(), gr.update() def convert_people_to_bodies_format(person_data, resolution): """people形式からbodies形式に変換(表示互換性用)""" bodies_data = { 'bodies': {'candidate': [], 'subset': []}, 'hands': [], 'faces': [], 'resolution': resolution } # pose_keypoints_2d をcandidate形式に変換 if 'pose_keypoints_2d' in person_data: pose_keypoints = person_data['pose_keypoints_2d'] for i in range(0, len(pose_keypoints), 3): if i + 2 < len(pose_keypoints): x = pose_keypoints[i] y = pose_keypoints[i + 1] conf = pose_keypoints[i + 2] bodies_data['bodies']['candidate'].append([x, y, conf, 0]) # 手と顔データ left_hand = person_data.get('hand_left_keypoints_2d', []) right_hand = person_data.get('hand_right_keypoints_2d', []) face_data = person_data.get('face_keypoints_2d', []) bodies_data['hands'] = [left_hand, right_hand] bodies_data['faces'] = [face_data] if face_data else [] return bodies_data def validate_pose_json(data): """ポーズJSONデータの検証""" if not isinstance(data, dict): return False # 最低限必要なキーをチェック required_keys = ['bodies'] for key in required_keys: if key not in data: return False # bodiesの構造をチェック if 'bodies' in data: bodies = data['bodies'] if not isinstance(bodies, dict): return False if 'candidate' not in bodies and 'subset' not in bodies: return False return True # 隠しコンポーネント(JavaScript実行用) js_executor = gr.HTML(visible=False, elem_id="js_executor") # 画像アップロードイベント input_image.change( fn=on_image_upload, inputs=[input_image], outputs=[output_json, pose_data, js_executor, canvas_width, canvas_height] ) # pose_data変更時にCanvas更新(重要!)- 無限ループ防止 pose_data.change( fn=None, # JavaScript側で処理 inputs=pose_data, outputs=[], # 出力なし!無限ループ防止 js="(pose_data) => { if (window.gradioCanvasUpdate) { window.gradioCanvasUpdate(JSON.stringify(pose_data)); } }" ) # 表示設定変更イベント draw_hand.change( fn=on_display_settings_change, inputs=[draw_hand, draw_face, edit_mode], outputs=[js_executor] ) draw_face.change( fn=on_display_settings_change, inputs=[draw_hand, draw_face, edit_mode], outputs=[js_executor] ) edit_mode.change( fn=on_display_settings_change, inputs=[draw_hand, draw_face, edit_mode], outputs=[js_executor] ) # テンプレート適用ボタンクリックイベント template_update_btn.click( fn=load_template_pose, inputs=[template_dropdown], outputs=[output_json, pose_data, js_executor, canvas_width, canvas_height] ) # エクスポートイベント (refs互換DownloadButton方式) download_image_btn.click( fn=export_image, inputs=[pose_data, draw_hand, draw_face, canvas_width, canvas_height], outputs=[download_image_file] ) download_json_btn.click( fn=export_json, inputs=[pose_data], outputs=[download_json_file] ) # JavaScript→Python データ転送イベント js_pose_update.change( fn=on_js_pose_update, inputs=[js_pose_update], outputs=[pose_data, js_pose_update] # pose_data更新 + テキストボックスクリア ) # JSONファイルアップロードイベント json_upload.change( fn=on_json_upload, inputs=[json_upload], outputs=[output_json, pose_data, js_executor, canvas_width, canvas_height] ) # 手動解像度更新ボタン canvas_update_btn.click( fn=on_canvas_size_update, inputs=[canvas_width, canvas_height], outputs=[js_executor] ) return demo if __name__ == "__main__": import argparse, sys parser = argparse.ArgumentParser() parser.add_argument("--setup-models", action="store_true", help="Download required models into ./models and exit") args, _ = parser.parse_known_args() if args.setup_models: code = _download_models_setup() # exit with code so CI/automation can check sys.exit(code) else: demo = main() demo.launch()