service.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. # Copyright (C) 2025 AIDC-AI
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. # See the License for the specific language governing permissions and
  11. # limitations under the License.
  12. """
  13. Pixelle-Video Core - Service Layer
  14. Provides unified access to all capabilities (LLM, TTS, Image, etc.)
  15. """
  16. import hashlib
  17. import json
  18. from typing import Optional
  19. from loguru import logger
  20. from comfykit import ComfyKit
  21. from pixelle_video.config import config_manager
  22. from pixelle_video.services.llm_service import LLMService
  23. from pixelle_video.services.tts_service import TTSService
  24. from pixelle_video.services.media import MediaService
  25. from pixelle_video.services.image_analysis import ImageAnalysisService
  26. from pixelle_video.services.video_analysis import VideoAnalysisService
  27. from pixelle_video.services.video import VideoService
  28. from pixelle_video.services.frame_processor import FrameProcessor
  29. from pixelle_video.services.persistence import PersistenceService
  30. from pixelle_video.services.history_manager import HistoryManager
  31. from pixelle_video.pipelines.standard import StandardPipeline
  32. from pixelle_video.pipelines.custom import CustomPipeline
  33. from pixelle_video.pipelines.asset_based import AssetBasedPipeline
  34. class PixelleVideoCore:
  35. """
  36. Pixelle-Video Core - Service Layer
  37. Provides unified access to all capabilities.
  38. Usage:
  39. from pixelle_video import pixelle_video
  40. # Initialize
  41. await pixelle_video.initialize()
  42. # Use capabilities directly
  43. answer = await pixelle_video.llm("Explain atomic habits")
  44. audio = await pixelle_video.tts("Hello world")
  45. media = await pixelle_video.media(prompt="a cat")
  46. # Check active capabilities
  47. print(f"Using LLM: {pixelle_video.llm.active}")
  48. print(f"Available TTS: {pixelle_video.tts.available}")
  49. Architecture (Simplified):
  50. PixelleVideoCore (this class)
  51. ├── config (configuration)
  52. ├── llm (LLM service - direct OpenAI SDK)
  53. ├── tts (TTS service - ComfyKit workflows)
  54. ├── media (Media service - ComfyKit workflows, supports image & video)
  55. └── pipelines (video generation pipelines)
  56. ├── standard (standard workflow)
  57. ├── custom (custom workflow template)
  58. └── ... (extensible)
  59. """
  60. def __init__(self, config_path: str = "config.yaml"):
  61. """
  62. Initialize Pixelle-Video Core
  63. Args:
  64. config_path: Path to configuration file
  65. """
  66. # Use global config manager singleton
  67. self.config = config_manager.config.to_dict()
  68. self._initialized = False
  69. # ComfyKit lazy initialization (created on first use, recreated on config change)
  70. self._comfykit: Optional[ComfyKit] = None
  71. self._comfykit_config_hash: Optional[str] = None
  72. # Core services (initialized in initialize())
  73. self.llm: Optional[LLMService] = None
  74. self.tts: Optional[TTSService] = None
  75. self.media: Optional[MediaService] = None
  76. self.video: Optional[VideoService] = None
  77. self.frame_processor: Optional[FrameProcessor] = None
  78. self.persistence: Optional[PersistenceService] = None
  79. self.history: Optional[HistoryManager] = None
  80. # Video generation pipelines (dictionary of pipeline_name -> pipeline_instance)
  81. self.pipelines = {}
  82. # Default pipeline callable (for backward compatibility)
  83. self.generate_video = None
  84. def _get_comfykit_config(self) -> dict:
  85. """
  86. Get current ComfyKit configuration from config_manager
  87. Returns:
  88. ComfyKit configuration dict
  89. """
  90. # Reload config from global config_manager (to support hot reload)
  91. self.config = config_manager.config.to_dict()
  92. comfyui_config = self.config.get("comfyui", {})
  93. kit_config = {}
  94. if comfyui_config.get("comfyui_url"):
  95. kit_config["comfyui_url"] = comfyui_config["comfyui_url"]
  96. if comfyui_config.get("comfyui_api_key"):
  97. kit_config["api_key"] = comfyui_config["comfyui_api_key"]
  98. if comfyui_config.get("runninghub_api_key"):
  99. kit_config["runninghub_api_key"] = comfyui_config["runninghub_api_key"]
  100. # Only pass instance_type if it has a non-empty value
  101. instance_type = comfyui_config.get("runninghub_instance_type")
  102. if instance_type and instance_type.strip():
  103. kit_config["runninghub_instance_type"] = instance_type
  104. return kit_config
  105. def _compute_comfykit_config_hash(self, config: dict) -> str:
  106. """
  107. Compute hash of ComfyKit configuration for change detection
  108. Args:
  109. config: ComfyKit configuration dict
  110. Returns:
  111. MD5 hash of config
  112. """
  113. # Sort keys for consistent hash
  114. config_str = json.dumps(config, sort_keys=True)
  115. return hashlib.md5(config_str.encode()).hexdigest()
  116. async def _get_or_create_comfykit(self) -> ComfyKit:
  117. """
  118. Get or create ComfyKit instance (lazy initialization with config change detection)
  119. This method:
  120. 1. Creates ComfyKit on first use (lazy initialization)
  121. 2. Detects configuration changes and recreates instance if needed
  122. 3. Ensures proper cleanup of old instances
  123. Returns:
  124. ComfyKit instance
  125. """
  126. current_config = self._get_comfykit_config()
  127. current_hash = self._compute_comfykit_config_hash(current_config)
  128. # Check if we need to create or recreate ComfyKit
  129. if self._comfykit is None or self._comfykit_config_hash != current_hash:
  130. # Close old instance if exists
  131. if self._comfykit is not None:
  132. logger.info("🔄 ComfyUI configuration changed, recreating ComfyKit instance...")
  133. try:
  134. await self._comfykit.close()
  135. except Exception as e:
  136. logger.warning(f"Failed to close old ComfyKit instance: {e}")
  137. self._comfykit = None
  138. # Create new instance with current config
  139. logger.info("✨ Creating ComfyKit instance...")
  140. logger.debug(f"ComfyKit config: {current_config}")
  141. self._comfykit = ComfyKit(**current_config)
  142. self._comfykit_config_hash = current_hash
  143. logger.info("✅ ComfyKit instance created")
  144. return self._comfykit
  145. async def initialize(self):
  146. """
  147. Initialize core capabilities
  148. This initializes all services and must be called before using any capabilities.
  149. Note: ComfyKit is NOT initialized here - it's lazily initialized on first use.
  150. Example:
  151. await pixelle_video.initialize()
  152. """
  153. if self._initialized:
  154. logger.warning("Pixelle-Video already initialized")
  155. return
  156. logger.info("🚀 Initializing Pixelle-Video...")
  157. # 1. Initialize core services (ComfyKit will be lazy-loaded later)
  158. # Initialize services
  159. self.llm = LLMService(self.config)
  160. self.tts = TTSService(self.config, core=self)
  161. self.media = MediaService(self.config, core=self)
  162. self.image = self.media # Alias for backward compatibility
  163. self.image_analysis = ImageAnalysisService(self.config, core=self)
  164. self.video_analysis = VideoAnalysisService(self.config, core=self)
  165. self.video = VideoService()
  166. self.frame_processor = FrameProcessor(self)
  167. self.persistence = PersistenceService(output_dir="output")
  168. self.history = HistoryManager(self.persistence)
  169. # 2. Register video generation pipelines
  170. self.pipelines = {
  171. "standard": StandardPipeline(self),
  172. "custom": CustomPipeline(self),
  173. "asset_based": AssetBasedPipeline(self),
  174. }
  175. logger.info(f"📹 Registered pipelines: {', '.join(self.pipelines.keys())}")
  176. # 3. Set default pipeline callable (for backward compatibility)
  177. self.generate_video = self._create_generate_video_wrapper()
  178. self._initialized = True
  179. logger.info("✅ Pixelle-Video initialized successfully\n")
  180. async def cleanup(self):
  181. """
  182. Cleanup resources (close ComfyKit session)
  183. Example:
  184. await pixelle_video.cleanup()
  185. """
  186. if self._comfykit:
  187. logger.info("🧹 Closing ComfyKit session...")
  188. try:
  189. await self._comfykit.close()
  190. logger.info("✅ ComfyKit session closed")
  191. except Exception as e:
  192. logger.error(f"Failed to close ComfyKit: {e}")
  193. finally:
  194. self._comfykit = None
  195. self._comfykit_config_hash = None
  196. async def __aenter__(self):
  197. """Async context manager entry"""
  198. await self.initialize()
  199. return self
  200. async def __aexit__(self, exc_type, exc_val, exc_tb):
  201. """Async context manager exit"""
  202. await self.cleanup()
  203. def _create_generate_video_wrapper(self):
  204. """
  205. Create a wrapper function for generate_video that supports pipeline selection
  206. This maintains backward compatibility while adding pipeline support.
  207. """
  208. async def generate_video_wrapper(
  209. text: str,
  210. pipeline: str = "standard",
  211. **kwargs
  212. ):
  213. """
  214. Generate video using specified pipeline
  215. Args:
  216. text: Input text
  217. pipeline: Pipeline name ("standard", "book_summary", etc.)
  218. **kwargs: Pipeline-specific parameters
  219. Returns:
  220. VideoGenerationResult
  221. Examples:
  222. # Use standard pipeline (default)
  223. result = await pixelle_video.generate_video(
  224. text="如何提高学习效率",
  225. n_scenes=5
  226. )
  227. # Use custom pipeline
  228. result = await pixelle_video.generate_video(
  229. text=your_content,
  230. pipeline="custom",
  231. custom_param_example="custom_value"
  232. )
  233. """
  234. if pipeline not in self.pipelines:
  235. available = ", ".join(self.pipelines.keys())
  236. raise ValueError(
  237. f"Unknown pipeline: '{pipeline}'. "
  238. f"Available pipelines: {available}"
  239. )
  240. pipeline_instance = self.pipelines[pipeline]
  241. return await pipeline_instance(text=text, **kwargs)
  242. return generate_video_wrapper
  243. @property
  244. def project_name(self) -> str:
  245. """Get project name from config"""
  246. return self.config.get("project_name", "Pixelle-Video")
  247. def __repr__(self) -> str:
  248. """String representation"""
  249. status = "initialized" if self._initialized else "not initialized"
  250. pipelines = f"pipelines={list(self.pipelines.keys())}" if self._initialized else ""
  251. return f"<PixelleVideoCore project={self.project_name!r} status={status} {pipelines}>"
  252. # Global instance
  253. pixelle_video = PixelleVideoCore()