Skip to main content
Glama
mod_avatar_part.py16.7 kB
from PySide6.QtWidgets import ( QApplication, QWidget, QLabel, QComboBox, QListWidget, QListWidgetItem, QVBoxLayout, QHBoxLayout, QScrollArea, QCheckBox ) from PySide6.QtWidgets import QHBoxLayout, QLabel, QRadioButton, QButtonGroup from PySide6.QtCore import Qt import sys import random import logging # ロガーの設定 logger = logging.getLogger(__name__) class AvatarPartWidget(QWidget): def __init__(self, part_name, image_files, config=None): """ Args: part_name: パーツ名 image_files: 画像ファイルのリスト config: 初期設定辞書(Noneならデフォルト値) """ super().__init__() self.part_name = part_name self.image_files = image_files # デフォルト値で初期化 self._init_default_values() # 設定が渡されていれば適用 if config: self.load_config(config) # GUI構築(設定を反映した状態で) self._setup_gui() def _init_default_values(self): """デフォルト値で初期化""" if len(self.image_files) > 0: self.base_image = self.image_files[0] else: self.base_image = None self.current_image = self.base_image self.selected_files = [] self.update_idx = 0 self.interval = 3 self.anime_type = "固定" self.is_enabled = True # パーツの有効状態 self.random_wait_tick = random.choice([10, 20, 30, 40, 50]) self.random_wait_idx = 0 self.random_anime_idx = 0 self.loop_anime_idx = 0 self.oneshot_idx = 0 # # save/load confg # def save_config(self): """ 現在の設定を辞書形式で返す Returns: dict: 設定辞書 """ config = { "part_name": self.part_name, "base_image": self.base_image, "selected_files": self.selected_files.copy(), "interval": self.interval, "anime_type": self.anime_type, "is_enabled": self.is_enabled } logger.info(f"save_config [{self.part_name}]: {config}") return config def load_config(self, config): """ 設定辞書から値を読み込む Args: config: save_config()で保存した辞書 """ logger.info(f"load_config [{self.part_name}]: {config}") if "base_image" in config: self.base_image = config["base_image"] self.current_image = self.base_image if "selected_files" in config: self.selected_files = config["selected_files"].copy() if "interval" in config: self.interval = config["interval"] if "anime_type" in config: self.anime_type = config["anime_type"] if "is_enabled" in config: self.is_enabled = config["is_enabled"] # カウンタ類をリセット self.update_idx = 0 self.loop_anime_idx = 0 self.random_anime_idx = 0 self.random_wait_idx = 0 self.random_wait_tick = random.choice([10, 20, 30, 40, 50]) # GUIが既に構築されていれば、GUIにも反映 if hasattr(self, 'combo_base'): self._apply_config_to_gui() # # GUI # def _setup_gui(self): main_layout = QVBoxLayout(self) # 1段目: パーツ名 part_name_layout = QHBoxLayout() part_name_layout.addWidget(QLabel("パーツ名:")) part_name_layout.addWidget(QLabel(self.part_name)) part_name_layout.addStretch(1) main_layout.addLayout(part_name_layout) # 2段目: 有効/無効チェックボックス enabled_layout = QHBoxLayout() enabled_layout.addWidget(QLabel("有効/無効:")) self.check_enabled = QCheckBox() self.check_enabled.setChecked(True) self.check_enabled.stateChanged.connect(self._on_enabled_changed) enabled_layout.addWidget(self.check_enabled) enabled_layout.addStretch(1) main_layout.addLayout(enabled_layout) # 3段目: ベース画像 base_layout = QHBoxLayout() base_layout.addWidget(QLabel("ベース画像:")) self.combo_base = QComboBox() self.combo_base.addItems(self.image_files) self.combo_base.currentTextChanged.connect(self._update_base_image) base_layout.addWidget(self.combo_base) base_layout.addStretch(1) main_layout.addLayout(base_layout) # 4段目: アニメ画像 anim_layout = QHBoxLayout() anim_layout.addWidget(QLabel("アニメ画像:"), alignment=Qt.AlignTop) self.list_anim = QListWidget() self.list_anim.setSelectionMode(QListWidget.MultiSelection) for f in self.image_files: self.list_anim.addItem(QListWidgetItem(f)) self.list_anim.itemSelectionChanged.connect(self._update_selected_files) self.list_anim.setMaximumHeight(100) anim_layout.addWidget(self.list_anim) main_layout.addLayout(anim_layout) # 5段目: interval interval_layout = QHBoxLayout() interval_layout.addWidget(QLabel("インターバル:")) self.combo_interval = QComboBox() self.combo_interval.addItems(["1", "2", "4", "8", "10","12", "14", "16", "18", "20", "22", "24", "26", "28", "30"]) self.combo_interval.setCurrentIndex(2) self.combo_interval.currentTextChanged.connect(self._update_interval) interval_layout.addWidget(self.combo_interval) interval_layout.addStretch(1) main_layout.addLayout(interval_layout) # 6段目: アニメーションタイプ anim_type_layout = QHBoxLayout() anim_type_layout.addWidget(QLabel("アニメタイプ:")) self.radio_fixed = QRadioButton("固定") self.radio_loop = QRadioButton("ループ") self.radio_random_a = QRadioButton("ランダムA") self.radio_random_b = QRadioButton("ランダムB") self.radio_oneshot = QRadioButton("ワンショット") self.anim_type_group = QButtonGroup(self) self.anim_type_group.addButton(self.radio_fixed) self.anim_type_group.addButton(self.radio_loop) self.anim_type_group.addButton(self.radio_random_a) self.anim_type_group.addButton(self.radio_random_b) self.anim_type_group.addButton(self.radio_oneshot) self.anim_type_group.buttonClicked.connect(self._on_anim_type_changed) self.radio_fixed.setChecked(True) anim_type_layout.addWidget(self.radio_fixed) anim_type_layout.addWidget(self.radio_loop) anim_type_layout.addWidget(self.radio_random_a) anim_type_layout.addWidget(self.radio_random_b) anim_type_layout.addWidget(self.radio_oneshot) anim_type_layout.addStretch(1) main_layout.addLayout(anim_type_layout) # 最後に設定をGUIに反映 self._apply_config_to_gui() def _apply_config_to_gui(self): """設定値をGUIウィジェットに反映""" logger.info(f"_apply_config_to_gui [{self.part_name}]") logger.info(f" selected_files: {self.selected_files}") # 有効/無効のチェックボックス self.check_enabled.blockSignals(True) self.check_enabled.setChecked(self.is_enabled) self.check_enabled.blockSignals(False) # ベース画像のコンボボックス if self.base_image: index = self.combo_base.findText(self.base_image) if index >= 0: self.combo_base.setCurrentIndex(index) # アニメ画像のリスト選択 # シグナルを一時的にブロックして、余計なイベント発火を防ぐ self.list_anim.blockSignals(True) # まず全選択解除 for i in range(self.list_anim.count()): self.list_anim.item(i).setSelected(False) # selected_filesに含まれるものだけ選択 for i in range(self.list_anim.count()): item = self.list_anim.item(i) if item.text() in self.selected_files: item.setSelected(True) logger.info(f" 選択: {item.text()}") # シグナルのブロックを解除 self.list_anim.blockSignals(False) # インターバル index = self.combo_interval.findText(str(self.interval)) if index >= 0: self.combo_interval.setCurrentIndex(index) # アニメタイプのラジオボタン if self.anime_type == "固定": self.radio_fixed.setChecked(True) elif self.anime_type == "ループ": self.radio_loop.setChecked(True) elif self.anime_type == "ランダムA": self.radio_random_a.setChecked(True) elif self.anime_type == "ランダムB": self.radio_random_b.setChecked(True) elif self.anime_type == "ワンショット": self.radio_oneshot.setChecked(True) # # gui handlers # def _on_enabled_changed(self, state): """有効/無効チェックボックスの状態変化ハンドラ""" self.is_enabled = bool(state) logger.info(f"is_enabled [{self.part_name}]: {self.is_enabled}") def _update_selected_files(self): self.selected_files = [item.text() for item in self.list_anim.selectedItems()] logger.info(f"selected_files: {self.selected_files}") def _update_base_image(self, text): self.base_image = text logger.info(f"base_image: {self.base_image}") def _update_interval(self, text): self.interval = int(text) logger.info(f"interval: {self.interval}") def _on_anim_type_changed(self, button): self.anime_type = button.text() logger.info(f"選択されたアニメーションタイプ: {self.anime_type}") # 共通のカウンタをリセット self.update_idx = 0 self.loop_anime_idx = 0 self.random_anime_idx = 0 self.random_wait_idx = 0 self.random_wait_tick = random.choice([10, 20, 30, 40, 50]) self.start_oneshot() logger.info(f"random_wait_tick : {self.random_wait_tick}") # # animation # def start_oneshot(self): """外部からoneshotアニメを開始するトリガー""" if not self.is_enabled: return if len(self.selected_files) > 0: logger.info(f"{self.part_name}: start_oneshot") self.oneshot_idx = 1 def update(self): if not self.is_enabled: return None if len(self.image_files) == 0: return None if self.update_idx < self.interval: self.update_idx = self.update_idx + 1 return self.current_image self.update_idx = 0 if self.anime_type == "固定": self.current_image = self.base_image if self.anime_type == "ループ": self._update_loop() if self.anime_type == "ランダムA": self._update_random_a() if self.anime_type == "ランダムB": self._update_random_b() if self.anime_type == "ワンショット": self._update_oneshot() return self.current_image def _update_loop(self): self.current_image = self.selected_files[self.loop_anime_idx] self.loop_anime_idx = (self.loop_anime_idx + 1) % len(self.selected_files) def _update_random_a(self): """ランダムA: base画像をランダム時間表示 → アニメ画像をワンショット再生""" if self.random_anime_idx == 0: # 待機モード: base画像を表示 self.current_image = self.base_image # カウントアップ self.random_wait_idx += 1 # ランダム待機時間に達したらアニメ開始 if self.random_wait_idx >= self.random_wait_tick: self.random_wait_idx = 0 # 次のランダム待機時間を設定 self.random_wait_tick = random.choice([10, 20, 30, 40, 50]) logger.info(f"random_wait_tick : {self.random_wait_tick}") # アニメ画像が選択されていればアニメ開始 if len(self.selected_files) > 0: self.random_anime_idx = 1 # アニメ開始フラグ else: # アニメ再生モード idx = self.random_anime_idx - 1 if idx < len(self.selected_files): self.current_image = self.selected_files[idx] self.random_anime_idx += 1 else: # アニメ終了: 待機モードに戻る self.random_anime_idx = 0 self.current_image = self.base_image def _update_random_b(self): """ランダムB: selected_filesからランダムに1つ選んで、ランダムな時間表示""" # カウンタをインクリメント self.random_wait_idx += 1 # まだ待機時間に達していない場合は、現在の画像をそのまま返す if self.random_wait_idx < self.random_wait_tick: return # 待機時間に達したら、次の画像に切り替え self.random_wait_idx = 0 # 次のランダムな待機時間を設定 self.random_wait_tick = random.choice([10, 20, 30, 40, 50]) logger.info(f"random_wait_tick : {self.random_wait_tick}") # ランダムに画像を選択 if len(self.selected_files) > 0: self.current_image = random.choice(self.selected_files) else: # フォールバック self.current_image = self.base_image def _update_oneshot(self): """ワンショット: selected_filesを順番に表示→base_imageに戻る""" if self.oneshot_idx == 0: # 待機モード: base画像を表示 self.current_image = self.base_image elif self.oneshot_idx > 0: # アニメ再生モード idx = self.oneshot_idx - 1 if idx < len(self.selected_files): self.current_image = self.selected_files[idx] self.oneshot_idx += 1 else: # アニメ終了: 待機モードに戻る self.oneshot_idx = 0 self.current_image = self.base_image if __name__ == "__main__": app = QApplication(sys.argv) # テスト1: デフォルトで生成 print("=== テスト1: デフォルト生成 ===") part_widget1 = AvatarPartWidget("目", ["01.png", "02.png", "03.png"]) # テスト2: 設定を保存 print("\n=== テスト2: 設定を保存 ===") config = part_widget1.save_config() print(f"保存された設定: {config}") # テスト3: 設定を変更 print("\n=== テスト3: 設定を変更 ===") part_widget1.anime_type = "ランダムA" part_widget1.selected_files = ["01.png", "02.png"] part_widget1.interval = 5 # テスト4: 変更した設定を保存 print("\n=== テスト4: 変更後の設定を保存 ===") config2 = part_widget1.save_config() print(f"変更後の設定: {config2}") # テスト5: 設定付きで新規生成 print("\n=== テスト5: 設定付きで生成 ===") preset_config = { "part_name": "口", "base_image": "02.png", "selected_files": ["01.png", "03.png"], "interval": 2, "anime_type": "ループ", "is_enabled": True } part_widget2 = AvatarPartWidget("口", ["01.png", "02.png", "03.png"], config=preset_config) part_widget1.show() part_widget2.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