# [メタ情報] # 識別子: 自作パペット_py_exe # 補足: # [/メタ情報] 要約: このシステムは、顔認識と音声入力を利用してリアルタイムで動作するデスクトップパペットを実現します。 `run_ui.py`は、パペットの各種設定を行うGUIアプリケーションです。キャラクターの新規作成、複製、選択、そしてボリューム閾値、グローバルスケール、顔の傾き制限、目の感度、カメラ/マイクIDといったパラメータ調整機能を提供します。これらの設定はキャラクターごとにJSONファイルとして保存されます。 `run_puppet.py`は、UIで選択・設定されたキャラクターとパラメータに基づき、実際のパペット動作を制御するスクリプトです。OpenCVとMediaPipe Face Meshを用いてカメラ映像から顔の傾き、目の動き、口の形を検出し、マイクからの音声ボリュームと連携させてアバターの頭の回転、視線、口の動きをリアルタイムで再現します。キャリブレーションデータもキャラクターごとに保存されます。 Automatorスクリプトは、これらのPythonスクリプトの実行を補助します。「自作パペット起動.app」は設定UIを立ち上げ、「パペット書き出し起動.app」はPixelmator Proで開いているドキュメントの各レイヤーをPNG画像として書き出し、キャラクターごとの設定JSONを自動生成した後、該当するパペットを起動する一連の処理を行います。これにより、Pixelmator Proでの画像編集からパペット起動までがスムーズに連携されます。 /Users/XXXXXX/mypuppet_system/avatar_module/run_ui.py ``` import tkinter as tk from tkinter import ttk, messagebox, simpledialog import os import json import subprocess import sys import shutil # --- 初期設定 --- 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="キャラクター選択:").pack(side="left") self.char_combo = ttk.Combobox(frame_top, state="readonly", width=20) self.char_combo.pack(side="left", fill="x", expand=True, padx=5) self.char_combo.bind("<>", self.on_char_selected) # --- キャラクター管理エリア(新規・複製) --- frame_manage = tk.Frame(self.root, pady=5) frame_manage.pack(fill="x", padx=10) btn_new = tk.Button(frame_manage, text="+ 新規作成", command=self.create_new_char) btn_new.pack(side="left", expand=True, fill="x", padx=(0, 2)) btn_dup = tk.Button(frame_manage, text="⧉ 現在のキャラを複製", command=self.duplicate_char) btn_dup.pack(side="left", expand=True, fill="x", padx=(2, 0)) # --- パラメータ調整エリア --- self.frame_params = tk.LabelFrame(self.root, text="パラメータ設定", pady=10, padx=10) self.frame_params.pack(fill="both", expand=True, padx=10, pady=5) for key in DEFAULT_PARAMS.keys(): row = tk.Frame(self.frame_params) row.pack(fill="x", pady=2) # ★追加:[?]ボタンを配置し、クリックで説明文を表示する設定 desc = PARAM_DESCRIPTIONS.get(key, "説明がありません。") btn_help = tk.Button(row, text="?", fg="blue", command=lambda k=key, d=desc: messagebox.showinfo(f"{k} の説明", d)) btn_help.pack(side="left", padx=(0, 5)) tk.Label(row, text=key, width=17, anchor="w").pack(side="left") var = tk.StringVar() entry = tk.Entry(row, textvariable=var, width=10) entry.pack(side="right") self.param_vars[key] = var # --- 実行ボタンエリア --- frame_bottom = tk.Frame(self.root, pady=10) frame_bottom.pack(fill="x") btn_run = tk.Button(frame_bottom, text="▶ キャラを実行 (保存して起動)", font=("Arial", 14, "bold"), command=self.run_puppet, bg="#ccffcc") btn_run.pack(pady=10) 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))] self.char_combo['values'] = chars if chars and not self.char_combo.get(): self.char_combo.current(0) self.on_char_selected() def on_char_selected(self, event=None): char_name = self.char_combo.get() if not char_name: return json_path = os.path.join(ASSETS_DIR, char_name, 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, val in self.current_params.items(): if key in self.param_vars: self.param_vars[key].set(str(val)) def create_new_char(self): new_name = simpledialog.askstring("新規作成", "新しいキャラクター名を入力してください:") if not new_name: return char_dir = os.path.join(ASSETS_DIR, new_name) if os.path.exists(char_dir): messagebox.showerror("エラー", "既に同名のキャラクターが存在します。") return os.makedirs(char_dir) json_path = os.path.join(char_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}」を作成しました。\nPixelmator Proでこのフォルダにpxdを保存してください。") def duplicate_char(self): src_name = self.char_combo.get() if not src_name: return new_name = simpledialog.askstring("複製", f"「{src_name}」を複製します。\n新しいキャラクター名を入力してください:") if not new_name or new_name == src_name: return src_dir = os.path.join(ASSETS_DIR, src_name) dst_dir = os.path.join(ASSETS_DIR, new_name) if os.path.exists(dst_dir): messagebox.showerror("エラー", "既に同名のキャラクターが存在します。") return try: shutil.copytree(src_dir, dst_dir) 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) old_pxd = os.path.join(dst_dir, f"{src_name}.pxd") new_pxd = os.path.join(dst_dir, f"{new_name}.pxd") if os.path.exists(old_pxd): os.rename(old_pxd, new_pxd) calib_file = os.path.join(dst_dir, f"{src_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 val.is_integer() or 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) if not os.path.exists(char_dir): os.makedirs(char_dir) 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() subprocess.Popen([sys.executable, 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.0125, "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) # ★読み込み先を assets_v2 から char_dir に変更 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) # ★指定されたマイクデバイス(MIC_ID)を使用 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() # ★指定されたカメラ(CAMERA_ID)を使用 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 debug_x = 0.0 debug_y = 0.0 last_display_tilt = 0.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_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" camera_mouth_open = False detected_mouth = "close" 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: target_tilt = (last_detected_angle - base_angle) target_tilt = max(min(target_tilt, MAX_TILT), -MAX_TILT) diff_x, diff_y = last_iris_x - base_iris_x, last_iris_y - base_iris_y is_left = diff_x < -EYE_SENSITIVITY is_right = diff_x > EYE_SENSITIVITY is_up = diff_y < -EYE_SENSITIVITY is_down = diff_y > EYE_SENSITIVITY 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" m_top = lm.__getitem__(13) m_bottom = lm.__getitem__(14) m_left = lm.__getitem__(78) m_right = lm.__getitem__(308) m_dist_y = abs(m_top.y - m_bottom.y) m_dist_x = abs(m_right.x - m_left.x) debug_x = m_dist_x debug_y = m_dist_y if m_dist_y > 0.010: detected_mouth = "o" if m_dist_x <= 0.029 else "a" elif m_dist_y > 0.005: detected_mouth = "u" if m_dist_x <= 0.029 else "e" else: if m_dist_x >= 0.032: detected_mouth = "i" elif m_dist_x <= 0.029: detected_mouth = "u" else: detected_mouth = "close" if continuous_voice_frames >= 1: camera_mouth_open = True if detected_mouth == "close": detected_mouth = random.choice(["a", "a", "e"]) elif random.random() < 0.10: detected_mouth = random.choice(["a", "i"]) elif m_dist_y > 0.005: camera_mouth_open = True else: camera_mouth_open = False 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) -- ★ここが要! 親フォルダの名前(=キャラクター名)を取得する set charName to name of (theContainer as alias) end tell tell application "Pixelmator Pro" set allLayers to layers of doc repeat with currentLayer in allLayers set visible of currentLayer to false end repeat repeat with currentLayer in allLayers set layerName to name of currentLayer if layerName is not "Background" and layerName is not "背景" then 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 run ``` シェルスクリプトを実行 シェル:/bin/zsh 引数として ``` # AppleScriptから渡されたキャラ名(例: XXXXXX2号)を受け取る CHAR_NAME=$1 # すでに起動しているパペットがあれば終了させる pkill -f "run_puppet.py" || true sleep 1 # プロジェクトのフォルダに移動 cd /Users/XXXXXX/mypuppet_system # ★追加:JSONファイルがなければ、デフォルト値(マイク18番)で自動作成する JSON_FILE="avatar_module/run_assets/${CHAR_NAME}/${CHAR_NAME}.json" if [ ! -f "$JSON_FILE" ]; then 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 # 仮想環境に入ってパペットを起動 source venv/bin/activate nohup python3 avatar_module/run_puppet.py "$CHAR_NAME" > /dev/null 2>&1 & ```