aiImageGenerationService = $aiImageGenerationService; } /** * 创建图片生成任务 * * @param Request $request * @return JsonResponse */ public function createTask(Request $request): JsonResponse { $data = $request->all(); // 首先验证基本参数 $baseRules = [ 'prompt' => 'required|string|max:2000', 'width' => 'required|numeric|between:1024,4096', 'height' => 'required|numeric|between:1024,4096', 'image_num' => 'required|numeric|between:1,4', 'scale' => 'required|numeric|between:0,100', 'model' => 'nullable|string|in:jimeng_4.0,' . implode(',', BaseConst::VOLC_PIC_MODELS), ]; $baseMessages = [ 'prompt.required' => '提示词不能为空', 'prompt.max' => '提示词不能超过2000个字符', 'width.required' => '宽不能为空', 'width.numeric' => '宽必须是数字', 'width.between' => '宽必须在1024到4096之间', 'height.required' => '高不能为空', 'height.numeric' => '高必须是数字', 'height.between' => '高必须在1024到4096之间', 'image_num.required' => '生成图片数量不能为空', 'image_num.numeric' => '生成图片数量必须是数字', 'image_num.between' => '生成图片数量必须在1到4之间', 'scale.required' => '缩放比例不能为空', 'scale.numeric' => '缩放比例必须是数字', 'scale.between' => '缩放比例必须在0到100之间', 'model.in' => '模型类型无效', ]; // 根据传入参数决定验证规则 if (isset($data['ref_img_url'])) { // URL 方式 - 支持单个或多个URL(逗号分隔或数组) $validationRules = array_merge($baseRules, [ 'ref_img_url' => 'required', ]); $validationMessages = array_merge($baseMessages, [ 'ref_img_url.required' => '参考图片URL不能为空', ]); } else { // 上传文件方式 - 支持多个文件 $validationRules = array_merge($baseRules, [ 'ref_img_file' => 'required', 'ref_img_file.*' => 'image|mimes:jpeg,png|max:4800', ]); $validationMessages = array_merge($baseMessages, [ 'ref_img_file.required' => '参考图片不能为空', 'ref_img_file.*.image' => '参考图片必须是图片文件', 'ref_img_file.*.mimes' => '参考图片必须是jpeg,png格式', 'ref_img_file.*.max' => '参考图片大小不能超过4.7MB', ]); } $validator = Validator::make($data, $validationRules, $validationMessages); if ($validator->fails()) { Utils::throwError('1002:'.$validator->errors()->first()); } $refImgUrls = []; $refImgBase64List = []; // 处理上传的图片 if ($request->hasFile('ref_img_file')) { $refImgFiles = is_array($request->file('ref_img_file')) ? $request->file('ref_img_file') : [$request->file('ref_img_file')]; foreach ($refImgFiles as $index => $refImgFile) { // 验证图片分辨率和比例 $refImgInfo = @getimagesize($refImgFile->getPathname()); if (!$refImgInfo) { Utils::throwError('1003:图片' . ($index + 1) . '格式无效'); } $refImg = [ 'width' => $refImgInfo[0], 'height' => $refImgInfo[1], 'size' => $refImgFile->getSize(), ]; // 验证图片规格 $this->validateImageSpecs($refImg, $index + 1); // 上传图片到服务器并获取URL $filename = randStr(10) . '.' . $refImgFile->getClientOriginalExtension(); $uploaded_url = uploadStreamByTos('image', file_get_contents($refImgFile->getPathname()), $filename); if (!$uploaded_url) { Utils::throwError('1003:图片' . ($index + 1) . '保存失败'); } $refImgUrls[] = $uploaded_url; } } else { // 使用URL方式,支持逗号分隔的字符串或数组 $urls = is_array($data['ref_img_url']) ? $data['ref_img_url'] : array_map('trim', explode(',', $data['ref_img_url'])); foreach ($urls as $index => $url) { if (empty($url)) { continue; } // 验证URL格式 if (!filter_var($url, FILTER_VALIDATE_URL)) { Utils::throwError('1003:图片URL' . ($index + 1) . '格式无效'); } // 验证远程图片 $refImg = Utils::getRemoteImageInfo($url, true); if (!$refImg) { Utils::throwError('1003:图片' . ($index + 1) . '获取失败'); } // 验证图片规格 $this->validateImageSpecs($refImg, $index + 1); $refImgUrls[] = $url; } } if (empty($refImgUrls)) { Utils::throwError('1003:至少需要一张参考图片'); } // 将处理后的URL存入data(支持多个) $data['ref_img_urls'] = $refImgUrls; $data['ref_img_url'] = $refImgUrls[0]; // 保持向后兼容 try { $task = $this->aiImageGenerationService->createImageGenerationTask($data); return $this->success(['task_id' => $task->id, 'status' => $task->status]); } catch (\Exception $e) { Utils::throwError('1001:'.$e->getMessage()); } } /** * 图片超清(超分辨率)放大 * * 支持传入图片URL或上传图片文件,放大到 1080p / 2k / 4k 或指定宽高。 * 默认使用 ffmpeg(本地环境或未安装时自动回退 gd)。 * * @param Request $request * @return JsonResponse */ public function upscaleImage(Request $request): JsonResponse { $data = $request->all(); // 参数验证 $rules = [ 'image_url' => 'nullable|string', 'image_file' => 'nullable|image|mimes:jpeg,png,webp|max:13312', 'target_size' => 'nullable|string|in:1080p,2k,4k', 'target_width' => 'nullable|numeric|between:1,8192', 'target_height' => 'nullable|numeric|between:1,8192', 'format' => 'nullable|string|in:jpg,jpeg,png,webp', 'quality' => 'nullable|numeric|between:1,100', 'sharpen' => 'nullable|boolean', 'sharpen_amount' => 'nullable|numeric|between:0,2', 'driver' => 'nullable|string|in:gd,ffmpeg', ]; $messages = [ 'image_url.string' => '图片URL格式不正确', 'image_file.image' => '上传文件必须是图片', 'image_file.mimes' => '图片格式必须是jpeg、png或webp', 'image_file.max' => '图片大小不能超过13MB', 'target_size.in' => '目标档位必须是1080p、2k或4k', 'target_width.numeric' => '宽度必须是数字', 'target_width.between' => '宽度必须在1到8192之间', 'target_height.numeric' => '高度必须是数字', 'target_height.between' => '高度必须在1到8192之间', 'format.in' => '输出格式必须是jpg、png或webp', 'quality.numeric' => '输出质量必须是数字', 'quality.between' => '输出质量必须在1到100之间', 'sharpen.boolean' => 'sharpen必须是布尔值', 'sharpen_amount.numeric' => '锐化强度必须是数字', 'sharpen_amount.between' => '锐化强度必须在0到2之间', 'driver.in' => '处理驱动必须是gd或ffmpeg', ]; $validator = Validator::make($data, $rules, $messages); if ($validator->fails()) { Utils::throwError('1002:'.$validator->errors()->first()); } // 获取图片URL:优先使用上传文件,其次使用传入URL $imageUrl = trim((string)($data['image_url'] ?? '')); if ($request->hasFile('image_file')) { $imageFile = $request->file('image_file'); $imageInfo = @getimagesize($imageFile->getPathname()); if (!$imageInfo) { Utils::throwError('1003:图片格式无效'); } // 复用已有图片规格校验 $this->validateImageSpecs([ 'width' => $imageInfo[0], 'height' => $imageInfo[1], 'size' => $imageFile->getSize(), ], 1); $filename = randStr(10) . '.' . $imageFile->getClientOriginalExtension(); $imageUrl = uploadStreamByTos('image', file_get_contents($imageFile->getPathname()), $filename); if (!$imageUrl) { Utils::throwError('1003:图片保存失败'); } } if ($imageUrl === '') { Utils::throwError('1002:image_url或image_file不能为空'); } if (!filter_var($imageUrl, FILTER_VALIDATE_URL)) { Utils::throwError('1002:图片URL格式无效'); } // 仅允许 http/https,防止通过API读取服务器本地文件 $scheme = strtolower((string)parse_url($imageUrl, PHP_URL_SCHEME)); if (!in_array($scheme, ['http', 'https'], true)) { Utils::throwError('1002:图片URL格式无效'); } // 组装超分参数(null 自动走默认值) $upscaleParams = [ 'image_url' => $imageUrl, 'target' => $data['target_size'] ?? null, 'width' => isset($data['target_width']) ? (int)$data['target_width'] : null, 'height' => isset($data['target_height']) ? (int)$data['target_height'] : null, 'format' => $data['format'] ?? null, 'quality' => isset($data['quality']) ? (int)$data['quality'] : null, 'sharpen' => isset($data['sharpen']) ? filter_var($data['sharpen'], FILTER_VALIDATE_BOOLEAN) : null, 'sharpen_amount' => isset($data['sharpen_amount']) ? (float)$data['sharpen_amount'] : null, 'driver' => $data['driver'] ?? null, ]; try { $result = upscaleImage($upscaleParams); return $this->success([ 'url' => $result['url'], 'width' => $result['width'], 'height' => $result['height'], 'src_width' => $result['src_width'], 'src_height' => $result['src_height'], 'upscaled' => $result['upscaled'], 'driver' => $result['driver'], ]); } catch (\Exception $e) { Utils::throwError('1001:'.$e->getMessage()); } } /** * 验证图片规格 * * @param array $refImg * @param int $index * @return void */ private function validateImageSpecs(array $refImg, int $index): void { // 判断图片是否满足以下条件: // 1.图片文件最大4.7MB // 2.图片分辨率最大: 4096*4096, 最短边不低于320 // 3.图片长边与短边比例在3以内 if ($refImg['size'] > 13 * 1024 * 1024) { Utils::throwError('1003:图片' . $index . '大小不能超过10MB'); } $minDimension = min($refImg['width'], $refImg['height']); $maxDimension = max($refImg['width'], $refImg['height']); if ($maxDimension > 4096 || $minDimension < 320) { Utils::throwError('1003:图片' . $index . '分辨率不能超过4096*4096,且最短边不能低于320'); } if ($maxDimension / $minDimension > 3) { Utils::throwError('1003:图片' . $index . '长边与短边比例不能超过3'); } } /** * 查询任务状态 * * @param string $taskId * @return JsonResponse */ public function taskStatus(string $taskId): JsonResponse { $task = \App\Models\MpGeneratePicTask::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 Request $request * @return mixed */ public function taskList(Request $request) { $data = $request->all(); $result = $this->aiImageGenerationService->getTaskList($data); return $this->success($result, [new AIGenerationTransformer(), 'newBuildTaskList']); } /** * 批量上传图片或视频 * * @param Request $request * @return void */ public function batchUploadImg(Request $request) { $refImgUrls = []; $refVideoUrls = []; // 处理上传的图片文件 if ($request->hasFile('ref_img_file')) { $refImgFiles = is_array($request->file('ref_img_file')) ? $request->file('ref_img_file') : [$request->file('ref_img_file')]; foreach ($refImgFiles as $index => $refImgFile) { // 验证图片分辨率和比例 $refImgInfo = @getimagesize($refImgFile->getPathname()); if (!$refImgInfo) { Utils::throwError('1003:图片' . ($index + 1) . '格式无效'); } $refImg = [ 'width' => $refImgInfo[0], 'height' => $refImgInfo[1], 'size' => $refImgFile->getSize(), ]; // 验证图片规格 $this->validateImageSpecs($refImg, $index + 1); // 上传图片到服务器并获取URL $filename = randStr(10) . '.' . $refImgFile->getClientOriginalExtension(); $uploaded_url = uploadStreamByTos('image', file_get_contents($refImgFile->getPathname()), $filename); if (!$uploaded_url) { Utils::throwError('1003:图片' . ($index + 1) . '保存失败'); } $refImgUrls[] = $uploaded_url; } } // 处理图片二进制内容(ref_img数组) if ($request->has('ref_img')) { $refImgs = $request->input('ref_img'); // 确保是数组格式 if (!is_array($refImgs)) { $refImgs = [$refImgs]; } foreach ($refImgs as $index => $refImgBinary) { $tempFile = null; $tempPath = null; try { // 解码base64(如果是base64格式) if (preg_match('/^data:image\/(\w+);base64,/', $refImgBinary, $matches)) { $refImgBinary = base64_decode(substr($refImgBinary, strpos($refImgBinary, ',') + 1)); } // 创建临时文件以验证图片 $tempFile = tmpfile(); $tempPath = stream_get_meta_data($tempFile)['uri']; fwrite($tempFile, $refImgBinary); // 验证图片分辨率和比例 $refImgInfo = @getimagesize($tempPath); if (!$refImgInfo) { Utils::throwError('1003:图片' . ($index + 1) . '格式无效'); } $refImg = [ 'width' => $refImgInfo[0], 'height' => $refImgInfo[1], 'size' => strlen($refImgBinary), ]; // 验证图片规格 $this->validateImageSpecs($refImg, $index + 1); // 上传图片到服务器并获取URL(默认使用.jpg格式) $filename = randStr(10) . '.jpg'; $uploaded_url = uploadStreamByTos('image', $refImgBinary, $filename); if (!$uploaded_url) { Utils::throwError('1003:图片' . ($index + 1) . '保存失败'); } $refImgUrls[] = $uploaded_url; } finally { // 确保临时文件被关闭和删除 if ($tempFile) { fclose($tempFile); } } } } // 处理上传的视频文件 if ($request->hasFile('ref_video_file')) { $refVideoFiles = is_array($request->file('ref_video_file')) ? $request->file('ref_video_file') : [$request->file('ref_video_file')]; foreach ($refVideoFiles as $index => $refVideoFile) { // 验证视频文件大小(最大100MB) if ($refVideoFile->getSize() > 100 * 1024 * 1024) { Utils::throwError('1003:视频' . ($index + 1) . '大小不能超过100MB'); } // 验证视频文件类型 $allowedMimeTypes = ['video/mp4', 'video/mpeg', 'video/quicktime', 'video/x-msvideo']; if (!in_array($refVideoFile->getMimeType(), $allowedMimeTypes)) { Utils::throwError('1003:视频' . ($index + 1) . '格式无效,仅支持mp4、mpeg、mov、avi格式'); } // 上传视频到服务器并获取URL $filename = randStr(10) . '.' . $refVideoFile->getClientOriginalExtension(); $uploaded_url = uploadStreamByTos('video', file_get_contents($refVideoFile->getPathname()), $filename); if (!$uploaded_url) { Utils::throwError('1003:视频' . ($index + 1) . '保存失败'); } $refVideoUrls[] = $uploaded_url; } } // 处理视频二进制内容(ref_video数组) if ($request->has('ref_video')) { $refVideos = $request->input('ref_video'); // 确保是数组格式 if (!is_array($refVideos)) { $refVideos = [$refVideos]; } foreach ($refVideos as $index => $refVideoBinary) { try { // 解码base64(如果是base64格式) if (preg_match('/^data:video\/(\w+);base64,/', $refVideoBinary, $matches)) { $extension = $matches[1]; $refVideoBinary = base64_decode(substr($refVideoBinary, strpos($refVideoBinary, ',') + 1)); } else { $extension = 'mp4'; // 默认扩展名 } // 验证视频大小(最大100MB) if (strlen($refVideoBinary) > 100 * 1024 * 1024) { Utils::throwError('1003:视频' . ($index + 1) . '大小不能超过100MB'); } // 上传视频到服务器并获取URL $filename = randStr(10) . '.' . $extension; $uploaded_url = uploadStreamByTos('video', $refVideoBinary, $filename); if (!$uploaded_url) { Utils::throwError('1003:视频' . ($index + 1) . '保存失败'); } $refVideoUrls[] = $uploaded_url; } catch (\Exception $e) { Utils::throwError('1003:视频' . ($index + 1) . '处理失败:' . $e->getMessage()); } } } return $this->success([ 'image_urls' => $refImgUrls, 'video_urls' => $refVideoUrls ]); } }