resources.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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. Resource discovery endpoints
  14. Provides endpoints to discover available workflows, templates, and BGM.
  15. """
  16. from pathlib import Path
  17. from fastapi import APIRouter, HTTPException
  18. from loguru import logger
  19. from api.dependencies import PixelleVideoDep
  20. from api.schemas.resources import (
  21. WorkflowInfo,
  22. WorkflowListResponse,
  23. TemplateInfo,
  24. TemplateListResponse,
  25. BGMInfo,
  26. BGMListResponse,
  27. )
  28. from pixelle_video.utils.os_util import list_resource_files, get_root_path, get_data_path
  29. from pixelle_video.utils.template_util import get_all_templates_with_info
  30. router = APIRouter(prefix="/resources", tags=["Resources"])
  31. @router.get("/workflows/tts", response_model=WorkflowListResponse)
  32. async def list_tts_workflows(pixelle_video: PixelleVideoDep):
  33. """
  34. List available TTS workflows
  35. Returns list of TTS workflows from both RunningHub and self-hosted sources.
  36. Example response:
  37. ```json
  38. {
  39. "workflows": [
  40. {
  41. "name": "tts_edge.json",
  42. "display_name": "tts_edge.json - Runninghub",
  43. "source": "runninghub",
  44. "path": "workflows/runninghub/tts_edge.json",
  45. "key": "runninghub/tts_edge.json",
  46. "workflow_id": "123456"
  47. }
  48. ]
  49. }
  50. ```
  51. """
  52. try:
  53. # Get all workflows from TTS service
  54. all_workflows = pixelle_video.tts.list_workflows()
  55. # Filter to TTS workflows only (filename starts with "tts_")
  56. tts_workflows = [
  57. WorkflowInfo(**wf)
  58. for wf in all_workflows
  59. if wf["name"].startswith("tts_")
  60. ]
  61. return WorkflowListResponse(workflows=tts_workflows)
  62. except Exception as e:
  63. logger.error(f"List TTS workflows error: {e}")
  64. raise HTTPException(status_code=500, detail=str(e))
  65. @router.get("/workflows/media", response_model=WorkflowListResponse)
  66. async def list_media_workflows(pixelle_video: PixelleVideoDep):
  67. """
  68. List available media workflows (both image and video)
  69. Returns list of all media workflows from both RunningHub and self-hosted sources.
  70. Example response:
  71. ```json
  72. {
  73. "workflows": [
  74. {
  75. "name": "image_flux.json",
  76. "display_name": "image_flux.json - Runninghub",
  77. "source": "runninghub",
  78. "path": "workflows/runninghub/image_flux.json",
  79. "key": "runninghub/image_flux.json",
  80. "workflow_id": "123456"
  81. },
  82. {
  83. "name": "video_wan2.1.json",
  84. "display_name": "video_wan2.1.json - Runninghub",
  85. "source": "runninghub",
  86. "path": "workflows/runninghub/video_wan2.1.json",
  87. "key": "runninghub/video_wan2.1.json",
  88. "workflow_id": "123457"
  89. }
  90. ]
  91. }
  92. ```
  93. """
  94. try:
  95. # Get all workflows from media service (includes both image and video)
  96. all_workflows = pixelle_video.media.list_workflows()
  97. media_workflows = [WorkflowInfo(**wf) for wf in all_workflows]
  98. return WorkflowListResponse(workflows=media_workflows)
  99. except Exception as e:
  100. logger.error(f"List media workflows error: {e}")
  101. raise HTTPException(status_code=500, detail=str(e))
  102. # Keep old endpoint for backward compatibility
  103. @router.get("/workflows/image", response_model=WorkflowListResponse)
  104. async def list_image_workflows(pixelle_video: PixelleVideoDep):
  105. """
  106. List available image workflows (deprecated, use /workflows/media instead)
  107. This endpoint is kept for backward compatibility but will filter to image_ workflows only.
  108. """
  109. try:
  110. all_workflows = pixelle_video.media.list_workflows()
  111. # Filter to image workflows only (filename starts with "image_")
  112. image_workflows = [
  113. WorkflowInfo(**wf)
  114. for wf in all_workflows
  115. if wf["name"].startswith("image_")
  116. ]
  117. return WorkflowListResponse(workflows=image_workflows)
  118. except Exception as e:
  119. logger.error(f"List image workflows error: {e}")
  120. raise HTTPException(status_code=500, detail=str(e))
  121. @router.get("/templates", response_model=TemplateListResponse)
  122. async def list_templates():
  123. """
  124. List available video templates
  125. Returns list of HTML templates grouped by size (portrait, landscape, square).
  126. Templates are merged from both default (templates/) and custom (data/templates/) directories.
  127. Example response:
  128. ```json
  129. {
  130. "templates": [
  131. {
  132. "name": "default.html",
  133. "display_name": "default.html",
  134. "size": "1080x1920",
  135. "width": 1080,
  136. "height": 1920,
  137. "orientation": "portrait",
  138. "path": "templates/1080x1920/default.html",
  139. "key": "1080x1920/default.html"
  140. }
  141. ]
  142. }
  143. ```
  144. """
  145. try:
  146. # Get all templates with info
  147. all_templates = get_all_templates_with_info()
  148. # Convert to API response format
  149. templates = []
  150. for t in all_templates:
  151. templates.append(TemplateInfo(
  152. name=t.display_info.name,
  153. display_name=t.display_info.name,
  154. size=t.display_info.size,
  155. width=t.display_info.width,
  156. height=t.display_info.height,
  157. orientation=t.display_info.orientation,
  158. path=t.template_path,
  159. key=t.template_path
  160. ))
  161. return TemplateListResponse(templates=templates)
  162. except Exception as e:
  163. logger.error(f"List templates error: {e}")
  164. raise HTTPException(status_code=500, detail=str(e))
  165. @router.get("/bgm", response_model=BGMListResponse)
  166. async def list_bgm():
  167. """
  168. List available background music files
  169. Returns list of BGM files merged from both default (bgm/) and custom (data/bgm/) directories.
  170. Custom files take precedence over default files with the same name.
  171. Supported formats: mp3, wav, flac, m4a, aac, ogg
  172. Example response:
  173. ```json
  174. {
  175. "bgm_files": [
  176. {
  177. "name": "default.mp3",
  178. "path": "bgm/default.mp3",
  179. "source": "default"
  180. },
  181. {
  182. "name": "happy.mp3",
  183. "path": "data/bgm/happy.mp3",
  184. "source": "custom"
  185. }
  186. ]
  187. }
  188. ```
  189. """
  190. try:
  191. # Supported audio extensions
  192. audio_extensions = ('.mp3', '.wav', '.flac', '.m4a', '.aac', '.ogg')
  193. # Collect BGM files from both locations
  194. bgm_files_dict = {} # {filename: {"path": str, "source": str}}
  195. # Scan default bgm/ directory
  196. default_bgm_dir = Path(get_root_path("bgm"))
  197. if default_bgm_dir.exists() and default_bgm_dir.is_dir():
  198. for item in default_bgm_dir.iterdir():
  199. if item.is_file() and item.suffix.lower() in audio_extensions:
  200. bgm_files_dict[item.name] = {
  201. "path": f"bgm/{item.name}",
  202. "source": "default"
  203. }
  204. # Scan custom data/bgm/ directory (overrides default)
  205. custom_bgm_dir = Path(get_data_path("bgm"))
  206. if custom_bgm_dir.exists() and custom_bgm_dir.is_dir():
  207. for item in custom_bgm_dir.iterdir():
  208. if item.is_file() and item.suffix.lower() in audio_extensions:
  209. bgm_files_dict[item.name] = {
  210. "path": f"data/bgm/{item.name}",
  211. "source": "custom"
  212. }
  213. # Convert to response format
  214. bgm_files = [
  215. BGMInfo(
  216. name=name,
  217. path=info["path"],
  218. source=info["source"]
  219. )
  220. for name, info in sorted(bgm_files_dict.items())
  221. ]
  222. return BGMListResponse(bgm_files=bgm_files)
  223. except Exception as e:
  224. logger.error(f"List BGM error: {e}")
  225. raise HTTPException(status_code=500, detail=str(e))