优化ComfyKit实例管理逻辑, 提升调用runninghub时的稳定性

This commit is contained in:
puke
2025-11-19 17:14:27 +08:00
parent 374246237d
commit 8aad91006d
8 changed files with 176 additions and 38 deletions

View File

@@ -45,10 +45,11 @@ async def get_pixelle_video() -> PixelleVideoCore:
async def shutdown_pixelle_video(): async def shutdown_pixelle_video():
"""Shutdown Pixelle-Video instance""" """Shutdown Pixelle-Video instance and cleanup resources"""
global _pixelle_video_instance global _pixelle_video_instance
if _pixelle_video_instance: if _pixelle_video_instance:
logger.info("Shutting down Pixelle-Video...") logger.info("Shutting down Pixelle-Video...")
await _pixelle_video_instance.cleanup()
_pixelle_video_instance = None _pixelle_video_instance = None

View File

@@ -16,9 +16,12 @@ Pixelle-Video Core - Service Layer
Provides unified access to all capabilities (LLM, TTS, Image, etc.) Provides unified access to all capabilities (LLM, TTS, Image, etc.)
""" """
import hashlib
import json
from typing import Optional from typing import Optional
from loguru import logger from loguru import logger
from comfykit import ComfyKit
from pixelle_video.config import config_manager from pixelle_video.config import config_manager
from pixelle_video.services.llm_service import LLMService from pixelle_video.services.llm_service import LLMService
@@ -76,6 +79,10 @@ class PixelleVideoCore:
self.config = config_manager.config.to_dict() self.config = config_manager.config.to_dict()
self._initialized = False 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()) # Core services (initialized in initialize())
self.llm: Optional[LLMService] = None self.llm: Optional[LLMService] = None
self.tts: Optional[TTSService] = None self.tts: Optional[TTSService] = None
@@ -91,11 +98,81 @@ class PixelleVideoCore:
# Default pipeline callable (for backward compatibility) # Default pipeline callable (for backward compatibility)
self.generate_video = None 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): async def initialize(self):
""" """
Initialize core capabilities Initialize core capabilities
This initializes all services and must be called before using any 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: Example:
await pixelle_video.initialize() await pixelle_video.initialize()
@@ -106,10 +183,10 @@ class PixelleVideoCore:
logger.info("🚀 Initializing Pixelle-Video...") 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.llm = LLMService(self.config)
self.tts = TTSService(self.config) self.tts = TTSService(self.config, self)
self.media = MediaService(self.config) self.media = MediaService(self.config, self)
self.video = VideoService() self.video = VideoService()
self.frame_processor = FrameProcessor(self) self.frame_processor = FrameProcessor(self)
self.persistence = PersistenceService(output_dir="output") self.persistence = PersistenceService(output_dir="output")
@@ -128,6 +205,33 @@ class PixelleVideoCore:
self._initialized = True self._initialized = True
logger.info("✅ Pixelle-Video initialized successfully\n") 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): def _create_generate_video_wrapper(self):
""" """
Create a wrapper function for generate_video that supports pipeline selection Create a wrapper function for generate_video that supports pipeline selection

View File

@@ -45,13 +45,14 @@ class ComfyBaseService:
DEFAULT_WORKFLOW: str = "" # Must be overridden by subclass DEFAULT_WORKFLOW: str = "" # Must be overridden by subclass
WORKFLOWS_DIR: str = "workflows" 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 Initialize ComfyUI base service
Args: Args:
config: Full application config dict config: Full application config dict
service_name: Service name in config (e.g., "tts", "image") 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"]) # Service-specific config (e.g., config["comfyui"]["tts"])
comfyui_config = config.get("comfyui", {}) comfyui_config = config.get("comfyui", {})
@@ -62,6 +63,9 @@ class ComfyBaseService:
self.service_name = service_name self.service_name = service_name
self._workflows_cache: Optional[List[str]] = None 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]]: def _scan_workflows(self) -> List[Dict[str, Any]]:
""" """

View File

