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