video.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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. Video generation endpoints
  14. Supports both synchronous and asynchronous video generation.
  15. """
  16. import os
  17. from fastapi import APIRouter, HTTPException, Request
  18. from loguru import logger
  19. from api.dependencies import PixelleVideoDep
  20. from api.schemas.video import (
  21. VideoGenerateRequest,
  22. VideoGenerateResponse,
  23. VideoGenerateAsyncResponse,
  24. )
  25. from api.tasks import task_manager, TaskType
  26. router = APIRouter(prefix="/video", tags=["Video Generation"])
  27. def path_to_url(request: Request, file_path: str) -> str:
  28. """
  29. Convert file path to accessible URL
  30. Handles both absolute and relative paths, extracting the path relative
  31. to the output directory for URL construction.
  32. Args:
  33. request: FastAPI Request object (provides base_url from actual request)
  34. file_path: Absolute or relative file path
  35. Returns:
  36. Full URL to access the file
  37. Examples:
  38. Windows: G:\\...\\output\\20251205_233630_c939\\final.mp4
  39. -> http://localhost:8000/api/files/20251205_233630_c939/final.mp4
  40. Linux: /home/user/.../output/20251205_233630_c939/final.mp4
  41. -> http://localhost:8000/api/files/20251205_233630_c939/final.mp4
  42. Domain: With domain request -> https://your-domain.com/api/files/...
  43. """
  44. from pathlib import Path
  45. import os
  46. # Normalize path separators to forward slashes first (for cross-platform compatibility)
  47. file_path = file_path.replace("\\", "/")
  48. # Check if it's an absolute path (works for both Windows and Linux)
  49. is_absolute = os.path.isabs(file_path) or Path(file_path).is_absolute()
  50. if is_absolute:
  51. # Find "output" in the path and get everything after it
  52. # Split by / to work with normalized paths
  53. parts = file_path.split("/")
  54. try:
  55. output_idx = parts.index("output")
  56. # Get all parts after "output" and join them
  57. relative_parts = parts[output_idx + 1:]
  58. file_path = "/".join(relative_parts)
  59. except ValueError:
  60. # If "output" not in path, use the filename only
  61. file_path = Path(file_path).name
  62. else:
  63. # If relative path starting with "output/", remove it
  64. if file_path.startswith("output/"):
  65. file_path = file_path[7:] # Remove "output/"
  66. # Build URL using request's base_url (automatically matches the request host)
  67. base_url = str(request.base_url).rstrip('/')
  68. return f"{base_url}/api/files/{file_path}"
  69. @router.post("/generate/sync", response_model=VideoGenerateResponse)
  70. async def generate_video_sync(
  71. request_body: VideoGenerateRequest,
  72. pixelle_video: PixelleVideoDep,
  73. request: Request
  74. ):
  75. """
  76. Generate video synchronously
  77. This endpoint blocks until video generation is complete.
  78. Suitable for small videos (< 30 seconds).
  79. **Note**: May timeout for large videos. Use `/generate/async` instead.
  80. Request body includes all video generation parameters.
  81. See VideoGenerateRequest schema for details.
  82. Returns path to generated video, duration, and file size.
  83. """
  84. try:
  85. logger.info(f"Sync video generation: {request_body.text[:50]}...")
  86. # Auto-determine media_width and media_height from template meta tags (required)
  87. if not request_body.frame_template:
  88. raise ValueError("frame_template is required to determine media size")
  89. from pixelle_video.services.frame_html import HTMLFrameGenerator
  90. from pixelle_video.utils.template_util import resolve_template_path
  91. template_path = resolve_template_path(request_body.frame_template)
  92. generator = HTMLFrameGenerator(template_path)
  93. media_width, media_height = generator.get_media_size()
  94. logger.debug(f"Auto-determined media size from template: {media_width}x{media_height}")
  95. # Build video generation parameters
  96. video_params = {
  97. "text": request_body.text,
  98. "mode": request_body.mode,
  99. "title": request_body.title,
  100. "n_scenes": request_body.n_scenes,
  101. "min_narration_words": request_body.min_narration_words,
  102. "max_narration_words": request_body.max_narration_words,
  103. "min_image_prompt_words": request_body.min_image_prompt_words,
  104. "max_image_prompt_words": request_body.max_image_prompt_words,
  105. "media_width": media_width,
  106. "media_height": media_height,
  107. "media_workflow": request_body.media_workflow,
  108. "video_fps": request_body.video_fps,
  109. "frame_template": request_body.frame_template,
  110. "prompt_prefix": request_body.prompt_prefix,
  111. "bgm_path": request_body.bgm_path,
  112. "bgm_volume": request_body.bgm_volume,
  113. }
  114. # Add TTS workflow if specified
  115. if request_body.tts_workflow:
  116. video_params["tts_workflow"] = request_body.tts_workflow
  117. # Add ref_audio if specified
  118. if request_body.ref_audio:
  119. video_params["ref_audio"] = request_body.ref_audio
  120. # Legacy voice_id support (deprecated)
  121. if request_body.voice_id:
  122. logger.warning("voice_id parameter is deprecated, please use tts_workflow instead")
  123. video_params["voice_id"] = request_body.voice_id
  124. # Add custom template parameters if specified
  125. if request_body.template_params:
  126. video_params["template_params"] = request_body.template_params
  127. # Call video generator service
  128. result = await pixelle_video.generate_video(**video_params)
  129. # Get file size
  130. file_size = os.path.getsize(result.video_path) if os.path.exists(result.video_path) else 0
  131. # Convert path to URL
  132. video_url = path_to_url(request, result.video_path)
  133. return VideoGenerateResponse(
  134. video_url=video_url,
  135. duration=result.duration,
  136. file_size=file_size
  137. )
  138. except Exception as e:
  139. logger.error(f"Sync video generation error: {e}")
  140. raise HTTPException(status_code=500, detail=str(e))
  141. @router.post("/generate/async", response_model=VideoGenerateAsyncResponse)
  142. async def generate_video_async(
  143. request_body: VideoGenerateRequest,
  144. pixelle_video: PixelleVideoDep,
  145. request: Request
  146. ):
  147. """
  148. Generate video asynchronously
  149. Creates a background task for video generation.
  150. Returns immediately with a task_id for tracking progress.
  151. **Workflow:**
  152. 1. Submit video generation request
  153. 2. Receive task_id in response
  154. 3. Poll `/api/tasks/{task_id}` to check status
  155. 4. When status is "completed", retrieve video from result
  156. Request body includes all video generation parameters.
  157. See VideoGenerateRequest schema for details.
  158. Returns task_id for tracking progress.
  159. """
  160. try:
  161. logger.info(f"Async video generation: {request_body.text[:50]}...")
  162. # Create task
  163. task = task_manager.create_task(
  164. task_type=TaskType.VIDEO_GENERATION,
  165. request_params=request_body.model_dump()
  166. )
  167. # Define async execution function
  168. async def execute_video_generation():
  169. """Execute video generation in background"""
  170. # Auto-determine media_width and media_height from template meta tags (required)
  171. if not request_body.frame_template:
  172. raise ValueError("frame_template is required to determine media size")
  173. from pixelle_video.services.frame_html import HTMLFrameGenerator
  174. from pixelle_video.utils.template_util import resolve_template_path
  175. template_path = resolve_template_path(request_body.frame_template)
  176. generator = HTMLFrameGenerator(template_path)
  177. media_width, media_height = generator.get_media_size()
  178. logger.debug(f"Auto-determined media size from template: {media_width}x{media_height}")
  179. # Build video generation parameters
  180. video_params = {
  181. "text": request_body.text,
  182. "mode": request_body.mode,
  183. "title": request_body.title,
  184. "n_scenes": request_body.n_scenes,
  185. "min_narration_words": request_body.min_narration_words,
  186. "max_narration_words": request_body.max_narration_words,
  187. "min_image_prompt_words": request_body.min_image_prompt_words,
  188. "max_image_prompt_words": request_body.max_image_prompt_words,
  189. "media_width": media_width,
  190. "media_height": media_height,
  191. "media_workflow": request_body.media_workflow,
  192. "video_fps": request_body.video_fps,
  193. "frame_template": request_body.frame_template,
  194. "prompt_prefix": request_body.prompt_prefix,
  195. "bgm_path": request_body.bgm_path,
  196. "bgm_volume": request_body.bgm_volume,
  197. # Progress callback can be added here if needed
  198. # "progress_callback": lambda event: task_manager.update_progress(...)
  199. }
  200. # Add TTS workflow if specified
  201. if request_body.tts_workflow:
  202. video_params["tts_workflow"] = request_body.tts_workflow
  203. # Add ref_audio if specified
  204. if request_body.ref_audio:
  205. video_params["ref_audio"] = request_body.ref_audio
  206. # Legacy voice_id support (deprecated)
  207. if request_body.voice_id:
  208. logger.warning("voice_id parameter is deprecated, please use tts_workflow instead")
  209. video_params["voice_id"] = request_body.voice_id
  210. # Add custom template parameters if specified
  211. if request_body.template_params:
  212. video_params["template_params"] = request_body.template_params
  213. result = await pixelle_video.generate_video(**video_params)
  214. # Get file size
  215. file_size = os.path.getsize(result.video_path) if os.path.exists(result.video_path) else 0
  216. # Convert path to URL
  217. video_url = path_to_url(request, result.video_path)
  218. return {
  219. "video_url": video_url,
  220. "duration": result.duration,
  221. "file_size": file_size
  222. }
  223. # Start execution
  224. await task_manager.execute_task(
  225. task_id=task.task_id,
  226. coro_func=execute_video_generation
  227. )
  228. return VideoGenerateAsyncResponse(
  229. task_id=task.task_id
  230. )
  231. except Exception as e:
  232. logger.error(f"Async video generation error: {e}")
  233. raise HTTPException(status_code=500, detail=str(e))