VideoGenerationController.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <?php
  2. namespace App\Http\Controllers\AIGeneration;
  3. use Illuminate\Routing\Controller as BaseController;
  4. use App\Libs\Utils;
  5. use App\Services\AIGeneration\AIVideoGenerationService;
  6. use App\Transformer\AIGeneration\AIGenerationTransformer;
  7. use Illuminate\Http\Request;
  8. use Illuminate\Http\JsonResponse;
  9. use Illuminate\Support\Facades\Validator;
  10. use App\Libs\ApiResponse;
  11. class VideoGenerationController extends BaseController
  12. {
  13. use ApiResponse;
  14. private $aiVideoGenerationService;
  15. public function __construct(
  16. AIVideoGenerationService $aiVideoGenerationService
  17. ) {
  18. $this->aiVideoGenerationService = $aiVideoGenerationService;
  19. }
  20. /**
  21. * 创建视频生成任务
  22. *
  23. * @param Request $request
  24. * @return JsonResponse
  25. */
  26. public function createTask(Request $request): JsonResponse
  27. {
  28. $data = $request->all();
  29. // 首先验证基本参数
  30. $baseRules = [
  31. 'prompt' => 'required|string|max:1000',
  32. 'seed' => 'required|integer',
  33. 'video_duration' => 'required|in:5,10',
  34. 'video_resolution' => 'required|in:720P,1080P'
  35. ];
  36. $baseMessages = [
  37. 'prompt.required' => '提示词不能为空',
  38. 'prompt.max' => '提示词不能超过1000个字符',
  39. 'seed.required' => '随机种子不能为空',
  40. 'seed.integer' => '随机种子必须是整数',
  41. 'video_duration.required' => '请选择视频时长',
  42. 'video_duration.in' => '视频时长只能是5s或10s',
  43. 'video_resolution.required' => '请选择视频分辨率',
  44. 'video_resolution.in' => '视频分辨率只能是720P或1080P',
  45. ];
  46. // 动态构建验证规则
  47. $validationRules = $baseRules;
  48. $validationMessages = $baseMessages;
  49. // 检查是否有首帧URL
  50. if (isset($data['first_frame_url'])) {
  51. $validationRules['first_frame_url'] = 'url';
  52. $validationMessages['first_frame_url.url'] = '首帧图片URL格式不正确';
  53. }
  54. // 检查是否有尾帧URL
  55. if (isset($data['tail_frame_url'])) {
  56. $validationRules['tail_frame_url'] = 'url';
  57. $validationMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确';
  58. }
  59. // 检查是否有首帧文件
  60. if ($request->hasFile('first_frame_file')) {
  61. $validationRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  62. $validationMessages = array_merge($validationMessages, [
  63. 'first_frame_file.image' => '首帧必须是图片文件',
  64. 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式',
  65. 'first_frame_file.max' => '首帧图片大小不能超过4.7MB',
  66. ]);
  67. }
  68. // 检查是否有尾帧文件
  69. if ($request->hasFile('tail_frame_file')) {
  70. $validationRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  71. $validationMessages = array_merge($validationMessages, [
  72. 'tail_frame_file.image' => '尾帧必须是图片文件',
  73. 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式',
  74. 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB',
  75. ]);
  76. }
  77. if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) {
  78. Utils::throwError('1002:提供尾帧URL时必须提供首帧URL');
  79. }
  80. if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) {
  81. Utils::throwError('1002:提供尾帧文件时必须提供首帧文件');
  82. }
  83. $validator = Validator::make($data, $validationRules, $validationMessages);
  84. if ($validator->fails()) {
  85. Utils::throwError('1002:'.$validator->errors()->first());
  86. }
  87. $firstFrame = null;
  88. $tailFrame = null;
  89. // 处理首帧图片(文件上传方式)
  90. if ($request->hasFile('first_frame_file')) {
  91. $firstFrameFile = $request->file('first_frame_file');
  92. $firstFrame = $this->processFrameFile($firstFrameFile, 'first');
  93. $data['first_frame_url'] = $firstFrame['url'];
  94. }
  95. // 处理首帧图片(URL方式)
  96. elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) {
  97. $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true);
  98. if (!$firstFrame) {
  99. Utils::throwError('1003:首帧图片获取失败');
  100. }
  101. }
  102. // 处理尾帧图片(文件上传方式)
  103. if ($request->hasFile('tail_frame_file')) {
  104. $tailFrameFile = $request->file('tail_frame_file');
  105. $tailFrame = $this->processFrameFile($tailFrameFile, 'tail');
  106. $data['tail_frame_url'] = $tailFrame['url'];
  107. }
  108. // 处理尾帧图片(URL方式)
  109. elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) {
  110. $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true);
  111. if (!$tailFrame) {
  112. Utils::throwError('1003:尾帧图片获取失败');
  113. }
  114. }
  115. // 验证首帧图片规格(如果存在)
  116. if ($firstFrame) {
  117. $this->validateFrameSpecs($firstFrame, '首帧');
  118. }
  119. // 验证尾帧图片规格(如果存在)
  120. if ($tailFrame) {
  121. $this->validateFrameSpecs($tailFrame, '尾帧');
  122. }
  123. // 如果首尾帧都存在,验证比例是否相同
  124. if ($firstFrame && $tailFrame) {
  125. $firstRatio = round($firstFrame['width'] / $firstFrame['height'], 2);
  126. $tailRatio = round($tailFrame['width'] / $tailFrame['height'], 2);
  127. if ($firstRatio != $tailRatio) {
  128. Utils::throwError('1003:尾帧图片与首帧图片比例必须相同');
  129. }
  130. }
  131. try {
  132. $task = $this->aiVideoGenerationService->createVideoGenerationTask($data);
  133. return $this->success(['task_id' => $task->task_id, 'status' => $task->status]);
  134. } catch (\Exception $e) {
  135. Utils::throwError('1001:'.$e->getMessage());
  136. }
  137. }
  138. /**
  139. * 查询视频任务状态
  140. *
  141. * @param string $taskId
  142. * @return JsonResponse
  143. */
  144. public function taskStatus(string $taskId): JsonResponse
  145. {
  146. $task = \App\Models\MpGenerateVideoTask::where('task_id', $taskId)->first();
  147. if (!$task) {
  148. return response()->json([
  149. 'success' => false,
  150. 'message' => '任务不存在'
  151. ], 404);
  152. }
  153. return response()->json([
  154. 'success' => true,
  155. 'data' => [
  156. 'task_id' => $task->task_id,
  157. 'status' => $task->status,
  158. 'result_url' => $task->result_url,
  159. 'error_message' => $task->error_message,
  160. 'created_at' => $task->created_at,
  161. 'started_at' => $task->started_at,
  162. 'completed_at' => $task->completed_at
  163. ]
  164. ]);
  165. }
  166. /**
  167. * 处理帧图片文件
  168. *
  169. * @param \Illuminate\Http\UploadedFile $file
  170. * @param string $frameType 帧类型(first/tail)
  171. * @return array
  172. */
  173. private function processFrameFile($file, string $frameType): array
  174. {
  175. // 验证图片信息
  176. $frameInfo = @getimagesize($file->getPathname());
  177. if (!$frameInfo) {
  178. Utils::throwError('1003:' . ($frameType === 'first' ? '首帧' : '尾帧') . '图片格式无效');
  179. }
  180. $frameData = [
  181. 'width' => $frameInfo[0],
  182. 'height' => $frameInfo[1],
  183. 'size' => $file->getSize(),
  184. ];
  185. // 上传图片到服务器
  186. try {
  187. $frameData['url'] = uploadFileByTos('image', $file);
  188. } catch (\Exception $e) {
  189. Utils::throwError('1003:' . ($frameType === 'first' ? '首帧' : '尾帧') . '图片上传失败:' . $e->getMessage());
  190. }
  191. return $frameData;
  192. }
  193. /**
  194. * 验证帧图片规格
  195. *
  196. * @param array $frame
  197. * @param string $frameName
  198. * @return void
  199. */
  200. private function validateFrameSpecs(array $frame, string $frameName): void
  201. {
  202. // 1.图片文件最大4.7MB
  203. if ($frame['size'] > 4.7 * 1024 * 1024) {
  204. Utils::throwError('1003:' . $frameName . '图片大小不能超过4.7MB');
  205. }
  206. // 2.图片分辨率最大: 4096*4096, 最短边不低于320
  207. $minDimension = min($frame['width'], $frame['height']);
  208. $maxDimension = max($frame['width'], $frame['height']);
  209. if ($maxDimension > 4096 || $minDimension < 320) {
  210. Utils::throwError('1003:' . $frameName . '图片分辨率不能超过4096*4096,且最短边不能低于320');
  211. }
  212. // 3.图片长边与短边比例在3以内
  213. if ($maxDimension / $minDimension > 3) {
  214. Utils::throwError('1003:' . $frameName . '图片长边与短边比例不能超过3');
  215. }
  216. }
  217. /**
  218. * 获取视频任务列表
  219. *
  220. * @param Request $request
  221. * @return mixed
  222. */
  223. public function taskList(Request $request)
  224. {
  225. $data = $request->all();
  226. $result = $this->aiVideoGenerationService->getTaskList($data);
  227. return $this->success($result, [new AIGenerationTransformer(), 'newBuildTaskList']);
  228. }
  229. }