VideoGenerationController.php 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267
  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\Services\PointsService;
  7. use App\Transformer\AIGeneration\AIGenerationTransformer;
  8. use Illuminate\Http\Request;
  9. use Illuminate\Http\JsonResponse;
  10. use Illuminate\Support\Facades\Validator;
  11. use Illuminate\Support\Facades\DB;
  12. use App\Libs\ApiResponse;
  13. use App\Models\AIElement;
  14. class VideoGenerationController extends BaseController
  15. {
  16. use ApiResponse;
  17. private $aiVideoGenerationService;
  18. private $pointsService;
  19. public function __construct(
  20. AIVideoGenerationService $aiVideoGenerationService,
  21. PointsService $pointsService
  22. ) {
  23. $this->aiVideoGenerationService = $aiVideoGenerationService;
  24. $this->pointsService = $pointsService;
  25. }
  26. /**
  27. * 创建视频生成任务(即梦AI)
  28. *
  29. * @param Request $request
  30. * @return JsonResponse
  31. */
  32. public function createJimengTask(Request $request): JsonResponse
  33. {
  34. $data = $request->all();
  35. // 首先验证基本参数
  36. $baseRules = [
  37. 'prompt' => 'required|string|max:1000',
  38. 'seed' => 'required|integer',
  39. 'video_duration' => 'required|in:5,10',
  40. 'video_resolution' => 'required|in:720P,1080P'
  41. ];
  42. $baseMessages = [
  43. 'prompt.required' => '提示词不能为空',
  44. 'prompt.max' => '提示词不能超过1000个字符',
  45. 'seed.required' => '随机种子不能为空',
  46. 'seed.integer' => '随机种子必须是整数',
  47. 'video_duration.required' => '请选择视频时长',
  48. 'video_duration.in' => '视频时长只能是5s或10s',
  49. 'video_resolution.required' => '请选择视频分辨率',
  50. 'video_resolution.in' => '视频分辨率只能是720P或1080P',
  51. ];
  52. // 动态构建验证规则
  53. $validationRules = $baseRules;
  54. $validationMessages = $baseMessages;
  55. // 检查是否有首帧URL
  56. if (isset($data['first_frame_url'])) {
  57. $validationRules['first_frame_url'] = 'url';
  58. $validationMessages['first_frame_url.url'] = '首帧图片URL格式不正确';
  59. }
  60. // 检查是否有尾帧URL
  61. if (isset($data['tail_frame_url'])) {
  62. $validationRules['tail_frame_url'] = 'url';
  63. $validationMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确';
  64. }
  65. // 检查是否有首帧文件
  66. if ($request->hasFile('first_frame_file')) {
  67. $validationRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  68. $validationMessages = array_merge($validationMessages, [
  69. 'first_frame_file.image' => '首帧必须是图片文件',
  70. 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式',
  71. 'first_frame_file.max' => '首帧图片大小不能超过4.7MB',
  72. ]);
  73. }
  74. // 检查是否有尾帧文件
  75. if ($request->hasFile('tail_frame_file')) {
  76. $validationRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  77. $validationMessages = array_merge($validationMessages, [
  78. 'tail_frame_file.image' => '尾帧必须是图片文件',
  79. 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式',
  80. 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB',
  81. ]);
  82. }
  83. if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) {
  84. Utils::throwError('1002:提供尾帧URL时必须提供首帧URL');
  85. }
  86. if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) {
  87. Utils::throwError('1002:提供尾帧文件时必须提供首帧文件');
  88. }
  89. $validator = Validator::make($data, $validationRules, $validationMessages);
  90. if ($validator->fails()) {
  91. Utils::throwError('1002:'.$validator->errors()->first());
  92. }
  93. $firstFrame = null;
  94. $tailFrame = null;
  95. // 处理首帧图片(文件上传方式)
  96. if ($request->hasFile('first_frame_file')) {
  97. $firstFrameFile = $request->file('first_frame_file');
  98. $firstFrame = $this->processFrameFile($firstFrameFile, 'first');
  99. $data['first_frame_url'] = $firstFrame['url'];
  100. }
  101. // 处理首帧图片(URL方式)
  102. elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) {
  103. $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true);
  104. if (!$firstFrame) {
  105. Utils::throwError('1003:首帧图片获取失败');
  106. }
  107. }
  108. // 处理尾帧图片(文件上传方式)
  109. if ($request->hasFile('tail_frame_file')) {
  110. $tailFrameFile = $request->file('tail_frame_file');
  111. $tailFrame = $this->processFrameFile($tailFrameFile, 'tail');
  112. $data['tail_frame_url'] = $tailFrame['url'];
  113. }
  114. // 处理尾帧图片(URL方式)
  115. elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) {
  116. $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true);
  117. if (!$tailFrame) {
  118. Utils::throwError('1003:尾帧图片获取失败');
  119. }
  120. }
  121. // 验证首帧图片规格(如果存在)
  122. if ($firstFrame) {
  123. $this->validateFrameSpecs($firstFrame, '首帧');
  124. }
  125. // 验证尾帧图片规格(如果存在)
  126. if ($tailFrame) {
  127. $this->validateFrameSpecs($tailFrame, '尾帧');
  128. }
  129. // 如果首尾帧都存在,验证比例是否相同
  130. if ($firstFrame && $tailFrame) {
  131. $firstRatio = round($firstFrame['width'] / $firstFrame['height'], 2);
  132. $tailRatio = round($tailFrame['width'] / $tailFrame['height'], 2);
  133. if ($firstRatio != $tailRatio) {
  134. Utils::throwError('1003:尾帧图片与首帧图片比例必须相同');
  135. }
  136. }
  137. // 余额预检:按预估时长计算所需积分,不足直接报错
  138. $chargeDuration = (int)getProp($data, 'video_duration', 5);
  139. if ($chargeDuration <= 0) {
  140. $chargeDuration = 5;
  141. }
  142. $this->pointsService->checkUserPointsEnough(
  143. $this->pointsService->getVideoChargePoints([
  144. 'model' => (string)getProp($data, 'model', ''),
  145. 'video_resolution' => strtolower((string)getProp($data, 'video_resolution', '720p')),
  146. 'video_duration' => $chargeDuration,
  147. 'ratio' => (string)getProp($data, 'ratio', '9:16'),
  148. 'generate_audio' => (int)getProp($data, 'generate_audio', 1),
  149. ])
  150. );
  151. $task = $this->aiVideoGenerationService->createJimengTask($data);
  152. return $this->success(['task_id' => $task->id, 'status' => $task->status]);
  153. }
  154. /**
  155. * 查询视频任务状态
  156. *
  157. * @param string $taskId
  158. * @return JsonResponse
  159. */
  160. public function taskStatus(string $taskId): JsonResponse
  161. {
  162. $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first();
  163. if (!$task) {
  164. return response()->json([
  165. 'success' => false,
  166. 'message' => '任务不存在'
  167. ], 404);
  168. }
  169. return response()->json([
  170. 'success' => true,
  171. 'data' => [
  172. 'task_id' => $task->id,
  173. 'status' => $task->status,
  174. 'result_url' => $task->result_url,
  175. 'error_message' => $task->error_message,
  176. 'created_at' => $task->created_at,
  177. 'started_at' => $task->started_at,
  178. 'completed_at' => $task->completed_at
  179. ]
  180. ]);
  181. }
  182. /**
  183. * 处理帧图片文件
  184. *
  185. * @param \Illuminate\Http\UploadedFile $file
  186. * @param string $frameType 帧类型(first/tail)
  187. * @return array
  188. */
  189. private function processFrameFile($file, string $frameType): array
  190. {
  191. // 验证图片信息
  192. $frameInfo = @getimagesize($file->getPathname());
  193. if (!$frameInfo) {
  194. Utils::throwError('1003:' . ($frameType === 'first' ? '首帧' : '尾帧') . '图片格式无效');
  195. }
  196. $frameData = [
  197. 'width' => $frameInfo[0],
  198. 'height' => $frameInfo[1],
  199. 'size' => $file->getSize(),
  200. ];
  201. // 上传图片到服务器
  202. try {
  203. $frameData['url'] = uploadFileByTos('image', $file);
  204. } catch (\Exception $e) {
  205. Utils::throwError('1003:' . ($frameType === 'first' ? '首帧' : '尾帧') . '图片上传失败:' . $e->getMessage());
  206. }
  207. return $frameData;
  208. }
  209. /**
  210. * 验证帧图片规格
  211. *
  212. * @param array $frame
  213. * @param string $frameName
  214. * @return void
  215. */
  216. private function validateFrameSpecs(array $frame, string $frameName): void
  217. {
  218. // 1.图片文件最大4.7MB
  219. if ($frame['size'] > 13 * 1024 * 1024) {
  220. Utils::throwError('1003:' . $frameName . '图片大小不能超过10MB');
  221. }
  222. // 2.图片分辨率最大: 4096*4096, 最短边不低于320
  223. $minDimension = min($frame['width'], $frame['height']);
  224. $maxDimension = max($frame['width'], $frame['height']);
  225. if ($maxDimension > 4096 || $minDimension < 320) {
  226. Utils::throwError('1003:' . $frameName . '图片分辨率不能超过4096*4096,且最短边不能低于320');
  227. }
  228. // 3.图片长边与短边比例在3以内
  229. if ($maxDimension / $minDimension > 3) {
  230. Utils::throwError('1003:' . $frameName . '图片长边与短边比例不能超过3');
  231. }
  232. }
  233. /**
  234. * 创建 Seedance 1.5 Pro 视频生成任务
  235. *
  236. * @param Request $request
  237. * @return JsonResponse
  238. */
  239. public function createSeedanceTask(Request $request): JsonResponse
  240. {
  241. $data = $request->all();
  242. // 验证基本参数
  243. $baseRules = [
  244. 'model' => 'required|string|in:doubao-seedance-1-5-pro-251215,doubao-seedance-1-0-pro-250528,doubao-seedance-1-0-pro-fast-251015',
  245. 'prompt' => 'required|max:1000',
  246. // 'content' => 'required|array|min:1',
  247. ];
  248. $baseMessages = [
  249. 'model.required' => '模型参数不能为空',
  250. 'model.in' => '模型只支持 doubao-seedance-1-5-pro-251215、doubao-seedance-1-0-pro-250528、doubao-seedance-1-0-pro-fast-251015',
  251. 'prompt.required' => '提示词不能为空',
  252. 'prompt.max' => '提示词不能超过1000个字符',
  253. // 'content.required' => '内容参数不能为空',
  254. // 'content.array' => '内容参数必须是数组',
  255. // 'content.min' => '内容参数至少包含一个元素',
  256. ];
  257. // 可选参数验证
  258. $optionalRules = [
  259. 'callback_url' => 'nullable|url',
  260. 'return_last_frame' => 'nullable|boolean',
  261. 'service_tier' => 'nullable|in:default,flex',
  262. 'execution_expires_after' => 'nullable|integer|min:3600|max:259200',
  263. 'generate_audio' => 'nullable|boolean',
  264. 'draft' => 'nullable|boolean',
  265. 'video_resolution' => 'required|in:480P,SR_720P,720P,1080P',
  266. 'ratio' => 'nullable|in:16:9,4:3,1:1,3:4,9:16,21:9,adaptive',
  267. 'video_duration' => 'required|integer|min:2|max:12',
  268. 'frames' => 'nullable|integer|min:29|max:289',
  269. 'seed' => 'nullable|integer|min:-1|max:4294967295',
  270. 'camera_fixed' => 'nullable|boolean',
  271. 'watermark' => 'nullable|boolean',
  272. ];
  273. $optionalMessages = [
  274. 'callback_url.url' => '回调地址格式不正确',
  275. 'service_tier.in' => '服务等级只能是 default 或 flex',
  276. 'execution_expires_after.integer' => '任务超时时间必须是整数',
  277. 'execution_expires_after.min' => '任务超时时间不能少于3600秒',
  278. 'execution_expires_after.max' => '任务超时时间不能超过259200秒',
  279. 'video_resolution.required' => '请选择视频分辨率',
  280. 'video_resolution.in' => '分辨率只能是 480P、SR_720P、720P 或 1080P',
  281. 'ratio.in' => '宽高比参数不正确',
  282. 'video_duration.required' => '请填写视频时长',
  283. 'video_duration.integer' => '视频时长必须是整数',
  284. 'video_duration.min' => '视频时长不能少于2秒',
  285. 'video_duration.max' => '视频时长不能超过12秒',
  286. 'frames.integer' => '帧数必须是整数',
  287. 'frames.min' => '帧数不能少于29',
  288. 'frames.max' => '帧数不能超过289',
  289. 'seed.integer' => '随机种子必须是整数',
  290. 'seed.min' => '随机种子不能小于-1',
  291. 'seed.max' => '随机种子不能超过4294967295',
  292. ];
  293. $validationRules = array_merge($baseRules, $optionalRules);
  294. $validationMessages = array_merge($baseMessages, $optionalMessages);
  295. // 检查是否有首帧URL
  296. if (isset($data['first_frame_url'])) {
  297. $optionalRules['first_frame_url'] = 'url';
  298. $optionalMessages['first_frame_url.url'] = '首帧图片URL格式不正确';
  299. }
  300. // 检查是否有尾帧URL
  301. if (isset($data['tail_frame_url'])) {
  302. $optionalRules['tail_frame_url'] = 'url';
  303. $optionalMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确';
  304. }
  305. // 检查是否有首帧文件
  306. if ($request->hasFile('first_frame_file')) {
  307. $optionalRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  308. $optionalMessages = array_merge($optionalMessages, [
  309. 'first_frame_file.image' => '首帧必须是图片文件',
  310. 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式',
  311. 'first_frame_file.max' => '首帧图片大小不能超过4.7MB',
  312. ]);
  313. }
  314. // 检查是否有尾帧文件
  315. if ($request->hasFile('tail_frame_file')) {
  316. $optionalRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  317. $optionalMessages = array_merge($optionalMessages, [
  318. 'tail_frame_file.image' => '尾帧必须是图片文件',
  319. 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式',
  320. 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB',
  321. ]);
  322. }
  323. if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) {
  324. Utils::throwError('1002:提供尾帧URL时必须提供首帧URL');
  325. }
  326. if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) {
  327. Utils::throwError('1002:提供尾帧文件时必须提供首帧文件');
  328. }
  329. $validator = Validator::make($data, $validationRules, $validationMessages);
  330. if ($validator->fails()) {
  331. Utils::throwError('1002:'.$validator->errors()->first());
  332. }
  333. $data['content'] = [
  334. [
  335. 'type' => 'text',
  336. 'text' => $data['prompt'],
  337. ],
  338. ];
  339. $firstFrame = null;
  340. $tailFrame = null;
  341. // 以下首尾帧做模型兼容:
  342. // doubao-seedance-1-0-pro-fast-251015只支持文生视频、图生视频-首帧
  343. // 处理首帧图片(文件上传方式)
  344. if ($request->hasFile('first_frame_file')) {
  345. $firstFrameFile = $request->file('first_frame_file');
  346. $firstFrame = $this->processFrameFile($firstFrameFile, 'first');
  347. $data['first_frame_url'] = $firstFrame['url'];
  348. $data['content'][] = [
  349. 'type' => 'image_url',
  350. 'image_url' => [
  351. 'url' => $data['first_frame_url'],
  352. ],
  353. 'role' => 'first_frame',
  354. ];
  355. }
  356. // 处理首帧图片(URL方式)
  357. elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) {
  358. $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true);
  359. if (!$firstFrame) {
  360. Utils::throwError('1003:首帧图片获取失败');
  361. }
  362. $data['content'][] = [
  363. 'type' => 'image_url',
  364. 'image_url' => [
  365. 'url' => $data['first_frame_url'],
  366. ],
  367. 'role' => 'first_frame',
  368. ];
  369. }
  370. // 处理尾帧图片(文件上传方式)
  371. if ($request->hasFile('tail_frame_file')) {
  372. $tailFrameFile = $request->file('tail_frame_file');
  373. $tailFrame = $this->processFrameFile($tailFrameFile, 'tail');
  374. if (!in_array($data['model'], ['doubao-seedance-1-0-pro-fast-251015'])) {
  375. $data['tail_frame_url'] = $tailFrame['url'];
  376. $data['content'][] = [
  377. 'type' => 'image_url',
  378. 'image_url' => [
  379. 'url' => $data['tail_frame_url'],
  380. ],
  381. 'role' => 'last_frame',
  382. ];
  383. }
  384. }
  385. // 处理尾帧图片(URL方式)
  386. elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) {
  387. $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true);
  388. if (!$tailFrame) {
  389. Utils::throwError('1003:尾帧图片获取失败');
  390. }
  391. if (!in_array($data['model'], ['doubao-seedance-1-0-pro-fast-251015'])) {
  392. $data['content'][] = [
  393. 'type' => 'image_url',
  394. 'image_url' => [
  395. 'url' => $data['tail_frame_url'],
  396. ],
  397. 'role' => 'last_frame',
  398. ];
  399. }else {
  400. $data['tail_frame_url'] = '';
  401. }
  402. }
  403. // 验证模型参数
  404. if (!in_array($data['model'], ['doubao-seedance-1-5-pro-251215'])) {
  405. $data['generate_audio'] = false;
  406. $data['draft'] = false;
  407. }
  408. if (in_array($data['model'], ['doubao-seedance-1-5-pro-251215'])) {
  409. if ($data['video_duration'] < 4) $data['video_duration'] = 4;
  410. if (!isset($data['ratio'])) $data['ratio'] = 'adaptive';
  411. }
  412. // 验证 content 数组内容
  413. $this->validateSeedanceContent($data['content']);
  414. // 余额预检:按预估时长计算所需积分,不足直接报错
  415. $chargeDuration = (int)getProp($data, 'video_duration', 5);
  416. if ($chargeDuration <= 0) {
  417. $chargeDuration = 5;
  418. }
  419. $this->pointsService->checkUserPointsEnough(
  420. $this->pointsService->getVideoChargePoints([
  421. 'model' => (string)getProp($data, 'model', ''),
  422. 'video_resolution' => strtolower((string)getProp($data, 'video_resolution', '720p')),
  423. 'video_duration' => $chargeDuration,
  424. 'ratio' => (string)getProp($data, 'ratio', '9:16'),
  425. 'generate_audio' => (int)getProp($data, 'generate_audio', 1),
  426. ])
  427. );
  428. $task = $this->aiVideoGenerationService->createSeedanceTask($data);
  429. return $this->success(['task_id' => $task->id, 'status' => $task->status]);
  430. }
  431. /**
  432. * 上传素材到快快AI素材库(单个或批量)
  433. * 参数:url 或 urls 二选一;type:image/video/audio(默认image);purpose:素材用途;wait:是否等待素材就绪(默认1)
  434. *
  435. * @param Request $request
  436. * @return JsonResponse
  437. */
  438. public function kuaikuaiAssetUpload(Request $request): JsonResponse
  439. {
  440. $data = $request->all();
  441. $url = getProp($data, 'url');
  442. $urls = getProp($data, 'urls', []);
  443. if (is_string($urls)) {
  444. $urls = json_decode($urls, true) ?: [];
  445. }
  446. if (!is_array($urls)) {
  447. Utils::throwError('20003:urls格式不正确');
  448. }
  449. $type = strtolower(getProp($data, 'type', 'image'));
  450. if (!in_array($type, ['image', 'video', 'audio'], true)) {
  451. Utils::throwError('1002:素材类型只能是image/video/audio');
  452. }
  453. $assetType = $type === 'video'
  454. ? \App\Models\MpAsset::TYPE_VIDEO
  455. : ($type === 'audio' ? \App\Models\MpAsset::TYPE_AUDIO : \App\Models\MpAsset::TYPE_IMAGE);
  456. $purpose = getProp($data, 'purpose', '');
  457. $wait = (int)getProp($data, 'wait', 1) === 1;
  458. if (!empty($urls)) {
  459. $result = $this->aiVideoGenerationService->createKuaikuaiAssetsBatch($urls, $assetType, $purpose, '素材批量上传');
  460. } elseif (!empty($url)) {
  461. $result = $this->aiVideoGenerationService->createKuaikuaiAsset($url, getProp($data, 'name', '素材上传'), $assetType, $purpose, $wait);
  462. } else {
  463. Utils::throwError('1002:url与urls必须二选一');
  464. }
  465. if ($result['code'] !== 0) {
  466. Utils::throwError('20003:' . $result['msg']);
  467. }
  468. return $this->success($result['data']);
  469. }
  470. /**
  471. * 查询快快AI素材是否可用于生成视频
  472. * 参数:asset_id
  473. *
  474. * @param Request $request
  475. * @return JsonResponse
  476. */
  477. public function kuaikuaiAssetStatus(Request $request): JsonResponse
  478. {
  479. $assetId = getProp($request->all(), 'asset_id');
  480. if (empty($assetId)) {
  481. Utils::throwError('1002:asset_id不能为空');
  482. }
  483. $result = $this->aiVideoGenerationService->queryKuaikuaiAssetUsable($assetId);
  484. if ($result['code'] !== 0) {
  485. Utils::throwError('20003:' . $result['msg']);
  486. }
  487. return $this->success($result['data']);
  488. }
  489. /**
  490. * 删除快快AI素材(同时清理本地记录)
  491. * 参数:asset_id
  492. *
  493. * @param Request $request
  494. * @return JsonResponse
  495. */
  496. public function kuaikuaiAssetDelete(Request $request): JsonResponse
  497. {
  498. $assetId = getProp($request->all(), 'asset_id');
  499. if (empty($assetId)) {
  500. Utils::throwError('1002:asset_id不能为空');
  501. }
  502. $result = $this->aiVideoGenerationService->deleteKuaikuaiAsset($assetId);
  503. if ($result['code'] !== 0) {
  504. Utils::throwError('20003:' . $result['msg']);
  505. }
  506. return $this->success($result['data']);
  507. }
  508. /**
  509. * 本地快快AI素材列表
  510. * 参数:status、keyword、page、page_size
  511. *
  512. * @param Request $request
  513. * @return JsonResponse
  514. */
  515. public function kuaikuaiAssetList(Request $request): JsonResponse
  516. {
  517. $data = $request->all();
  518. $query = DB::table('mp_assets')->where('source_type', 'kuaikuai');
  519. if (!empty($data['status'])) {
  520. $query->where('status', $data['status']);
  521. }
  522. if (!empty($data['keyword'])) {
  523. $query->where(function ($q) use ($data) {
  524. $q->where('asset_id', 'like', '%' . $data['keyword'] . '%')
  525. ->orWhere('name', 'like', '%' . $data['keyword'] . '%')
  526. ->orWhere('url', 'like', '%' . $data['keyword'] . '%');
  527. });
  528. }
  529. $list = $query->orderBy('id', 'desc')->paginate((int)getProp($data, 'page_size', 10));
  530. return $this->paginate($list);
  531. }
  532. public function seedanceCallback(Request $request) {
  533. $data = $request->all();
  534. $result = $this->aiVideoGenerationService->seedanceCallback($data);
  535. return $this->success(['success'=> $result ? 1 : 0]);
  536. }
  537. /**
  538. * 查询 Seedance 1.5 Pro 任务状态
  539. *
  540. * @param string $taskId
  541. * @return JsonResponse
  542. */
  543. public function getSeedanceTaskStatus(string $taskId): JsonResponse
  544. {
  545. try {
  546. $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first();
  547. if (!$task) {
  548. return response()->json([
  549. 'success' => false,
  550. 'message' => '任务不存在'
  551. ], 404);
  552. }
  553. $result = $this->aiVideoGenerationService->querySeedanceTaskStatus($task);
  554. return $this->success($result);
  555. } catch (\Exception $e) {
  556. Utils::throwError('1001:'.$e->getMessage());
  557. }
  558. }
  559. /**
  560. * 验证 Seedance content 内容
  561. *
  562. * @param array $content
  563. * @return void
  564. */
  565. private function validateSeedanceContent(array $content): void
  566. {
  567. $hasText = false;
  568. $hasFirstFrame = false;
  569. $hasLastFrame = false;
  570. $hasReferenceImage = false;
  571. $hasDraftTask = false;
  572. foreach ($content as $index => $item) {
  573. if (!isset($item['type'])) {
  574. Utils::throwError('1002:content[' . $index . '] 缺少 type 字段');
  575. }
  576. switch ($item['type']) {
  577. case 'text':
  578. if (!isset($item['text']) || empty($item['text'])) {
  579. Utils::throwError('1002:text 类型的 content 必须包含非空的 text 字段');
  580. }
  581. if (mb_strlen($item['text']) > 1000) {
  582. Utils::throwError('1002:文本提示词不能超过1000个字符');
  583. }
  584. $hasText = true;
  585. break;
  586. case 'image_url':
  587. if (!isset($item['image_url']['url']) || empty($item['image_url']['url'])) {
  588. Utils::throwError('1002:image_url 类型的 content 必须包含 image_url.url 字段');
  589. }
  590. $role = $item['role'] ?? null;
  591. if ($role === 'first_frame') {
  592. $hasFirstFrame = true;
  593. } elseif ($role === 'last_frame') {
  594. $hasLastFrame = true;
  595. } elseif ($role === 'reference_image') {
  596. $hasReferenceImage = true;
  597. } elseif (empty($role)) {
  598. // 默认为首帧
  599. $hasFirstFrame = true;
  600. }
  601. break;
  602. case 'draft_task':
  603. if (!isset($item['draft_task']['id']) || empty($item['draft_task']['id'])) {
  604. Utils::throwError('1002:draft_task 类型的 content 必须包含 draft_task.id 字段');
  605. }
  606. $hasDraftTask = true;
  607. break;
  608. default:
  609. Utils::throwError('1002:不支持的 content type: ' . $item['type']);
  610. }
  611. }
  612. // 验证组合规则
  613. if ($hasLastFrame && !$hasFirstFrame) {
  614. Utils::throwError('1002:使用尾帧时必须同时提供首帧');
  615. }
  616. if ($hasReferenceImage && ($hasFirstFrame || $hasLastFrame)) {
  617. Utils::throwError('1002:参考图模式不能与首帧/尾帧模式混用');
  618. }
  619. if ($hasDraftTask && ($hasText || $hasFirstFrame || $hasLastFrame || $hasReferenceImage)) {
  620. Utils::throwError('1002:使用样片任务时不能同时提供其他内容');
  621. }
  622. }
  623. /**
  624. * 创建可灵AI Omni视频生成任务
  625. *
  626. * @param Request $request
  627. * @return JsonResponse
  628. */
  629. public function createKelingTask(Request $request): JsonResponse
  630. {
  631. $data = $request->all();
  632. // 验证基本参数
  633. $baseRules = [
  634. 'model' => 'required|string|in:kling-video-o1,kling-v3-omni',
  635. 'prompt' => 'required|max:2500',
  636. 'mode' => 'nullable|in:pro,std',
  637. 'aspect_ratio' => 'nullable|in:16:9,4:3,1:1,3:4,9:16,21:9',
  638. 'video_duration' => 'required|integer|min:3|max:15',
  639. ];
  640. $baseMessages = [
  641. 'model.required' => '模型参数不能为空',
  642. 'model.in' => '模型只支持 kling-video-o1、kling-v3-omni',
  643. 'prompt.required' => '提示词不能为空',
  644. 'prompt.max' => '提示词不能超过2500个字符',
  645. 'mode.in' => '模式只能是 pro 或 std',
  646. 'aspect_ratio.in' => '宽高比只能是 16:9、4:3、1:1、3:4、9:16、21:9',
  647. 'video_duration.required' => '请填写视频时长',
  648. 'video_duration.integer' => '视频时长必须是整数',
  649. 'video_duration.min' => '视频时长不能少于3秒',
  650. 'video_duration.max' => '视频时长不能超过15秒',
  651. ];
  652. // 可选参数验证
  653. $optionalRules = [
  654. 'multi_prompt' => 'nullable|array',
  655. 'multi_prompt.*' => 'string|max:2500',
  656. 'shot_type' => 'nullable|in:customize,single_shot,multi_shot',
  657. 'generate_audio' => 'nullable|boolean',
  658. 'webhook_url' => 'nullable|url',
  659. 'seed' => 'nullable|integer|min:-1|max:4294967295',
  660. 'video_resolution' => 'nullable|in:720P,1080P',
  661. ];
  662. $optionalMessages = [
  663. 'multi_prompt.array' => '多段提示词必须是数组',
  664. 'multi_prompt.*.string' => '每段提示词必须是字符串',
  665. 'multi_prompt.*.max' => '每段提示词不能超过2500个字符',
  666. 'shot_type.in' => '镜头类型只能是 customize、single_shot、multi_shot',
  667. 'webhook_url.url' => '回调地址格式不正确',
  668. 'seed.integer' => '随机种子必须是整数',
  669. 'seed.min' => '随机种子不能小于-1',
  670. 'seed.max' => '随机种子不能超过4294967295',
  671. 'video_resolution.in' => '分辨率只能是 720P 或 1080P',
  672. ];
  673. // 检查是否有首帧URL
  674. if (isset($data['first_frame_url'])) {
  675. $optionalRules['first_frame_url'] = 'url';
  676. $optionalMessages['first_frame_url.url'] = '首帧图片URL格式不正确';
  677. }
  678. // 检查是否有尾帧URL
  679. if (isset($data['tail_frame_url'])) {
  680. $optionalRules['tail_frame_url'] = 'url';
  681. $optionalMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确';
  682. }
  683. // 检查是否有首帧文件
  684. if ($request->hasFile('first_frame_file')) {
  685. $optionalRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  686. $optionalMessages = array_merge($optionalMessages, [
  687. 'first_frame_file.image' => '首帧必须是图片文件',
  688. 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式',
  689. 'first_frame_file.max' => '首帧图片大小不能超过4.7MB',
  690. ]);
  691. }
  692. // 检查是否有尾帧文件
  693. if ($request->hasFile('tail_frame_file')) {
  694. $optionalRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  695. $optionalMessages = array_merge($optionalMessages, [
  696. 'tail_frame_file.image' => '尾帧必须是图片文件',
  697. 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式',
  698. 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB',
  699. ]);
  700. }
  701. if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) {
  702. Utils::throwError('1002:提供尾帧URL时必须提供首帧URL');
  703. }
  704. if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) {
  705. Utils::throwError('1002:提供尾帧文件时必须提供首帧文件');
  706. }
  707. $validationRules = array_merge($baseRules, $optionalRules);
  708. $validationMessages = array_merge($baseMessages, $optionalMessages);
  709. // 处理图片相关参数
  710. $validator = Validator::make($data, $validationRules, $validationMessages);
  711. if ($validator->fails()) {
  712. Utils::throwError('1002:'.$validator->errors()->first());
  713. }
  714. $firstFrame = null;
  715. $tailFrame = null;
  716. $data['image_list'] = [];
  717. // 处理首帧图片(文件上传方式)
  718. if ($request->hasFile('first_frame_file')) {
  719. $firstFrameFile = $request->file('first_frame_file');
  720. $firstFrame = $this->processFrameFile($firstFrameFile, 'first');
  721. $data['first_frame_url'] = $firstFrame['url'];
  722. $data['image_list'][] = [
  723. 'image_url' => $data['first_frame_url'],
  724. 'type' => 'first_frame',
  725. ];
  726. }
  727. // 处理首帧图片(URL方式)
  728. elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) {
  729. $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true);
  730. if (!$firstFrame) {
  731. Utils::throwError('1003:首帧图片获取失败');
  732. }
  733. $data['image_list'][] = [
  734. 'image_url' => $data['first_frame_url'],
  735. 'type' => 'first_frame',
  736. ];
  737. }
  738. // 处理尾帧图片(文件上传方式)
  739. if ($request->hasFile('tail_frame_file')) {
  740. $tailFrameFile = $request->file('tail_frame_file');
  741. $tailFrame = $this->processFrameFile($tailFrameFile, 'tail');
  742. $data['tail_frame_url'] = $tailFrame['url'];
  743. $data['image_list'][] = [
  744. 'image_url' => $data['tail_frame_url'],
  745. 'type' => 'end_frame',
  746. ];
  747. }
  748. // 处理尾帧图片(URL方式)
  749. elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) {
  750. $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true);
  751. if (!$tailFrame) {
  752. Utils::throwError('1003:尾帧图片获取失败');
  753. }
  754. $data['image_list'][] = [
  755. 'image_url' => $data['tail_frame_url'],
  756. 'type' => 'end_frame',
  757. ];
  758. }
  759. // 余额预检:按预估时长计算所需积分,不足直接报错
  760. $chargeDuration = (int)getProp($data, 'video_duration', 5);
  761. if ($chargeDuration <= 0) {
  762. $chargeDuration = 5;
  763. }
  764. $this->pointsService->checkUserPointsEnough(
  765. $this->pointsService->getVideoChargePoints([
  766. 'model' => (string)getProp($data, 'model', ''),
  767. 'video_resolution' => strtolower((string)getProp($data, 'video_resolution', '720p')),
  768. 'video_duration' => $chargeDuration,
  769. 'ratio' => (string)getProp($data, 'aspect_ratio', getProp($data, 'ratio', '9:16')),
  770. 'generate_audio' => (int)getProp($data, 'generate_audio', 1),
  771. ])
  772. );
  773. $task = $this->aiVideoGenerationService->createKelingOmniTask($data);
  774. return $this->success(['task_id' => $task->id, 'status' => $task->status]);
  775. }
  776. /**
  777. * 查询可灵AI Omni任务状态
  778. *
  779. * @param string $taskId
  780. * @return JsonResponse
  781. */
  782. public function getKelingTaskStatus(string $taskId): JsonResponse
  783. {
  784. try {
  785. $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first();
  786. if (!$task) {
  787. return response()->json([
  788. 'success' => false,
  789. 'message' => '任务不存在'
  790. ], 404);
  791. }
  792. $result = $this->aiVideoGenerationService->queryKelingOmniTaskStatus($task);
  793. return $this->success($result);
  794. } catch (\Exception $e) {
  795. Utils::throwError('1001:'.$e->getMessage());
  796. }
  797. }
  798. /**
  799. * 获取视频任务列表
  800. *
  801. * @param Request $request
  802. * @return mixed
  803. */
  804. public function taskList(Request $request)
  805. {
  806. $data = $request->all();
  807. $result = $this->aiVideoGenerationService->getTaskList($data);
  808. return $this->success($result, [new AIGenerationTransformer(), 'newBuildTaskList']);
  809. }
  810. // ==================== 主体管理相关接口 ====================
  811. /**
  812. * 创建主体
  813. *
  814. * @param Request $request
  815. * @return JsonResponse
  816. */
  817. public function createElement(Request $request): JsonResponse
  818. {
  819. try {
  820. // 验证请求参数
  821. $validated = $request->validate([
  822. 'element_name' => 'required|string|max:20',
  823. 'element_description' => 'required|string|max:100',
  824. 'reference_type' => 'required|in:image_refer,video_refer',
  825. 'element_image_list' => 'required_if:reference_type,image_refer|array',
  826. 'element_image_list.frontal_image' => 'required_if:reference_type,image_refer|string',
  827. 'element_image_list.refer_images' => 'required_if:reference_type,image_refer|array|min:1|max:3',
  828. 'element_image_list.refer_images.*.image_url' => 'required|string',
  829. 'element_video_list' => 'required_if:reference_type,video_refer|array',
  830. 'element_video_list.refer_videos' => 'required_if:reference_type,video_refer|array|size:1',
  831. 'element_video_list.refer_videos.*.video_url' => 'required|string',
  832. 'element_voice_id' => 'nullable|string',
  833. 'tag_list' => 'nullable|array',
  834. 'tag_list.*.tag_id' => 'required|in:o_101,o_102,o_103,o_104,o_105,o_106,o_107,o_108',
  835. 'callback_url' => 'nullable|url',
  836. 'external_task_id' => 'nullable|string',
  837. ]);
  838. // 调用服务层创建主体
  839. $element = $this->aiVideoGenerationService->createElement($validated);
  840. return response()->json([
  841. 'code' => 0,
  842. 'message' => '主体创建任务已提交',
  843. 'request_id' => uniqid(),
  844. 'data' => [
  845. 'task_id' => $element->id,
  846. 'task_info' => [
  847. 'external_task_id' => $element->external_task_id,
  848. ],
  849. 'task_status' => $element->task_status,
  850. 'created_at' => $element->created_at->timestamp * 1000,
  851. 'updated_at' => $element->updated_at->timestamp * 1000,
  852. ]
  853. ]);
  854. } catch (\Illuminate\Validation\ValidationException $e) {
  855. return response()->json([
  856. 'code' => 400,
  857. 'message' => '参数验证失败',
  858. 'request_id' => uniqid(),
  859. 'errors' => $e->errors()
  860. ], 400);
  861. } catch (\Exception $e) {
  862. dLog('generate')->error('创建主体失败: ' . $e->getMessage());
  863. return response()->json([
  864. 'code' => 500,
  865. 'message' => '创建主体失败: ' . $e->getMessage(),
  866. 'request_id' => uniqid(),
  867. ], 500);
  868. }
  869. }
  870. /**
  871. * 查询单个主体状态
  872. *
  873. * @param string $taskId
  874. * @param Request $request
  875. * @return JsonResponse
  876. */
  877. public function getElementStatus(string $taskId, Request $request): JsonResponse
  878. {
  879. try {
  880. $externalTaskId = $request->get('external_task_id');
  881. // 先查询本地数据库
  882. $query = AIElement::where('id', $taskId);
  883. if ($externalTaskId) {
  884. $query->orWhere('external_task_id', $externalTaskId);
  885. }
  886. $localElement = $query->first();
  887. if (!$localElement) {
  888. return response()->json([
  889. 'code' => 404,
  890. 'message' => '主体任务不存在',
  891. 'request_id' => uniqid(),
  892. ], 404);
  893. }
  894. // 如果任务还在处理中,查询最新状态
  895. if ($localElement->task_status === AIElement::STATUS_PROCESSING) {
  896. $apiResult = $this->aiVideoGenerationService->queryElementStatus($taskId, $externalTaskId);
  897. if ($apiResult['success'] && isset($apiResult['data'])) {
  898. $localElement->refresh(); // 重新加载数据
  899. }
  900. }
  901. // 构建响应数据
  902. $responseData = [
  903. 'task_id' => $localElement->id,
  904. 'task_status' => $localElement->task_status,
  905. 'task_status_msg' => $localElement->task_status_msg,
  906. 'task_info' => [
  907. 'external_task_id' => $localElement->external_task_id,
  908. ],
  909. 'created_at' => $localElement->created_at->timestamp * 1000,
  910. 'updated_at' => $localElement->updated_at->timestamp * 1000,
  911. ];
  912. // 如果任务成功,添加主体结果信息
  913. if ($localElement->task_status === AIElement::STATUS_SUCCEED && $localElement->element_id) {
  914. $responseData['task_result'] = [
  915. 'elements' => [
  916. [
  917. 'element_id' => $localElement->element_id,
  918. 'element_name' => $localElement->element_name,
  919. 'element_description' => $localElement->element_description,
  920. 'reference_type' => $localElement->reference_type,
  921. 'element_image_list' => $localElement->element_image_list,
  922. 'element_video_list' => $localElement->element_video_list,
  923. 'element_voice_info' => $localElement->element_voice_info,
  924. 'tag_list' => $localElement->tag_list,
  925. 'owned_by' => $localElement->owned_by,
  926. 'status' => $localElement->status,
  927. ]
  928. ]
  929. ];
  930. $responseData['final_unit_deduction'] = $localElement->final_unit_deduction;
  931. }
  932. return response()->json([
  933. 'code' => 0,
  934. 'message' => 'success',
  935. 'request_id' => uniqid(),
  936. 'data' => $responseData
  937. ]);
  938. } catch (\Exception $e) {
  939. dLog('generate')->error('查询主体状态失败: ' . $e->getMessage());
  940. return response()->json([
  941. 'code' => 500,
  942. 'message' => '查询主体状态失败: ' . $e->getMessage(),
  943. 'request_id' => uniqid(),
  944. ], 500);
  945. }
  946. }
  947. /**
  948. * 查询所有主体列表(包含自定义和官方)
  949. *
  950. * @param Request $request
  951. * @return JsonResponse
  952. */
  953. public function getElementsList(Request $request): JsonResponse
  954. {
  955. try {
  956. $pageNum = (int)$request->get('pageNum', 1);
  957. $pageSize = (int)$request->get('pageSize', 30);
  958. $type = $request->get('type', 'all'); // all, custom, official
  959. // 限制分页参数范围
  960. $pageNum = max(1, min($pageNum, 1000));
  961. $pageSize = max(1, min($pageSize, 500));
  962. $customElements = [];
  963. $officialElements = [];
  964. // 查询自定义主体
  965. if (in_array($type, ['all', 'custom'])) {
  966. $customResult = $this->aiVideoGenerationService->queryCustomElementsList($pageNum, $pageSize);
  967. if ($customResult['success']) {
  968. $customElements = $customResult['data'];
  969. }
  970. }
  971. // 查询官方主体
  972. if (in_array($type, ['all', 'official'])) {
  973. $officialResult = $this->aiVideoGenerationService->queryOfficialElementsList($pageNum, $pageSize);
  974. if ($officialResult['success']) {
  975. $officialElements = $officialResult['data'];
  976. }
  977. }
  978. // 合并结果
  979. $allElements = array_merge($customElements, $officialElements);
  980. return response()->json([
  981. 'code' => 0,
  982. 'message' => 'success',
  983. 'request_id' => uniqid(),
  984. 'data' => $allElements
  985. ]);
  986. } catch (\Exception $e) {
  987. dLog('generate')->error('查询主体列表失败: ' . $e->getMessage());
  988. return response()->json([
  989. 'code' => 500,
  990. 'message' => '查询主体列表失败: ' . $e->getMessage(),
  991. 'request_id' => uniqid(),
  992. ], 500);
  993. }
  994. }
  995. /**
  996. * 删除自定义主体
  997. *
  998. * @param Request $request
  999. * @return JsonResponse
  1000. */
  1001. public function deleteElement(Request $request): JsonResponse
  1002. {
  1003. try {
  1004. // 验证请求参数
  1005. $validated = $request->validate([
  1006. 'element_id' => 'required|string',
  1007. ]);
  1008. // 检查主体是否存在且为自定义主体
  1009. $element = AIElement::where('element_id', $validated['element_id'])->first();
  1010. if (!$element) {
  1011. return response()->json([
  1012. 'code' => 404,
  1013. 'message' => '主体不存在',
  1014. 'request_id' => uniqid(),
  1015. ], 404);
  1016. }
  1017. if ($element->owned_by === 'kling') {
  1018. return response()->json([
  1019. 'code' => 400,
  1020. 'message' => '不能删除官方主体',
  1021. 'request_id' => uniqid(),
  1022. ], 400);
  1023. }
  1024. // 调用服务层删除主体
  1025. $result = $this->aiVideoGenerationService->deleteElement($validated['element_id']);
  1026. if ($result['success']) {
  1027. return response()->json([
  1028. 'code' => 0,
  1029. 'message' => $result['message'],
  1030. 'request_id' => uniqid(),
  1031. 'data' => $result['data']
  1032. ]);
  1033. } else {
  1034. return response()->json([
  1035. 'code' => 500,
  1036. 'message' => $result['message'],
  1037. 'request_id' => uniqid(),
  1038. ], 500);
  1039. }
  1040. } catch (\Illuminate\Validation\ValidationException $e) {
  1041. return response()->json([
  1042. 'code' => 400,
  1043. 'message' => '参数验证失败',
  1044. 'request_id' => uniqid(),
  1045. 'errors' => $e->errors()
  1046. ], 400);
  1047. } catch (\Exception $e) {
  1048. dLog('generate')->error('删除主体失败: ' . $e->getMessage());
  1049. return response()->json([
  1050. 'code' => 500,
  1051. 'message' => '删除主体失败: ' . $e->getMessage(),
  1052. 'request_id' => uniqid(),
  1053. ], 500);
  1054. }
  1055. }
  1056. /**
  1057. * 获取本地主体列表(用于内部管理)
  1058. *
  1059. * @param Request $request
  1060. * @return JsonResponse
  1061. */
  1062. public function getLocalElementsList(Request $request): JsonResponse
  1063. {
  1064. try {
  1065. // 构建筛选条件
  1066. $filters = [
  1067. 'owned_by' => $request->get('owned_by'),
  1068. 'reference_type' => $request->get('reference_type'),
  1069. 'status' => $request->get('status'),
  1070. 'task_status' => $request->get('task_status'),
  1071. 'keyword' => $request->get('keyword'),
  1072. 'page_size' => (int)$request->get('page_size', 20),
  1073. ];
  1074. // 移除空值
  1075. $filters = array_filter($filters, function($value) {
  1076. return $value !== null && $value !== '';
  1077. });
  1078. // 调用服务层获取列表
  1079. $elements = $this->aiVideoGenerationService->getLocalElementsList($filters);
  1080. // 转换数据格式
  1081. $data = $elements->map(function ($element) {
  1082. return [
  1083. 'task_id' => $element->id,
  1084. 'external_task_id' => $element->external_task_id,
  1085. 'task_status' => $element->task_status,
  1086. 'task_status_msg' => $element->task_status_msg,
  1087. 'element_id' => $element->element_id,
  1088. 'element_name' => $element->element_name,
  1089. 'element_description' => $element->element_description,
  1090. 'reference_type' => $element->reference_type,
  1091. 'element_image_list' => $element->element_image_list,
  1092. 'element_video_list' => $element->element_video_list,
  1093. 'element_voice_id' => $element->element_voice_id,
  1094. 'element_voice_info' => $element->element_voice_info,
  1095. 'tag_list' => $element->tag_list,
  1096. 'tag_names' => $element->tag_names, // 使用模型的访问器
  1097. 'owned_by' => $element->owned_by,
  1098. 'status' => $element->status,
  1099. 'final_unit_deduction' => $element->final_unit_deduction,
  1100. 'created_at' => $element->created_at->format('Y-m-d H:i:s'),
  1101. 'updated_at' => $element->updated_at->format('Y-m-d H:i:s'),
  1102. ];
  1103. });
  1104. return response()->json([
  1105. 'code' => 0,
  1106. 'message' => 'success',
  1107. 'request_id' => uniqid(),
  1108. 'data' => [
  1109. 'data' => $data,
  1110. 'current_page' => $elements->currentPage(),
  1111. 'last_page' => $elements->lastPage(),
  1112. 'per_page' => $elements->perPage(),
  1113. 'total' => $elements->total(),
  1114. ]
  1115. ]);
  1116. } catch (\Exception $e) {
  1117. dLog('generate')->error('获取本地主体列表失败: ' . $e->getMessage());
  1118. return response()->json([
  1119. 'code' => 500,
  1120. 'message' => '获取本地主体列表失败: ' . $e->getMessage(),
  1121. 'request_id' => uniqid(),
  1122. ], 500);
  1123. }
  1124. }
  1125. }