Skip to main content
Glama
mod_avatar.py11.4 kB
import sys from PySide6.QtWidgets import QWidget, QLabel, QApplication from PySide6.QtCore import Qt, QTimer, QPoint, Slot from PySide6.QtGui import QPixmap, QShortcut, QKeySequence from pvv_mcp_server.avatar.mod_load_image import load_image from pvv_mcp_server.avatar.mod_update_frame import update_frame from pvv_mcp_server.avatar.mod_right_click_context_menu import right_click_context_menu from pvv_mcp_server.avatar.mod_avatar_dialog import AvatarDialog import pvv_mcp_server.avatar.mod_update_position import logging # ロガーの設定 logger = logging.getLogger(__name__) class AvatarWindow(QWidget): """YMMアバター表示ウィンドウ""" def __init__(self, style_id, speaker_name, zip_path=None, app_title="Claude", anime_types=None, flip=False, scale_percent=100, position="right_out", config=None): """ コンストラクタ Args: zip_path: YMM立ち絵ZIPファイルのパス app_title: 追随対象アプリケーションのウィンドウタイトル anime_types: アニメーションタイプのリスト (例: ["stand", "mouth"]) flip: 左右反転フラグ scale_percent: 縮尺パーセント position: 表示位置 (left_out, left_in, right_in, right_out) """ logger.info(f"AvatarDialog.__init__ 開始: config={config is not None}") super().__init__() # 基本設定 self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool) self.setAttribute(Qt.WA_TranslucentBackground) # Escキーで非表示 QShortcut(QKeySequence("Escape"), self, self.hide) #QShortcut(QKeySequence("Escape"), self, QApplication.quit) # UI初期化 self.label = QLabel(self) self.label.setAlignment(Qt.AlignCenter) # メンバ変数初期化 self.style_id = style_id self.speaker_name = speaker_name self.zip_path = zip_path self.app_title = app_title self.position = position # 表示位置: left_out, left_in, right_in, right_out self.flip = flip # 左右反転 self.scale_percent = scale_percent # 縮尺パーセント self.anime_types = anime_types or ["立ち絵", "口パク"] self.frame_timer_interval = 50 self.follow_timer_interval = 150 # zip読み込み self.zip_data = load_image(self.zip_path, self.speaker_name) # [パーツ][PNGファイル[バイナリデータ] # アニメーション設定 self.anime_types = anime_types or ["立ち絵", "口パク"] self.anime_type = anime_types[0] if anime_types else "立ち絵" # 現在のアニメーションタイプ # YMMダイアログ管理 self.dialogs = {} for anime_type in self.anime_types: conf = None if config and "dialogs" in config and anime_type in config["dialogs"]: conf = config["dialogs"][anime_type] dialog = AvatarDialog(self, self.zip_data, self.scale_percent, self.flip, self.frame_timer_interval, conf) dialog.setWindowTitle(f"pvv-mcp-server - {self.speaker_name} - {anime_type} ダイアログ") self.dialogs[anime_type] = dialog # ★ 初回show→hideで初期化(ウィンドウマネージャーに登録) #dialog.show() #QApplication.processEvents() # イベント処理を強制 #dialog.hide() # 初期表示 update_frame(self) self.update_position() # タイマー設定 self.frame_timer = QTimer() self.frame_timer.timeout.connect(lambda: update_frame(self)) self.frame_timer.start(self.frame_timer_interval) self.follow_timer = QTimer() self.follow_timer.timeout.connect(lambda: self.update_position()) self.follow_timer.start(self.follow_timer_interval) # ドラッグ用変数 self._drag_pos = None # 右クリックメニューを有効化 self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.right_click_context_menu) logger.info(f"AvatarDialog.__init__ 完了") # # save/load confg # def save_config(self): """ 設定を辞書形式で返す Returns: dict: 設定辞書 """ self.frame_timer.stop() config = { "zip_path": self.zip_path, "app_title": self.app_title, "position": self.position, "flip": self.flip, "scale": self.scale_percent, "anime_types": self.anime_types, "frame_timer_interval": self.frame_timer_interval, "follow_timer_interval": self.follow_timer_interval, "dialogs" : {} } for animetype, dialog in self.dialogs.items(): conf = dialog.save_config() config["dialogs"][animetype] = conf logger.info(f"save_config [AvatarWindow]: {config}") self.frame_timer.start(self.frame_timer_interval) return config def load_config(self, config): """ 設定辞書から設定を読み込む Args: config: save_config()で保存した辞書 """ logger.info(f"load_config [AvatarWindow]") self.frame_timer.stop() if "zip_path" in config: self.zip_path = config["zip_path"] logger.info(f" zip_path: {config['zip_path']}") if "app_title" in config: self.app_title = config["app_title"] logger.info(f" app_title: {config['app_title']}") if "position" in config: self.set_position(config["position"]) logger.info(f" position: {config['position']}") if "flip" in config: self.set_flip(config["flip"]) logger.info(f" flip: {config['flip']}") if "scale" in config: self.set_scale(config["scale"]) logger.info(f" scale: {config['scale']}") if "anime_types" in config: self.anime_types = config["anime_types"] logger.info(f" anime_types: {config['anime_types']}") if "frame_timer_interval" in config: self.set_frame_timer_interval(config["frame_timer_interval"]) logger.info(f" frame_timer_interval: {config['frame_timer_interval']}") if "follow_timer_interval" in config: self.follow_timer_interval = config["follow_timer_interval"] logger.info(f" follow_timer_interval: {config['follow_timer_interval']}") if "dialogs" in config: for anitype, dialog_config in config["dialogs"].items(): if anitype in self.dialogs: logger.info(f" loading dialog: {anitype}") self.dialogs[anitype].load_config(dialog_config) else: logger.warning(f" unknown dialog: {anitype}") self.frame_timer.start(self.frame_timer_interval) # # GUI # def update_position(self): # Claude ウィンドウに追従 pvv_mcp_server.avatar.mod_update_position.update_position(self) return def show(self): """show()をオーバーライドしてログ出力""" logger.info(f"AvatarDialog.show() called. title={self.windowTitle()}") super().show() logger.info(f"AvatarDialog.show() completed. isVisible={self.isVisible()}") @Slot() def showWindow(self): """スレッドセーフなshow""" self.show() def right_click_context_menu(self, position: QPoint) -> None: """右クリックメニュー""" right_click_context_menu(self, position) return def mousePressEvent(self, event): """マウス押下イベント""" if event.button() == Qt.LeftButton: self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft() self.follow_timer.stop() event.accept() def mouseMoveEvent(self, event): """マウス移動イベント(ドラッグ)""" if self._drag_pos is not None and event.buttons() & Qt.LeftButton: self.move(event.globalPosition().toPoint() - self._drag_pos) event.accept() def mouseReleaseEvent(self, event): """マウスボタン離したらドラッグ終了""" if event.button() == Qt.LeftButton: self._drag_pos = None event.accept() # # セッター # @Slot(str) def set_anime_type(self, anime_type): """アニメーションタイプを設定""" if anime_type in self.anime_types: self.frame_timer.stop() self.anime_type = anime_type self.dialogs[anime_type].start_oneshot() self.frame_timer.start() def set_frame_timer_interval(self, val): """フレーム更新間隔を設定""" self.frame_timer_interval = val self.frame_timer.setInterval(self.frame_timer_interval) for animetype, dialog in self.dialogs.items(): dialog.set_frame_timer_interval(self.frame_timer_interval) def set_position(self, val): """表示位置を設定""" self.position = val def set_flip(self, val): """左右反転を設定""" self.flip = val for animetype, dialog in self.dialogs.items(): dialog.set_flip(self.flip) def set_scale(self, val): """スケール設定""" self.scale_percent = val for animetype, dialog in self.dialogs.items(): dialog.set_scale(self.scale_percent) if __name__ == "__main__": zip_file = "C:\\work\\lambda-tuber\\ai-trial\\mission16\\docs\\ゆっくり霊夢改.zip" #zip_file = "C:\\work\\lambda-tuber\\ai-trial\\mission16\\docs\\れいむ.zip" #zip_file = "C:\\work\\lambda-tuber\\ai-trial\\mission16\\docs\\josei_20_pw.zip" app = QApplication(sys.argv) # YMMアバターウィンドウを作成 # 実際のZIPファイルパスを指定してください avatar = AvatarWindow( zip_path=zip_file, app_title="Claude", anime_types=["立ち絵", "口パク"], flip=False, scale_percent=50, position="right_out" ) avatar.show() conf = avatar.save_config() print(conf) conf["dialogs"]["立ち絵"]["parts"]["顔"]["base_image"] = "06b.png" avatar2 = AvatarWindow( zip_path=zip_file, app_title="Claude", anime_types=["立ち絵", "口パク"], flip=False, scale_percent=50, position="right_out", config=conf) avatar2.show() sys.exit(app.exec())

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lambda-tuber/pvv-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server