# [メタ情報] # 識別子: 自作パペット_py_exe # 補足: [2026-07-31 10:33:38]  env終了 # [/メタ情報] 要約: このシステムは、ユーザーが作成したパペットを、顔認識と音声入力に基づきリアルタイムで動かす。主な構成要素は以下の通り。 1. **パペット設定UI (`run_ui.py`)**: 声の反応感度、表示サイズ、首の傾き、目の感度、カメラ/マイクIDなど、パペットの動作パラメータをGUIで調整・保存する。キャラクターの新規作成や複製、各パラメータの詳細説明機能も提供。 2. **パペット実行スクリプト (`run_puppet.py`)**: 設定UIで指定されたキャラクターとパラメータを使用し、カメラからの顔認識データ(傾き、目の動き、口の開閉)とマイクからの音声レベルに応じて、パペットの頭、目、口のパーツをリアルタイムでアニメーションさせる。スペースキーでの姿勢キャリブレーションも可能。 3. **Automatorスクリプト**: Pixelmator Proで制作されたパペットの各レイヤーをPNG画像として書き出し、そのキャラクター名でパペット実行スクリプトを自動起動する。設定JSONファイルがなければデフォルト値で自動生成する機能も備える。 これにより、ユーザーは制作したパペットを簡単に設定・運用し、表情豊かな表現を可能にする。 /Users/XXXXXX/mypuppet_system/avatar_module/run_ui.py ``` #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ run_ui.py (完全env・ポータブル仕様版) - 金庫 (.env) からの動的パス解決機能を追加 - UI Preferenceやrun_puppet.pyへのパスを完全に自動追従 """ import tkinter as tk from tkinter import ttk, messagebox, simpledialog import os import json import subprocess import sys import shutil from pathlib import Path from dotenv import load_dotenv # --- 1. 環境設定(金庫 .env からの動的ロード) --- def load_env_portable(): home_env = Path.home() / "python_scripts" / ".env" if home_env.exists(): load_dotenv(home_env) load_env_portable() # システム格納先ディレクトリの自動特定 (未設定時はスクリプト親の親フォルダに自律フォールバック) PUPPET_DIR_ENV = os.getenv("PUPPET_SYSTEM_DIR") if PUPPET_DIR_ENV: BASE_DIR = os.path.join(PUPPET_DIR_ENV, "avatar_module") else: BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ASSETS_DIR = os.path.join(BASE_DIR, "run_assets") PREFS_FILE = os.path.join(BASE_DIR, "ui_prefs.json") PUPPET_SCRIPT = os.path.join(BASE_DIR, "run_puppet.py") DEFAULT_PARAMS = { "VOLUME_THRESHOLD": 1.5, "GLOBAL_SCALE": 0.6, "SMOOTH_FACTOR": 0.05, "MAX_TILT": 12.5, "RETURN_FORCE": 0.98, "EYE_SENSITIVITY": 0.0125, "CAMERA_ID": 1, "MIC_ID": 18 } PARAM_DESCRIPTIONS = { "VOLUME_THRESHOLD": "【声の反応感度】\nこの数値より大きい声に反応して口が動きます。\n数値を小さくすると、小さな声でもパクパクしやすくなります。", "GLOBAL_SCALE": "【キャラクターの表示サイズ】\nパペットの大きさを倍率で指定します。\n1.0が元の画像サイズ、0.6なら60%の大きさになります。", "SMOOTH_FACTOR": "【動きの滑らかさ】\n顔の動きの機敏さを調整します。\n小さくするとゆっくりヌルヌル動き、大きくするとキビキビ動きます。", "MAX_TILT": "【首の最大傾き角度】\n顔を傾けたときに、パペットの首が曲がる限界の角度です。\n大きすぎると不自然に曲がってしまいます。", "RETURN_FORCE": "【正面に戻る力】\n顔が元の位置(正面)に戻ろうとする力です。\n0.98など、1.0に近い数値にするほどスッと戻りやすくなります。", "EYE_SENSITIVITY": "【目の動きの感度】\n数値を小さくするほど、あなたの黒目が少し動いただけでパペットの目も敏感に反応して動くようになります。", "CAMERA_ID": "【カメラの認識番号】\n顔認識に使用するカメラのIDです。\nMac内蔵カメラは「0」や「1」になることが多いです。", "MIC_ID": "【マイクの認識番号】\n声に合わせて口を動かすためのマイクIDです。\nターミナルで確認した番号(18など)を入力します。" } class PuppetUI: def __init__(self, root): self.root = root self.root.title("パペット設定") self.root.geometry("400x530") self.current_params = DEFAULT_PARAMS.copy() self.param_vars = {} # 起動時安全チェック(フォルダ構成の自動補完) if not os.path.exists(ASSETS_DIR): os.makedirs(ASSETS_DIR) self.create_widgets() self.load_prefs() def create_widgets(self): # --- キャラクター選択エリア --- frame_top = tk.Frame(self.root, pady=5) frame_top.pack(fill="x", padx=10) tk.Label(frame_top, text="キャラクター選択:", font=("Arial", 11, "bold")).pack(side="left") self.char_combo = ttk.Combobox(frame_top, state="readonly", width=15) self.char_combo.pack(side="left", padx=5) self.char_combo.bind("<>", lambda e: self.on_char_selected()) # --- 管理操作ボタン --- btn_new = tk.Button(frame_top, text="新規", command=self.new_character, bg="#eeeeee") btn_new.pack(side="left", padx=2) btn_copy = tk.Button(frame_top, text="複製", command=self.copy_character, bg="#eeeeee") btn_copy.pack(side="left", padx=2) # --- スクロール可能な設定エリア --- canvas = tk.Canvas(self.root, borderwidth=0, highlightthickness=0) scrollbar = ttk.Scrollbar(self.root, orient="vertical", command=canvas.yview) self.scrollable_frame = tk.Frame(canvas) self.scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True, padx=10, pady=5) scrollbar.pack(side="right", fill="y", pady=5) # 各パラメータ設定項目の配置 for key, default_val in DEFAULT_PARAMS.items(): row = tk.Frame(self.scrollable_frame, pady=3) row.pack(fill="x", expand=True) # ヘルプ付きLabel lbl = tk.Label(row, text=key, font=("Arial", 10), cursor="question_arrow") lbl.pack(side="left") lbl.bind("", lambda e, k=key: self.show_help(k)) var = tk.StringVar() entry = tk.Entry(row, textvariable=var, width=10) entry.pack(side="right") self.param_vars[key] = var # --- 実行ボタンエリア --- btn_run = tk.Button(self.root, text="▶ 設定を保存して起動", font=("Arial", 13, "bold"), command=self.run_puppet, bg="#4CAF50", fg="black") btn_run.place(relx=0.5, rely=0.92, anchor="center", relwidth=0.85, height=45) def refresh_chars(self): if not os.path.exists(ASSETS_DIR): return chars = [d for d in os.listdir(ASSETS_DIR) if os.path.isdir(os.path.join(ASSETS_DIR, d))] if not chars: # default のみ強制作成 os.makedirs(os.path.join(ASSETS_DIR, "default"), exist_ok=True) chars = ["default"] self.char_combo['values'] = chars def on_char_selected(self): char_name = self.char_combo.get() if not char_name: return char_dir = os.path.join(ASSETS_DIR, char_name) json_path = os.path.join(char_dir, f"{char_name}.json") # 個別設定のロード if os.path.exists(json_path): try: with open(json_path, 'r', encoding='utf-8') as f: self.current_params = json.load(f) except Exception: self.current_params = DEFAULT_PARAMS.copy() else: self.current_params = DEFAULT_PARAMS.copy() # 画面入力欄の更新 for key, var in self.param_vars.items(): var.set(str(self.current_params.get(key, DEFAULT_PARAMS[key]))) def show_help(self, key): desc = PARAM_DESCRIPTIONS.get(key, "説明はありません。") messagebox.showinfo("パラメータ説明", desc) def new_character(self): new_name = simpledialog.askstring("新規作成", "新しいキャラクター名(半角英数推奨)を入力してください:") if not new_name: return new_name = new_name.strip() dst_dir = os.path.join(ASSETS_DIR, new_name) if os.path.exists(dst_dir): messagebox.showerror("エラー", "既に同名キャラクターが存在します。") return try: os.makedirs(dst_dir, exist_ok=True) # デフォルト設定の書き出し json_path = os.path.join(dst_dir, f"{new_name}.json") with open(json_path, 'w', encoding='utf-8') as f: json.dump(DEFAULT_PARAMS, f, indent=4) self.refresh_chars() self.char_combo.set(new_name) self.on_char_selected() messagebox.showinfo("成功", f"「{new_name}」を作成しました!\nrun_assets/{new_name}/ フォルダに画像パーツを配置してください。") except Exception as e: messagebox.showerror("エラー", f"作成中にエラーが発生しました:\n{e}") def copy_character(self): src_name = self.char_combo.get() if not src_name: messagebox.showwarning("警告", "コピー元となるキャラクターを選択してください。") return new_name = simpledialog.askstring("キャラクター複製", f"「{src_name}」の複製名を入力してください:") if not new_name: return new_name = new_name.strip() dst_dir = os.path.join(ASSETS_DIR, new_name) if os.path.exists(dst_dir): messagebox.showerror("エラー", "既に同名キャラクターが存在します。") return try: src_dir = os.path.join(ASSETS_DIR, src_name) shutil.copytree(src_dir, dst_dir) # コピー先JSONファイル名の正規化リネーム old_json = os.path.join(dst_dir, f"{src_name}.json") new_json = os.path.join(dst_dir, f"{new_name}.json") if os.path.exists(old_json): os.rename(old_json, new_json) # キャリブレーションファイルの初期化(複製時は自動削除) calib_file = os.path.join(dst_dir, f"{new_name}_calib.json") if os.path.exists(calib_file): os.remove(calib_file) self.refresh_chars() self.char_combo.set(new_name) self.on_char_selected() messagebox.showinfo("成功", f"「{new_name}」として複製が完了しました!") except Exception as e: messagebox.showerror("エラー", f"複製の処理中にエラーが発生しました:\n{e}") def run_puppet(self): char_name = self.char_combo.get() if not char_name: return # 画面パラメータの入力値バリデーションと読み込み for key, var in self.param_vars.items(): try: val = float(var.get()) # 整数が必要なパラメータはキャスト if key in ["CAMERA_ID", "MIC_ID"]: val = int(val) self.current_params[key] = val except ValueError: messagebox.showerror("入力エラー", f"「{key}」の数値が正しくありません。") return # 設定の保存 char_dir = os.path.join(ASSETS_DIR, char_name) json_path = os.path.join(char_dir, f"{char_name}.json") with open(json_path, 'w', encoding='utf-8') as f: json.dump(self.current_params, f, indent=4) self.save_prefs() # 【デトックス・ポータブル】仮想環境のpython実行バイナリを動的に突き止めて起動 python_bin = sys.executable subprocess.Popen([python_bin, PUPPET_SCRIPT, char_name]) def save_prefs(self): with open(PREFS_FILE, 'w', encoding='utf-8') as f: json.dump({"last_character": self.char_combo.get()}, f) def load_prefs(self): self.refresh_chars() if os.path.exists(PREFS_FILE): try: with open(PREFS_FILE, 'r', encoding='utf-8') as f: last_char = json.load(f).get("last_character") if last_char in self.char_combo['values']: self.char_combo.set(last_char) self.on_char_selected() except Exception: pass if __name__ == "__main__": root = tk.Tk() app = PuppetUI(root) root.mainloop() ``` /Users/XXXXXX/mypuppet_system/avatar_module/run_puppet.py ``` import pygame import sys import cv2 import sounddevice as sd import numpy as np import os import random import math import mediapipe as mp import json # --- 1. 基本設定(UIから受け取る準備) --- WINDOW_TITLE = "XXXXXX system - Auto Memory Edition" WINDOW_WIDTH = 800 WINDOW_HEIGHT = 600 FPS = 30 GREEN_SCREEN = (0, 255, 0) NECK_PIVOT_X = 500 NECK_PIVOT_Y = 300 # UIから渡されたキャラクター名を受け取る(指定がなければ 'default') if len(sys.argv) > 1: CHAR_NAME = sys.argv[1] else: CHAR_NAME = "default" WINDOW_TITLE = f"{WINDOW_TITLE} [{CHAR_NAME}]" # --- 2. キャラクターごとの設定読み込み --- current_dir = os.path.dirname(os.path.abspath(__file__)) char_dir = os.path.join(current_dir, "run_assets", CHAR_NAME) json_path = os.path.join(char_dir, f"{CHAR_NAME}.json") # デフォルトパラメータ PARAMS = { "VOLUME_THRESHOLD": 1.5, "GLOBAL_SCALE": 0.6, "SMOOTH_FACTOR": 0.05, "MAX_TILT": 12.5, "RETURN_FORCE": 0.98, "EYE_SENSITIVITY": 0.008, "CAMERA_ID": 1, "MIC_ID": 0 } # JSONが存在すれば上書き if os.path.exists(json_path): try: with open(json_path, 'r', encoding='utf-8') as f: PARAMS.update(json.load(f)) except Exception as e: print(f"JSON読み込みエラー: {e}") # パラメータを変数に展開 VOLUME_THRESHOLD = PARAMS["VOLUME_THRESHOLD"] GLOBAL_SCALE = PARAMS["GLOBAL_SCALE"] SMOOTH_FACTOR = PARAMS["SMOOTH_FACTOR"] MAX_TILT = PARAMS["MAX_TILT"] RETURN_FORCE = PARAMS["RETURN_FORCE"] EYE_SENSITIVITY = PARAMS["EYE_SENSITIVITY"] CAMERA_ID = PARAMS["CAMERA_ID"] MIC_ID = PARAMS["MIC_ID"] current_volume = 0 current_tilt = 0 def audio_callback(indata, frames, time, status): global current_volume current_volume = np.linalg.norm(indata) * 15 def enhance_for_ai(image): img_yuv = cv2.cvtColor(image, cv2.COLOR_RGB2YUV) clahe = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(8,8)) img_yuv[:,:,0] = clahe.apply(img_yuv[:,:,0]) return cv2.cvtColor(img_yuv, cv2.COLOR_YUV2RGB) def load_and_scale(name, scale): path = os.path.join(char_dir, name) if os.path.exists(path): img = pygame.image.load(path).convert_alpha() w, h = img.get_size() return pygame.transform.scale(img, (int(w * scale), int(h * scale))) return None def main(): global current_tilt pygame.init() screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(WINDOW_TITLE) clock = pygame.time.Clock() font = pygame.font.Font(None, 24) eyes = {k: load_and_scale(f"eye_{v}.png", GLOBAL_SCALE) for k, v in {"front":"open", "up":"up", "down":"down", "left":"left", "right":"right", "up_left":"up_left", "up_right":"up_right", "down_left":"down_left", "down_right":"down_right", "blink":"close"}.items()} mouths = {k: load_and_scale(f"mouth_{v}.png", GLOBAL_SCALE) for k, v in {"a":"a", "i":"i", "u":"u", "e":"e", "o":"o", "close":"close"}.items()} img_body = load_and_scale("body.png", GLOBAL_SCALE) img_head = load_and_scale("head.png", GLOBAL_SCALE) try: stream = sd.InputStream(device=MIC_ID, callback=audio_callback, channels=1, samplerate=44100) stream.start() except Exception as e: print(f"マイクエラー: {e}") stream = sd.InputStream(callback=audio_callback, channels=1, samplerate=44100) stream.start() cap = cv2.VideoCapture(CAMERA_ID) cap.set(cv2.CAP_PROP_BRIGHTNESS, 180) mp_face_mesh = mp.solutions.face_mesh face_mesh = mp_face_mesh.FaceMesh( max_num_faces=1, refine_landmarks=True, min_detection_confidence=0.5, min_tracking_confidence=0.5 ) calib_file = os.path.join(char_dir, f"{CHAR_NAME}_calib.json") base_angle = 0 base_iris_x, base_iris_y = 0.5, 0.5 tracking_enabled = False if os.path.exists(calib_file): try: with open(calib_file, "r") as f: data = json.load(f) base_angle = data.get("base_angle", 0) base_iris_x = data.get("base_iris_x", 0.5) base_iris_y = data.get("base_iris_y", 0.5) tracking_enabled = True except: pass last_detected_angle = 0 last_iris_x, last_iris_y = 0.5, 0.5 is_blinking = False current_mouth_type = "close" current_eye_type = "front" blink_timer = 0 mouth_change_timer = 0 continuous_voice_frames = 0 last_display_tilt = 0.0 current_target_eye = "front" running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: base_angle = last_detected_angle base_iris_x, base_iris_y = last_iris_x, last_iris_y current_tilt = 0 tracking_enabled = True try: with open(calib_file, "w") as f: json.dump({ "base_angle": base_angle, "base_iris_x": base_iris_x, "base_iris_y": base_iris_y }, f) except: pass screen.fill(GREEN_SCREEN) ret, frame = cap.read() if current_volume > VOLUME_THRESHOLD: continuous_voice_frames += 1 else: continuous_voice_frames = 0 target_tilt = 0 target_eye = "front" if ret: frame = cv2.flip(frame, 1) rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) ai_frame = enhance_for_ai(rgb_frame) results = face_mesh.process(ai_frame) is_detected = True if results and results.multi_face_landmarks else False try: h, w, _ = rgb_frame.shape crop_w, crop_h = int(w * 0.40), int(h * 0.40) x1, y1 = (w - crop_w) // 2, (h - crop_h) // 2 zoom_img = rgb_frame[y1:y1+crop_h, x1:x1+crop_w] wipe_w, wipe_h = 180, 135 wipe_img = cv2.resize(zoom_img, (wipe_w, wipe_h)) wipe_surf = pygame.image.frombuffer(wipe_img.tobytes(), wipe_img.shape[1::-1], "RGB") screen.blit(wipe_surf, (10, 10)) pygame.draw.rect(screen, (0, 0, 0), (10, 10, wipe_w, wipe_h), 2) except: pass if is_detected: for face_landmarks in results.multi_face_landmarks: lm = face_landmarks.landmark l_pt = lm.__getitem__(133) r_pt = lm.__getitem__(362) dx, dy = r_pt.x - l_pt.x, r_pt.y - l_pt.y last_detected_angle = math.degrees(math.atan2(dy, dx)) iris = lm.__getitem__(468) last_iris_x, last_iris_y = iris.x, iris.y if tracking_enabled: # 1. 首の傾き算出 raw_tilt = (last_detected_angle - base_angle) target_tilt = max(min(raw_tilt, MAX_TILT), -MAX_TILT) # 2. 黒目座標から基準座標への相対差分と逆回転補正 raw_diff_x = last_iris_x - base_iris_x raw_diff_y = last_iris_y - base_iris_y rad = math.radians(target_tilt) diff_x = raw_diff_x * math.cos(-rad) - raw_diff_y * math.sin(-rad) diff_y = raw_diff_x * math.sin(-rad) + raw_diff_y * math.cos(-rad) # 3. 閾値とヒステリシス(不感帯)の設定 thresh_x = EYE_SENSITIVITY thresh_y = EYE_SENSITIVITY * 0.6 release_x = thresh_x * 0.6 release_y = thresh_y * 0.6 tx = release_x if "left" in current_target_eye or "right" in current_target_eye else thresh_x ty = release_y if "up" in current_target_eye or "down" in current_target_eye else thresh_y is_left = diff_x < -tx is_right = diff_x > tx is_up = diff_y < -ty is_down = diff_y > ty if is_up and is_left: target_eye = "up_left" elif is_up and is_right: target_eye = "up_right" elif is_down and is_left: target_eye = "down_left" elif is_down and is_right: target_eye = "down_right" elif is_left: target_eye = "left" elif is_right: target_eye = "right" elif is_up: target_eye = "up" elif is_down: target_eye = "down" else: target_eye = "front" current_target_eye = target_eye # --- 【改修ポイント】口の動き制御:完全音声連動化(カメラ映像解析をカット) --- if continuous_voice_frames >= 1: camera_mouth_open = True # 音声検出時、音量に合わせて母音口パーツ(a, i, u, e, o)を自然にパクパクランダム切り替え detected_mouth = random.choice(["a", "a", "e", "o", "i"]) else: camera_mouth_open = False detected_mouth = "close" current_tilt += (target_tilt - current_tilt) * (SMOOTH_FACTOR if is_detected else 0.05) current_tilt *= RETURN_FORCE blink_timer += 1 if not is_blinking and random.random() < 0.02: is_blinking = True blink_timer = 0 if is_blinking and blink_timer > 4: is_blinking = False if camera_mouth_open: if current_mouth_type == "close": current_mouth_type = detected_mouth mouth_change_timer = 0 else: mouth_change_timer += 1 if mouth_change_timer >= 3: current_mouth_type = detected_mouth mouth_change_timer = 0 current_eye_type = target_eye if is_detected else "front" else: current_mouth_type = "close" current_eye_type = "front" if not is_detected else target_eye mouth_change_timer = 0 if img_body: screen.blit(img_body, img_body.get_rect(center=(NECK_PIVOT_X, NECK_PIVOT_Y))) canvas_size = int(1000 * GLOBAL_SCALE) temp_canvas = pygame.Surface((canvas_size, canvas_size), pygame.SRCALPHA) if img_head: temp_canvas.blit(img_head, (0, 0)) eye_key = "blink" if is_blinking else current_eye_type cur_eye = eyes.get(eye_key, eyes["front"]) if cur_eye: temp_canvas.blit(cur_eye, (0, 0)) img_mouth = mouths.get(current_mouth_type, mouths["close"]) if img_mouth: temp_canvas.blit(img_mouth, (0, 0)) temp_display_tilt = round(current_tilt, 1) if abs(temp_display_tilt - last_display_tilt) >= 0.2: last_display_tilt = temp_display_tilt rotated_head = pygame.transform.rotozoom(temp_canvas, last_display_tilt, 1.0) rot_rect = rotated_head.get_rect() rot_rect.center = (NECK_PIVOT_X, NECK_PIVOT_Y) screen.blit(rotated_head, rot_rect) if not is_detected: status_text = "WAITING" elif not tracking_enabled: status_text = "READY (SPACE)" else: status_text = "TRACKING" bg_rect = pygame.Rect(10, 150, 180, 26) pygame.draw.rect(screen, (255, 255, 255), bg_rect) pygame.draw.rect(screen, (0, 0, 0), bg_rect, 2) text_surface = font.render(status_text, True, (0, 0, 0)) text_rect = text_surface.get_rect(center=bg_rect.center) screen.blit(text_surface, text_rect) pygame.display.flip() clock.tick(FPS) stream.stop() cap.release() face_mesh.close() pygame.quit() sys.exit() if __name__ == "__main__": main() ``` Automator 自作パペット起動.app シェル:/bin/zsh 入力の引き渡し方法 stdinへ ``` # 仮想環境に入って、UI(司令塔)を起動する cd /Users/XXXXXX/mypuppet_system source venv/bin/activate python3 avatar_module/run_ui.py ``` ◆◆ここから下が、PixelmatorProでのパーツ作成からパペット起動までのパイプライン処理を行う部分です。 Automator パペット書き出し起動.app AppleScriptを実行、とシェルスクリプトを実行を、パイプライン。 AppleScriptを実行 ``` on run {input, parameters} tell application "Pixelmator Pro" if not (exists front document) then display alert "エラー" message "Pixelmator Proでパペットのファイルを開いてから実行してください。" return input end if set doc to front document try set docFile to file of doc on error display alert "エラー" message "ファイルがまだ保存されていません。一度pxdファイルとして保存してから書き出しを実行してください。" return input end try end tell tell application "Finder" set theContainer to container of (docFile as alias) set parentFolder to POSIX path of (theContainer as alias) end tell tell application "Pixelmator Pro" tell front document set charName to name -- ファイル名から拡張子 (.pxd) を美しく除去 if charName ends with ".pxd" then set charName to text 1 thru -5 of charName end if -- レイヤーの書き出し格納先を自動特定 set exportPOSIX to parentFolder & "body.png" set allLayers to layers -- レイヤーの表示・非表示を精密にコントロールしてパーツ個別PNG書き出し repeat with currentLayer in allLayers set layerName to name of currentLayer if (layerName is "body") or (layerName is "head") or (layerName is "eye_left") or (layerName is "eye_right") or (layerName is "eye_up") or (layerName is "eye_down") or (layerName is "eye_front") or (layerName is "eye_blink") or (layerName is "mouth_open") or (layerName is "mouth_close") then -- 一時的に対象のみを表示して書き出し repeat with l in allLayers set visible of l to false end repeat set visible of currentLayer to true set exportPOSIX to parentFolder & layerName & ".png" set exportPath to (POSIX file exportPOSIX) as string export doc to file exportPath as PNG set visible of currentLayer to false end if end repeat -- レイヤーの表示状態を全復元 repeat with currentLayer in allLayers set visible of currentLayer to true end repeat display notification "パーツ書き出し完了。「" & charName & "」を起動します!" with title "XXXXXXシステム" end tell -- 次のブロック(シェルスクリプト)にキャラクター名をバトンタッチ return charName end tell end run ``` シェルスクリプトを実行 シェル:/bin/zsh 引数として ``` #!/bin/zsh # ============================================================================== # パペット書き出し起動 - 連動起動部 (完全env・ポータブル・自動復旧付き) # ============================================================================== export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" # 前段の AppleScript から引き渡されたキャラクター名を受け取る CHAR_NAME=$1 # すでに起動している古いパペットの安全なキル (二重起動、カメラ奪い合い衝突の完全防止) pkill -f "run_puppet.py" || true sleep 1 # 1. 金庫 (.env) から環境設定を安全ロード ENV_FILE="$HOME/python_scripts/.env" if [ -f "$ENV_FILE" ]; then set -a source "$ENV_FILE" set +a fi # 2. 格納先ディレクトリの動的自律解決 (未定義時は $HOME/mypuppet_system に自動フォールバック) PUPPET_DIR="${PUPPET_SYSTEM_DIR:-$HOME/mypuppet_system}" # 3. 起動フォルダに移動 cd "$PUPPET_DIR" || exit 1 # 4. キャラクター個別フォルダにJSONが存在しない場合、デフォルトパラメータ(マイク18番など)で安全自動補完 JSON_FILE="avatar_module/run_assets/${CHAR_NAME}/${CHAR_NAME}.json" if [ ! -f "$JSON_FILE" ]; then mkdir -p "avatar_module/run_assets/${CHAR_NAME}" cat << 'EOF' > "$JSON_FILE" { "VOLUME_THRESHOLD": 1.5, "GLOBAL_SCALE": 0.6, "SMOOTH_FACTOR": 0.05, "MAX_TILT": 12.5, "RETURN_FORCE": 0.98, "EYE_SENSITIVITY": 0.0125, "CAMERA_ID": 1, "MIC_ID": 18 } EOF fi # 5. 仮想環境をアクティベートし、バックグラウンドにて安全起動 source venv/bin/activate nohup python3 avatar_module/run_puppet.py "$CHAR_NAME" > /dev/null 2>&1 & ```