# [メタ情報] # 識別子: 自作パペット_py_exe # 補足: # [/メタ情報] 要約: このPythonスクリプトは、pygame、OpenCV、sounddeviceを活用したライブ配信向けキャラクターパペットシステムです。ウェブカメラによる顔認識とマイクからの音声入力に基づき、パペットの頭の傾き、口の形、目の動き(瞬き、視線)をリアルタイムで制御します。 最大の特徴は「超・安定ロック版」と称される、徹底した動きの安定化設定です。カメラのノイズや顔の微細な動きに過剰に反応しないよう、顔位置のノイズを極限まで丸める`SMOOTH_FACTOR`を設定。特に、`CENTER_LOCK_ZONE`により、顔が画面中央の一定範囲内にある場合は頭の傾きを完全に0度にロックし、安定感を高めています。傾く量、速度、戻る速度も控えめに調整され、全体的に落ち着いた自然な動きを実現。音声入力が閾値を超えると口が開き、ランダムな瞬きや視線移動も行われます。スペースキーで顔の基準位置をリセット可能です。 付属のAutomatorスクリプトは、指定された仮想環境をアクティブ化した後、このパペットシステムを起動するためのものです。 /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 # --- 設定項目 --- WINDOW_TITLE = "XXXXXX system - Solid Rock Lock Version" WINDOW_WIDTH = 800 WINDOW_HEIGHT = 600 FPS = 30 GREEN_SCREEN = (0, 255, 0) VOLUME_THRESHOLD = 1.2 # 見た目の微調整 BODY_SCALE = 0.8 HEAD_SCALE = 0.5 EYE_SCALE = 0.5 MOUTH_SCALE = 0.5 # 座標設定(髪の毛保護・ベストバランス) FIXED_NECK_X = 400 FIXED_NECK_Y = 380 BODY_CENTER_Y = 480 HEAD_UP_OFFSET = 25 # 共通変数 EYE_DIST = 35 EYE_Y_OFFSET = 10 MOUTH_Y_OFFSET = 80 # ★徹底的に動きを落ち着かせるための「鈍感」設定 SMOOTH_FACTOR = 0.05 # カメラの振動を極限まで丸め込む CENTER_LOCK_ZONE = 0.08 # ★画面中央から±8%以内の動きは「完全に0度」にロック TILT_BOOST = 0.8 # 傾く量自体を控えめに(大げさに動かない) MAX_TILT = 15 # 最大でも15度までしか傾かせない TILT_SPEED = 0.15 # 動くスピードをゆっくりに TILT_RETURN = 0.3 # 戻るスピードも自然に current_volume = 0 current_tilt = 0 def audio_callback(indata, frames, time, status): global current_volume current_volume = np.linalg.norm(indata) * 10 def load_and_scale(name, scale): current_dir = os.path.dirname(os.path.abspath(__file__)) path = os.path.join(current_dir, "assets", 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(): print("=== XXXXXX system (超・安定ロック版) 起動開始 ===") global current_tilt pygame.init() screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(WINDOW_TITLE) clock = pygame.time.Clock() eyes = {k: load_and_scale(f"eye_{v}.png", EYE_SCALE) for k, v in {"front":"open", "up":"up", "down":"down", "left":"left", "right":"right", "blink":"close"}.items()} mouths = {k: load_and_scale(f"mouth_{v}.png", MOUTH_SCALE) for k, v in {"a":"a", "i":"i", "u":"u", "close":"close"}.items()} img_body = load_and_scale("body.png", BODY_SCALE) img_head = load_and_scale("head.png", HEAD_SCALE) stream = sd.InputStream(callback=audio_callback, channels=1, samplerate=44100); stream.start() cap = cv2.VideoCapture(1); face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') smooth_cx = 0.5 base_cx = 0.5 blink_timer = 0; is_blinking = False current_mouth_type = "close"; current_eye_type = "front"; eye_look_timer = 0 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_cx = smooth_cx print(f"基準位置をリセットしました! (base_cx: {base_cx:.3f})") screen.fill(GREEN_SCREEN) # 瞬き blink_timer += 1 if not is_blinking and random.random() < 0.01: is_blinking = True; blink_timer = 0 if is_blinking and blink_timer > 5: is_blinking = False ret, frame = cap.read() target_tilt = 0 if ret: frame = cv2.flip(frame, 1); h, w, _ = frame.shape cv2.rectangle(frame, (int(w*0.7), 0), (w, int(h*0.4)), (0, 0, 0), -1) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(50, 50)) if len(faces) > 0: x, y, fw, fh = sorted(faces, key=lambda f: f[2]*f[3], reverse=True)[0] raw_cx = (x + fw/2) / w # カメラのノイズを強力に丸める smooth_cx += (raw_cx - smooth_cx) * SMOOTH_FACTOR # 基準位置からのズレを計算 diff_cx = smooth_cx - base_cx # ★修正の核心:ズレがロックゾーン以内の場合は「0度」を強制 if abs(diff_cx) < CENTER_LOCK_ZONE: target_tilt = 0 else: # ロックゾーンを大きく超えた時だけ、超えた分だけ傾ける active_diff = diff_cx - CENTER_LOCK_ZONE if diff_cx > 0 else diff_cx + CENTER_LOCK_ZONE target_tilt = -active_diff * 100 * TILT_BOOST target_tilt = max(min(target_tilt, MAX_TILT), -MAX_TILT) # 角度を更新する処理 if target_tilt == 0: # ターゲットが0(静止ゾーン)に入ったら、スッと0に戻る current_tilt += (0 - current_tilt) * TILT_RETURN else: # ターゲットにゆっくり追従 current_tilt += (target_tilt - current_tilt) * TILT_SPEED # 1度未満の微細なブレは完全に静止させる(吸着処理) if abs(current_tilt) < 1.0 and target_tilt == 0: current_tilt = 0 # --- 描画レイヤー --- if img_body: screen.blit(img_body, img_body.get_rect(center=(400, BODY_CENTER_Y))) if img_head: hw, hh = img_head.get_size() canvas_size = 1000 temp_canvas = pygame.Surface((canvas_size, canvas_size), pygame.SRCALPHA) pivot_x = canvas_size // 2 pivot_y = canvas_size // 2 head_draw_x = pivot_x - hw // 2 head_draw_y = pivot_y - hh - HEAD_UP_OFFSET temp_canvas.blit(img_head, (head_draw_x, head_draw_y)) face_center_x = pivot_x face_center_y = head_draw_y + hh // 2 eye_key = "blink" if is_blinking else current_eye_type cur_eye = eyes.get(eye_key, eyes["front"]) if cur_eye: ew, eh = cur_eye.get_size() temp_canvas.blit(cur_eye, (face_center_x - EYE_DIST - ew//2, face_center_y + EYE_Y_OFFSET - eh//2)) temp_canvas.blit(cur_eye, (face_center_x + EYE_DIST - ew//2, face_center_y + EYE_Y_OFFSET - eh//2)) if current_volume > VOLUME_THRESHOLD: if random.random() < 0.3: current_mouth_type = random.choice(["a", "i", "u"]) eye_look_timer += 1 if eye_look_timer > 15: eye_look_timer = 0 current_eye_type = random.choice(["front"]*6 + ["up","down","left","right"]) else: current_mouth_type = "close"; current_eye_type = "front" img_mouth = mouths.get(current_mouth_type, mouths["close"]) if img_mouth: mw, mh = img_mouth.get_size() temp_canvas.blit(img_mouth, (face_center_x - mw//2, face_center_y + MOUTH_Y_OFFSET - mh//2)) rotated_head = pygame.transform.rotate(temp_canvas, current_tilt) rot_rect = rotated_head.get_rect() rot_rect.center = (FIXED_NECK_X, FIXED_NECK_Y) screen.blit(rotated_head, rot_rect) pygame.display.flip() clock.tick(FPS) stream.stop(); cap.release(); pygame.quit(); sys.exit() if __name__ == "__main__": main() ``` Automator 自作パペット起動.app シェル:/bin/zsh 入力の引き渡し方法 stdinへ ``` # 仮想環境に入って、プログラムを実行する cd /Users/XXXXXX/mypuppet_system source venv/bin/activate python avatar_module/run_puppet.py ```