grmchn's picture
chore(setup): add model pre-download utility and CLI flag; update README usage
9b07278
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"<script>{js_content}</script>"
except FileNotFoundError:
print(f"[ERROR] JavaScript file not found: {js_path}")
return "<script>console.error('pose_editor.js not found');</script>"
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='<canvas id="pose_canvas" width="640" height="640" style="border: 1px solid #ccc;"></canvas>'
)
# 非表示のデータ保持用コンポーネント
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"<script>{js_code}</script>"), # 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"<script>{js_code}</script>")
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"<script>{js_code}</script>")
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"<script>{js_code}</script>"), # 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"<script>{js_code}</script>"), # 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"<script>{js_code}</script>"),
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()