| import os |
| import sys |
| import importlib |
| import importlib.util |
| import inspect |
| from typing import Dict, Any, Optional, List, Union |
| from dataclasses import dataclass |
| import gradio as gr |
| import traceback |
| import subprocess |
| import git |
| import shutil |
| import stat |
| import json |
|
|
| def auto_install_and_enable_default_plugins(manager: 'PluginManager', wgp_globals: dict): |
| server_config = wgp_globals.get("server_config") |
| server_config_filename = wgp_globals.get("server_config_filename") |
|
|
| if not server_config or not server_config_filename: |
| print("[Plugins] WARNING: Cannot auto-install/enable default plugins. Server config not found.") |
| return |
|
|
| default_plugins = { |
| "wan2gp-gallery": "https://github.com/Tophness/wan2gp-gallery.git", |
| "wan2gp-lora-multipliers-ui": "https://github.com/Tophness/wan2gp-lora-multipliers-ui.git" |
| } |
| |
| config_modified = False |
| enabled_plugins = server_config.get("enabled_plugins", []) |
|
|
| for repo_name, url in default_plugins.items(): |
| target_dir = os.path.join(manager.plugins_dir, repo_name) |
| if not os.path.isdir(target_dir): |
| print(f"[Plugins] Auto-installing default plugin: {repo_name}...") |
| result = manager.install_plugin_from_url(url) |
| print(f"[Plugins] Install result for {repo_name}: {result}") |
| |
| if "[Success]" in result: |
| if repo_name not in enabled_plugins: |
| enabled_plugins.append(repo_name) |
| config_modified = True |
| |
| if config_modified: |
| print("[Plugins] Disabling newly installed default plugins...") |
| server_config["enabled_plugins"] = [] |
| try: |
| with open(server_config_filename, 'w', encoding='utf-8') as f: |
| json.dump(server_config, f, indent=4) |
| except Exception as e: |
| print(f"[Plugins] ERROR: Failed to update config file '{server_config_filename}': {e}") |
|
|
|
|
| SYSTEM_PLUGINS = [ |
| "wan2gp-about", |
| "wan2gp-configuration", |
| "wan2gp-downloads", |
| "wan2gp-guides", |
| "wan2gp-plugin-manager", |
| "wan2gp-video-mask-creator", |
| ] |
|
|
| USER_PLUGIN_INSERT_POSITION = 4 |
|
|
| @dataclass |
| class InsertAfterRequest: |
| target_component_id: str |
| new_component_constructor: callable |
|
|
| @dataclass |
| class PluginTab: |
| id: str |
| label: str |
| component_constructor: callable |
| position: int = -1 |
|
|
| class WAN2GPPlugin: |
| def __init__(self): |
| self.tabs: Dict[str, PluginTab] = {} |
| self.name = self.__class__.__name__ |
| self.version = "1.0.0" |
| self.description = "No description provided." |
| self._component_requests: List[str] = [] |
| self._global_requests: List[str] = [] |
| self._insert_after_requests: List[InsertAfterRequest] = [] |
| self._setup_complete = False |
| self._data_hooks: Dict[str, List[callable]] = {} |
| self.tab_ids: List[str] = [] |
| self._set_wgp_global_func = None |
| |
| def setup_ui(self) -> None: |
| pass |
| |
| def add_tab(self, tab_id: str, label: str, component_constructor: callable, position: int = -1): |
| self.tabs[tab_id] = PluginTab(id=tab_id, label=label, component_constructor=component_constructor, position=position) |
|
|
| def post_ui_setup(self, components: Dict[str, gr.components.Component]) -> Dict[gr.components.Component, Union[gr.update, Any]]: |
| return {} |
|
|
| def on_tab_select(self, state: Dict[str, Any]) -> None: |
| pass |
|
|
| def on_tab_deselect(self, state: Dict[str, Any]) -> None: |
| pass |
|
|
| def request_component(self, component_id: str) -> None: |
| if component_id not in self._component_requests: |
| self._component_requests.append(component_id) |
| |
| def request_global(self, global_name: str) -> None: |
| if global_name not in self._global_requests: |
| self._global_requests.append(global_name) |
|
|
| def set_global(self, variable_name: str, new_value: Any): |
| if self._set_wgp_global_func: |
| return self._set_wgp_global_func(variable_name, new_value) |
|
|
| @property |
| def component_requests(self) -> List[str]: |
| return self._component_requests.copy() |
|
|
| @property |
| def global_requests(self) -> List[str]: |
| return self._global_requests.copy() |
| |
| def register_data_hook(self, hook_name: str, callback: callable): |
| if hook_name not in self._data_hooks: |
| self._data_hooks[hook_name] = [] |
| self._data_hooks[hook_name].append(callback) |
|
|
| def insert_after(self, target_component_id: str, new_component_constructor: callable) -> None: |
| if not hasattr(self, '_insert_after_requests'): |
| self._insert_after_requests = [] |
| self._insert_after_requests.append( |
| InsertAfterRequest( |
| target_component_id=target_component_id, |
| new_component_constructor=new_component_constructor |
| ) |
| ) |
|
|
| class PluginManager: |
| def __init__(self, plugins_dir="plugins"): |
| self.plugins: Dict[str, WAN2GPPlugin] = {} |
| self.plugins_dir = plugins_dir |
| os.makedirs(self.plugins_dir, exist_ok=True) |
| if self.plugins_dir not in sys.path: |
| sys.path.insert(0, self.plugins_dir) |
| self.data_hooks: Dict[str, List[callable]] = {} |
|
|
| def get_plugins_info(self) -> List[Dict[str, str]]: |
| plugins_info = [] |
| for dir_name in self.discover_plugins(): |
| plugin_path = os.path.join(self.plugins_dir, dir_name) |
| is_system = dir_name in SYSTEM_PLUGINS |
| info = {'id': dir_name, 'name': dir_name, 'version': 'N/A', 'description': 'No description provided.', 'path': plugin_path, 'system': is_system} |
| try: |
| module = importlib.import_module(f"{dir_name}.plugin") |
| for name, obj in inspect.getmembers(module, inspect.isclass): |
| if issubclass(obj, WAN2GPPlugin) and obj != WAN2GPPlugin: |
| instance = obj() |
| info['name'] = instance.name |
| info['version'] = instance.version |
| info['description'] = instance.description |
| break |
| except Exception as e: |
| print(f"Could not load metadata for plugin {dir_name}: {e}") |
| plugins_info.append(info) |
| |
| plugins_info.sort(key=lambda p: (not p['system'], p['name'])) |
| return plugins_info |
|
|
| def _remove_readonly(self, func, path, exc_info): |
| if not os.access(path, os.W_OK): |
| os.chmod(path, stat.S_IWRITE) |
| func(path) |
| else: |
| raise |
|
|
| def uninstall_plugin(self, plugin_id: str): |
| if not plugin_id: |
| return "[Error] No plugin selected for uninstallation." |
| |
| if plugin_id in SYSTEM_PLUGINS: |
| return f"[Error] Cannot uninstall system plugin '{plugin_id}'." |
|
|
| target_dir = os.path.join(self.plugins_dir, plugin_id) |
| if not os.path.isdir(target_dir): |
| return f"[Error] Plugin '{plugin_id}' directory not found." |
|
|
| try: |
| shutil.rmtree(target_dir, onerror=self._remove_readonly) |
| return f"[Success] Plugin '{plugin_id}' uninstalled. Please restart WanGP." |
| except Exception as e: |
| return f"[Error] Failed to remove plugin '{plugin_id}': {e}" |
|
|
| def update_plugin(self, plugin_id: str, progress=None): |
| if not plugin_id: |
| return "[Error] No plugin selected for update." |
| |
| target_dir = os.path.join(self.plugins_dir, plugin_id) |
| if not os.path.isdir(os.path.join(target_dir, '.git')): |
| return f"[Error] '{plugin_id}' is not a git repository and cannot be updated automatically." |
|
|
| try: |
| if progress is not None: progress(0, desc=f"Updating '{plugin_id}'...") |
| repo = git.Repo(target_dir) |
| origin = repo.remotes.origin |
| |
| if progress is not None: progress(0.2, desc=f"Fetching updates for '{plugin_id}'...") |
| origin.fetch() |
| |
| local_commit = repo.head.commit |
| remote_commit = origin.refs[repo.active_branch.name].commit |
|
|
| if local_commit == remote_commit: |
| return f"[Info] Plugin '{plugin_id}' is already up to date." |
|
|
| if progress is not None: progress(0.6, desc=f"Pulling updates for '{plugin_id}'...") |
| origin.pull() |
| |
| requirements_path = os.path.join(target_dir, 'requirements.txt') |
| if os.path.exists(requirements_path): |
| if progress is not None: progress(0.8, desc=f"Re-installing dependencies for '{plugin_id}'...") |
| subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', requirements_path]) |
|
|
| if progress is not None: progress(1.0, desc="Update complete.") |
| return f"[Success] Plugin '{plugin_id}' updated. Please restart WanGP for changes to take effect." |
| except git.exc.GitCommandError as e: |
| traceback.print_exc() |
| return f"[Error] Git update failed for '{plugin_id}': {e.stderr}" |
| except Exception as e: |
| traceback.print_exc() |
| return f"[Error] An unexpected error occurred during update of '{plugin_id}': {str(e)}" |
|
|
| def reinstall_plugin(self, plugin_id: str, progress=None): |
| if not plugin_id: |
| return "[Error] No plugin selected for reinstallation." |
|
|
| target_dir = os.path.join(self.plugins_dir, plugin_id) |
| if not os.path.isdir(target_dir): |
| return f"[Error] Plugin '{plugin_id}' not found." |
|
|
| git_url = None |
| if os.path.isdir(os.path.join(target_dir, '.git')): |
| try: |
| repo = git.Repo(target_dir) |
| git_url = repo.remotes.origin.url |
| except Exception as e: |
| traceback.print_exc() |
| return f"[Error] Could not get remote URL for '{plugin_id}': {e}" |
| |
| if not git_url: |
| return f"[Error] Could not determine remote URL for '{plugin_id}'. Cannot reinstall." |
|
|
| if progress is not None: progress(0, desc=f"Reinstalling '{plugin_id}'...") |
|
|
| backup_dir = f"{target_dir}.bak" |
| if os.path.exists(backup_dir): |
| try: |
| shutil.rmtree(backup_dir, onerror=self._remove_readonly) |
| except Exception as e: |
| return f"[Error] Could not remove old backup directory '{backup_dir}'. Please remove it manually and try again. Error: {e}" |
|
|
| try: |
| if progress is not None: progress(0.2, desc=f"Moving old version of '{plugin_id}' aside...") |
| os.rename(target_dir, backup_dir) |
| except OSError as e: |
| traceback.print_exc() |
| return f"[Error] Could not move the existing plugin directory for '{plugin_id}'. It may be in use by another process. Please close any file explorers or editors in that folder and try again. Error: {e}" |
| |
| install_msg = self.install_plugin_from_url(git_url, progress=progress) |
| |
| if "[Success]" in install_msg: |
| try: |
| shutil.rmtree(backup_dir, onerror=self._remove_readonly) |
| except Exception: |
| pass |
| return f"[Success] Plugin '{plugin_id}' reinstalled. Please restart WanGP." |
| else: |
| try: |
| os.rename(backup_dir, target_dir) |
| return f"[Error] Reinstallation failed during install step: {install_msg}. The original plugin has been restored." |
| except Exception as restore_e: |
| return f"[CRITICAL ERROR] Reinstallation failed AND could not restore backup. Plugin '{plugin_id}' is now in a broken state. Please manually rename '{backup_dir}' back to '{target_dir}'. Original error: {install_msg}. Restore error: {restore_e}" |
|
|
| def install_plugin_from_url(self, git_url: str, progress=None): |
| if not git_url or not git_url.startswith("https://github.com/"): |
| return "[Error] Invalid GitHub URL." |
|
|
| try: |
| repo_name = git_url.split('/')[-1].replace('.git', '') |
| target_dir = os.path.join(self.plugins_dir, repo_name) |
|
|
| if os.path.exists(target_dir): |
| return f"[Warning] Plugin '{repo_name}' already exists. Please remove it manually to reinstall." |
|
|
| if progress is not None: progress(0.1, desc=f"Cloning '{repo_name}'...") |
| git.Repo.clone_from(git_url, target_dir) |
|
|
| requirements_path = os.path.join(target_dir, 'requirements.txt') |
| if os.path.exists(requirements_path): |
| if progress is not None: progress(0.5, desc=f"Installing dependencies for '{repo_name}'...") |
| try: |
| subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', requirements_path]) |
| except subprocess.CalledProcessError as e: |
| traceback.print_exc() |
| return f"[Error] Failed to install dependencies for {repo_name}. Check console for details. Error: {e}" |
|
|
| setup_path = os.path.join(target_dir, 'setup.py') |
| if os.path.exists(setup_path): |
| if progress is not None: progress(0.8, desc=f"Running setup for '{repo_name}'...") |
| try: |
| subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-e', target_dir]) |
| except subprocess.CalledProcessError as e: |
| traceback.print_exc() |
| return f"[Error] Failed to run setup.py for {repo_name}. Check console for details. Error: {e}" |
| |
| init_path = os.path.join(target_dir, '__init__.py') |
| if not os.path.exists(init_path): |
| with open(init_path, 'w') as f: |
| pass |
| |
| if progress is not None: progress(1.0, desc="Installation complete.") |
| return f"[Success] Plugin '{repo_name}' installed. Please enable it in the list and restart WanGP." |
|
|
| except git.exc.GitCommandError as e: |
| traceback.print_exc() |
| return f"[Error] Git clone failed: {e.stderr}" |
| except Exception as e: |
| traceback.print_exc() |
| return f"[Error] An unexpected error occurred: {str(e)}" |
|
|
| def discover_plugins(self) -> List[str]: |
| discovered = [] |
| for item in os.listdir(self.plugins_dir): |
| path = os.path.join(self.plugins_dir, item) |
| if os.path.isdir(path) and os.path.exists(os.path.join(path, '__init__.py')): |
| discovered.append(item) |
| return sorted(discovered) |
|
|
| def load_plugins_from_directory(self, enabled_user_plugins: List[str]) -> None: |
| plugins_to_load = SYSTEM_PLUGINS + [p for p in enabled_user_plugins if p not in SYSTEM_PLUGINS] |
|
|
| for plugin_dir_name in self.discover_plugins(): |
| if plugin_dir_name not in plugins_to_load: |
| continue |
| try: |
| module = importlib.import_module(f"{plugin_dir_name}.plugin") |
|
|
| for name, obj in inspect.getmembers(module, inspect.isclass): |
| if issubclass(obj, WAN2GPPlugin) and obj != WAN2GPPlugin: |
| plugin = obj() |
| plugin.setup_ui() |
| self.plugins[plugin_dir_name] = plugin |
| for hook_name, callbacks in plugin._data_hooks.items(): |
| if hook_name not in self.data_hooks: |
| self.data_hooks[hook_name] = [] |
| self.data_hooks[hook_name].extend(callbacks) |
| if plugin_dir_name not in SYSTEM_PLUGINS: |
| print(f"Loaded plugin: {plugin.name} (from {plugin_dir_name})") |
| break |
| except Exception as e: |
| print(f"Error loading plugin from directory {plugin_dir_name}: {e}") |
| traceback.print_exc() |
|
|
| def get_all_plugins(self) -> Dict[str, WAN2GPPlugin]: |
| return self.plugins.copy() |
|
|
| def inject_globals(self, global_references: Dict[str, Any]) -> None: |
| for plugin_id, plugin in self.plugins.items(): |
| try: |
| if 'set_wgp_global' in global_references: |
| plugin._set_wgp_global_func = global_references['set_wgp_global'] |
| for global_name in plugin.global_requests: |
| if global_name in global_references: |
| setattr(plugin, global_name, global_references[global_name]) |
| except Exception as e: |
| print(f" [!] ERROR injecting globals for {plugin_id}: {str(e)}") |
|
|
| def setup_ui(self) -> Dict[str, Dict[str, Any]]: |
| tabs = {} |
| for plugin_id, plugin in self.plugins.items(): |
| try: |
| for tab_id, tab in plugin.tabs.items(): |
| tabs[tab_id] = { |
| 'label': tab.label, |
| 'component_constructor': tab.component_constructor, |
| 'position': tab.position |
| } |
| except Exception as e: |
| print(f"Error in setup_ui for plugin {plugin_id}: {str(e)}") |
| return {'tabs': tabs} |
| |
| def run_data_hooks(self, hook_name: str, *args, **kwargs): |
| if hook_name not in self.data_hooks: |
| return kwargs.get('configs') |
|
|
| callbacks = self.data_hooks[hook_name] |
| data = kwargs.get('configs') |
|
|
| if 'configs' in kwargs: |
| kwargs.pop('configs') |
|
|
| for callback in callbacks: |
| try: |
| data = callback(data, **kwargs) |
| except Exception as e: |
| print(f"[PluginManager] Error running hook '{hook_name}' from {callback.__module__}: {e}") |
| traceback.print_exc() |
| return data |
| |
| def run_component_insertion_and_setup(self, all_components: Dict[str, Any]): |
| all_insert_requests: List[InsertAfterRequest] = [] |
|
|
| for plugin_id, plugin in self.plugins.items(): |
| try: |
| for comp_id in plugin.component_requests: |
| if comp_id in all_components and (not hasattr(plugin, comp_id) or getattr(plugin, comp_id) is None): |
| setattr(plugin, comp_id, all_components[comp_id]) |
|
|
| requested_components = { |
| comp_id: all_components[comp_id] |
| for comp_id in plugin.component_requests |
| if comp_id in all_components |
| } |
| |
| plugin.post_ui_setup(requested_components) |
| |
| insert_requests = getattr(plugin, '_insert_after_requests', []) |
| if insert_requests: |
| all_insert_requests.extend(insert_requests) |
| plugin._insert_after_requests.clear() |
| |
| except Exception as e: |
| print(f"[PluginManager] ERROR in post_ui_setup for {plugin_id}: {str(e)}") |
| traceback.print_exc() |
|
|
| if all_insert_requests: |
| for request in all_insert_requests: |
| try: |
| target = all_components.get(request.target_component_id) |
| parent = getattr(target, 'parent', None) |
| if not target or not parent or not hasattr(parent, 'children'): |
| print(f"[PluginManager] ERROR: Target '{request.target_component_id}' for insertion not found or invalid.") |
| continue |
| |
| target_index = parent.children.index(target) |
| with parent: |
| new_component = request.new_component_constructor() |
| |
| newly_added = parent.children.pop(-1) |
| parent.children.insert(target_index + 1, newly_added) |
|
|
| except Exception as e: |
| print(f"[PluginManager] ERROR processing insert_after for {request.target_component_id}: {str(e)}") |
| traceback.print_exc() |
|
|
| class WAN2GPApplication: |
| def __init__(self): |
| self.plugin_manager = PluginManager() |
| self.tab_to_plugin_map: Dict[str, WAN2GPPlugin] = {} |
| self.all_rendered_tabs: List[gr.Tab] = [] |
| self.enabled_plugins: List[str] = [] |
|
|
| def initialize_plugins(self, wgp_globals: dict): |
| if not hasattr(self, 'plugin_manager'): |
| return |
| |
| auto_install_and_enable_default_plugins(self.plugin_manager, wgp_globals) |
| |
| server_config = wgp_globals.get("server_config") |
| if not server_config: |
| print("[PluginManager] ERROR: server_config not found in globals.") |
| return |
|
|
| self.enabled_plugins = server_config.get("enabled_plugins", []) |
| self.plugin_manager.load_plugins_from_directory(self.enabled_plugins) |
| self.plugin_manager.inject_globals(wgp_globals) |
|
|
| def setup_ui_tabs(self, main_tabs_component: gr.Tabs, state_component: gr.State): |
| self._create_plugin_tabs() |
| self._setup_tab_events(main_tabs_component, state_component) |
| |
| def _create_plugin_tabs(self): |
| if not hasattr(self, 'plugin_manager'): |
| return |
| |
| loaded_plugins = self.plugin_manager.get_all_plugins() |
| system_tabs, user_tabs = [], [] |
|
|
| for plugin_id, plugin in loaded_plugins.items(): |
| for tab_id, tab in plugin.tabs.items(): |
| self.tab_to_plugin_map[tab.label] = plugin |
| tab_info = {'id': tab_id, 'label': tab.label, 'component_constructor': tab.component_constructor, 'position': tab.position} |
| if plugin_id in SYSTEM_PLUGINS: |
| system_tabs.append(tab_info) |
| else: |
| user_tabs.append((plugin_id, tab_info)) |
|
|
| system_tabs.sort(key=lambda t: (t.get('position', -1), t['label'])) |
|
|
| sorted_user_tabs = [tab_info for plugin_id in self.enabled_plugins for pid, tab_info in user_tabs if pid == plugin_id] |
|
|
| pre_user_tabs = [t for t in system_tabs if t.get('position', -1) < USER_PLUGIN_INSERT_POSITION] |
| post_user_tabs = [t for t in system_tabs if t.get('position', -1) >= USER_PLUGIN_INSERT_POSITION] |
|
|
| all_tabs_to_render = pre_user_tabs + sorted_user_tabs + post_user_tabs |
| |
| for tab_info in all_tabs_to_render: |
| with gr.Tab(tab_info['label'], id=f"plugin_{tab_info['id']}") as new_tab: |
| self.all_rendered_tabs.append(new_tab) |
| tab_info['component_constructor']() |
|
|
| def _setup_tab_events(self, main_tabs_component: gr.Tabs, state_component: gr.State): |
| if main_tabs_component and state_component: |
| main_tabs_component.select( |
| fn=self._handle_tab_selection, |
| inputs=[state_component], |
| outputs=None, |
| show_progress="hidden", |
| ) |
| |
| def _handle_tab_selection(self, state: dict, evt: gr.SelectData): |
| if not hasattr(self, 'previous_tab_id'): |
| self.previous_tab_id = "video_gen" |
| |
| new_tab_id = evt.value |
| |
| if self.previous_tab_id == new_tab_id: |
| return |
|
|
| if self.previous_tab_id and self.previous_tab_id in self.tab_to_plugin_map: |
| plugin_to_deselect = self.tab_to_plugin_map[self.previous_tab_id] |
| try: |
| plugin_to_deselect.on_tab_deselect(state) |
| except Exception as e: |
| print(f"[PluginManager] Error in on_tab_deselect for plugin {plugin_to_deselect.name}: {e}") |
| traceback.print_exc() |
|
|
| if new_tab_id and new_tab_id in self.tab_to_plugin_map: |
| plugin_to_select = self.tab_to_plugin_map[new_tab_id] |
| try: |
| plugin_to_select.on_tab_select(state) |
| except Exception as e: |
| print(f"[PluginManager] Error in on_tab_select for plugin {plugin_to_select.name}: {e}") |
| traceback.print_exc() |
|
|
| self.previous_tab_id = new_tab_id |
| |
| def run_component_insertion(self, components_dict: Dict[str, Any]): |
| if hasattr(self, 'plugin_manager'): |
| self.plugin_manager.run_component_insertion_and_setup(components_dict) |
|
|