返回 Pixelle-Video
comfy_base_service.py
根目录 / pixelle_video / services / comfy_base_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 ComfyUI Base Service - Common logic for ComfyUI-based services
15 """
16
17 import json
18 import os
19 from pathlib import Path
20 from typing import Optional, List, Dict, Any
21
22 from comfykit import ComfyKit
23 from loguru import logger
24
25 from pixelle_video.utils.os_util import (
26 get_resource_path,
27 list_resource_files,
28 list_resource_dirs
29 )
30
31
32 class ComfyBaseService:
33 """
34 Base service for ComfyUI workflow-based capabilities
35
36 Provides common functionality for TTS, Image, and other ComfyUI-based services.
37
38 Subclasses should define:
39 - WORKFLOW_PREFIX: Prefix for workflow files (e.g., "image_", "tts_")
40 - DEFAULT_WORKFLOW: Default workflow filename (e.g., "image_flux.json")
41 - WORKFLOWS_DIR: Directory containing workflows (default: "workflows")
42 """
43
44 WORKFLOW_PREFIX: str = "" # Must be overridden by subclass
45 DEFAULT_WORKFLOW: str = "" # Must be overridden by subclass
46 WORKFLOWS_DIR: str = "workflows"
47
48 def __init__(self, config: dict, service_name: str, core=None):
49 """
50 Initialize ComfyUI base service
51
52 Args:
53 config: Full application config dict
54 service_name: Service name in config (e.g., "tts", "image")
55 core: PixelleVideoCore instance (for accessing shared ComfyKit)
56 """
57 # Service-specific config (e.g., config["comfyui"]["tts"])
58 comfyui_config = config.get("comfyui", {})
59 self.config = comfyui_config.get(service_name, {})
60
61 # Global ComfyUI config (for comfyui_url and runninghub_api_key)
62 self.global_config = comfyui_config
63
64 self.service_name = service_name
65 self._workflows_cache: Optional[List[str]] = None
66
67 # Reference to core (for accessing shared ComfyKit)
68 self.core = core
69
70 def _scan_workflows(self) -> List[Dict[str, Any]]:
71 """
72 Scan workflows/source/*.json files from all source directories (merged from workflows/ and data/workflows/)
73
74 Results are cached after first scan to avoid repeated filesystem I/O.
75
76 Returns:
77 List of workflow info dicts
78 Example: [
79 {
80 "name": "image_flux.json",
81 "display_name": "image_flux.json - Selfhost",
82 "source": "selfhost",
83 "path": "workflows/selfhost/image_flux.json",
84 "key": "selfhost/image_flux.json"
85 },
86 {
87 "name": "image_flux.json",
88 "display_name": "image_flux.json - Runninghub",
89 "source": "runninghub",
90 "path": "workflows/runninghub/image_flux.json",
91 "key": "runninghub/image_flux.json",
92 "workflow_id": "123456"
93 }
94 ]
95 """
96 if self._workflows_cache is not None:
97 return self._workflows_cache
98
99 workflows = []
100
101 # Get all workflow source directories (merged from workflows/ and data/workflows/)
102 source_dirs = list_resource_dirs("workflows")
103
104 if not source_dirs:
105 logger.warning("No workflow source directories found")
106 return workflows
107
108 # Scan each source directory for workflow files
109 for source_name in source_dirs:
110 # Get all JSON files for this source (merged from both locations)
111 workflow_files = list_resource_files("workflows", source_name)
112
113 # Filter to only files matching the prefix
114 matching_files = [
115 f for f in workflow_files
116 if f.startswith(self.WORKFLOW_PREFIX) and f.endswith('.json')
117 ]
118
119 for filename in matching_files:
120 try:
121 # Get actual file path (custom > default)
122 file_path = Path(get_resource_path("workflows", source_name, filename))
123 workflow_info = self._parse_workflow_file(file_path, source_name)
124 workflows.append(workflow_info)
125 logger.debug(f"Found workflow: {workflow_info['key']}")
126 except Exception as e:
127 logger.error(f"Failed to parse workflow {source_name}/{filename}: {e}")
128
129 # Sort by key (source/name)
130 self._workflows_cache = sorted(workflows, key=lambda w: w["key"])
131 return self._workflows_cache
132
133 def _parse_workflow_file(self, file_path: Path, source: str) -> Dict[str, Any]:
134 """
135 Parse workflow file and extract metadata
136
137 Args:
138 file_path: Path to workflow JSON file
139 source: Source directory name (e.g., "selfhost", "runninghub")
140
141 Returns:
142 Workflow info dict with structure:
143 {
144 "name": "image_flux.json",
145 "display_name": "image_flux.json - Runninghub",
146 "source": "runninghub",
147 "path": "workflows/runninghub/image_flux.json",
148 "key": "runninghub/image_flux.json",
149 "workflow_id": "123456" # Only for RunningHub
150 }
151 """
152 with open(file_path, 'r', encoding='utf-8') as f:
153 content = json.load(f)
154
155 # Build base info
156 workflow_info = {
157 "name": file_path.name,
158 "display_name": f"{file_path.name} - {source.title()}",
159 "source": source,
160 "path": str(file_path),
161 "key": f"{source}/{file_path.name}"
162 }
163
164 # Check if it's a wrapper format (RunningHub, etc.)
165 if "source" in content:
166 # Wrapper format: {"source": "runninghub", "workflow_id": "xxx", ...}
167 if "workflow_id" in content:
168 workflow_info["workflow_id"] = content["workflow_id"]
169
170 return workflow_info
171
172 def _get_default_workflow(self) -> str:
173 """
174 Get default workflow from config (required, no fallback)
175
176 Returns:
177 Default workflow key (e.g., "runninghub/image_flux.json")
178
179 Raises:
180 ValueError: If default_workflow not configured
181 """
182 default_workflow = self.config.get("default_workflow")
183
184 if not default_workflow:
185 raise ValueError(
186 f"No default workflow configured for {self.service_name}. "
187 f"Please set 'default_workflow' in config.yaml under '{self.service_name}' section. "
188 f"Available workflows: {', '.join(self.available)}"
189 )
190
191 return default_workflow
192
193 def _resolve_workflow(self, workflow: Optional[str] = None) -> Dict[str, Any]:
194 """
195 Resolve workflow key to workflow info
196
197 Args:
198 workflow: Workflow key (e.g., "runninghub/image_flux.json")
199 If None, uses default from config
200
201 Returns:
202 Workflow info dict with structure:
203 {
204 "name": "image_flux.json",
205 "display_name": "image_flux.json - Runninghub",
206 "source": "runninghub",
207 "path": "workflows/runninghub/image_flux.json",
208 "key": "runninghub/image_flux.json",
209 "workflow_id": "123456" # Only for RunningHub
210 }
211
212 Raises:
213 ValueError: If workflow not found
214 """
215 # 1. If not specified, use default from config
216 if workflow is None:
217 workflow = self._get_default_workflow()
218
219 # 2. Scan available workflows
220 available_workflows = self._scan_workflows()
221
222 # 3. Find matching workflow by key
223 for wf_info in available_workflows:
224 if wf_info["key"] == workflow:
225 logger.info(f"🎬 Using {self.service_name} workflow: {workflow}")
226 return wf_info
227
228 # 4. Not found - generate error message
229 available_keys = [wf["key"] for wf in available_workflows]
230 available_str = ", ".join(available_keys) if available_keys else "none"
231 raise ValueError(
232 f"Workflow '{workflow}' not found. "
233 f"Available workflows: {available_str}"
234 )
235
236 def _prepare_comfykit_config(
237 self,
238 comfyui_url: Optional[str] = None,
239 runninghub_api_key: Optional[str] = None,
240 runninghub_instance_type: Optional[str] = None,
241 ) -> Dict[str, Any]:
242 """
243 Prepare ComfyKit configuration
244
245 Args:
246 comfyui_url: ComfyUI URL (optional, overrides config)
247 runninghub_api_key: RunningHub API key (optional, overrides config)
248 runninghub_instance_type: RunningHub instance type (optional, overrides config)
249
250 Returns:
251 ComfyKit configuration dict
252 """
253 kit_config = {}
254
255 # ComfyUI URL (priority: param > global config > env > default)
256 final_comfyui_url = (
257 comfyui_url
258 or self.global_config.get("comfyui_url")
259 or os.getenv("COMFYUI_BASE_URL")
260 or "http://127.0.0.1:8188"
261 )
262 kit_config["comfyui_url"] = final_comfyui_url
263
264 # RunningHub API key (priority: param > global config > env)
265 final_rh_key = (
266 runninghub_api_key
267 or self.global_config.get("runninghub_api_key")
268 or os.getenv("RUNNINGHUB_API_KEY")
269 )
270 if final_rh_key:
271 kit_config["runninghub_api_key"] = final_rh_key
272
273 # RunningHub instance type (priority: param > global config > env)
274 # Only pass if non-empty value
275 final_instance_type = (
276 runninghub_instance_type
277 or self.global_config.get("runninghub_instance_type")
278 or os.getenv("RUNNINGHUB_INSTANCE_TYPE")
279 )
280 if final_instance_type and final_instance_type.strip():
281 kit_config["runninghub_instance_type"] = final_instance_type
282
283 logger.debug(f"ComfyKit config: {kit_config}")
284 return kit_config
285
286 def list_workflows(self) -> List[Dict[str, Any]]:
287 """
288 List all available workflows with full metadata
289
290 Returns:
291 List of workflow info dicts (sorted by key)
292
293 Example:
294 workflows = service.list_workflows()
295 # [
296 # {
297 # "name": "image_flux.json",
298 # "display_name": "image_flux.json - Runninghub",
299 # "source": "runninghub",
300 # "path": "workflows/runninghub/image_flux.json",
301 # "key": "runninghub/image_flux.json",
302 # "workflow_id": "123456"
303 # },
304 # ...
305 # ]
306 """
307 return self._scan_workflows()
308
309 @property
310 def available(self) -> List[str]:
311 """
312 List available workflow keys
313
314 Returns:
315 List of available workflow keys (e.g., ["runninghub/image_flux.json", ...])
316
317 Example:
318 print(f"Available workflows: {service.available}")
319 """
320 workflows = self.list_workflows()
321 return [wf["key"] for wf in workflows]
322
323 def __repr__(self) -> str:
324 """String representation"""
325 default = self._get_default_workflow()
326 available = ", ".join(self.available) if self.available else "none"
327 return (
328 f"<{self.__class__.__name__} "
329 f"default={default!r} "
330 f"available=[{available}]>"
331 )
332
333
333 lines PYTHON