mod_avatar.py•11.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())