@@ -55,14 +55,15 @@ class MediaService(ComfyBaseService):
DEFAULT_WORKFLOW = None # No hardcoded default, must be configured DEFAULT_WORKFLOW = None # No hardcoded default, must be configured
WORKFLOWS_DIR = "workflows" WORKFLOWS_DIR = "workflows"
def __init__(self, config: dict): def __init__(self, config: dict, core=None):
""" """
Initialize media service Initialize media service
Args: Args:
config: Full application config dict 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): def _scan_workflows(self):
""" """
@@ -194,13 +195,7 @@ class MediaService(ComfyBaseService):
# 1. Resolve workflow (returns structured info) # 1. Resolve workflow (returns structured info)
workflow_info = self._resolve_workflow(workflow=workflow) workflow_info = self._resolve_workflow(workflow=workflow)
# 2. Prepare ComfyKit config (supports both selfhost and runninghub) # 2. Build workflow parameters (ComfyKit config is now managed by core)
kit_config = self._prepare_comfykit_config(
comfyui_url=comfyui_url,
runninghub_api_key=runninghub_api_key
)
# 3. Build workflow parameters
workflow_params = {"prompt": prompt} workflow_params = {"prompt": prompt}
# Add optional parameters # Add optional parameters
@@ -224,9 +219,10 @@ class MediaService(ComfyBaseService):
logger.debug(f"Workflow parameters: {workflow_params}") 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: 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 # Determine what to pass to ComfyKit based on source
if workflow_info["source"] == "runninghub" and "workflow_id" in workflow_info: if workflow_info["source"] == "runninghub" and "workflow_id" in workflow_info:

View File

@@ -51,14 +51,15 @@ class TTSService(ComfyBaseService):
DEFAULT_WORKFLOW = None # No hardcoded default, must be configured DEFAULT_WORKFLOW = None # No hardcoded default, must be configured
WORKFLOWS_DIR = "workflows" WORKFLOWS_DIR = "workflows"
def __init__(self, config: dict): def __init__(self, config: dict, core=None):
""" """
Initialize TTS service Initialize TTS service
Args: Args:
config: Full application config dict 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__( async def __call__(
@@ -222,13 +223,7 @@ class TTSService(ComfyBaseService):
""" """
logger.info(f"🎙️ Using workflow: {workflow_info['key']}") logger.info(f"🎙️ Using workflow: {workflow_info['key']}")
# 1. Prepare ComfyKit config (supports both selfhost and runninghub) # 1. Build workflow parameters (ComfyKit config is now managed by core)
kit_config = self._prepare_comfykit_config(
comfyui_url=comfyui_url,
runninghub_api_key=runninghub_api_key
)
# 2. Build workflow parameters
workflow_params = {"text": text} workflow_params = {"text": text}
# Add optional TTS parameters (only if explicitly provided and not None) # 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}") 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: 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 # Determine what to pass to ComfyKit based on source
if workflow_info["source"] == "runninghub" and "workflow_id" in workflow_info: if workflow_info["source"] == "runninghub" and "workflow_id" in workflow_info:

View File

@@ -24,7 +24,7 @@ dependencies = [
"fastapi>=0.115.0", "fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0", "uvicorn[standard]>=0.32.0",
"python-multipart>=0.0.12", "python-multipart>=0.0.12",
"comfykit>=0.1.10", "comfykit>=0.1.11",
"beautifulsoup4>=4.14.2", "beautifulsoup4>=4.14.2",
] ]

8
uv.lock generated
View File

@@ -432,7 +432,7 @@ wheels = [
[[package]] [[package]]
name = "comfykit" name = "comfykit"
version = "0.1.10" version = "0.1.11"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },
@@ -442,9 +442,9 @@ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "websockets" }, { 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 = [ 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]] [[package]]
@@ -1697,7 +1697,7 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "beautifulsoup4", specifier = ">=4.14.2" }, { name = "beautifulsoup4", specifier = ">=4.14.2" },
{ name = "certifi", specifier = ">=2025.10.5" }, { 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 = "edge-tts", specifier = ">=7.2.3" },
{ name = "fastapi", specifier = ">=0.115.0" }, { name = "fastapi", specifier = ">=0.115.0" },
{ name = "fastmcp", specifier = ">=2.0.0" }, { name = "fastmcp", specifier = ">=2.0.0" },

View File

@@ -40,13 +40,50 @@ def init_i18n():
def get_pixelle_video(): 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...") Uses st.session_state to cache the instance per user session.
pixelle_video = PixelleVideoCore() ComfyKit is lazily initialized and automatically recreated on config changes.
run_async(pixelle_video.initialize()) """
logger.info("Pixelle-Video initialized") 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 return pixelle_video