comfy_base_service.py 12 KB

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