| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267 |
- <?php
- namespace App\Http\Controllers\AIGeneration;
- use Illuminate\Routing\Controller as BaseController;
- use App\Libs\Utils;
- use App\Services\AIGeneration\AIVideoGenerationService;
- use App\Services\PointsService;
- use App\Transformer\AIGeneration\AIGenerationTransformer;
- use Illuminate\Http\Request;
- use Illuminate\Http\JsonResponse;
- use Illuminate\Support\Facades\Validator;
- use Illuminate\Support\Facades\DB;
- use App\Libs\ApiResponse;
- use App\Models\AIElement;
- class VideoGenerationController extends BaseController
- {
- use ApiResponse;
- private $aiVideoGenerationService;
- private $pointsService;
- public function __construct(
- AIVideoGenerationService $aiVideoGenerationService,
- PointsService $pointsService
- ) {
- $this->aiVideoGenerationService = $aiVideoGenerationService;
- $this->pointsService = $pointsService;
- }
- /**
- * 创建视频生成任务(即梦AI)
- *
- * @param Request $request
- * @return JsonResponse
- */
- public function createJimengTask(Request $request): JsonResponse
- {
- $data = $request->all();
-
- // 首先验证基本参数
- $baseRules = [
- 'prompt' => 'required|string|max:1000',
- 'seed' => 'required|integer',
- 'video_duration' => 'required|in:5,10',
- 'video_resolution' => 'required|in:720P,1080P'
- ];
-
- $baseMessages = [
- 'prompt.required' => '提示词不能为空',
- 'prompt.max' => '提示词不能超过1000个字符',
- 'seed.required' => '随机种子不能为空',
- 'seed.integer' => '随机种子必须是整数',
- 'video_duration.required' => '请选择视频时长',
- 'video_duration.in' => '视频时长只能是5s或10s',
- 'video_resolution.required' => '请选择视频分辨率',
- 'video_resolution.in' => '视频分辨率只能是720P或1080P',
- ];
-
- // 动态构建验证规则
- $validationRules = $baseRules;
- $validationMessages = $baseMessages;
-
- // 检查是否有首帧URL
- if (isset($data['first_frame_url'])) {
- $validationRules['first_frame_url'] = 'url';
- $validationMessages['first_frame_url.url'] = '首帧图片URL格式不正确';
- }
-
- // 检查是否有尾帧URL
- if (isset($data['tail_frame_url'])) {
- $validationRules['tail_frame_url'] = 'url';
- $validationMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确';
- }
-
- // 检查是否有首帧文件
- if ($request->hasFile('first_frame_file')) {
- $validationRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800';
- $validationMessages = array_merge($validationMessages, [
- 'first_frame_file.image' => '首帧必须是图片文件',
- 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式',
- 'first_frame_file.max' => '首帧图片大小不能超过4.7MB',
- ]);
- }
-
- // 检查是否有尾帧文件
- if ($request->hasFile('tail_frame_file')) {
- $validationRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800';
- $validationMessages = array_merge($validationMessages, [
- 'tail_frame_file.image' => '尾帧必须是图片文件',
- 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式',
- 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB',
- ]);
- }
- if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) {
- Utils::throwError('1002:提供尾帧URL时必须提供首帧URL');
- }
- if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) {
- Utils::throwError('1002:提供尾帧文件时必须提供首帧文件');
- }
-
- $validator = Validator::make($data, $validationRules, $validationMessages);
-
- if ($validator->fails()) {
- Utils::throwError('1002:'.$validator->errors()->first());
- }
- $firstFrame = null;
- $tailFrame = null;
- // 处理首帧图片(文件上传方式)
- if ($request->hasFile('first_frame_file')) {
- $firstFrameFile = $request->file('first_frame_file');
- $firstFrame = $this->processFrameFile($firstFrameFile, 'first');
- $data['first_frame_url'] = $firstFrame['url'];
- }
- // 处理首帧图片(URL方式)
- elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) {
- $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true);
- if (!$firstFrame) {
- Utils::throwError('1003:首帧图片获取失败');
- }
- }
- // 处理尾帧图片(文件上传方式)
- if ($request->hasFile('tail_frame_file')) {
- $tailFrameFile = $request->file('tail_frame_file');
- $tailFrame = $this->processFrameFile($tailFrameFile, 'tail');
- $data['tail_frame_url'] = $tailFrame['url'];
- }
- // 处理尾帧图片(URL方式)
- elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) {
- $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true);
- if (!$tailFrame) {
- Utils::throwError('1003:尾帧图片获取失败');
- }
- }
-
- // 验证首帧图片规格(如果存在)
- if ($firstFrame) {
- $this->validateFrameSpecs($firstFrame, '首帧');
- }
-
- // 验证尾帧图片规格(如果存在)
- if ($tailFrame) {
- $this->validateFrameSpecs($tailFrame, '尾帧');
- }
-
- // 如果首尾帧都存在,验证比例是否相同
- if ($firstFrame && $tailFrame) {
- $firstRatio = round($firstFrame['width'] / $firstFrame['height'], 2);
- $tailRatio = round($tailFrame['width'] / $tailFrame['height'], 2);
-
- if ($firstRatio != $tailRatio) {
- Utils::throwError('1003:尾帧图片与首帧图片比例必须相同');
- }
- }
- // 余额预检:按预估时长计算所需积分,不足直接报错
- $chargeDuration = (int)getProp($data, 'video_duration', 5);
- if ($chargeDuration <= 0) {
- $chargeDuration = 5;
- }
- $this->pointsService->checkUserPointsEnough(
- $this->pointsService->getVideoChargePoints([
- 'model' => (string)getProp($data, 'model', ''),
- 'video_resolution' => strtolower((string)getProp($data, 'video_resolution', '720p')),
- 'video_duration' => $chargeDuration,
- 'ratio' => (string)getProp($data, 'ratio', '9:16'),
- 'generate_audio' => (int)getProp($data, 'generate_audio', 1),
- ])
- );
- $task = $this->aiVideoGenerationService->createJimengTask($data);
- return $this->success(['task_id' => $task->id, 'status' => $task->status]);
- }
- /**
- * 查询视频任务状态
- *
- * @param string $taskId
- * @return JsonResponse
- */
- public function taskStatus(string $taskId): JsonResponse
- {
- $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first();
- if (!$task) {
- return response()->json([
- 'success' => false,
- 'message' => '任务不存在'
- ], 404);
- }
- return response()->json([
- 'success' => true,
- 'data' => [
- 'task_id' => $task->id,
- 'status' => $task->status,
- 'result_url' => $task->result_url,
- 'error_message' => $task->error_message,
- 'created_at' => $task->created_at,
- 'started_at' => $task->started_at,
- 'completed_at' => $task->completed_at
- ]
- ]);
- }
- /**
- * 处理帧图片文件
- *
- * @param \Illuminate\Http\UploadedFile $file
- * @param string $frameType 帧类型(first/tail)
- * @return array
- */
- private function processFrameFile($file, string $frameType): array
- {
- // 验证图片信息
- $frameInfo = @getimagesize($file->getPathname());
-
- if (!$frameInfo) {
- Utils::throwError('1003:' . ($frameType === 'first' ? '首帧' : '尾帧') . '图片格式无效');
- }
-
- $frameData = [
- 'width' => $frameInfo[0],
- 'height' => $frameInfo[1],
- 'size' => $file->getSize(),
- ];
-
- // 上传图片到服务器
- try {
- $frameData['url'] = uploadFileByTos('image', $file);
- } catch (\Exception $e) {
- Utils::throwError('1003:' . ($frameType === 'first' ? '首帧' : '尾帧') . '图片上传失败:' . $e->getMessage());
- }
-
- return $frameData;
- }
- /**
- * 验证帧图片规格
- *
- * @param array $frame
- * @param string $frameName
- * @return void
- */
- private function validateFrameSpecs(array $frame, string $frameName): void
- {
- // 1.图片文件最大4.7MB
- if ($frame['size'] > 13 * 1024 * 1024) {
- Utils::throwError('1003:' . $frameName . '图片大小不能超过10MB');
- }
-
- // 2.图片分辨率最大: 4096*4096, 最短边不低于320
- $minDimension = min($frame['width'], $frame['height']);
- $maxDimension = max($frame['width'], $frame['height']);
-
- if ($maxDimension > 4096 || $minDimension < 320) {
- Utils::throwError('1003:' . $frameName . '图片分辨率不能超过4096*4096,且最短边不能低于320');
- }
-
- // 3.图片长边与短边比例在3以内
- if ($maxDimension / $minDimension > 3) {
- Utils::throwError('1003:' . $frameName . '图片长边与短边比例不能超过3');
- }
- }
- /**
- * 创建 Seedance 1.5 Pro 视频生成任务
- *
- * @param Request $request
- * @return JsonResponse
- */
- public function createSeedanceTask(Request $request): JsonResponse
- {
- $data = $request->all();
-
- // 验证基本参数
- $baseRules = [
- 'model' => 'required|string|in:doubao-seedance-1-5-pro-251215,doubao-seedance-1-0-pro-250528,doubao-seedance-1-0-pro-fast-251015',
- 'prompt' => 'required|max:1000',
- // 'content' => 'required|array|min:1',
- ];
-
- $baseMessages = [
- 'model.required' => '模型参数不能为空',
- 'model.in' => '模型只支持 doubao-seedance-1-5-pro-251215、doubao-seedance-1-0-pro-250528、doubao-seedance-1-0-pro-fast-251015',
- 'prompt.required' => '提示词不能为空',
- 'prompt.max' => '提示词不能超过1000个字符',
- // 'content.required' => '内容参数不能为空',
- // 'content.array' => '内容参数必须是数组',
- // 'content.min' => '内容参数至少包含一个元素',
- ];
-
- // 可选参数验证
- $optionalRules = [
- 'callback_url' => 'nullable|url',
- 'return_last_frame' => 'nullable|boolean',
- 'service_tier' => 'nullable|in:default,flex',
- 'execution_expires_after' => 'nullable|integer|min:3600|max:259200',
- 'generate_audio' => 'nullable|boolean',
- 'draft' => 'nullable|boolean',
- 'video_resolution' => 'required|in:480P,SR_720P,720P,1080P',
- 'ratio' => 'nullable|in:16:9,4:3,1:1,3:4,9:16,21:9,adaptive',
- 'video_duration' => 'required|integer|min:2|max:12',
- 'frames' => 'nullable|integer|min:29|max:289',
- 'seed' => 'nullable|integer|min:-1|max:4294967295',
- 'camera_fixed' => 'nullable|boolean',
- 'watermark' => 'nullable|boolean',
- ];
-
- $optionalMessages = [
- 'callback_url.url' => '回调地址格式不正确',
- 'service_tier.in' => '服务等级只能是 default 或 flex',
- 'execution_expires_after.integer' => '任务超时时间必须是整数',
- 'execution_expires_after.min' => '任务超时时间不能少于3600秒',
- 'execution_expires_after.max' => '任务超时时间不能超过259200秒',
- 'video_resolution.required' => '请选择视频分辨率',
- 'video_resolution.in' => '分辨率只能是 480P、SR_720P、720P 或 1080P',
- 'ratio.in' => '宽高比参数不正确',
- 'video_duration.required' => '请填写视频时长',
- 'video_duration.integer' => '视频时长必须是整数',
- 'video_duration.min' => '视频时长不能少于2秒',
- 'video_duration.max' => '视频时长不能超过12秒',
- 'frames.integer' => '帧数必须是整数',
- 'frames.min' => '帧数不能少于29',
- 'frames.max' => '帧数不能超过289',
- 'seed.integer' => '随机种子必须是整数',
- 'seed.min' => '随机种子不能小于-1',
- 'seed.max' => '随机种子不能超过4294967295',
- ];
- $validationRules = array_merge($baseRules, $optionalRules);
- $validationMessages = array_merge($baseMessages, $optionalMessages);
- // 检查是否有首帧URL
- if (isset($data['first_frame_url'])) {
- $optionalRules['first_frame_url'] = 'url';
- $optionalMessages['first_frame_url.url'] = '首帧图片URL格式不正确';
- }
-
- // 检查是否有尾帧URL
- if (isset($data['tail_frame_url'])) {
- $optionalRules['tail_frame_url'] = 'url';
- $optionalMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确';
- }
-
- // 检查是否有首帧文件
- if ($request->hasFile('first_frame_file')) {
- $optionalRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800';
- $optionalMessages = array_merge($optionalMessages, [
- 'first_frame_file.image' => '首帧必须是图片文件',
- 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式',
- 'first_frame_file.max' => '首帧图片大小不能超过4.7MB',
- ]);
- }
-
- // 检查是否有尾帧文件
- if ($request->hasFile('tail_frame_file')) {
- $optionalRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800';
- $optionalMessages = array_merge($optionalMessages, [
- 'tail_frame_file.image' => '尾帧必须是图片文件',
- 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式',
- 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB',
- ]);
- }
- if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) {
- Utils::throwError('1002:提供尾帧URL时必须提供首帧URL');
- }
- if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) {
- Utils::throwError('1002:提供尾帧文件时必须提供首帧文件');
- }
-
- $validator = Validator::make($data, $validationRules, $validationMessages);
-
- if ($validator->fails()) {
- Utils::throwError('1002:'.$validator->errors()->first());
- }
- $data['content'] = [
- [
- 'type' => 'text',
- 'text' => $data['prompt'],
- ],
- ];
- $firstFrame = null;
- $tailFrame = null;
- // 以下首尾帧做模型兼容:
- // doubao-seedance-1-0-pro-fast-251015只支持文生视频、图生视频-首帧
- // 处理首帧图片(文件上传方式)
- if ($request->hasFile('first_frame_file')) {
- $firstFrameFile = $request->file('first_frame_file');
- $firstFrame = $this->processFrameFile($firstFrameFile, 'first');
- $data['first_frame_url'] = $firstFrame['url'];
- $data['content'][] = [
- 'type' => 'image_url',
- 'image_url' => [
- 'url' => $data['first_frame_url'],
- ],
- 'role' => 'first_frame',
- ];
- }
- // 处理首帧图片(URL方式)
- elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) {
- $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true);
- if (!$firstFrame) {
- Utils::throwError('1003:首帧图片获取失败');
- }
- $data['content'][] = [
- 'type' => 'image_url',
- 'image_url' => [
- 'url' => $data['first_frame_url'],
- ],
- 'role' => 'first_frame',
- ];
- }
- // 处理尾帧图片(文件上传方式)
- if ($request->hasFile('tail_frame_file')) {
- $tailFrameFile = $request->file('tail_frame_file');
- $tailFrame = $this->processFrameFile($tailFrameFile, 'tail');
- if (!in_array($data['model'], ['doubao-seedance-1-0-pro-fast-251015'])) {
- $data['tail_frame_url'] = $tailFrame['url'];
- $data['content'][] = [
- 'type' => 'image_url',
- 'image_url' => [
- 'url' => $data['tail_frame_url'],
- ],
- 'role' => 'last_frame',
- ];
- }
- }
- // 处理尾帧图片(URL方式)
- elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) {
- $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true);
- if (!$tailFrame) {
- Utils::throwError('1003:尾帧图片获取失败');
- }
-
- if (!in_array($data['model'], ['doubao-seedance-1-0-pro-fast-251015'])) {
- $data['content'][] = [
- 'type' => 'image_url',
- 'image_url' => [
- 'url' => $data['tail_frame_url'],
- ],
- 'role' => 'last_frame',
- ];
- }else {
- $data['tail_frame_url'] = '';
- }
- }
-
- // 验证模型参数
- if (!in_array($data['model'], ['doubao-seedance-1-5-pro-251215'])) {
- $data['generate_audio'] = false;
- $data['draft'] = false;
- }
- if (in_array($data['model'], ['doubao-seedance-1-5-pro-251215'])) {
- if ($data['video_duration'] < 4) $data['video_duration'] = 4;
- if (!isset($data['ratio'])) $data['ratio'] = 'adaptive';
- }
- // 验证 content 数组内容
- $this->validateSeedanceContent($data['content']);
-
- // 余额预检:按预估时长计算所需积分,不足直接报错
- $chargeDuration = (int)getProp($data, 'video_duration', 5);
- if ($chargeDuration <= 0) {
- $chargeDuration = 5;
- }
- $this->pointsService->checkUserPointsEnough(
- $this->pointsService->getVideoChargePoints([
- 'model' => (string)getProp($data, 'model', ''),
- 'video_resolution' => strtolower((string)getProp($data, 'video_resolution', '720p')),
- 'video_duration' => $chargeDuration,
- 'ratio' => (string)getProp($data, 'ratio', '9:16'),
- 'generate_audio' => (int)getProp($data, 'generate_audio', 1),
- ])
- );
- $task = $this->aiVideoGenerationService->createSeedanceTask($data);
- return $this->success(['task_id' => $task->id, 'status' => $task->status]);
- }
- /**
- * 上传素材到快快AI素材库(单个或批量)
- * 参数:url 或 urls 二选一;type:image/video/audio(默认image);purpose:素材用途;wait:是否等待素材就绪(默认1)
- *
- * @param Request $request
- * @return JsonResponse
- */
- public function kuaikuaiAssetUpload(Request $request): JsonResponse
- {
- $data = $request->all();
- $url = getProp($data, 'url');
- $urls = getProp($data, 'urls', []);
- if (is_string($urls)) {
- $urls = json_decode($urls, true) ?: [];
- }
- if (!is_array($urls)) {
- Utils::throwError('20003:urls格式不正确');
- }
- $type = strtolower(getProp($data, 'type', 'image'));
- if (!in_array($type, ['image', 'video', 'audio'], true)) {
- Utils::throwError('1002:素材类型只能是image/video/audio');
- }
- $assetType = $type === 'video'
- ? \App\Models\MpAsset::TYPE_VIDEO
- : ($type === 'audio' ? \App\Models\MpAsset::TYPE_AUDIO : \App\Models\MpAsset::TYPE_IMAGE);
- $purpose = getProp($data, 'purpose', '');
- $wait = (int)getProp($data, 'wait', 1) === 1;
- if (!empty($urls)) {
- $result = $this->aiVideoGenerationService->createKuaikuaiAssetsBatch($urls, $assetType, $purpose, '素材批量上传');
- } elseif (!empty($url)) {
- $result = $this->aiVideoGenerationService->createKuaikuaiAsset($url, getProp($data, 'name', '素材上传'), $assetType, $purpose, $wait);
- } else {
- Utils::throwError('1002:url与urls必须二选一');
- }
- if ($result['code'] !== 0) {
- Utils::throwError('20003:' . $result['msg']);
- }
- return $this->success($result['data']);
- }
- /**
- * 查询快快AI素材是否可用于生成视频
- * 参数:asset_id
- *
- * @param Request $request
- * @return JsonResponse
- */
- public function kuaikuaiAssetStatus(Request $request): JsonResponse
- {
- $assetId = getProp($request->all(), 'asset_id');
- if (empty($assetId)) {
- Utils::throwError('1002:asset_id不能为空');
- }
- $result = $this->aiVideoGenerationService->queryKuaikuaiAssetUsable($assetId);
- if ($result['code'] !== 0) {
- Utils::throwError('20003:' . $result['msg']);
- }
- return $this->success($result['data']);
- }
- /**
- * 删除快快AI素材(同时清理本地记录)
- * 参数:asset_id
- *
- * @param Request $request
- * @return JsonResponse
- */
- public function kuaikuaiAssetDelete(Request $request): JsonResponse
- {
- $assetId = getProp($request->all(), 'asset_id');
- if (empty($assetId)) {
- Utils::throwError('1002:asset_id不能为空');
- }
- $result = $this->aiVideoGenerationService->deleteKuaikuaiAsset($assetId);
- if ($result['code'] !== 0) {
- Utils::throwError('20003:' . $result['msg']);
- }
- return $this->success($result['data']);
- }
- /**
- * 本地快快AI素材列表
- * 参数:status、keyword、page、page_size
- *
- * @param Request $request
- * @return JsonResponse
- */
- public function kuaikuaiAssetList(Request $request): JsonResponse
- {
- $data = $request->all();
- $query = DB::table('mp_assets')->where('source_type', 'kuaikuai');
- if (!empty($data['status'])) {
- $query->where('status', $data['status']);
- }
- if (!empty($data['keyword'])) {
- $query->where(function ($q) use ($data) {
- $q->where('asset_id', 'like', '%' . $data['keyword'] . '%')
- ->orWhere('name', 'like', '%' . $data['keyword'] . '%')
- ->orWhere('url', 'like', '%' . $data['keyword'] . '%');
- });
- }
- $list = $query->orderBy('id', 'desc')->paginate((int)getProp($data, 'page_size', 10));
- return $this->paginate($list);
- }
- public function seedanceCallback(Request $request) {
- $data = $request->all();
- $result = $this->aiVideoGenerationService->seedanceCallback($data);
- return $this->success(['success'=> $result ? 1 : 0]);
- }
- /**
- * 查询 Seedance 1.5 Pro 任务状态
- *
- * @param string $taskId
- * @return JsonResponse
- */
- public function getSeedanceTaskStatus(string $taskId): JsonResponse
- {
- try {
- $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first();
- if (!$task) {
- return response()->json([
- 'success' => false,
- 'message' => '任务不存在'
- ], 404);
- }
- $result = $this->aiVideoGenerationService->querySeedanceTaskStatus($task);
- return $this->success($result);
- } catch (\Exception $e) {
- Utils::throwError('1001:'.$e->getMessage());
- }
- }
- /**
- * 验证 Seedance content 内容
- *
- * @param array $content
- * @return void
- */
- private function validateSeedanceContent(array $content): void
- {
- $hasText = false;
- $hasFirstFrame = false;
- $hasLastFrame = false;
- $hasReferenceImage = false;
- $hasDraftTask = false;
-
- foreach ($content as $index => $item) {
- if (!isset($item['type'])) {
- Utils::throwError('1002:content[' . $index . '] 缺少 type 字段');
- }
-
- switch ($item['type']) {
- case 'text':
- if (!isset($item['text']) || empty($item['text'])) {
- Utils::throwError('1002:text 类型的 content 必须包含非空的 text 字段');
- }
- if (mb_strlen($item['text']) > 1000) {
- Utils::throwError('1002:文本提示词不能超过1000个字符');
- }
- $hasText = true;
- break;
-
- case 'image_url':
- if (!isset($item['image_url']['url']) || empty($item['image_url']['url'])) {
- Utils::throwError('1002:image_url 类型的 content 必须包含 image_url.url 字段');
- }
-
- $role = $item['role'] ?? null;
- if ($role === 'first_frame') {
- $hasFirstFrame = true;
- } elseif ($role === 'last_frame') {
- $hasLastFrame = true;
- } elseif ($role === 'reference_image') {
- $hasReferenceImage = true;
- } elseif (empty($role)) {
- // 默认为首帧
- $hasFirstFrame = true;
- }
- break;
-
- case 'draft_task':
- if (!isset($item['draft_task']['id']) || empty($item['draft_task']['id'])) {
- Utils::throwError('1002:draft_task 类型的 content 必须包含 draft_task.id 字段');
- }
- $hasDraftTask = true;
- break;
-
- default:
- Utils::throwError('1002:不支持的 content type: ' . $item['type']);
- }
- }
-
- // 验证组合规则
- if ($hasLastFrame && !$hasFirstFrame) {
- Utils::throwError('1002:使用尾帧时必须同时提供首帧');
- }
-
- if ($hasReferenceImage && ($hasFirstFrame || $hasLastFrame)) {
- Utils::throwError('1002:参考图模式不能与首帧/尾帧模式混用');
- }
-
- if ($hasDraftTask && ($hasText || $hasFirstFrame || $hasLastFrame || $hasReferenceImage)) {
- Utils::throwError('1002:使用样片任务时不能同时提供其他内容');
- }
- }
- /**
- * 创建可灵AI Omni视频生成任务
- *
- * @param Request $request
- * @return JsonResponse
- */
- public function createKelingTask(Request $request): JsonResponse
- {
- $data = $request->all();
-
- // 验证基本参数
- $baseRules = [
- 'model' => 'required|string|in:kling-video-o1,kling-v3-omni',
- 'prompt' => 'required|max:2500',
- 'mode' => 'nullable|in:pro,std',
- 'aspect_ratio' => 'nullable|in:16:9,4:3,1:1,3:4,9:16,21:9',
- 'video_duration' => 'required|integer|min:3|max:15',
- ];
-
- $baseMessages = [
- 'model.required' => '模型参数不能为空',
- 'model.in' => '模型只支持 kling-video-o1、kling-v3-omni',
- 'prompt.required' => '提示词不能为空',
- 'prompt.max' => '提示词不能超过2500个字符',
- 'mode.in' => '模式只能是 pro 或 std',
- 'aspect_ratio.in' => '宽高比只能是 16:9、4:3、1:1、3:4、9:16、21:9',
- 'video_duration.required' => '请填写视频时长',
- 'video_duration.integer' => '视频时长必须是整数',
- 'video_duration.min' => '视频时长不能少于3秒',
- 'video_duration.max' => '视频时长不能超过15秒',
- ];
-
- // 可选参数验证
- $optionalRules = [
- 'multi_prompt' => 'nullable|array',
- 'multi_prompt.*' => 'string|max:2500',
- 'shot_type' => 'nullable|in:customize,single_shot,multi_shot',
- 'generate_audio' => 'nullable|boolean',
- 'webhook_url' => 'nullable|url',
- 'seed' => 'nullable|integer|min:-1|max:4294967295',
- 'video_resolution' => 'nullable|in:720P,1080P',
- ];
-
- $optionalMessages = [
- 'multi_prompt.array' => '多段提示词必须是数组',
- 'multi_prompt.*.string' => '每段提示词必须是字符串',
- 'multi_prompt.*.max' => '每段提示词不能超过2500个字符',
- 'shot_type.in' => '镜头类型只能是 customize、single_shot、multi_shot',
- 'webhook_url.url' => '回调地址格式不正确',
- 'seed.integer' => '随机种子必须是整数',
- 'seed.min' => '随机种子不能小于-1',
- 'seed.max' => '随机种子不能超过4294967295',
- 'video_resolution.in' => '分辨率只能是 720P 或 1080P',
- ];
- // 检查是否有首帧URL
- if (isset($data['first_frame_url'])) {
- $optionalRules['first_frame_url'] = 'url';
- $optionalMessages['first_frame_url.url'] = '首帧图片URL格式不正确';
- }
-
- // 检查是否有尾帧URL
- if (isset($data['tail_frame_url'])) {
- $optionalRules['tail_frame_url'] = 'url';
- $optionalMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确';
- }
-
- // 检查是否有首帧文件
- if ($request->hasFile('first_frame_file')) {
- $optionalRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800';
- $optionalMessages = array_merge($optionalMessages, [
- 'first_frame_file.image' => '首帧必须是图片文件',
- 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式',
- 'first_frame_file.max' => '首帧图片大小不能超过4.7MB',
- ]);
- }
-
- // 检查是否有尾帧文件
- if ($request->hasFile('tail_frame_file')) {
- $optionalRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800';
- $optionalMessages = array_merge($optionalMessages, [
- 'tail_frame_file.image' => '尾帧必须是图片文件',
- 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式',
- 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB',
- ]);
- }
- if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) {
- Utils::throwError('1002:提供尾帧URL时必须提供首帧URL');
- }
- if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) {
- Utils::throwError('1002:提供尾帧文件时必须提供首帧文件');
- }
- $validationRules = array_merge($baseRules, $optionalRules);
- $validationMessages = array_merge($baseMessages, $optionalMessages);
- // 处理图片相关参数
-
- $validator = Validator::make($data, $validationRules, $validationMessages);
-
- if ($validator->fails()) {
- Utils::throwError('1002:'.$validator->errors()->first());
- }
- $firstFrame = null;
- $tailFrame = null;
- $data['image_list'] = [];
- // 处理首帧图片(文件上传方式)
- if ($request->hasFile('first_frame_file')) {
- $firstFrameFile = $request->file('first_frame_file');
- $firstFrame = $this->processFrameFile($firstFrameFile, 'first');
- $data['first_frame_url'] = $firstFrame['url'];
- $data['image_list'][] = [
- 'image_url' => $data['first_frame_url'],
- 'type' => 'first_frame',
- ];
- }
- // 处理首帧图片(URL方式)
- elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) {
- $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true);
- if (!$firstFrame) {
- Utils::throwError('1003:首帧图片获取失败');
- }
- $data['image_list'][] = [
- 'image_url' => $data['first_frame_url'],
- 'type' => 'first_frame',
- ];
- }
- // 处理尾帧图片(文件上传方式)
- if ($request->hasFile('tail_frame_file')) {
- $tailFrameFile = $request->file('tail_frame_file');
- $tailFrame = $this->processFrameFile($tailFrameFile, 'tail');
- $data['tail_frame_url'] = $tailFrame['url'];
- $data['image_list'][] = [
- 'image_url' => $data['tail_frame_url'],
- 'type' => 'end_frame',
- ];
- }
- // 处理尾帧图片(URL方式)
- elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) {
- $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true);
- if (!$tailFrame) {
- Utils::throwError('1003:尾帧图片获取失败');
- }
- $data['image_list'][] = [
- 'image_url' => $data['tail_frame_url'],
- 'type' => 'end_frame',
- ];
- }
- // 余额预检:按预估时长计算所需积分,不足直接报错
- $chargeDuration = (int)getProp($data, 'video_duration', 5);
- if ($chargeDuration <= 0) {
- $chargeDuration = 5;
- }
- $this->pointsService->checkUserPointsEnough(
- $this->pointsService->getVideoChargePoints([
- 'model' => (string)getProp($data, 'model', ''),
- 'video_resolution' => strtolower((string)getProp($data, 'video_resolution', '720p')),
- 'video_duration' => $chargeDuration,
- 'ratio' => (string)getProp($data, 'aspect_ratio', getProp($data, 'ratio', '9:16')),
- 'generate_audio' => (int)getProp($data, 'generate_audio', 1),
- ])
- );
- $task = $this->aiVideoGenerationService->createKelingOmniTask($data);
- return $this->success(['task_id' => $task->id, 'status' => $task->status]);
- }
- /**
- * 查询可灵AI Omni任务状态
- *
- * @param string $taskId
- * @return JsonResponse
- */
- public function getKelingTaskStatus(string $taskId): JsonResponse
- {
- try {
- $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first();
- if (!$task) {
- return response()->json([
- 'success' => false,
- 'message' => '任务不存在'
- ], 404);
- }
- $result = $this->aiVideoGenerationService->queryKelingOmniTaskStatus($task);
- return $this->success($result);
- } catch (\Exception $e) {
- Utils::throwError('1001:'.$e->getMessage());
- }
- }
- /**
- * 获取视频任务列表
- *
- * @param Request $request
- * @return mixed
- */
- public function taskList(Request $request)
- {
- $data = $request->all();
- $result = $this->aiVideoGenerationService->getTaskList($data);
- return $this->success($result, [new AIGenerationTransformer(), 'newBuildTaskList']);
- }
- // ==================== 主体管理相关接口 ====================
- /**
- * 创建主体
- *
- * @param Request $request
- * @return JsonResponse
- */
- public function createElement(Request $request): JsonResponse
- {
- try {
- // 验证请求参数
- $validated = $request->validate([
- 'element_name' => 'required|string|max:20',
- 'element_description' => 'required|string|max:100',
- 'reference_type' => 'required|in:image_refer,video_refer',
- 'element_image_list' => 'required_if:reference_type,image_refer|array',
- 'element_image_list.frontal_image' => 'required_if:reference_type,image_refer|string',
- 'element_image_list.refer_images' => 'required_if:reference_type,image_refer|array|min:1|max:3',
- 'element_image_list.refer_images.*.image_url' => 'required|string',
- 'element_video_list' => 'required_if:reference_type,video_refer|array',
- 'element_video_list.refer_videos' => 'required_if:reference_type,video_refer|array|size:1',
- 'element_video_list.refer_videos.*.video_url' => 'required|string',
- 'element_voice_id' => 'nullable|string',
- 'tag_list' => 'nullable|array',
- 'tag_list.*.tag_id' => 'required|in:o_101,o_102,o_103,o_104,o_105,o_106,o_107,o_108',
- 'callback_url' => 'nullable|url',
- 'external_task_id' => 'nullable|string',
- ]);
- // 调用服务层创建主体
- $element = $this->aiVideoGenerationService->createElement($validated);
- return response()->json([
- 'code' => 0,
- 'message' => '主体创建任务已提交',
- 'request_id' => uniqid(),
- 'data' => [
- 'task_id' => $element->id,
- 'task_info' => [
- 'external_task_id' => $element->external_task_id,
- ],
- 'task_status' => $element->task_status,
- 'created_at' => $element->created_at->timestamp * 1000,
- 'updated_at' => $element->updated_at->timestamp * 1000,
- ]
- ]);
- } catch (\Illuminate\Validation\ValidationException $e) {
- return response()->json([
- 'code' => 400,
- 'message' => '参数验证失败',
- 'request_id' => uniqid(),
- 'errors' => $e->errors()
- ], 400);
- } catch (\Exception $e) {
- dLog('generate')->error('创建主体失败: ' . $e->getMessage());
- return response()->json([
- 'code' => 500,
- 'message' => '创建主体失败: ' . $e->getMessage(),
- 'request_id' => uniqid(),
- ], 500);
- }
- }
- /**
- * 查询单个主体状态
- *
- * @param string $taskId
- * @param Request $request
- * @return JsonResponse
- */
- public function getElementStatus(string $taskId, Request $request): JsonResponse
- {
- try {
- $externalTaskId = $request->get('external_task_id');
-
- // 先查询本地数据库
- $query = AIElement::where('id', $taskId);
- if ($externalTaskId) {
- $query->orWhere('external_task_id', $externalTaskId);
- }
-
- $localElement = $query->first();
-
- if (!$localElement) {
- return response()->json([
- 'code' => 404,
- 'message' => '主体任务不存在',
- 'request_id' => uniqid(),
- ], 404);
- }
- // 如果任务还在处理中,查询最新状态
- if ($localElement->task_status === AIElement::STATUS_PROCESSING) {
- $apiResult = $this->aiVideoGenerationService->queryElementStatus($taskId, $externalTaskId);
- if ($apiResult['success'] && isset($apiResult['data'])) {
- $localElement->refresh(); // 重新加载数据
- }
- }
- // 构建响应数据
- $responseData = [
- 'task_id' => $localElement->id,
- 'task_status' => $localElement->task_status,
- 'task_status_msg' => $localElement->task_status_msg,
- 'task_info' => [
- 'external_task_id' => $localElement->external_task_id,
- ],
- 'created_at' => $localElement->created_at->timestamp * 1000,
- 'updated_at' => $localElement->updated_at->timestamp * 1000,
- ];
- // 如果任务成功,添加主体结果信息
- if ($localElement->task_status === AIElement::STATUS_SUCCEED && $localElement->element_id) {
- $responseData['task_result'] = [
- 'elements' => [
- [
- 'element_id' => $localElement->element_id,
- 'element_name' => $localElement->element_name,
- 'element_description' => $localElement->element_description,
- 'reference_type' => $localElement->reference_type,
- 'element_image_list' => $localElement->element_image_list,
- 'element_video_list' => $localElement->element_video_list,
- 'element_voice_info' => $localElement->element_voice_info,
- 'tag_list' => $localElement->tag_list,
- 'owned_by' => $localElement->owned_by,
- 'status' => $localElement->status,
- ]
- ]
- ];
- $responseData['final_unit_deduction'] = $localElement->final_unit_deduction;
- }
- return response()->json([
- 'code' => 0,
- 'message' => 'success',
- 'request_id' => uniqid(),
- 'data' => $responseData
- ]);
- } catch (\Exception $e) {
- dLog('generate')->error('查询主体状态失败: ' . $e->getMessage());
- return response()->json([
- 'code' => 500,
- 'message' => '查询主体状态失败: ' . $e->getMessage(),
- 'request_id' => uniqid(),
- ], 500);
- }
- }
- /**
- * 查询所有主体列表(包含自定义和官方)
- *
- * @param Request $request
- * @return JsonResponse
- */
- public function getElementsList(Request $request): JsonResponse
- {
- try {
- $pageNum = (int)$request->get('pageNum', 1);
- $pageSize = (int)$request->get('pageSize', 30);
- $type = $request->get('type', 'all'); // all, custom, official
-
- // 限制分页参数范围
- $pageNum = max(1, min($pageNum, 1000));
- $pageSize = max(1, min($pageSize, 500));
- $customElements = [];
- $officialElements = [];
- // 查询自定义主体
- if (in_array($type, ['all', 'custom'])) {
- $customResult = $this->aiVideoGenerationService->queryCustomElementsList($pageNum, $pageSize);
- if ($customResult['success']) {
- $customElements = $customResult['data'];
- }
- }
- // 查询官方主体
- if (in_array($type, ['all', 'official'])) {
- $officialResult = $this->aiVideoGenerationService->queryOfficialElementsList($pageNum, $pageSize);
- if ($officialResult['success']) {
- $officialElements = $officialResult['data'];
- }
- }
- // 合并结果
- $allElements = array_merge($customElements, $officialElements);
- return response()->json([
- 'code' => 0,
- 'message' => 'success',
- 'request_id' => uniqid(),
- 'data' => $allElements
- ]);
- } catch (\Exception $e) {
- dLog('generate')->error('查询主体列表失败: ' . $e->getMessage());
- return response()->json([
- 'code' => 500,
- 'message' => '查询主体列表失败: ' . $e->getMessage(),
- 'request_id' => uniqid(),
- ], 500);
- }
- }
- /**
- * 删除自定义主体
- *
- * @param Request $request
- * @return JsonResponse
- */
- public function deleteElement(Request $request): JsonResponse
- {
- try {
- // 验证请求参数
- $validated = $request->validate([
- 'element_id' => 'required|string',
- ]);
- // 检查主体是否存在且为自定义主体
- $element = AIElement::where('element_id', $validated['element_id'])->first();
- if (!$element) {
- return response()->json([
- 'code' => 404,
- 'message' => '主体不存在',
- 'request_id' => uniqid(),
- ], 404);
- }
- if ($element->owned_by === 'kling') {
- return response()->json([
- 'code' => 400,
- 'message' => '不能删除官方主体',
- 'request_id' => uniqid(),
- ], 400);
- }
- // 调用服务层删除主体
- $result = $this->aiVideoGenerationService->deleteElement($validated['element_id']);
- if ($result['success']) {
- return response()->json([
- 'code' => 0,
- 'message' => $result['message'],
- 'request_id' => uniqid(),
- 'data' => $result['data']
- ]);
- } else {
- return response()->json([
- 'code' => 500,
- 'message' => $result['message'],
- 'request_id' => uniqid(),
- ], 500);
- }
- } catch (\Illuminate\Validation\ValidationException $e) {
- return response()->json([
- 'code' => 400,
- 'message' => '参数验证失败',
- 'request_id' => uniqid(),
- 'errors' => $e->errors()
- ], 400);
- } catch (\Exception $e) {
- dLog('generate')->error('删除主体失败: ' . $e->getMessage());
- return response()->json([
- 'code' => 500,
- 'message' => '删除主体失败: ' . $e->getMessage(),
- 'request_id' => uniqid(),
- ], 500);
- }
- }
- /**
- * 获取本地主体列表(用于内部管理)
- *
- * @param Request $request
- * @return JsonResponse
- */
- public function getLocalElementsList(Request $request): JsonResponse
- {
- try {
- // 构建筛选条件
- $filters = [
- 'owned_by' => $request->get('owned_by'),
- 'reference_type' => $request->get('reference_type'),
- 'status' => $request->get('status'),
- 'task_status' => $request->get('task_status'),
- 'keyword' => $request->get('keyword'),
- 'page_size' => (int)$request->get('page_size', 20),
- ];
- // 移除空值
- $filters = array_filter($filters, function($value) {
- return $value !== null && $value !== '';
- });
- // 调用服务层获取列表
- $elements = $this->aiVideoGenerationService->getLocalElementsList($filters);
- // 转换数据格式
- $data = $elements->map(function ($element) {
- return [
- 'task_id' => $element->id,
- 'external_task_id' => $element->external_task_id,
- 'task_status' => $element->task_status,
- 'task_status_msg' => $element->task_status_msg,
- 'element_id' => $element->element_id,
- 'element_name' => $element->element_name,
- 'element_description' => $element->element_description,
- 'reference_type' => $element->reference_type,
- 'element_image_list' => $element->element_image_list,
- 'element_video_list' => $element->element_video_list,
- 'element_voice_id' => $element->element_voice_id,
- 'element_voice_info' => $element->element_voice_info,
- 'tag_list' => $element->tag_list,
- 'tag_names' => $element->tag_names, // 使用模型的访问器
- 'owned_by' => $element->owned_by,
- 'status' => $element->status,
- 'final_unit_deduction' => $element->final_unit_deduction,
- 'created_at' => $element->created_at->format('Y-m-d H:i:s'),
- 'updated_at' => $element->updated_at->format('Y-m-d H:i:s'),
- ];
- });
- return response()->json([
- 'code' => 0,
- 'message' => 'success',
- 'request_id' => uniqid(),
- 'data' => [
- 'data' => $data,
- 'current_page' => $elements->currentPage(),
- 'last_page' => $elements->lastPage(),
- 'per_page' => $elements->perPage(),
- 'total' => $elements->total(),
- ]
- ]);
- } catch (\Exception $e) {
- dLog('generate')->error('获取本地主体列表失败: ' . $e->getMessage());
- return response()->json([
- 'code' => 500,
- 'message' => '获取本地主体列表失败: ' . $e->getMessage(),
- 'request_id' => uniqid(),
- ], 500);
- }
- }
- }
-
|