ImageGenerationController.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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\AIImageGenerationService;
  6. use App\Transformer\AIGeneration\AIGenerationTransformer;
  7. use Illuminate\Http\Request;
  8. use Illuminate\Http\JsonResponse;
  9. use Illuminate\Support\Facades\Validator;
  10. use App\Libs\ApiResponse;
  11. use App\Consts\BaseConst;
  12. class ImageGenerationController extends BaseController
  13. {
  14. use ApiResponse;
  15. private $aiImageGenerationService;
  16. public function __construct(
  17. AIImageGenerationService $aiImageGenerationService
  18. ) {
  19. $this->aiImageGenerationService = $aiImageGenerationService;
  20. }
  21. /**
  22. * 创建图片生成任务
  23. *
  24. * @param Request $request
  25. * @return JsonResponse
  26. */
  27. public function createTask(Request $request): JsonResponse
  28. {
  29. $data = $request->all();
  30. // 首先验证基本参数
  31. $baseRules = [
  32. 'prompt' => 'required|string|max:2000',
  33. 'width' => 'required|numeric|between:1024,4096',
  34. 'height' => 'required|numeric|between:1024,4096',
  35. 'image_num' => 'required|numeric|between:1,4',
  36. 'scale' => 'required|numeric|between:0,100',
  37. 'model' => 'nullable|string|in:jimeng_4.0,' . implode(',', BaseConst::VOLC_PIC_MODELS),
  38. ];
  39. $baseMessages = [
  40. 'prompt.required' => '提示词不能为空',
  41. 'prompt.max' => '提示词不能超过2000个字符',
  42. 'width.required' => '宽不能为空',
  43. 'width.numeric' => '宽必须是数字',
  44. 'width.between' => '宽必须在1024到4096之间',
  45. 'height.required' => '高不能为空',
  46. 'height.numeric' => '高必须是数字',
  47. 'height.between' => '高必须在1024到4096之间',
  48. 'image_num.required' => '生成图片数量不能为空',
  49. 'image_num.numeric' => '生成图片数量必须是数字',
  50. 'image_num.between' => '生成图片数量必须在1到4之间',
  51. 'scale.required' => '缩放比例不能为空',
  52. 'scale.numeric' => '缩放比例必须是数字',
  53. 'scale.between' => '缩放比例必须在0到100之间',
  54. 'model.in' => '模型类型无效',
  55. ];
  56. // 根据传入参数决定验证规则
  57. if (isset($data['ref_img_url'])) {
  58. // URL 方式 - 支持单个或多个URL(逗号分隔或数组)
  59. $validationRules = array_merge($baseRules, [
  60. 'ref_img_url' => 'required',
  61. ]);
  62. $validationMessages = array_merge($baseMessages, [
  63. 'ref_img_url.required' => '参考图片URL不能为空',
  64. ]);
  65. } else {
  66. // 上传文件方式 - 支持多个文件
  67. $validationRules = array_merge($baseRules, [
  68. 'ref_img_file' => 'required',
  69. 'ref_img_file.*' => 'image|mimes:jpeg,png|max:4800',
  70. ]);
  71. $validationMessages = array_merge($baseMessages, [
  72. 'ref_img_file.required' => '参考图片不能为空',
  73. 'ref_img_file.*.image' => '参考图片必须是图片文件',
  74. 'ref_img_file.*.mimes' => '参考图片必须是jpeg,png格式',
  75. 'ref_img_file.*.max' => '参考图片大小不能超过4.7MB',
  76. ]);
  77. }
  78. $validator = Validator::make($data, $validationRules, $validationMessages);
  79. if ($validator->fails()) {
  80. Utils::throwError('1002:'.$validator->errors()->first());
  81. }
  82. $refImgUrls = [];
  83. $refImgBase64List = [];
  84. // 处理上传的图片
  85. if ($request->hasFile('ref_img_file')) {
  86. $refImgFiles = is_array($request->file('ref_img_file'))
  87. ? $request->file('ref_img_file')
  88. : [$request->file('ref_img_file')];
  89. foreach ($refImgFiles as $index => $refImgFile) {
  90. // 验证图片分辨率和比例
  91. $refImgInfo = @getimagesize($refImgFile->getPathname());
  92. if (!$refImgInfo) {
  93. Utils::throwError('1003:图片' . ($index + 1) . '格式无效');
  94. }
  95. $refImg = [
  96. 'width' => $refImgInfo[0],
  97. 'height' => $refImgInfo[1],
  98. 'size' => $refImgFile->getSize(),
  99. ];
  100. // 验证图片规格
  101. $this->validateImageSpecs($refImg, $index + 1);
  102. // 上传图片到服务器并获取URL
  103. $filename = randStr(10) . '.' . $refImgFile->getClientOriginalExtension();
  104. $uploaded_url = uploadStreamByTos('image', file_get_contents($refImgFile->getPathname()), $filename);
  105. if (!$uploaded_url) {
  106. Utils::throwError('1003:图片' . ($index + 1) . '保存失败');
  107. }
  108. $refImgUrls[] = $uploaded_url;
  109. }
  110. } else {
  111. // 使用URL方式,支持逗号分隔的字符串或数组
  112. $urls = is_array($data['ref_img_url'])
  113. ? $data['ref_img_url']
  114. : array_map('trim', explode(',', $data['ref_img_url']));
  115. foreach ($urls as $index => $url) {
  116. if (empty($url)) {
  117. continue;
  118. }
  119. // 验证URL格式
  120. if (!filter_var($url, FILTER_VALIDATE_URL)) {
  121. Utils::throwError('1003:图片URL' . ($index + 1) . '格式无效');
  122. }
  123. // 验证远程图片
  124. $refImg = Utils::getRemoteImageInfo($url, true);
  125. if (!$refImg) {
  126. Utils::throwError('1003:图片' . ($index + 1) . '获取失败');
  127. }
  128. // 验证图片规格
  129. $this->validateImageSpecs($refImg, $index + 1);
  130. $refImgUrls[] = $url;
  131. }
  132. }
  133. if (empty($refImgUrls)) {
  134. Utils::throwError('1003:至少需要一张参考图片');
  135. }
  136. // 将处理后的URL存入data(支持多个)
  137. $data['ref_img_urls'] = $refImgUrls;
  138. $data['ref_img_url'] = $refImgUrls[0]; // 保持向后兼容
  139. try {
  140. $task = $this->aiImageGenerationService->createImageGenerationTask($data);
  141. return $this->success(['task_id' => $task->id, 'status' => $task->status]);
  142. } catch (\Exception $e) {
  143. Utils::throwError('1001:'.$e->getMessage());
  144. }
  145. }
  146. /**
  147. * 验证图片规格
  148. *
  149. * @param array $refImg
  150. * @param int $index
  151. * @return void
  152. */
  153. private function validateImageSpecs(array $refImg, int $index): void
  154. {
  155. // 判断图片是否满足以下条件:
  156. // 1.图片文件最大4.7MB
  157. // 2.图片分辨率最大: 4096*4096, 最短边不低于320
  158. // 3.图片长边与短边比例在3以内
  159. if ($refImg['size'] > 13 * 1024 * 1024) {
  160. Utils::throwError('1003:图片' . $index . '大小不能超过10MB');
  161. }
  162. $minDimension = min($refImg['width'], $refImg['height']);
  163. $maxDimension = max($refImg['width'], $refImg['height']);
  164. if ($maxDimension > 4096 || $minDimension < 320) {
  165. Utils::throwError('1003:图片' . $index . '分辨率不能超过4096*4096,且最短边不能低于320');
  166. }
  167. if ($maxDimension / $minDimension > 3) {
  168. Utils::throwError('1003:图片' . $index . '长边与短边比例不能超过3');
  169. }
  170. }
  171. /**
  172. * 查询任务状态
  173. *
  174. * @param string $taskId
  175. * @return JsonResponse
  176. */
  177. public function taskStatus(string $taskId): JsonResponse
  178. {
  179. $task = \App\Models\MpGeneratePicTask::where('id', $taskId)->first();
  180. if (!$task) {
  181. return response()->json([
  182. 'success' => false,
  183. 'message' => '任务不存在'
  184. ], 404);
  185. }
  186. return response()->json([
  187. 'success' => true,
  188. 'data' => [
  189. 'task_id' => $task->id,
  190. 'status' => $task->status,
  191. 'result_url' => $task->result_url,
  192. 'error_message' => $task->error_message,
  193. 'created_at' => $task->created_at,
  194. 'started_at' => $task->started_at,
  195. 'completed_at' => $task->completed_at
  196. ]
  197. ]);
  198. }
  199. /**
  200. * 获取任务列表
  201. *
  202. * @param Request $request
  203. * @return mixed
  204. */
  205. public function taskList(Request $request)
  206. {
  207. $data = $request->all();
  208. $result = $this->aiImageGenerationService->getTaskList($data);
  209. return $this->success($result, [new AIGenerationTransformer(), 'newBuildTaskList']);
  210. }
  211. /**
  212. * 批量上传图片或视频
  213. *
  214. * @param Request $request
  215. * @return void
  216. */
  217. public function batchUploadImg(Request $request) {
  218. $refImgUrls = [];
  219. $refVideoUrls = [];
  220. // 处理上传的图片文件
  221. if ($request->hasFile('ref_img_file')) {
  222. $refImgFiles = is_array($request->file('ref_img_file'))
  223. ? $request->file('ref_img_file')
  224. : [$request->file('ref_img_file')];
  225. foreach ($refImgFiles as $index => $refImgFile) {
  226. // 验证图片分辨率和比例
  227. $refImgInfo = @getimagesize($refImgFile->getPathname());
  228. if (!$refImgInfo) {
  229. Utils::throwError('1003:图片' . ($index + 1) . '格式无效');
  230. }
  231. $refImg = [
  232. 'width' => $refImgInfo[0],
  233. 'height' => $refImgInfo[1],
  234. 'size' => $refImgFile->getSize(),
  235. ];
  236. // 验证图片规格
  237. $this->validateImageSpecs($refImg, $index + 1);
  238. // 上传图片到服务器并获取URL
  239. $filename = randStr(10) . '.' . $refImgFile->getClientOriginalExtension();
  240. $uploaded_url = uploadStreamByTos('image', file_get_contents($refImgFile->getPathname()), $filename);
  241. if (!$uploaded_url) {
  242. Utils::throwError('1003:图片' . ($index + 1) . '保存失败');
  243. }
  244. $refImgUrls[] = $uploaded_url;
  245. }
  246. }
  247. // 处理图片二进制内容(ref_img数组)
  248. if ($request->has('ref_img')) {
  249. $refImgs = $request->input('ref_img');
  250. // 确保是数组格式
  251. if (!is_array($refImgs)) {
  252. $refImgs = [$refImgs];
  253. }
  254. foreach ($refImgs as $index => $refImgBinary) {
  255. $tempFile = null;
  256. $tempPath = null;
  257. try {
  258. // 解码base64(如果是base64格式)
  259. if (preg_match('/^data:image\/(\w+);base64,/', $refImgBinary, $matches)) {
  260. $refImgBinary = base64_decode(substr($refImgBinary, strpos($refImgBinary, ',') + 1));
  261. }
  262. // 创建临时文件以验证图片
  263. $tempFile = tmpfile();
  264. $tempPath = stream_get_meta_data($tempFile)['uri'];
  265. fwrite($tempFile, $refImgBinary);
  266. // 验证图片分辨率和比例
  267. $refImgInfo = @getimagesize($tempPath);
  268. if (!$refImgInfo) {
  269. Utils::throwError('1003:图片' . ($index + 1) . '格式无效');
  270. }
  271. $refImg = [
  272. 'width' => $refImgInfo[0],
  273. 'height' => $refImgInfo[1],
  274. 'size' => strlen($refImgBinary),
  275. ];
  276. // 验证图片规格
  277. $this->validateImageSpecs($refImg, $index + 1);
  278. // 上传图片到服务器并获取URL(默认使用.jpg格式)
  279. $filename = randStr(10) . '.jpg';
  280. $uploaded_url = uploadStreamByTos('image', $refImgBinary, $filename);
  281. if (!$uploaded_url) {
  282. Utils::throwError('1003:图片' . ($index + 1) . '保存失败');
  283. }
  284. $refImgUrls[] = $uploaded_url;
  285. } finally {
  286. // 确保临时文件被关闭和删除
  287. if ($tempFile) {
  288. fclose($tempFile);
  289. }
  290. }
  291. }
  292. }
  293. // 处理上传的视频文件
  294. if ($request->hasFile('ref_video_file')) {
  295. $refVideoFiles = is_array($request->file('ref_video_file'))
  296. ? $request->file('ref_video_file')
  297. : [$request->file('ref_video_file')];
  298. foreach ($refVideoFiles as $index => $refVideoFile) {
  299. // 验证视频文件大小(最大100MB)
  300. if ($refVideoFile->getSize() > 100 * 1024 * 1024) {
  301. Utils::throwError('1003:视频' . ($index + 1) . '大小不能超过100MB');
  302. }
  303. // 验证视频文件类型
  304. $allowedMimeTypes = ['video/mp4', 'video/mpeg', 'video/quicktime', 'video/x-msvideo'];
  305. if (!in_array($refVideoFile->getMimeType(), $allowedMimeTypes)) {
  306. Utils::throwError('1003:视频' . ($index + 1) . '格式无效,仅支持mp4、mpeg、mov、avi格式');
  307. }
  308. // 上传视频到服务器并获取URL
  309. $filename = randStr(10) . '.' . $refVideoFile->getClientOriginalExtension();
  310. $uploaded_url = uploadStreamByTos('video', file_get_contents($refVideoFile->getPathname()), $filename);
  311. if (!$uploaded_url) {
  312. Utils::throwError('1003:视频' . ($index + 1) . '保存失败');
  313. }
  314. $refVideoUrls[] = $uploaded_url;
  315. }
  316. }
  317. // 处理视频二进制内容(ref_video数组)
  318. if ($request->has('ref_video')) {
  319. $refVideos = $request->input('ref_video');
  320. // 确保是数组格式
  321. if (!is_array($refVideos)) {
  322. $refVideos = [$refVideos];
  323. }
  324. foreach ($refVideos as $index => $refVideoBinary) {
  325. try {
  326. // 解码base64(如果是base64格式)
  327. if (preg_match('/^data:video\/(\w+);base64,/', $refVideoBinary, $matches)) {
  328. $extension = $matches[1];
  329. $refVideoBinary = base64_decode(substr($refVideoBinary, strpos($refVideoBinary, ',') + 1));
  330. } else {
  331. $extension = 'mp4'; // 默认扩展名
  332. }
  333. // 验证视频大小(最大100MB)
  334. if (strlen($refVideoBinary) > 100 * 1024 * 1024) {
  335. Utils::throwError('1003:视频' . ($index + 1) . '大小不能超过100MB');
  336. }
  337. // 上传视频到服务器并获取URL
  338. $filename = randStr(10) . '.' . $extension;
  339. $uploaded_url = uploadStreamByTos('video', $refVideoBinary, $filename);
  340. if (!$uploaded_url) {
  341. Utils::throwError('1003:视频' . ($index + 1) . '保存失败');
  342. }
  343. $refVideoUrls[] = $uploaded_url;
  344. } catch (\Exception $e) {
  345. Utils::throwError('1003:视频' . ($index + 1) . '处理失败:' . $e->getMessage());
  346. }
  347. }
  348. }
  349. return $this->success([
  350. 'image_urls' => $refImgUrls,
  351. 'video_urls' => $refVideoUrls
  352. ]);
  353. }
  354. }