优化ComfyKit实例管理逻辑, 提升调用runninghub时的稳定性
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]:
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
8
uv.lock
generated
8
uv.lock
generated
@@ -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" },
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user