From 8aad91006d947718e6ba18b72fac5d2d21d65fd0 Mon Sep 17 00:00:00 2001 From: puke <1129090915@qq.com> Date: Wed, 19 Nov 2025 17:14:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96ComfyKit=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E9=80=BB=E8=BE=91,=20=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E8=B0=83=E7=94=A8runninghub=E6=97=B6=E7=9A=84=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/dependencies.py | 3 +- pixelle_video/service.py | 110 ++++++++++++++++++- pixelle_video/services/comfy_base_service.py | 6 +- pixelle_video/services/media.py | 18 ++- pixelle_video/services/tts_service.py | 18 ++- pyproject.toml | 2 +- uv.lock | 8 +- web/state/session.py | 49 ++++++++- 8 files changed, 176 insertions(+), 38 deletions(-) diff --git a/api/dependencies.py b/api/dependencies.py index 41fb14b..70029ad 100644 --- a/api/dependencies.py +++ b/api/dependencies.py @@ -45,10 +45,11 @@ async def get_pixelle_video() -> PixelleVideoCore: async def shutdown_pixelle_video(): - """Shutdown Pixelle-Video instance""" + """Shutdown Pixelle-Video instance and cleanup resources""" global _pixelle_video_instance if _pixelle_video_instance: logger.info("Shutting down Pixelle-Video...") + await _pixelle_video_instance.cleanup() _pixelle_video_instance = None diff --git a/pixelle_video/service.py b/pixelle_video/service.py index 8f2b9d9..6de4f50 100644 --- a/pixelle_video/service.py +++ b/pixelle_video/service.py @@ -16,9 +16,12 @@ Pixelle-Video Core - Service Layer Provides unified access to all capabilities (LLM, TTS, Image, etc.) """ +import hashlib +import json from typing import Optional from loguru import logger +from comfykit import ComfyKit from pixelle_video.config import config_manager from pixelle_video.services.llm_service import LLMService @@ -76,6 +79,10 @@ class PixelleVideoCore: self.config = config_manager.config.to_dict() self._initialized = False + # ComfyKit lazy initialization (created on first use, recreated on config change) + self._comfykit: Optional[ComfyKit] = None + self._comfykit_config_hash: Optional[str] = None + # Core services (initialized in initialize()) self.llm: Optional[LLMService] = None self.tts: Optional[TTSService] = None @@ -91,11 +98,81 @@ class PixelleVideoCore: # Default pipeline callable (for backward compatibility) self.generate_video = None + def _get_comfykit_config(self) -> dict: + """ + Get current ComfyKit configuration from config_manager + + Returns: + ComfyKit configuration dict + """ + # Reload config from global config_manager (to support hot reload) + self.config = config_manager.config.to_dict() + + comfyui_config = self.config.get("comfyui", {}) + kit_config = {} + + if comfyui_config.get("comfyui_url"): + kit_config["comfyui_url"] = comfyui_config["comfyui_url"] + if comfyui_config.get("runninghub_api_key"): + kit_config["runninghub_api_key"] = comfyui_config["runninghub_api_key"] + + return kit_config + + def _compute_comfykit_config_hash(self, config: dict) -> str: + """ + Compute hash of ComfyKit configuration for change detection + + Args: + config: ComfyKit configuration dict + + Returns: + MD5 hash of config + """ + # Sort keys for consistent hash + config_str = json.dumps(config, sort_keys=True) + return hashlib.md5(config_str.encode()).hexdigest() + + async def _get_or_create_comfykit(self) -> ComfyKit: + """ + Get or create ComfyKit instance (lazy initialization with config change detection) + + This method: + 1. Creates ComfyKit on first use (lazy initialization) + 2. Detects configuration changes and recreates instance if needed + 3. Ensures proper cleanup of old instances + + Returns: + ComfyKit instance + """ + current_config = self._get_comfykit_config() + current_hash = self._compute_comfykit_config_hash(current_config) + + # Check if we need to create or recreate ComfyKit + if self._comfykit is None or self._comfykit_config_hash != current_hash: + # Close old instance if exists + if self._comfykit is not None: + logger.info("๐Ÿ”„ ComfyUI configuration changed, recreating ComfyKit instance...") + try: + await self._comfykit.close() + except Exception as e: + logger.warning(f"Failed to close old ComfyKit instance: {e}") + self._comfykit = None + + # Create new instance with current config + logger.info("โœจ Creating ComfyKit instance...") + logger.debug(f"ComfyKit config: {current_config}") + self._comfykit = ComfyKit(**current_config) + self._comfykit_config_hash = current_hash + logger.info("โœ… ComfyKit instance created") + + return self._comfykit + async def initialize(self): """ Initialize core capabilities This initializes all services and must be called before using any capabilities. + Note: ComfyKit is NOT initialized here - it's lazily initialized on first use. Example: await pixelle_video.initialize() @@ -106,10 +183,10 @@ class PixelleVideoCore: logger.info("๐Ÿš€ Initializing Pixelle-Video...") - # 1. Initialize core services + # 1. Initialize core services (ComfyKit will be lazy-loaded later) self.llm = LLMService(self.config) - self.tts = TTSService(self.config) - self.media = MediaService(self.config) + self.tts = TTSService(self.config, self) + self.media = MediaService(self.config, self) self.video = VideoService() self.frame_processor = FrameProcessor(self) self.persistence = PersistenceService(output_dir="output") @@ -128,6 +205,33 @@ class PixelleVideoCore: self._initialized = True logger.info("โœ… Pixelle-Video initialized successfully\n") + async def cleanup(self): + """ + Cleanup resources (close ComfyKit session) + + Example: + await pixelle_video.cleanup() + """ + if self._comfykit: + logger.info("๐Ÿงน Closing ComfyKit session...") + try: + await self._comfykit.close() + logger.info("โœ… ComfyKit session closed") + except Exception as e: + logger.error(f"Failed to close ComfyKit: {e}") + finally: + self._comfykit = None + self._comfykit_config_hash = None + + async def __aenter__(self): + """Async context manager entry""" + await self.initialize() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit""" + await self.cleanup() + def _create_generate_video_wrapper(self): """ Create a wrapper function for generate_video that supports pipeline selection diff --git a/pixelle_video/services/comfy_base_service.py b/pixelle_video/services/comfy_base_service.py index a7648a7..576f65e 100644 --- a/pixelle_video/services/comfy_base_service.py +++ b/pixelle_video/services/comfy_base_service.py @@ -45,13 +45,14 @@ class ComfyBaseService: DEFAULT_WORKFLOW: str = "" # Must be overridden by subclass WORKFLOWS_DIR: str = "workflows" - def __init__(self, config: dict, service_name: str): + def __init__(self, config: dict, service_name: str, core=None): """ Initialize ComfyUI base service Args: config: Full application config dict service_name: Service name in config (e.g., "tts", "image") + core: PixelleVideoCore instance (for accessing shared ComfyKit) """ # Service-specific config (e.g., config["comfyui"]["tts"]) comfyui_config = config.get("comfyui", {}) @@ -62,6 +63,9 @@ class ComfyBaseService: self.service_name = service_name self._workflows_cache: Optional[List[str]] = None + + # Reference to core (for accessing shared ComfyKit) + self.core = core def _scan_workflows(self) -> List[Dict[str, Any]]: """ diff --git a/pixelle_video/services/media.py b/pixelle_video/services/media.py index c915df1..75d9e33 100644 --- a/pixelle_video/services/media.py +++ b/pixelle_video/services/media.py @@ -55,14 +55,15 @@ class MediaService(ComfyBaseService): DEFAULT_WORKFLOW = None # No hardcoded default, must be configured WORKFLOWS_DIR = "workflows" - def __init__(self, config: dict): + def __init__(self, config: dict, core=None): """ Initialize media service Args: config: Full application config dict + core: PixelleVideoCore instance (for accessing shared ComfyKit) """ - super().__init__(config, service_name="image") # Keep "image" for config compatibility + super().__init__(config, service_name="image", core=core) # Keep "image" for config compatibility def _scan_workflows(self): """ @@ -194,13 +195,7 @@ class MediaService(ComfyBaseService): # 1. Resolve workflow (returns structured info) workflow_info = self._resolve_workflow(workflow=workflow) - # 2. Prepare ComfyKit config (supports both selfhost and runninghub) - kit_config = self._prepare_comfykit_config( - comfyui_url=comfyui_url, - runninghub_api_key=runninghub_api_key - ) - - # 3. Build workflow parameters + # 2. Build workflow parameters (ComfyKit config is now managed by core) workflow_params = {"prompt": prompt} # Add optional parameters @@ -224,9 +219,10 @@ class MediaService(ComfyBaseService): logger.debug(f"Workflow parameters: {workflow_params}") - # 4. Execute workflow (ComfyKit auto-detects based on input type) + # 4. Execute workflow using shared ComfyKit instance from core try: - kit = ComfyKit(**kit_config) + # Get shared ComfyKit instance (lazy initialization + config hot-reload) + kit = await self.core._get_or_create_comfykit() # Determine what to pass to ComfyKit based on source if workflow_info["source"] == "runninghub" and "workflow_id" in workflow_info: diff --git a/pixelle_video/services/tts_service.py b/pixelle_video/services/tts_service.py index 53f5d8e..dcb1707 100644 --- a/pixelle_video/services/tts_service.py +++ b/pixelle_video/services/tts_service.py @@ -51,14 +51,15 @@ class TTSService(ComfyBaseService): DEFAULT_WORKFLOW = None # No hardcoded default, must be configured WORKFLOWS_DIR = "workflows" - def __init__(self, config: dict): + def __init__(self, config: dict, core=None): """ Initialize TTS service Args: config: Full application config dict + core: PixelleVideoCore instance (for accessing shared ComfyKit) """ - super().__init__(config, service_name="tts") + super().__init__(config, service_name="tts", core=core) async def __call__( @@ -222,13 +223,7 @@ class TTSService(ComfyBaseService): """ logger.info(f"๐ŸŽ™๏ธ Using workflow: {workflow_info['key']}") - # 1. Prepare ComfyKit config (supports both selfhost and runninghub) - kit_config = self._prepare_comfykit_config( - comfyui_url=comfyui_url, - runninghub_api_key=runninghub_api_key - ) - - # 2. Build workflow parameters + # 1. Build workflow parameters (ComfyKit config is now managed by core) workflow_params = {"text": text} # Add optional TTS parameters (only if explicitly provided and not None) @@ -242,9 +237,10 @@ class TTSService(ComfyBaseService): logger.debug(f"Workflow parameters: {workflow_params}") - # 3. Execute workflow (ComfyKit auto-detects based on input type) + # 3. Execute workflow using shared ComfyKit instance from core try: - kit = ComfyKit(**kit_config) + # Get shared ComfyKit instance (lazy initialization + config hot-reload) + kit = await self.core._get_or_create_comfykit() # Determine what to pass to ComfyKit based on source if workflow_info["source"] == "runninghub" and "workflow_id" in workflow_info: diff --git a/pyproject.toml b/pyproject.toml index 1ac47e7..268e79a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "fastapi>=0.115.0", "uvicorn[standard]>=0.32.0", "python-multipart>=0.0.12", - "comfykit>=0.1.10", + "comfykit>=0.1.11", "beautifulsoup4>=4.14.2", ] diff --git a/uv.lock b/uv.lock index 92a20bb..6cac74e 100644 --- a/uv.lock +++ b/uv.lock @@ -432,7 +432,7 @@ wheels = [ [[package]] name = "comfykit" -version = "0.1.10" +version = "0.1.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -442,9 +442,9 @@ dependencies = [ { name = "pydantic" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/d6/aaea4e51f89c52b1795ad658f3883414b57def440d66bef0be36be783784/comfykit-0.1.10.tar.gz", hash = "sha256:fd2f4c6db8b76abadc0e4b9be2decadb2c3f6f89f0a949a40b51cb6513ce96ec", size = 49093 } +sdist = { url = "https://files.pythonhosted.org/packages/2c/c2/55612d8ee85f0f003113b36795ee40c2ce5ea4f3806baa6b056fb88e696a/comfykit-0.1.11.tar.gz", hash = "sha256:1dc96c15da81a1c69a5f42af2f0f156f9574e534ac925476d1e469b02f2736b0", size = 50179 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/53/c0d6bc63168d7260cc92444ad3938fb85d09d71a60fee1ce72160fd751a5/comfykit-0.1.10-py3-none-any.whl", hash = "sha256:b4d66875dd888be90b8e89bfcfc635b13d51f40198ca4f995ad5dda35260834a", size = 52160 }, + { url = "https://files.pythonhosted.org/packages/c0/e5/9fdd7da9a944fa8ad32f1e7623604d5bbc0a37f8a6d9beda23e51e3e3698/comfykit-0.1.11-py3-none-any.whl", hash = "sha256:d23cf636adc360976f4724cfc5a0f3ef23dd77735c852a1f83f061256a7b9a2e", size = 53286 }, ] [[package]] @@ -1697,7 +1697,7 @@ dev = [ requires-dist = [ { name = "beautifulsoup4", specifier = ">=4.14.2" }, { name = "certifi", specifier = ">=2025.10.5" }, - { name = "comfykit", specifier = ">=0.1.10" }, + { name = "comfykit", specifier = ">=0.1.11" }, { name = "edge-tts", specifier = ">=7.2.3" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "fastmcp", specifier = ">=2.0.0" }, diff --git a/web/state/session.py b/web/state/session.py index 475eb1c..efb3185 100644 --- a/web/state/session.py +++ b/web/state/session.py @@ -40,13 +40,50 @@ def init_i18n(): def get_pixelle_video(): - """Get initialized Pixelle-Video instance (no caching - always fresh)""" - from pixelle_video.service import PixelleVideoCore + """ + Get initialized Pixelle-Video instance with proper caching and cleanup - logger.info("Initializing Pixelle-Video...") - pixelle_video = PixelleVideoCore() - run_async(pixelle_video.initialize()) - logger.info("Pixelle-Video initialized") + Uses st.session_state to cache the instance per user session. + ComfyKit is lazily initialized and automatically recreated on config changes. + """ + from pixelle_video.service import PixelleVideoCore + from pixelle_video.config import config_manager + + # Compute config hash for change detection + import hashlib + import json + config_dict = config_manager.config.to_dict() + # Only track ComfyUI config for hash (other config changes don't need core recreation) + comfyui_config = config_dict.get("comfyui", {}) + config_hash = hashlib.md5(json.dumps(comfyui_config, sort_keys=True).encode()).hexdigest() + + # Check if we need to create or recreate core instance + need_recreate = False + if 'pixelle_video' not in st.session_state: + need_recreate = True + logger.info("Creating new PixelleVideoCore instance (first time)") + elif st.session_state.get('pixelle_video_config_hash') != config_hash: + need_recreate = True + logger.info("Configuration changed, recreating PixelleVideoCore instance") + # Cleanup old instance + old_core = st.session_state.pixelle_video + try: + run_async(old_core.cleanup()) + except Exception as e: + logger.warning(f"Failed to cleanup old PixelleVideoCore: {e}") + + if need_recreate: + # Create and initialize new instance + pixelle_video = PixelleVideoCore() + run_async(pixelle_video.initialize()) + + # Cache in session state + st.session_state.pixelle_video = pixelle_video + st.session_state.pixelle_video_config_hash = config_hash + logger.info("โœ… PixelleVideoCore initialized and cached") + else: + pixelle_video = st.session_state.pixelle_video + logger.debug("Reusing cached PixelleVideoCore instance") return pixelle_video