AIImageGenerationService.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050
  1. <?php
  2. namespace App\Services\AIGeneration;
  3. use App\Models\MpGeneratePicTask;
  4. use App\Services\VolcEngineService;
  5. use GuzzleHttp\Client;
  6. use GuzzleHttp\Exception\GuzzleException;
  7. use Illuminate\Support\Facades\DB;
  8. use Illuminate\Support\Facades\Log;
  9. use Illuminate\Support\Facades\Redis;
  10. use App\Consts\BaseConst;
  11. class AIImageGenerationService
  12. {
  13. private $volcEngineService;
  14. private $httpClient;
  15. // 模型常量
  16. const MODEL_JIMENG_4 = 'jimeng_4.0';
  17. const MODEL_NANO_BANANA_2 = 'NanoBanana2';
  18. const MODEL_NANO_BANANA_PRO = 'NanoBananaPro';
  19. public function __construct(VolcEngineService $volcEngineService)
  20. {
  21. $this->volcEngineService = $volcEngineService;
  22. $this->httpClient = new Client([
  23. 'timeout' => 300,
  24. ]);
  25. }
  26. /**
  27. * 异步创建图片生成任务
  28. *
  29. * @param array $params
  30. * @return MpGeneratePicTask
  31. */
  32. public function createImageGenerationTask(array $params): MpGeneratePicTask
  33. {
  34. // // 获取模型类型,默认使用即梦AI 4.0
  35. // $model = $params['model'] ?? self::MODEL_JIMENG_4;
  36. // 获取模型类型,默认使用即梦seedream5.0-lite
  37. $model = $params['model'] ?? 'doubao-seedream-5-0-lite-260128';
  38. // 检查是否有正在处理的任务(仅即梦AI需要排队)
  39. if ($model === self::MODEL_JIMENG_4 && $this->hasProcessingTask()) {
  40. // 如果有正在处理的任务,创建排队任务
  41. return $this->createQueuedTask($params);
  42. }
  43. // 生成唯一的任务ID
  44. $taskId = 'img_gen_' . time() . '_' . uniqid();
  45. // 创建任务记录
  46. $task = MpGeneratePicTask::create([
  47. 'task_id' => $taskId,
  48. 'alias_segment_id' => $params['alias_segment_id'] ?? 0,
  49. 'prompt' => $params['prompt'] ?? '',
  50. 'width' => $params['width'] ?? 1600,
  51. 'height' => $params['height'] ?? 2848,
  52. 'image_num' => $params['image_num'] ?? 1,
  53. 'scale' => $params['scale'] ?? 50,
  54. 'ref_img_url' => $params['ref_img_urls'] ?? null,
  55. 'mask_img_url' => $params['mask_img_url'] ?? null,
  56. 'extra_params' => $params['extra_params'] ?? null,
  57. 'status' => MpGeneratePicTask::STATUS_PENDING,
  58. 'model' => $model,
  59. ]);
  60. // 即梦AI和NanoBanana系列立即提交任务,火山API在定时任务中提交
  61. if ($model === self::MODEL_JIMENG_4) {
  62. $this->submitTaskToJimengApi($task);
  63. } elseif (in_array($model, BaseConst::NANO_BANANA_MODELS)) {
  64. $this->submitTaskToNanoBananaApi($task);
  65. }
  66. return $task;
  67. }
  68. /**
  69. * 检查是否有正在处理的任务
  70. *
  71. * @return bool
  72. */
  73. private function hasProcessingTask(): bool
  74. {
  75. return MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PROCESSING)
  76. ->exists();
  77. }
  78. /**
  79. * 创建排队任务
  80. *
  81. * @param array $params
  82. * @return MpGeneratePicTask
  83. */
  84. private function createQueuedTask(array $params): MpGeneratePicTask
  85. {
  86. // 生成唯一的任务ID
  87. $taskId = 'img_gen_' . time() . '_' . uniqid();
  88. // 获取模型类型
  89. $model = $params['model'] ?? self::MODEL_JIMENG_4;
  90. // 创建排队状态的任务记录
  91. return MpGeneratePicTask::create([
  92. 'task_id' => $taskId,
  93. 'alias_segment_id' => $params['alias_segment_id'] ?? 0,
  94. 'prompt' => $params['prompt'] ?? '',
  95. 'width' => $params['width'] ?? 1600,
  96. 'height' => $params['height'] ?? 2848,
  97. 'image_num' => $params['image_num'] ?? 1,
  98. 'scale' => $params['scale'] ?? 50,
  99. 'ref_img_url' => $params['ref_img_urls'] ?? null,
  100. 'mask_img_url' => $params['mask_img_url'] ?? null,
  101. 'extra_params' => $params['extra_params'] ?? null,
  102. 'status' => MpGeneratePicTask::STATUS_PENDING,
  103. 'model' => $model,
  104. ]);
  105. }
  106. /**
  107. * 提交任务到即梦AI API
  108. *
  109. * @param MpGeneratePicTask $task
  110. * @return void
  111. */
  112. private function submitTaskToJimengApi(MpGeneratePicTask $task): void
  113. {
  114. try {
  115. // 验证环境变量配置
  116. $accessKey = env('VOLC_AI_AK');
  117. $secretKey = env('VOLC_AI_SK');
  118. if (empty($accessKey) || empty($secretKey)) {
  119. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  120. 'error_message' => '火山引擎访问密钥未配置,请检查环境变量AK和SK'
  121. ]);
  122. return;
  123. }
  124. //构建即梦AI 4.0 API请求参数
  125. $apiParams = [
  126. 'req_key' => 'jimeng_t2i_v40',
  127. 'prompt' => $task->prompt,
  128. 'scale' => $task->scale / 100, //为0-1范围
  129. ];
  130. // 添加可选参数
  131. if ($task->image_num > 1) {
  132. $apiParams['force_single'] = false;
  133. } else {
  134. $apiParams['force_single'] = true;
  135. }
  136. // 参考图片
  137. if ($task->ref_img_url) {
  138. $apiParams['image_urls'] = $task->ref_img_url;
  139. }
  140. // if ($task->mask_img_url) {
  141. // $apiParams['image_urls'] = $task->mask_img_url;
  142. // }
  143. // 尺寸参数
  144. $area = $task->width * $task->height; //计算面积
  145. if ($area >= 1024*1024 && $area <= 4096*4096) {
  146. $apiParams['size'] = (int)$area;
  147. }
  148. dLog('generate')->info('即梦AI文生图参数: ', $apiParams);
  149. logDB('generate', 'info', '即梦AI文生图任务提交', ['task_id' => $task->task_id, 'params' => $apiParams]);
  150. //调用即梦AI 4.0 API提交任务
  151. $response = $this->volcEngineService->request(
  152. 'POST',
  153. 'visual.volcengineapi.com', //即梦AI API域名
  154. '/', // 使用根路径
  155. [], // Query参数
  156. json_encode($apiParams),
  157. $accessKey,
  158. $secretKey,
  159. 'cv', // 服务标识
  160. 'cn-north-1', //区域
  161. 'CVSync2AsyncSubmitTask',
  162. '2022-08-31',
  163. 'application/json'
  164. );
  165. $responseData = json_decode($response['body'], true);
  166. dLog('generate')->info('即梦AI 4.0 API提交任务响应: ', $responseData);
  167. logDB('generate', 'info', '即梦AI 4.0 API提交任务响应', ['task_id' => $task->task_id, 'response' => $responseData]);
  168. if ($responseData['code'] !== 10000) {
  169. // API返回错误
  170. logDB('generate', 'error', '即梦AI任务提交失败', ['task_id' => $task->task_id, 'error' => $responseData['message'] ?? 'API Error']);
  171. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  172. 'error_message' => $responseData['message'] ?? 'API Error',
  173. 'extra_params' => $responseData['data'] ?? []
  174. ]);
  175. } else {
  176. // 任务提交成功,更新状态为处理中
  177. $updateData = [];
  178. //存储API返回的任务ID
  179. if (isset($responseData['data']['task_id'])) {
  180. $updateData['task_id'] = $responseData['data']['task_id'];
  181. $updateData['extra_params'] = $responseData ?? [];
  182. }
  183. $task->updateStatus(MpGeneratePicTask::STATUS_PROCESSING, $updateData);
  184. }
  185. } catch (\Exception $e) {
  186. // 记录错误
  187. logDB('generate', 'error', '即梦AI任务提交异常', ['task_id' => $task->task_id, 'error' => $e->getMessage()]);
  188. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  189. 'error_message' => 'API请求失败: ' . $e->getMessage()
  190. ]);
  191. }
  192. }
  193. /**
  194. * 提交任务到GPT-Image2 API
  195. *
  196. * @param MpGeneratePicTask $task
  197. * @return void
  198. */
  199. public function submitTaskToGptImage2Api(MpGeneratePicTask $task): void
  200. {
  201. try {
  202. // 验证环境变量配置
  203. $apiKey = env('GPT_IMAGE2_API_KEY');
  204. if (empty($apiKey)) {
  205. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  206. 'error_message' => 'GPT-Image2 API KEY未配置,请检查环境变量GPT_IMAGE2_API_KEY'
  207. ]);
  208. return;
  209. }
  210. // 更新状态为处理中
  211. $task->updateStatus(MpGeneratePicTask::STATUS_PROCESSING);
  212. // 构建GPT-Image2 API请求参数
  213. $apiParams = [
  214. 'model' => 'gpt-image-2',
  215. 'prompt' => $task->prompt,
  216. ];
  217. // 尺寸参数
  218. if ($task->width && $task->height) {
  219. $apiParams['size'] = $task->width . 'x' . $task->height;
  220. }
  221. // 参考图片
  222. if ($task->ref_img_url) {
  223. $imageUrl = is_array($task->ref_img_url) ? $task->ref_img_url[0] : $task->ref_img_url;
  224. $apiParams['image_url'] = [
  225. 'url' => $imageUrl
  226. ];
  227. }
  228. dLog('generate')->info('GPT-Image2 API参数: ', $apiParams);
  229. logDB('generate', 'info', 'GPT-Image2任务提交', ['task_id' => $task->task_id, 'params' => $apiParams]);
  230. // 调用GPT-Image2 API
  231. $response = $this->httpClient->post('https://token.ithinkai.cn/v1/images/generations', [
  232. 'headers' => [
  233. 'Authorization' => 'Bearer ' . $apiKey,
  234. 'Content-Type' => 'application/json',
  235. ],
  236. 'json' => $apiParams,
  237. ]);
  238. $responseData = json_decode($response->getBody()->getContents(), true);
  239. dLog('generate')->info('GPT-Image2 API响应: ', $responseData);
  240. logDB('generate', 'info', 'GPT-Image2 API响应', ['task_id' => $task->task_id, 'response' => $responseData]);
  241. if (isset($responseData['error'])) {
  242. // API返回错误
  243. logDB('generate', 'error', 'GPT-Image2任务失败', ['task_id' => $task->task_id, 'error' => $responseData['error']['message'] ?? 'API Error']);
  244. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  245. 'error_message' => $responseData['error']['message'] ?? 'API Error',
  246. 'result_json' => $responseData
  247. ]);
  248. } else {
  249. // 任务提交成功,直接处理结果
  250. $this->processGptImage2ApiResponse($task, $responseData);
  251. }
  252. } catch (\Exception $e) {
  253. // 记录错误
  254. dLog('generate')->error('GPT-Image2 API请求失败: ' . $e->getMessage());
  255. logDB('generate', 'error', 'GPT-Image2任务异常', ['task_id' => $task->task_id, 'error' => $e->getMessage()]);
  256. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  257. 'error_message' => 'API请求失败: ' . $e->getMessage()
  258. ]);
  259. }
  260. }
  261. /**
  262. * 处理GPT-Image2 API响应结果
  263. *
  264. * @param MpGeneratePicTask $task
  265. * @param array $responseData
  266. * @return void
  267. */
  268. private function processGptImage2ApiResponse(MpGeneratePicTask $task, array $responseData): void
  269. {
  270. try {
  271. if (!isset($responseData['data']) || empty($responseData['data'])) {
  272. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  273. 'error_message' => '未返回图片数据',
  274. 'result_json' => $responseData
  275. ]);
  276. return;
  277. }
  278. $result_urls = [];
  279. foreach ($responseData['data'] as $imageData) {
  280. if (isset($imageData['url'])) {
  281. $url = $imageData['url'];
  282. $pic_name = 'ai_generation_gpt_' . time() . '_' . uniqid();
  283. $pic_ext = getImgExtFromUrl($url);
  284. $pic_name = $pic_name . $pic_ext;
  285. if ($pic_ext === '.png') {
  286. $comporessed_data = compressRemoteImageUrlToSize($url);
  287. $url = uploadStreamByTos('image', $comporessed_data, $pic_name);
  288. } else {
  289. // 将图片另存到tos
  290. $url = uploadStreamByTos('image', file_get_contents($url), $pic_name);
  291. }
  292. $result_urls[] = $url;
  293. }
  294. }
  295. if (empty($result_urls)) {
  296. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  297. 'error_message' => '所有图片生成失败',
  298. 'result_json' => $responseData
  299. ]);
  300. return;
  301. }
  302. // 更新任务状态为成功
  303. $task->updateStatus(MpGeneratePicTask::STATUS_SUCCESS, [
  304. 'result_url' => $result_urls,
  305. 'result_json' => $responseData
  306. ]);
  307. // 同步调整分镜图片状态和结果
  308. $segment_id = getProp($task, 'alias_segment_id');
  309. if ($segment_id) {
  310. DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
  311. 'img_url' => $result_urls[0],
  312. 'pic_task_status' => '已完成',
  313. ]);
  314. // 如果是首帧图ID则更新anime表
  315. if (Redis::sismember('anime_first_frame_urls', $segment_id)) {
  316. $anime_id = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->value('anime_id');
  317. DB::table('mp_animes')->where('id', $anime_id)->update(['first_frame_url' => $result_urls[0], 'updated_at' => date('Y-m-d H:i:s')]);
  318. Redis::srem('anime_first_frame_urls', $segment_id);
  319. }
  320. }
  321. } catch (\Exception $e) {
  322. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  323. 'error_message' => '处理响应失败: ' . $e->getMessage()
  324. ]);
  325. }
  326. }
  327. /**
  328. * 提交任务到火山图片生成API
  329. *
  330. * @param MpGeneratePicTask $task
  331. * @return void
  332. */
  333. public function submitTaskToVolcApi(MpGeneratePicTask $task): void
  334. {
  335. try {
  336. // 验证环境变量配置
  337. $apiKey = env('VOLC_AI_API_KEY');
  338. if (empty($apiKey)) {
  339. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  340. 'error_message' => '火山引擎API KEY未配置,请检查环境变量VOLC_AI_API_KEY'
  341. ]);
  342. return;
  343. }
  344. // 更新状态为处理中
  345. $task->updateStatus(MpGeneratePicTask::STATUS_PROCESSING);
  346. // 构建火山图片生成API请求参数
  347. $apiParams = [
  348. 'model' => isset(BaseConst::MODELS_MAP[$task->model]) ? BaseConst::MODELS_MAP[$task->model] : $task->model,
  349. 'prompt' => $task->prompt,
  350. 'watermark' => false,
  351. ];
  352. // 尺寸参数
  353. if ($task->width && $task->height) {
  354. $apiParams['size'] = $task->width . 'x' . $task->height;
  355. }
  356. // 参考图片
  357. if ($task->ref_img_url) {
  358. if (is_array($task->ref_img_url)) {
  359. // $apiParams['image'] = count($task->ref_img_url) === 1 ? $task->ref_img_url[0] : $task->ref_img_url;
  360. $apiParams['image'] = $task->ref_img_url;
  361. } else {
  362. $apiParams['image'] = $task->ref_img_url;
  363. }
  364. }
  365. // 生成多图或者单图
  366. if ((int)$task->image_num === 1) {
  367. $apiParams['sequential_image_generation'] = 'disabled';
  368. }else if ($task->image_num > 1) {
  369. $apiParams['sequential_image_generation'] = 'auto';
  370. $apiParams['sequential_image_generation_options']['max_images'] = $task->image_num;
  371. }
  372. dLog('generate')->info('火山图片生成API参数: ', $apiParams);
  373. logDB('generate', 'info', '火山图片生成任务提交', ['task_id' => $task->task_id, 'params' => $apiParams]);
  374. // 调用火山图片生成API
  375. $response = $this->httpClient->post('https://ark.cn-beijing.volces.com/api/v3/images/generations', [
  376. 'headers' => [
  377. 'Authorization' => 'Bearer ' . $apiKey,
  378. 'Content-Type' => 'application/json',
  379. ],
  380. 'json' => $apiParams,
  381. ]);
  382. $responseData = json_decode($response->getBody()->getContents(), true);
  383. dLog('generate')->info('火山图片生成API响应: ', $responseData);
  384. logDB('generate', 'info', '火山图片生成API响应', ['task_id' => $task->task_id, 'response' => $responseData]);
  385. if (isset($responseData['error'])) {
  386. // API返回错误
  387. logDB('generate', 'error', '火山图片生成任务失败', ['task_id' => $task->task_id, 'error' => $responseData['error']['message'] ?? 'API Error']);
  388. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  389. 'error_message' => $responseData['error']['message'] ?? 'API Error',
  390. 'result_json' => $responseData
  391. ]);
  392. } else {
  393. // 任务提交成功,直接处理结果
  394. $this->processVolcApiResponse($task, $responseData);
  395. }
  396. } catch (\Exception $e) {
  397. // 记录错误
  398. dLog('generate')->error('火山API请求失败: ' . $e->getMessage());
  399. logDB('generate', 'error', '火山图片生成任务异常', ['task_id' => $task->task_id, 'error' => $e->getMessage()]);
  400. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  401. 'error_message' => 'API请求失败: ' . $e->getMessage()
  402. ]);
  403. }
  404. }
  405. /**
  406. * 处理火山API响应结果
  407. *
  408. * @param MpGeneratePicTask $task
  409. * @param array $responseData
  410. * @return void
  411. */
  412. private function processVolcApiResponse(MpGeneratePicTask $task, array $responseData): void
  413. {
  414. try {
  415. if (!isset($responseData['data']) || empty($responseData['data'])) {
  416. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  417. 'error_message' => '未返回图片数据',
  418. 'result_json' => $responseData
  419. ]);
  420. return;
  421. }
  422. $result_urls = [];
  423. foreach ($responseData['data'] as $imageData) {
  424. if (isset($imageData['error'])) {
  425. // 单张图片生成失败
  426. dLog('generate')->warning('单张图片生成失败: ', $imageData['error']);
  427. continue;
  428. }
  429. if (isset($imageData['url'])) {
  430. $url = $imageData['url'];
  431. $pic_name = 'ai_generation_' . time() . '_' . uniqid();
  432. $pic_ext = getImgExtFromUrl($url);
  433. $pic_name = $pic_name . $pic_ext;
  434. if ($pic_ext === '.png') {
  435. $comporessed_data = compressRemoteImageUrlToSize($url);
  436. $url = uploadStreamByTos('image', $comporessed_data, $pic_name);
  437. } else {
  438. // 将图片另存到tos
  439. $url = uploadStreamByTos('image', file_get_contents($url), $pic_name);
  440. }
  441. $result_urls[] = $url;
  442. }
  443. }
  444. if (empty($result_urls)) {
  445. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  446. 'error_message' => '所有图片生成失败',
  447. 'result_json' => $responseData
  448. ]);
  449. return;
  450. }
  451. // 更新任务状态为成功
  452. $task->updateStatus(MpGeneratePicTask::STATUS_SUCCESS, [
  453. 'result_url' => $result_urls,
  454. 'result_json' => $responseData
  455. ]);
  456. // 同步调整分镜图片状态和结果
  457. $segment_id = getProp($task, 'alias_segment_id');
  458. if ($segment_id) {
  459. DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
  460. 'img_url' => $result_urls[0],
  461. 'pic_task_status' => '已完成',
  462. ]);
  463. // 如果是首帧图ID则更新anime表
  464. if (Redis::sismember('anime_first_frame_urls', $segment_id)) {
  465. $anime_id = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->value('anime_id');
  466. DB::table('mp_animes')->where('id', $anime_id)->update(['first_frame_url' => $result_urls[0], 'updated_at' => date('Y-m-d H:i:s')]);
  467. Redis::srem('anime_first_frame_urls', $segment_id);
  468. }
  469. }
  470. } catch (\Exception $e) {
  471. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  472. 'error_message' => '处理响应失败: ' . $e->getMessage()
  473. ]);
  474. }
  475. }
  476. public function getTaskList($data)
  477. {
  478. $task_id = getProp($data, 'task_id');
  479. $status = getProp($data, 'status');
  480. $query = MpGeneratePicTask::select('*');
  481. if ($task_id) {
  482. $query->where('id', $task_id);
  483. }
  484. if ($status) {
  485. $query->where('status', $status);
  486. }
  487. return $query->orderBy('created_at', 'desc')->paginate();
  488. }
  489. /**
  490. * 查询任务状态
  491. *
  492. * @param MpGeneratePicTask $task
  493. * @return array
  494. */
  495. public function queryTaskStatus(MpGeneratePicTask $task): array
  496. {
  497. // 根据模型类型调用不同的查询方法
  498. if ($task->model === self::MODEL_JIMENG_4) {
  499. return $this->queryJimengTaskStatus($task);
  500. } elseif (in_array($task->model, BaseConst::NANO_BANANA_MODELS)) {
  501. return $this->queryNanoBananaTaskStatus($task);
  502. } else {
  503. // 火山API是同步返回结果,不需要查询
  504. return [
  505. 'status' => $task->status === MpGeneratePicTask::STATUS_SUCCESS ? 'success' : 'failed',
  506. 'error_message' => $task->error_message,
  507. 'result_url' => $task->result_url,
  508. 'result_json' => $task->result_json ?? []
  509. ];
  510. }
  511. }
  512. /**
  513. * 查询即梦AI任务状态
  514. *
  515. * @param MpGeneratePicTask $task
  516. * @return array
  517. */
  518. private function queryJimengTaskStatus(MpGeneratePicTask $task): array
  519. {
  520. try {
  521. // 获取API任务ID
  522. $apiTaskId = $task->task_id ?? null;
  523. if (!$apiTaskId) {
  524. return [
  525. 'status' => 'failed',
  526. 'error_message' => 'API任务ID不存在',
  527. 'result_url' => null
  528. ];
  529. }
  530. //构建查询参数
  531. $apiParams = [
  532. 'req_key' => 'jimeng_t2i_v40',
  533. 'task_id' => $apiTaskId,
  534. 'req_json' => '{"return_url":true}' // 返回URL格式
  535. ];
  536. //调用即梦AI 4.0 API查询任务状态
  537. $response = $this->volcEngineService->request(
  538. 'POST',
  539. 'visual.volcengineapi.com',
  540. '/', // 使用根路径
  541. [], // Query参数
  542. json_encode($apiParams),
  543. env('VOLC_AI_AK'),
  544. env('VOLC_AI_SK'),
  545. 'cv',
  546. 'cn-north-1',
  547. 'CVSync2AsyncGetResult',
  548. '2022-08-31',
  549. 'application/json'
  550. );
  551. $responseData = json_decode($response['body'], true);
  552. dLog('generate')->info('即梦AI 4.0 API查询任务状态响应: ', $responseData);
  553. logDB('generate', 'info', '即梦AI任务状态查询', ['task_id' => $apiTaskId, 'response' => $responseData]);
  554. if ($responseData['code'] !== 10000) {
  555. // API返回错误
  556. logDB('generate', 'error', '即梦AI任务状态查询失败', ['task_id' => $apiTaskId, 'error' => $responseData['message'] ?? 'API Error']);
  557. return [
  558. 'status' => 'failed',
  559. 'error_message' => $responseData['message'] ?? 'API Error',
  560. 'result_url' => null,
  561. 'result_json' => $responseData ?? []
  562. ];
  563. }
  564. // 解析API响应
  565. $result = $responseData['data'] ?? [];
  566. $taskStatus = $result['status'] ?? 'failed';
  567. $returnData = [
  568. 'status' => $taskStatus,
  569. 'error_message' => null,
  570. 'result_url' => null,
  571. 'result_json' => $responseData ?? []
  572. ];
  573. // 如果任务成功,获取结果URL
  574. if ($taskStatus === 'done' && isset($result['image_urls']) && count($result['image_urls']) > 0) {
  575. $result_urls = [];
  576. foreach ($result['image_urls'] as $url) {
  577. $pic_name = 'ai_generation_' . time() . '_' . uniqid();
  578. $pic_ext = getImgExtFromUrl($url);
  579. $pic_name = $pic_name . $pic_ext;
  580. if ($pic_ext === '.png') {
  581. $comporessed_data = compressRemoteImageUrlToSize($url);
  582. $url = uploadStreamByTos('image', $comporessed_data, $pic_name);
  583. }else {
  584. // 将图片另存到tos
  585. $url = uploadStreamByTos('image', file_get_contents($url), $pic_name);
  586. }
  587. $result_urls[] = $url;
  588. }
  589. $returnData['result_url'] = $result_urls;
  590. $returnData['status'] = 'success';
  591. } elseif (in_array($taskStatus, ['not_found', 'expired'])) {
  592. $returnData['status'] = 'failed';
  593. $returnData['error_message'] = '任务未找到或已过期';
  594. } elseif ($taskStatus === 'failed') {
  595. $returnData['status'] = 'failed';
  596. $returnData['error_message'] = '任务执行失败';
  597. }
  598. // in_queue 或 generating状态保持原status
  599. return $returnData;
  600. } catch (\Exception $e) {
  601. return [
  602. 'status' => 'failed',
  603. 'error_message' => $e->getMessage(),
  604. 'result_url' => null
  605. ];
  606. }
  607. }
  608. /**
  609. * 提交任务到NanoBanana API
  610. *
  611. * @param MpGeneratePicTask $task
  612. * @return void
  613. */
  614. private function submitTaskToNanoBananaApi(MpGeneratePicTask $task): void
  615. {
  616. try {
  617. // 验证环境变量配置
  618. $apiKey = env('NANO_BANANA_API_KEY');
  619. if (empty($apiKey)) {
  620. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  621. 'error_message' => 'NanoBanana API KEY未配置,请检查环境变量NANO_BANANA_API_KEY'
  622. ]);
  623. return;
  624. }
  625. // 根据模型选择API地址
  626. switch ($task->model) {
  627. case self::MODEL_NANO_BANANA_2:
  628. $apiUrl = 'https://api.wuyinkeji.com/api/async/image_nanoBanana2';
  629. break;
  630. case self::MODEL_NANO_BANANA_PRO:
  631. $apiUrl = 'https://api.wuyinkeji.com/api/async/image_nanoBanana_pro';
  632. break;
  633. default:
  634. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  635. 'error_message' => '不支持的NanoBanana模型类型: ' . $task->model
  636. ]);
  637. logDB('generate', 'error', 'NanoBanana模型类型错误', ['task_id' => $task->task_id, 'model' => $task->model]);
  638. return;
  639. }
  640. // 构建API请求参数
  641. $apiParams = [
  642. 'key' => $apiKey,
  643. 'prompt' => $task->prompt,
  644. ];
  645. // 尺寸参数(根据宽高判断)
  646. $area = $task->width * $task->height;
  647. if ($area >= 1024*1024 && $area < 2048*2048) {
  648. $apiParams['size'] = '1k';
  649. } elseif ($area >= 2048*2048) {
  650. $apiParams['size'] = '2k';
  651. } else {
  652. $apiParams['size'] = '1k'; // 默认1k
  653. }
  654. // 画面比例
  655. if ($task->width && $task->height) {
  656. $ratio = $task->width . ':' . $task->height;
  657. // 简化比例
  658. $gcd = $this->gcd($task->width, $task->height);
  659. $ratio = ($task->width / $gcd) . ':' . ($task->height / $gcd);
  660. $apiParams['aspectRatio'] = $ratio;
  661. }
  662. // 参考图片
  663. if ($task->ref_img_url) {
  664. $apiParams['urls'] = is_array($task->ref_img_url) ? $task->ref_img_url : [$task->ref_img_url];
  665. }
  666. dLog('generate')->info('NanoBanana API参数: ', $apiParams);
  667. logDB('generate', 'info', 'NanoBanana任务提交', ['task_id' => $task->task_id, 'model' => $task->model, 'params' => $apiParams]);
  668. // 调用NanoBanana API
  669. $response = $this->httpClient->post($apiUrl, [
  670. 'query' => ['key' => $apiKey],
  671. 'json' => $apiParams,
  672. ]);
  673. $responseData = json_decode($response->getBody()->getContents(), true);
  674. dLog('generate')->info('NanoBanana API响应: ', $responseData);
  675. logDB('generate', 'info', 'NanoBanana API响应', ['task_id' => $task->task_id, 'response' => $responseData]);
  676. if ($responseData['code'] !== 200) {
  677. // API返回错误
  678. logDB('generate', 'error', 'NanoBanana任务提交失败', ['task_id' => $task->task_id, 'error' => $responseData['msg'] ?? 'API Error']);
  679. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  680. 'error_message' => $responseData['msg'] ?? 'API Error',
  681. 'extra_params' => $responseData
  682. ]);
  683. } else {
  684. // 任务提交成功,更新状态为处理中
  685. $updateData = [];
  686. // 存储API返回的任务ID
  687. if (isset($responseData['data']['id'])) {
  688. $updateData['task_id'] = $responseData['data']['id'];
  689. $updateData['extra_params'] = $responseData;
  690. }
  691. $task->updateStatus(MpGeneratePicTask::STATUS_PROCESSING, $updateData);
  692. }
  693. } catch (\Exception $e) {
  694. // 记录错误
  695. logDB('generate', 'error', 'NanoBanana任务提交异常', ['task_id' => $task->task_id, 'error' => $e->getMessage()]);
  696. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  697. 'error_message' => 'API请求失败: ' . $e->getMessage()
  698. ]);
  699. }
  700. }
  701. /**
  702. * 查询NanoBanana任务状态
  703. *
  704. * @param MpGeneratePicTask $task
  705. * @return array
  706. */
  707. private function queryNanoBananaTaskStatus(MpGeneratePicTask $task): array
  708. {
  709. try {
  710. // 获取API任务ID
  711. $apiTaskId = $task->task_id ?? null;
  712. $apiKey = env('NANO_BANANA_API_KEY');
  713. if (!$apiTaskId) {
  714. return [
  715. 'status' => 'failed',
  716. 'error_message' => 'API任务ID不存在',
  717. 'result_url' => null
  718. ];
  719. }
  720. if (empty($apiKey)) {
  721. return [
  722. 'status' => 'failed',
  723. 'error_message' => 'NanoBanana API KEY未配置',
  724. 'result_url' => null
  725. ];
  726. }
  727. // 调用统一查询接口
  728. $response = $this->httpClient->get('https://api.wuyinkeji.com/api/async/detail', [
  729. 'query' => [
  730. 'key' => $apiKey,
  731. 'id' => $apiTaskId
  732. ]
  733. ]);
  734. $responseData = json_decode($response->getBody()->getContents(), true);
  735. dLog('generate')->info('NanoBanana任务状态查询响应: ', $responseData);
  736. logDB('generate', 'info', 'NanoBanana任务状态查询', ['task_id' => $apiTaskId, 'response' => $responseData]);
  737. if ($responseData['code'] !== 200) {
  738. // API返回错误
  739. logDB('generate', 'error', 'NanoBanana任务状态查询失败', ['task_id' => $apiTaskId, 'error' => $responseData['msg'] ?? 'API Error']);
  740. return [
  741. 'status' => 'failed',
  742. 'error_message' => $responseData['msg'] ?? 'API Error',
  743. 'result_url' => null,
  744. 'result_json' => $responseData
  745. ];
  746. }
  747. // 解析API响应
  748. $data = $responseData['data'] ?? [];
  749. $taskStatus = $data['status'] ?? 0;
  750. $returnData = [
  751. 'status' => 'processing',
  752. 'error_message' => null,
  753. 'result_url' => null,
  754. 'result_json' => $responseData
  755. ];
  756. // status: 2表示成功
  757. if ($taskStatus === 2 && isset($data['result']) && !empty($data['result'])) {
  758. $result_urls = [];
  759. foreach ($data['result'] as $url) {
  760. $pic_name = 'ai_generation_nanobanana_' . time() . '_' . uniqid();
  761. $pic_ext = getImgExtFromUrl($url);
  762. $pic_name = $pic_name . $pic_ext;
  763. if ($pic_ext === '.png') {
  764. $comporessed_data = compressRemoteImageUrlToSize($url);
  765. $url = uploadStreamByTos('image', $comporessed_data, $pic_name);
  766. } else {
  767. // 将图片另存到tos
  768. $url = uploadStreamByTos('image', file_get_contents($url), $pic_name);
  769. }
  770. $result_urls[] = $url;
  771. }
  772. $returnData['result_url'] = $result_urls;
  773. $returnData['status'] = 'success';
  774. } elseif ($taskStatus === 3) {
  775. // status: 3表示失败
  776. $returnData['status'] = 'failed';
  777. $returnData['error_message'] = $data['message'] ?? '任务执行失败';
  778. }
  779. // status: 0或1表示处理中,保持processing状态
  780. return $returnData;
  781. } catch (\Exception $e) {
  782. return [
  783. 'status' => 'failed',
  784. 'error_message' => $e->getMessage(),
  785. 'result_url' => null
  786. ];
  787. }
  788. }
  789. /**
  790. * 计算最大公约数(用于简化比例)
  791. *
  792. * @param int $a
  793. * @param int $b
  794. * @return int
  795. */
  796. private function gcd(int $a, int $b): int
  797. {
  798. while ($b != 0) {
  799. $temp = $b;
  800. $b = $a % $b;
  801. $a = $temp;
  802. }
  803. return $a;
  804. }
  805. /**
  806. * 更新所有待处理任务的状态
  807. *
  808. * @return void
  809. */
  810. public function updatePendingTasks(): void
  811. {
  812. dLog('generate')->info('更新图片状态ing');
  813. // 1. 处理火山API的pending任务(批量提交到队列)
  814. $volcPendingTasks = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PENDING)
  815. ->whereIn('model', BaseConst::VOLC_PIC_MODELS)
  816. ->orderBy('created_at', 'asc')
  817. ->limit(50) // 每次最多处理50个任务,避免超时
  818. ->get();
  819. foreach ($volcPendingTasks as $task) {
  820. dLog('generate')->info('开始分发火山API任务到队列: ' . $task->task_id);
  821. // 分发到队列
  822. \App\Jobs\ProcessVolcImageGenerationJob::dispatch($task->id)->onConnection('redis');
  823. }
  824. // 2. 处理GPT-Image2的pending任务(批量提交到队列)
  825. $gptImage2PendingTasks = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PENDING)
  826. ->whereIn('model', BaseConst::GPT_IMAGE2_MODELS)
  827. ->orderBy('created_at', 'asc')
  828. ->limit(50) // 每次最多处理50个任务,避免超时
  829. ->get();
  830. foreach ($gptImage2PendingTasks as $task) {
  831. dLog('generate')->info('开始分发GPT-Image2任务到队列: ' . $task->task_id);
  832. // 分发到队列
  833. \App\Jobs\ProcessGptImage2GenerationJob::dispatch($task->id)->onConnection('redis');
  834. }
  835. // 3. 获取所有处理中的任务(即梦AI和NanoBanana系列需要查询状态)
  836. $processingTasks = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PROCESSING)
  837. ->whereIn('model', array_merge([self::MODEL_JIMENG_4], BaseConst::NANO_BANANA_MODELS))
  838. ->get();
  839. foreach ($processingTasks as $task) {
  840. $statusInfo = $this->queryTaskStatus($task);
  841. if ($statusInfo['status'] === 'success') {
  842. logDB('generate', 'info', '即梦AI图片生成任务成功', ['task_id' => $task->task_id, 'result_url' => $statusInfo['result_url']]);
  843. $task->updateStatus(MpGeneratePicTask::STATUS_SUCCESS, [
  844. 'result_url' => $statusInfo['result_url'],
  845. 'result_json' => $statusInfo['result_json'] ?? []
  846. ]);
  847. // 同步调整分镜图片状态和结果
  848. $segment_id = getProp($task, 'alias_segment_id');
  849. if ($segment_id) {
  850. DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([
  851. 'img_url' => $statusInfo['result_url'][0],
  852. 'pic_task_status' => '已完成',
  853. ]);
  854. // 如果是首帧图ID则更新anime表
  855. if (Redis::sismember('anime_first_frame_urls', $segment_id)) {
  856. $anime_id = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->value('anime_id');
  857. DB::table('mp_animes')->where('id', $anime_id)->update(['first_frame_url'=>$statusInfo['result_url'][0], 'updated_at'=>date('Y-m-d H:i:s')]);
  858. Redis::srem('anime_first_frame_urls', $segment_id);
  859. }
  860. }
  861. } elseif ($statusInfo['status'] === 'failed') {
  862. logDB('generate', 'error', '即梦AI图片生成任务失败', ['task_id' => $task->task_id, 'error' => $statusInfo['error_message']]);
  863. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  864. 'error_message' => $statusInfo['error_message'],
  865. 'result_json' => $statusInfo['result_json'] ?? []
  866. ]);
  867. if (getProp($task, 'alias_segment_id')) {
  868. DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([
  869. 'pic_task_status' => '失败',
  870. ]);
  871. }
  872. }
  873. // 如果仍然是处理中状态,不做任何操作
  874. //处理:如果任务处理超过12小时,标记为失败
  875. $processingTime = now()->diffInHours($task->created_at);
  876. if ($processingTime > 12) {
  877. logDB('generate', 'warning', '即梦AI图片生成任务超时', ['task_id' => $task->task_id, 'processing_hours' => $processingTime]);
  878. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  879. 'error_message' => '任务处理超时(超过12小时)'
  880. ]);
  881. }
  882. }
  883. // 4. 检查是否有任务完成,如果有则处理下一个排队任务(仅即梦AI)
  884. $this->processNextQueuedTask();
  885. }
  886. /**
  887. * 处理下一个排队的任务
  888. *
  889. * @return void
  890. */
  891. public function processNextQueuedTask(): void
  892. {
  893. // 检查是否还有正在处理的任务(仅即梦AI)
  894. if ($this->hasProcessingTask()) {
  895. return; // 如果还有处理中的任务,不处理新任务
  896. }
  897. // 获取最早的待处理任务(仅即梦AI)
  898. $nextTask = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PENDING)
  899. ->where('model', self::MODEL_JIMENG_4)
  900. ->orderBy('created_at', 'asc')
  901. ->first();
  902. if ($nextTask) {
  903. dLog('generate')->info('开始处理排队任务: ' . $nextTask->task_id);
  904. logDB('generate', 'info', '开始处理即梦AI排队任务', ['task_id' => $nextTask->task_id]);
  905. $this->submitTaskToJimengApi($nextTask);
  906. }
  907. }
  908. /**
  909. * 获取任务队列状态
  910. *
  911. * @return array
  912. */
  913. public function getQueueStatus(): array
  914. {
  915. $processingCount = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PROCESSING)->count();
  916. $pendingCount = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PENDING)->count();
  917. return [
  918. 'processing_count' => $processingCount,
  919. 'pending_count' => $pendingCount,
  920. 'can_submit_new_task' => $processingCount === 0
  921. ];
  922. }
  923. }