media.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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. Media Generation Service - ComfyUI Workflow-based implementation
  14. Supports both image and video generation workflows.
  15. Automatically detects output type based on ExecuteResult.
  16. """
  17. from typing import Optional
  18. from comfykit import ComfyKit
  19. from loguru import logger
  20. from pixelle_video.services.comfy_base_service import ComfyBaseService
  21. from pixelle_video.models.media import MediaResult
  22. class MediaService(ComfyBaseService):
  23. """
  24. Media generation service - Workflow-based
  25. Uses ComfyKit to execute image/video generation workflows.
  26. Supports both image_ and video_ workflow prefixes.
  27. Usage:
  28. # Use default workflow (workflows/image_flux.json)
  29. media = await pixelle_video.media(prompt="a cat")
  30. if media.is_image:
  31. print(f"Generated image: {media.url}")
  32. elif media.is_video:
  33. print(f"Generated video: {media.url} ({media.duration}s)")
  34. # Use specific workflow
  35. media = await pixelle_video.media(
  36. prompt="a cat",
  37. workflow="image_flux.json"
  38. )
  39. # List available workflows
  40. workflows = pixelle_video.media.list_workflows()
  41. """
  42. WORKFLOW_PREFIX = "" # Will be overridden by _scan_workflows
  43. DEFAULT_WORKFLOW = None # No hardcoded default, must be configured
  44. WORKFLOWS_DIR = "workflows"
  45. def __init__(self, config: dict, core=None):
  46. """
  47. Initialize media service
  48. Args:
  49. config: Full application config dict
  50. core: PixelleVideoCore instance (for accessing shared ComfyKit)
  51. """
  52. super().__init__(config, service_name="image", core=core) # Keep "image" for config compatibility
  53. def _scan_workflows(self):
  54. """
  55. Scan workflows for both image_ and video_ prefixes
  56. Override parent method to support multiple prefixes
  57. """
  58. from pixelle_video.utils.os_util import list_resource_dirs, list_resource_files, get_resource_path
  59. from pathlib import Path
  60. workflows = []
  61. # Get all workflow source directories
  62. source_dirs = list_resource_dirs("workflows")
  63. if not source_dirs:
  64. logger.warning("No workflow source directories found")
  65. return workflows
  66. # Scan each source directory for workflow files
  67. for source_name in source_dirs:
  68. # Get all JSON files for this source
  69. workflow_files = list_resource_files("workflows", source_name)
  70. # Filter to only files matching image_ or video_ prefix
  71. matching_files = [
  72. f for f in workflow_files
  73. if (f.startswith("image_") or f.startswith("video_")) and f.endswith('.json')
  74. ]
  75. for filename in matching_files:
  76. try:
  77. # Get actual file path
  78. file_path = Path(get_resource_path("workflows", source_name, filename))
  79. workflow_info = self._parse_workflow_file(file_path, source_name)
  80. workflows.append(workflow_info)
  81. logger.debug(f"Found workflow: {workflow_info['key']}")
  82. except Exception as e:
  83. logger.error(f"Failed to parse workflow {source_name}/{filename}: {e}")
  84. # Sort by key (source/name)
  85. return sorted(workflows, key=lambda w: w["key"])
  86. async def __call__(
  87. self,
  88. prompt: str,
  89. workflow: Optional[str] = None,
  90. # Media type specification (required for proper handling)
  91. media_type: str = "image", # "image" or "video"
  92. # ComfyUI connection (optional overrides)
  93. comfyui_url: Optional[str] = None,
  94. runninghub_api_key: Optional[str] = None,
  95. # Common workflow parameters
  96. width: Optional[int] = None,
  97. height: Optional[int] = None,
  98. duration: Optional[float] = None, # Video duration in seconds (for video workflows)
  99. negative_prompt: Optional[str] = None,
  100. steps: Optional[int] = None,
  101. seed: Optional[int] = None,
  102. cfg: Optional[float] = None,
  103. sampler: Optional[str] = None,
  104. **params
  105. ) -> MediaResult:
  106. """
  107. Generate media (image or video) using workflow
  108. Media type must be specified explicitly via media_type parameter.
  109. Returns a MediaResult object containing media type and URL.
  110. Args:
  111. prompt: Media generation prompt
  112. workflow: Workflow filename (default: from config or "image_flux.json")
  113. media_type: Type of media to generate - "image" or "video" (default: "image")
  114. comfyui_url: ComfyUI URL (optional, overrides config)
  115. runninghub_api_key: RunningHub API key (optional, overrides config)
  116. width: Media width
  117. height: Media height
  118. duration: Target video duration in seconds (only for video workflows, typically from TTS audio duration)
  119. negative_prompt: Negative prompt
  120. steps: Sampling steps
  121. seed: Random seed
  122. cfg: CFG scale
  123. sampler: Sampler name
  124. **params: Additional workflow parameters
  125. Returns:
  126. MediaResult object with media_type ("image" or "video") and url
  127. Examples:
  128. # Simplest: use default workflow (workflows/image_flux.json)
  129. media = await pixelle_video.media(prompt="a beautiful cat")
  130. if media.is_image:
  131. print(f"Image: {media.url}")
  132. # Use specific workflow
  133. media = await pixelle_video.media(
  134. prompt="a cat",
  135. workflow="image_flux.json"
  136. )
  137. # Video workflow
  138. media = await pixelle_video.media(
  139. prompt="a cat running",
  140. workflow="image_video.json"
  141. )
  142. if media.is_video:
  143. print(f"Video: {media.url}, duration: {media.duration}s")
  144. # With additional parameters
  145. media = await pixelle_video.media(
  146. prompt="a cat",
  147. workflow="image_flux.json",
  148. width=1024,
  149. height=1024,
  150. steps=20,
  151. seed=42
  152. )
  153. # With absolute path
  154. media = await pixelle_video.media(
  155. prompt="a cat",
  156. workflow="/path/to/custom.json"
  157. )
  158. # With custom ComfyUI server
  159. media = await pixelle_video.media(
  160. prompt="a cat",
  161. comfyui_url="http://192.168.1.100:8188"
  162. )
  163. """
  164. # 1. Resolve workflow (returns structured info)
  165. workflow_info = self._resolve_workflow(workflow=workflow)
  166. # 2. Build workflow parameters (ComfyKit config is now managed by core)
  167. workflow_params = {"prompt": prompt}
  168. # Add optional parameters
  169. if width is not None:
  170. workflow_params["width"] = width
  171. if height is not None:
  172. workflow_params["height"] = height
  173. if duration is not None:
  174. workflow_params["duration"] = duration
  175. if media_type == "video":
  176. logger.info(f"📏 Target video duration: {duration:.2f}s (from TTS audio)")
  177. if negative_prompt is not None:
  178. workflow_params["negative_prompt"] = negative_prompt
  179. if steps is not None:
  180. workflow_params["steps"] = steps
  181. if seed is not None:
  182. workflow_params["seed"] = seed
  183. if cfg is not None:
  184. workflow_params["cfg"] = cfg
  185. if sampler is not None:
  186. workflow_params["sampler"] = sampler
  187. # Add any additional parameters
  188. workflow_params.update(params)
  189. logger.debug(f"Workflow parameters: {workflow_params}")
  190. # 4. Execute workflow using shared ComfyKit instance from core
  191. try:
  192. # Get shared ComfyKit instance (lazy initialization + config hot-reload)
  193. kit = await self.core._get_or_create_comfykit()
  194. # Determine what to pass to ComfyKit based on source
  195. if workflow_info["source"] == "runninghub" and "workflow_id" in workflow_info:
  196. # RunningHub: pass workflow_id (ComfyKit will use runninghub backend)
  197. workflow_input = workflow_info["workflow_id"]
  198. logger.info(f"Executing RunningHub workflow: {workflow_input}")
  199. else:
  200. # Selfhost: pass file path (ComfyKit will use local ComfyUI)
  201. workflow_input = workflow_info["path"]
  202. logger.info(f"Executing selfhost workflow: {workflow_input}")
  203. result = await kit.execute(workflow_input, workflow_params)
  204. # 5. Handle result based on specified media_type
  205. if result.status != "completed":
  206. error_msg = result.msg or "Unknown error"
  207. logger.error(f"Media generation failed: {error_msg}")
  208. raise Exception(f"Media generation failed: {error_msg}")
  209. # Extract media based on specified type
  210. if media_type == "video":
  211. # Video workflow - get video from result
  212. if not result.videos:
  213. logger.error("No video generated (workflow returned no videos)")
  214. raise Exception("No video generated")
  215. video_url = result.videos[0]
  216. logger.info(f"✅ Generated video: {video_url}")
  217. # Try to extract duration from result (if available)
  218. duration = None
  219. if hasattr(result, 'duration') and result.duration:
  220. duration = result.duration
  221. return MediaResult(
  222. media_type="video",
  223. url=video_url,
  224. duration=duration
  225. )
  226. else: # image
  227. # Image workflow - get image from result
  228. if not result.images:
  229. logger.error("No image generated (workflow returned no images)")
  230. raise Exception("No image generated")
  231. image_url = result.images[0]
  232. logger.info(f"✅ Generated image: {image_url}")
  233. return MediaResult(
  234. media_type="image",
  235. url=image_url
  236. )
  237. except Exception as e:
  238. logger.error(f"Media generation error: {e}")
  239. raise