image_analysis.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. Image Analysis Service - ComfyUI Workflow-based implementation
  14. Uses Florence-2 or other vision models to analyze images and generate descriptions.
  15. """
  16. from typing import Optional, Literal
  17. from pathlib import Path
  18. from comfykit import ComfyKit
  19. from loguru import logger
  20. from pixelle_video.services.comfy_base_service import ComfyBaseService
  21. class ImageAnalysisService(ComfyBaseService):
  22. """
  23. Image analysis service - Workflow-based
  24. Uses ComfyKit to execute image analysis workflows (e.g., Florence-2, BLIP, etc.).
  25. Returns detailed textual descriptions of images.
  26. Convention: workflows follow {source}/analyse_image.json pattern
  27. - runninghub/analyse_image.json (default, cloud-based)
  28. - selfhost/analyse_image.json (local ComfyUI)
  29. Usage:
  30. # Use default (runninghub cloud)
  31. description = await pixelle_video.image_analysis("path/to/image.jpg")
  32. # Use local ComfyUI
  33. description = await pixelle_video.image_analysis(
  34. "path/to/image.jpg",
  35. source="selfhost"
  36. )
  37. # List available workflows
  38. workflows = pixelle_video.image_analysis.list_workflows()
  39. """
  40. WORKFLOW_PREFIX = "analyse_"
  41. WORKFLOWS_DIR = "workflows"
  42. def __init__(self, config: dict, core=None):
  43. """
  44. Initialize image analysis service
  45. Args:
  46. config: Full application config dict
  47. core: PixelleVideoCore instance (for accessing shared ComfyKit)
  48. """
  49. super().__init__(config, service_name="image_analysis", core=core)
  50. async def __call__(
  51. self,
  52. image_path: str,
  53. # Workflow source selection
  54. source: Literal['runninghub', 'selfhost'] = 'runninghub',
  55. workflow: Optional[str] = None,
  56. # ComfyUI connection (optional overrides)
  57. comfyui_url: Optional[str] = None,
  58. runninghub_api_key: Optional[str] = None,
  59. # Additional workflow parameters
  60. **params
  61. ) -> str:
  62. """
  63. Analyze an image using workflow
  64. Args:
  65. image_path: Path to the image file (local or URL)
  66. source: Workflow source - 'runninghub' (cloud, default) or 'selfhost' (local ComfyUI)
  67. workflow: Workflow filename (optional, overrides source-based resolution)
  68. comfyui_url: ComfyUI URL (optional, overrides config)
  69. runninghub_api_key: RunningHub API key (optional, overrides config)
  70. **params: Additional workflow parameters
  71. Returns:
  72. str: Text description of the image
  73. Examples:
  74. # Simplest: use default (runninghub cloud)
  75. description = await pixelle_video.image_analysis("temp/06.JPG")
  76. # Use local ComfyUI
  77. description = await pixelle_video.image_analysis(
  78. "temp/06.JPG",
  79. source="selfhost"
  80. )
  81. # Use specific workflow (bypass source-based resolution)
  82. description = await pixelle_video.image_analysis(
  83. "temp/06.JPG",
  84. workflow="selfhost/custom_analysis.json"
  85. )
  86. """
  87. from pixelle_video.utils.workflow_util import resolve_workflow_path
  88. # 1. Validate image path
  89. image_path_obj = Path(image_path)
  90. if not image_path_obj.exists():
  91. raise FileNotFoundError(f"Image file not found: {image_path}")
  92. # 2. Resolve workflow path using convention
  93. if workflow is None:
  94. # Use standardized naming: {source}/analyse_image.json
  95. workflow = resolve_workflow_path("analyse_image", source)
  96. logger.info(f"Using {source} workflow: {workflow}")
  97. # 2. Resolve workflow (returns structured info)
  98. workflow_info = self._resolve_workflow(workflow=workflow)
  99. # 3. Build workflow parameters
  100. workflow_params = {
  101. "image": str(image_path) # Pass image path to workflow
  102. }
  103. # Add any additional parameters
  104. workflow_params.update(params)
  105. logger.debug(f"Workflow parameters: {workflow_params}")
  106. # 4. Execute workflow using shared ComfyKit instance from core
  107. try:
  108. # Get shared ComfyKit instance (lazy initialization + config hot-reload)
  109. kit = await self.core._get_or_create_comfykit()
  110. # Determine what to pass to ComfyKit based on source
  111. if workflow_info["source"] == "runninghub" and "workflow_id" in workflow_info:
  112. # RunningHub: pass workflow_id
  113. workflow_input = workflow_info["workflow_id"]
  114. logger.info(f"Executing RunningHub workflow: {workflow_input}")
  115. else:
  116. # Selfhost: pass file path
  117. workflow_input = workflow_info["path"]
  118. logger.info(f"Executing selfhost workflow: {workflow_input}")
  119. result = await kit.execute(workflow_input, workflow_params)
  120. # 5. Extract description from result
  121. if result.status != "completed":
  122. error_msg = result.msg or "Unknown error"
  123. logger.error(f"Image analysis failed: {error_msg}")
  124. raise Exception(f"Image analysis failed: {error_msg}")
  125. # Extract text description from result (format varies by source)
  126. description = None
  127. # Try format 1: Selfhost outputs (direct text in outputs)
  128. # Format: {'6': {'text': ['description text']}}
  129. if result.outputs:
  130. for node_id, node_output in result.outputs.items():
  131. if 'text' in node_output:
  132. text_list = node_output['text']
  133. if text_list and len(text_list) > 0:
  134. description = text_list[0]
  135. break
  136. # Try format 2: RunningHub raw_data (text file URL)
  137. # Format: {'raw_data': [{'fileUrl': 'https://...txt', 'fileType': 'txt', ...}]}
  138. if not description and result.outputs and 'raw_data' in result.outputs:
  139. raw_data = result.outputs['raw_data']
  140. if raw_data and len(raw_data) > 0:
  141. # Find text file entry
  142. for item in raw_data:
  143. if item.get('fileType') == 'txt' and 'fileUrl' in item:
  144. # Download text content from URL
  145. import aiohttp
  146. async with aiohttp.ClientSession() as session:
  147. async with session.get(item['fileUrl']) as resp:
  148. if resp.status == 200:
  149. description = await resp.text()
  150. description = description.strip()
  151. break
  152. if not description:
  153. logger.error(f"No text found in outputs: {result.outputs}")
  154. raise Exception("No description generated")
  155. logger.info(f"✅ Image analyzed: {description[:100]}...")
  156. return description
  157. except Exception as e:
  158. logger.error(f"Image analysis error: {e}")
  159. raise