ImageGenerationController.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  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. * 支持传入图片URL或上传图片文件,放大到 1080p / 2k / 4k 或指定宽高。
  150. * 默认使用 ffmpeg(本地环境或未安装时自动回退 gd)。
  151. *
  152. * @param Request $request
  153. * @return JsonResponse
  154. */
  155. public function upscaleImage(Request $request): JsonResponse
  156. {
  157. $data = $request->all();
  158. // 参数验证
  159. $rules = [
  160. 'image_url' => 'nullable|string',
  161. 'image_file' => 'nullable|image|mimes:jpeg,png,webp|max:13312',
  162. 'target_size' => 'nullable|string|in:1080p,2k,4k',
  163. 'target_width' => 'nullable|numeric|between:1,8192',
  164. 'target_height' => 'nullable|numeric|between:1,8192',
  165. 'format' => 'nullable|string|in:jpg,jpeg,png,webp',
  166. 'quality' => 'nullable|numeric|between:1,100',
  167. 'sharpen' => 'nullable|boolean',
  168. 'sharpen_amount' => 'nullable|numeric|between:0,2',
  169. 'driver' => 'nullable|string|in:gd,ffmpeg',
  170. ];
  171. $messages = [
  172. 'image_url.string' => '图片URL格式不正确',
  173. 'image_file.image' => '上传文件必须是图片',
  174. 'image_file.mimes' => '图片格式必须是jpeg、png或webp',
  175. 'image_file.max' => '图片大小不能超过13MB',
  176. 'target_size.in' => '目标档位必须是1080p、2k或4k',
  177. 'target_width.numeric' => '宽度必须是数字',
  178. 'target_width.between' => '宽度必须在1到8192之间',
  179. 'target_height.numeric' => '高度必须是数字',
  180. 'target_height.between' => '高度必须在1到8192之间',
  181. 'format.in' => '输出格式必须是jpg、png或webp',
  182. 'quality.numeric' => '输出质量必须是数字',
  183. 'quality.between' => '输出质量必须在1到100之间',
  184. 'sharpen.boolean' => 'sharpen必须是布尔值',
  185. 'sharpen_amount.numeric' => '锐化强度必须是数字',
  186. 'sharpen_amount.between' => '锐化强度必须在0到2之间',
  187. 'driver.in' => '处理驱动必须是gd或ffmpeg',
  188. ];
  189. $validator = Validator::make($data, $rules, $messages);
  190. if ($validator->fails()) {
  191. Utils::throwError('1002:'.$validator->errors()->first());
  192. }
  193. // 获取图片URL:优先使用上传文件,其次使用传入URL
  194. $imageUrl = trim((string)($data['image_url'] ?? ''));
  195. if ($request->hasFile('image_file')) {
  196. $imageFile = $request->file('image_file');
  197. $imageInfo = @getimagesize($imageFile->getPathname());
  198. if (!$imageInfo) {
  199. Utils::throwError('1003:图片格式无效');
  200. }
  201. // 复用已有图片规格校验
  202. $this->validateImageSpecs([
  203. 'width' => $imageInfo[0],
  204. 'height' => $imageInfo[1],
  205. 'size' => $imageFile->getSize(),
  206. ], 1);
  207. $filename = randStr(10) . '.' . $imageFile->getClientOriginalExtension();
  208. $imageUrl = uploadStreamByTos('image', file_get_contents($imageFile->getPathname()), $filename);
  209. if (!$imageUrl) {
  210. Utils::throwError('1003:图片保存失败');
  211. }
  212. }
  213. if ($imageUrl === '') {
  214. Utils::throwError('1002:image_url或image_file不能为空');
  215. }
  216. if (!filter_var($imageUrl, FILTER_VALIDATE_URL)) {
  217. Utils::throwError('1002:图片URL格式无效');
  218. }
  219. // 仅允许 http/https,防止通过API读取服务器本地文件
  220. $scheme = strtolower((string)parse_url($imageUrl, PHP_URL_SCHEME));
  221. if (!in_array($scheme, ['http', 'https'], true)) {
  222. Utils::throwError('1002:图片URL格式无效');
  223. }
  224. // 组装超分参数(null 自动走默认值)
  225. $upscaleParams = [
  226. 'image_url' => $imageUrl,
  227. 'target' => $data['target_size'] ?? null,
  228. 'width' => isset($data['target_width']) ? (int)$data['target_width'] : null,
  229. 'height' => isset($data['target_height']) ? (int)$data['target_height'] : null,
  230. 'format' => $data['format'] ?? null,
  231. 'quality' => isset($data['quality']) ? (int)$data['quality'] : null,
  232. 'sharpen' => isset($data['sharpen']) ? filter_var($data['sharpen'], FILTER_VALIDATE_BOOLEAN) : null,
  233. 'sharpen_amount' => isset($data['sharpen_amount']) ? (float)$data['sharpen_amount'] : null,
  234. 'driver' => $data['driver'] ?? null,
  235. ];
  236. try {
  237. $result = upscaleImage($upscaleParams);
  238. return $this->success([
  239. 'url' => $result['url'],
  240. 'width' => $result['width'],
  241. 'height' => $result['height'],
  242. 'src_width' => $result['src_width'],
  243. 'src_height' => $result['src_height'],
  244. 'upscaled' => $result['upscaled'],
  245. 'driver' => $result['driver'],
  246. ]);
  247. } catch (\Exception $e) {
  248. Utils::throwError('1001:'.$e->getMessage());
  249. }
  250. }
  251. /**
  252. * 验证图片规格
  253. *
  254. * @param array $refImg
  255. * @param int $index
  256. * @return void
  257. */
  258. private function validateImageSpecs(array $refImg, int $index): void
  259. {
  260. // 判断图片是否满足以下条件:
  261. // 1.图片文件最大4.7MB
  262. // 2.图片分辨率最大: 4096*4096, 最短边不低于320
  263. // 3.图片长边与短边比例在3以内
  264. if ($refImg['size'] > 60 * 1024 * 1024) {
  265. Utils::throwError('1003:图片' . $index . '大小不能超过50MB');
  266. }
  267. $minDimension = min($refImg['width'], $refImg['height']);
  268. $maxDimension = max($refImg['width'], $refImg['height']);
  269. if ($maxDimension > 4096 || $minDimension < 320) {
  270. Utils::throwError('1003:图片' . $index . '分辨率不能超过4096*4096,且最短边不能低于320');
  271. }
  272. if ($maxDimension / $minDimension > 3) {
  273. Utils::throwError('1003:图片' . $index . '长边与短边比例不能超过3');
  274. }
  275. }
  276. /**
  277. * 查询任务状态
  278. *
  279. * @param string $taskId
  280. * @return JsonResponse
  281. */
  282. public function taskStatus(string $taskId): JsonResponse
  283. {
  284. $task = \App\Models\MpGeneratePicTask::where('id', $taskId)->first();
  285. if (!$task) {
  286. return response()->json([
  287. 'success' => false,
  288. 'message' => '任务不存在'
  289. ], 404);
  290. }
  291. return response()->json([
  292. 'success' => true,
  293. 'data' => [
  294. 'task_id' => $task->id,
  295. 'status' => $task->status,
  296. 'result_url' => $task->result_url,
  297. 'error_message' => $task->error_message,
  298. 'created_at' => $task->created_at,
  299. 'started_at' => $task->started_at,
  300. 'completed_at' => $task->completed_at
  301. ]
  302. ]);
  303. }
  304. /**
  305. * 获取任务列表
  306. *
  307. * @param Request $request
  308. * @return mixed
  309. */
  310. public function taskList(Request $request)
  311. {
  312. $data = $request->all();
  313. $result = $this->aiImageGenerationService->getTaskList($data);
  314. return $this->success($result, [new AIGenerationTransformer(), 'newBuildTaskList']);
  315. }
  316. /**
  317. * 批量上传图片或视频
  318. *
  319. * @param Request $request
  320. * @return void
  321. */
  322. public function batchUploadImg(Request $request) {
  323. $refImgUrls = [];
  324. $refVideoUrls = [];
  325. // 处理上传的图片文件
  326. if ($request->hasFile('ref_img_file')) {
  327. $refImgFiles = is_array($request->file('ref_img_file'))
  328. ? $request->file('ref_img_file')
  329. : [$request->file('ref_img_file')];
  330. foreach ($refImgFiles as $index => $refImgFile) {
  331. // 验证图片分辨率和比例
  332. $refImgInfo = @getimagesize($refImgFile->getPathname());
  333. if (!$refImgInfo) {
  334. Utils::throwError('1003:图片' . ($index + 1) . '格式无效');
  335. }
  336. $refImg = [
  337. 'width' => $refImgInfo[0],
  338. 'height' => $refImgInfo[1],
  339. 'size' => $refImgFile->getSize(),
  340. ];
  341. // 验证图片规格
  342. $this->validateImageSpecs($refImg, $index + 1);
  343. // 上传图片到服务器并获取URL
  344. $filename = randStr(10) . '.' . $refImgFile->getClientOriginalExtension();
  345. $uploaded_url = uploadStreamByTos('image', file_get_contents($refImgFile->getPathname()), $filename);
  346. if (!$uploaded_url) {
  347. Utils::throwError('1003:图片' . ($index + 1) . '保存失败');
  348. }
  349. $refImgUrls[] = $uploaded_url;
  350. }
  351. }
  352. // 处理图片二进制内容(ref_img数组)
  353. if ($request->has('ref_img')) {
  354. $refImgs = $request->input('ref_img');
  355. // 确保是数组格式
  356. if (!is_array($refImgs)) {
  357. $refImgs = [$refImgs];
  358. }
  359. foreach ($refImgs as $index => $refImgBinary) {
  360. $tempFile = null;
  361. $tempPath = null;
  362. try {
  363. // 解码base64(如果是base64格式)
  364. if (preg_match('/^data:image\/(\w+);base64,/', $refImgBinary, $matches)) {
  365. $refImgBinary = base64_decode(substr($refImgBinary, strpos($refImgBinary, ',') + 1));
  366. }
  367. // 创建临时文件以验证图片
  368. $tempFile = tmpfile();
  369. $tempPath = stream_get_meta_data($tempFile)['uri'];
  370. fwrite($tempFile, $refImgBinary);
  371. // 验证图片分辨率和比例
  372. $refImgInfo = @getimagesize($tempPath);
  373. if (!$refImgInfo) {
  374. Utils::throwError('1003:图片' . ($index + 1) . '格式无效');
  375. }
  376. $refImg = [
  377. 'width' => $refImgInfo[0],
  378. 'height' => $refImgInfo[1],
  379. 'size' => strlen($refImgBinary),
  380. ];
  381. // 验证图片规格
  382. $this->validateImageSpecs($refImg, $index + 1);
  383. // 上传图片到服务器并获取URL(默认使用.jpg格式)
  384. $filename = randStr(10) . '.jpg';
  385. $uploaded_url = uploadStreamByTos('image', $refImgBinary, $filename);
  386. if (!$uploaded_url) {
  387. Utils::throwError('1003:图片' . ($index + 1) . '保存失败');
  388. }
  389. $refImgUrls[] = $uploaded_url;
  390. } finally {
  391. // 确保临时文件被关闭和删除
  392. if ($tempFile) {
  393. fclose($tempFile);
  394. }
  395. }
  396. }
  397. }
  398. // 处理上传的视频文件
  399. if ($request->hasFile('ref_video_file')) {
  400. $refVideoFiles = is_array($request->file('ref_video_file'))
  401. ? $request->file('ref_video_file')
  402. : [$request->file('ref_video_file')];
  403. foreach ($refVideoFiles as $index => $refVideoFile) {
  404. // 验证视频文件大小(最大100MB)
  405. if ($refVideoFile->getSize() > 100 * 1024 * 1024) {
  406. Utils::throwError('1003:视频' . ($index + 1) . '大小不能超过100MB');
  407. }
  408. // 验证视频文件类型
  409. $allowedMimeTypes = ['video/mp4', 'video/mpeg', 'video/quicktime', 'video/x-msvideo'];
  410. if (!in_array($refVideoFile->getMimeType(), $allowedMimeTypes)) {
  411. Utils::throwError('1003:视频' . ($index + 1) . '格式无效,仅支持mp4、mpeg、mov、avi格式');
  412. }
  413. // 上传视频到服务器并获取URL
  414. $filename = randStr(10) . '.' . $refVideoFile->getClientOriginalExtension();
  415. $uploaded_url = uploadStreamByTos('video', file_get_contents($refVideoFile->getPathname()), $filename);
  416. if (!$uploaded_url) {
  417. Utils::throwError('1003:视频' . ($index + 1) . '保存失败');
  418. }
  419. $refVideoUrls[] = $uploaded_url;
  420. }
  421. }
  422. // 处理视频二进制内容(ref_video数组)
  423. if ($request->has('ref_video')) {
  424. $refVideos = $request->input('ref_video');
  425. // 确保是数组格式
  426. if (!is_array($refVideos)) {
  427. $refVideos = [$refVideos];
  428. }
  429. foreach ($refVideos as $index => $refVideoBinary) {
  430. try {
  431. // 解码base64(如果是base64格式)
  432. if (preg_match('/^data:video\/(\w+);base64,/', $refVideoBinary, $matches)) {
  433. $extension = $matches[1];
  434. $refVideoBinary = base64_decode(substr($refVideoBinary, strpos($refVideoBinary, ',') + 1));
  435. } else {
  436. $extension = 'mp4'; // 默认扩展名
  437. }
  438. // 验证视频大小(最大100MB)
  439. if (strlen($refVideoBinary) > 100 * 1024 * 1024) {
  440. Utils::throwError('1003:视频' . ($index + 1) . '大小不能超过100MB');
  441. }
  442. // 上传视频到服务器并获取URL
  443. $filename = randStr(10) . '.' . $extension;
  444. $uploaded_url = uploadStreamByTos('video', $refVideoBinary, $filename);
  445. if (!$uploaded_url) {
  446. Utils::throwError('1003:视频' . ($index + 1) . '保存失败');
  447. }
  448. $refVideoUrls[] = $uploaded_url;
  449. } catch (\Exception $e) {
  450. Utils::throwError('1003:视频' . ($index + 1) . '处理失败:' . $e->getMessage());
  451. }
  452. }
  453. }
  454. return $this->success([
  455. 'image_urls' => $refImgUrls,
  456. 'video_urls' => $refVideoUrls
  457. ]);
  458. }
  459. }