AIImageGenerationService.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  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. public function __construct(VolcEngineService $volcEngineService)
  18. {
  19. $this->volcEngineService = $volcEngineService;
  20. $this->httpClient = new Client([
  21. 'timeout' => 300,
  22. ]);
  23. }
  24. /**
  25. * 异步创建图片生成任务
  26. *
  27. * @param array $params
  28. * @return MpGeneratePicTask
  29. */
  30. public function createImageGenerationTask(array $params): MpGeneratePicTask
  31. {
  32. // 获取模型类型,默认使用即梦AI 4.0
  33. $model = $params['model'] ?? self::MODEL_JIMENG_4;
  34. // 检查是否有正在处理的任务(仅即梦AI需要排队)
  35. if ($model === self::MODEL_JIMENG_4 && $this->hasProcessingTask()) {
  36. // 如果有正在处理的任务,创建排队任务
  37. return $this->createQueuedTask($params);
  38. }
  39. // 生成唯一的任务ID
  40. $taskId = 'img_gen_' . time() . '_' . uniqid();
  41. // 创建任务记录
  42. $task = MpGeneratePicTask::create([
  43. 'task_id' => $taskId,
  44. 'alias_segment_id' => $params['alias_segment_id'] ?? 0,
  45. 'prompt' => $params['prompt'] ?? '',
  46. 'width' => $params['width'] ?? 2048,
  47. 'height' => $params['height'] ?? 2048,
  48. 'image_num' => $params['image_num'] ?? 1,
  49. 'scale' => $params['scale'] ?? 50,
  50. 'ref_img_url' => $params['ref_img_urls'] ?? null,
  51. 'mask_img_url' => $params['mask_img_url'] ?? null,
  52. 'extra_params' => $params['extra_params'] ?? null,
  53. 'status' => MpGeneratePicTask::STATUS_PENDING,
  54. 'model' => $model,
  55. ]);
  56. // 仅即梦AI立即提交任务,火山API在定时任务中提交
  57. if ($model === self::MODEL_JIMENG_4) {
  58. $this->submitTaskToJimengApi($task);
  59. }
  60. return $task;
  61. }
  62. /**
  63. * 检查是否有正在处理的任务
  64. *
  65. * @return bool
  66. */
  67. private function hasProcessingTask(): bool
  68. {
  69. return MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PROCESSING)
  70. ->exists();
  71. }
  72. /**
  73. * 创建排队任务
  74. *
  75. * @param array $params
  76. * @return MpGeneratePicTask
  77. */
  78. private function createQueuedTask(array $params): MpGeneratePicTask
  79. {
  80. // 生成唯一的任务ID
  81. $taskId = 'img_gen_' . time() . '_' . uniqid();
  82. // 获取模型类型
  83. $model = $params['model'] ?? self::MODEL_JIMENG_4;
  84. // 创建排队状态的任务记录
  85. return MpGeneratePicTask::create([
  86. 'task_id' => $taskId,
  87. 'alias_segment_id' => $params['alias_segment_id'] ?? 0,
  88. 'prompt' => $params['prompt'] ?? '',
  89. 'width' => $params['width'] ?? 2048,
  90. 'height' => $params['height'] ?? 2048,
  91. 'image_num' => $params['image_num'] ?? 1,
  92. 'scale' => $params['scale'] ?? 50,
  93. 'ref_img_url' => $params['ref_img_urls'] ?? null,
  94. 'mask_img_url' => $params['mask_img_url'] ?? null,
  95. 'extra_params' => $params['extra_params'] ?? null,
  96. 'status' => MpGeneratePicTask::STATUS_PENDING,
  97. 'model' => $model,
  98. ]);
  99. }
  100. /**
  101. * 提交任务到即梦AI API
  102. *
  103. * @param MpGeneratePicTask $task
  104. * @return void
  105. */
  106. private function submitTaskToJimengApi(MpGeneratePicTask $task): void
  107. {
  108. try {
  109. // 验证环境变量配置
  110. $accessKey = env('VOLC_AI_AK');
  111. $secretKey = env('VOLC_AI_SK');
  112. if (empty($accessKey) || empty($secretKey)) {
  113. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  114. 'error_message' => '火山引擎访问密钥未配置,请检查环境变量AK和SK'
  115. ]);
  116. return;
  117. }
  118. //构建即梦AI 4.0 API请求参数
  119. $apiParams = [
  120. 'req_key' => 'jimeng_t2i_v40',
  121. 'prompt' => $task->prompt,
  122. 'scale' => $task->scale / 100, //为0-1范围
  123. ];
  124. // 添加可选参数
  125. if ($task->image_num > 1) {
  126. $apiParams['force_single'] = false;
  127. } else {
  128. $apiParams['force_single'] = true;
  129. }
  130. // 参考图片
  131. if ($task->ref_img_url) {
  132. $apiParams['image_urls'] = $task->ref_img_url;
  133. }
  134. // if ($task->mask_img_url) {
  135. // $apiParams['image_urls'] = $task->mask_img_url;
  136. // }
  137. // 尺寸参数
  138. $area = $task->width * $task->height; //计算面积
  139. if ($area >= 1024*1024 && $area <= 4096*4096) {
  140. $apiParams['size'] = (int)$area;
  141. }
  142. dLog('generate')->info('即梦AI文生图参数: ', $apiParams);
  143. //调用即梦AI 4.0 API提交任务
  144. $response = $this->volcEngineService->request(
  145. 'POST',
  146. 'visual.volcengineapi.com', //即梦AI API域名
  147. '/', // 使用根路径
  148. [], // Query参数
  149. json_encode($apiParams),
  150. $accessKey,
  151. $secretKey,
  152. 'cv', // 服务标识
  153. 'cn-north-1', //区域
  154. 'CVSync2AsyncSubmitTask',
  155. '2022-08-31',
  156. 'application/json'
  157. );
  158. $responseData = json_decode($response['body'], true);
  159. dLog('generate')->info('即梦AI 4.0 API提交任务响应: ', $responseData);
  160. if ($responseData['code'] !== 10000) {
  161. // API返回错误
  162. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  163. 'error_message' => $responseData['message'] ?? 'API Error',
  164. 'extra_params' => $responseData['data'] ?? []
  165. ]);
  166. } else {
  167. // 任务提交成功,更新状态为处理中
  168. $updateData = [];
  169. //存储API返回的任务ID
  170. if (isset($responseData['data']['task_id'])) {
  171. $updateData['task_id'] = $responseData['data']['task_id'];
  172. $updateData['extra_params'] = $responseData ?? [];
  173. }
  174. $task->updateStatus(MpGeneratePicTask::STATUS_PROCESSING, $updateData);
  175. }
  176. } catch (\Exception $e) {
  177. // 记录错误
  178. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  179. 'error_message' => 'API请求失败: ' . $e->getMessage()
  180. ]);
  181. }
  182. }
  183. /**
  184. * 提交任务到火山图片生成API
  185. *
  186. * @param MpGeneratePicTask $task
  187. * @return void
  188. */
  189. private function submitTaskToVolcApi(MpGeneratePicTask $task): void
  190. {
  191. try {
  192. // 验证环境变量配置
  193. $apiKey = env('VOLC_AI_API_KEY');
  194. if (empty($apiKey)) {
  195. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  196. 'error_message' => '火山引擎API KEY未配置,请检查环境变量VOLC_AI_API_KEY'
  197. ]);
  198. return;
  199. }
  200. // 更新状态为处理中
  201. $task->updateStatus(MpGeneratePicTask::STATUS_PROCESSING);
  202. // 构建火山图片生成API请求参数
  203. $apiParams = [
  204. 'model' => $task->model,
  205. 'prompt' => $task->prompt,
  206. 'watermark' => false,
  207. ];
  208. // 尺寸参数
  209. if ($task->width && $task->height) {
  210. $apiParams['size'] = $task->width . 'x' . $task->height;
  211. }
  212. // 参考图片
  213. if ($task->ref_img_url) {
  214. if (is_array($task->ref_img_url)) {
  215. // $apiParams['image'] = count($task->ref_img_url) === 1 ? $task->ref_img_url[0] : $task->ref_img_url;
  216. $apiParams['image'] = $task->ref_img_url;
  217. } else {
  218. $apiParams['image'] = $task->ref_img_url;
  219. }
  220. }
  221. // 生成多图或者单图
  222. if ((int)$task->image_num === 1) {
  223. $apiParams['sequential_image_generation'] = 'disabled';
  224. }else if ($task->image_num > 1) {
  225. $apiParams['sequential_image_generation'] = 'auto';
  226. $apiParams['sequential_image_generation_options']['max_images'] = $task->image_num;
  227. }
  228. dLog('generate')->info('火山图片生成API参数: ', $apiParams);
  229. // 调用火山图片生成API
  230. $response = $this->httpClient->post('https://ark.cn-beijing.volces.com/api/v3/images/generations', [
  231. 'headers' => [
  232. 'Authorization' => 'Bearer ' . $apiKey,
  233. 'Content-Type' => 'application/json',
  234. ],
  235. 'json' => $apiParams,
  236. ]);
  237. $responseData = json_decode($response->getBody()->getContents(), true);
  238. dLog('generate')->info('火山图片生成API响应: ', $responseData);
  239. if (isset($responseData['error'])) {
  240. // API返回错误
  241. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  242. 'error_message' => $responseData['error']['message'] ?? 'API Error',
  243. 'result_json' => $responseData
  244. ]);
  245. } else {
  246. // 任务提交成功,直接处理结果
  247. $this->processVolcApiResponse($task, $responseData);
  248. }
  249. } catch (\Exception $e) {
  250. // 记录错误
  251. dLog('generate')->error('火山API请求失败: ' . $e->getMessage());
  252. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  253. 'error_message' => 'API请求失败: ' . $e->getMessage()
  254. ]);
  255. }
  256. }
  257. /**
  258. * 处理火山API响应结果
  259. *
  260. * @param MpGeneratePicTask $task
  261. * @param array $responseData
  262. * @return void
  263. */
  264. private function processVolcApiResponse(MpGeneratePicTask $task, array $responseData): void
  265. {
  266. try {
  267. if (!isset($responseData['data']) || empty($responseData['data'])) {
  268. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  269. 'error_message' => '未返回图片数据',
  270. 'result_json' => $responseData
  271. ]);
  272. return;
  273. }
  274. $result_urls = [];
  275. foreach ($responseData['data'] as $imageData) {
  276. if (isset($imageData['error'])) {
  277. // 单张图片生成失败
  278. dLog('generate')->warning('单张图片生成失败: ', $imageData['error']);
  279. continue;
  280. }
  281. if (isset($imageData['url'])) {
  282. $url = $imageData['url'];
  283. $pic_name = 'ai_generation_' . time() . '_' . uniqid();
  284. $pic_ext = getImgExtFromUrl($url);
  285. $pic_name = $pic_name . $pic_ext;
  286. if ($pic_ext === '.png') {
  287. $comporessed_data = compressRemoteImageUrlToSize($url);
  288. $url = uploadStreamByTos('image', $comporessed_data, $pic_name);
  289. } else {
  290. // 将图片另存到tos
  291. $url = uploadStreamByTos('image', file_get_contents($url), $pic_name);
  292. }
  293. $result_urls[] = $url;
  294. }
  295. }
  296. if (empty($result_urls)) {
  297. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  298. 'error_message' => '所有图片生成失败',
  299. 'result_json' => $responseData
  300. ]);
  301. return;
  302. }
  303. // 更新任务状态为成功
  304. $task->updateStatus(MpGeneratePicTask::STATUS_SUCCESS, [
  305. 'result_url' => $result_urls,
  306. 'result_json' => $responseData
  307. ]);
  308. // 同步调整分镜图片状态和结果
  309. $segment_id = getProp($task, 'alias_segment_id');
  310. if ($segment_id) {
  311. DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
  312. 'img_url' => $result_urls[0],
  313. 'pic_task_status' => '已完成',
  314. ]);
  315. // 如果是首帧图ID则更新anime表
  316. if (Redis::sismember('anime_first_frame_urls', $segment_id)) {
  317. $anime_id = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->value('anime_id');
  318. DB::table('mp_animes')->where('id', $anime_id)->update(['first_frame_url' => $result_urls[0], 'updated_at' => date('Y-m-d H:i:s')]);
  319. Redis::srem('anime_first_frame_urls', $segment_id);
  320. }
  321. }
  322. } catch (\Exception $e) {
  323. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  324. 'error_message' => '处理响应失败: ' . $e->getMessage()
  325. ]);
  326. }
  327. }
  328. public function getTaskList($data)
  329. {
  330. $task_id = getProp($data, 'task_id');
  331. $status = getProp($data, 'status');
  332. $query = MpGeneratePicTask::select('*');
  333. if ($task_id) {
  334. $query->where('id', $task_id);
  335. }
  336. if ($status) {
  337. $query->where('status', $status);
  338. }
  339. return $query->orderBy('created_at', 'desc')->paginate();
  340. }
  341. /**
  342. * 查询任务状态
  343. *
  344. * @param MpGeneratePicTask $task
  345. * @return array
  346. */
  347. public function queryTaskStatus(MpGeneratePicTask $task): array
  348. {
  349. // 根据模型类型调用不同的查询方法
  350. if ($task->model === self::MODEL_JIMENG_4) {
  351. return $this->queryJimengTaskStatus($task);
  352. } else {
  353. // 火山API是同步返回结果,不需要查询
  354. return [
  355. 'status' => $task->status === MpGeneratePicTask::STATUS_SUCCESS ? 'success' : 'failed',
  356. 'error_message' => $task->error_message,
  357. 'result_url' => $task->result_url,
  358. 'result_json' => $task->result_json ?? []
  359. ];
  360. }
  361. }
  362. /**
  363. * 查询即梦AI任务状态
  364. *
  365. * @param MpGeneratePicTask $task
  366. * @return array
  367. */
  368. private function queryJimengTaskStatus(MpGeneratePicTask $task): array
  369. {
  370. try {
  371. // 获取API任务ID
  372. $apiTaskId = $task->task_id ?? null;
  373. if (!$apiTaskId) {
  374. return [
  375. 'status' => 'failed',
  376. 'error_message' => 'API任务ID不存在',
  377. 'result_url' => null
  378. ];
  379. }
  380. //构建查询参数
  381. $apiParams = [
  382. 'req_key' => 'jimeng_t2i_v40',
  383. 'task_id' => $apiTaskId,
  384. 'req_json' => '{"return_url":true}' // 返回URL格式
  385. ];
  386. //调用即梦AI 4.0 API查询任务状态
  387. $response = $this->volcEngineService->request(
  388. 'POST',
  389. 'visual.volcengineapi.com',
  390. '/', // 使用根路径
  391. [], // Query参数
  392. json_encode($apiParams),
  393. env('VOLC_AI_AK'),
  394. env('VOLC_AI_SK'),
  395. 'cv',
  396. 'cn-north-1',
  397. 'CVSync2AsyncGetResult',
  398. '2022-08-31',
  399. 'application/json'
  400. );
  401. $responseData = json_decode($response['body'], true);
  402. dLog('generate')->info('即梦AI 4.0 API查询任务状态响应: ', $responseData);
  403. if ($responseData['code'] !== 10000) {
  404. // API返回错误
  405. return [
  406. 'status' => 'failed',
  407. 'error_message' => $responseData['message'] ?? 'API Error',
  408. 'result_url' => null,
  409. 'result_json' => $responseData ?? []
  410. ];
  411. }
  412. // 解析API响应
  413. $result = $responseData['data'] ?? [];
  414. $taskStatus = $result['status'] ?? 'failed';
  415. $returnData = [
  416. 'status' => $taskStatus,
  417. 'error_message' => null,
  418. 'result_url' => null,
  419. 'result_json' => $responseData ?? []
  420. ];
  421. // 如果任务成功,获取结果URL
  422. if ($taskStatus === 'done' && isset($result['image_urls']) && count($result['image_urls']) > 0) {
  423. $result_urls = [];
  424. foreach ($result['image_urls'] as $url) {
  425. $pic_name = 'ai_generation_' . time() . '_' . uniqid();
  426. $pic_ext = getImgExtFromUrl($url);
  427. $pic_name = $pic_name . $pic_ext;
  428. if ($pic_ext === '.png') {
  429. $comporessed_data = compressRemoteImageUrlToSize($url);
  430. $url = uploadStreamByTos('image', $comporessed_data, $pic_name);
  431. }else {
  432. // 将图片另存到tos
  433. $url = uploadStreamByTos('image', file_get_contents($url), $pic_name);
  434. }
  435. $result_urls[] = $url;
  436. }
  437. $returnData['result_url'] = $result_urls;
  438. $returnData['status'] = 'success';
  439. } elseif (in_array($taskStatus, ['not_found', 'expired'])) {
  440. $returnData['status'] = 'failed';
  441. $returnData['error_message'] = '任务未找到或已过期';
  442. } elseif ($taskStatus === 'failed') {
  443. $returnData['status'] = 'failed';
  444. $returnData['error_message'] = '任务执行失败';
  445. }
  446. // in_queue 或 generating状态保持原status
  447. return $returnData;
  448. } catch (\Exception $e) {
  449. return [
  450. 'status' => 'failed',
  451. 'error_message' => $e->getMessage(),
  452. 'result_url' => null
  453. ];
  454. }
  455. }
  456. /**
  457. * 更新所有待处理任务的状态
  458. *
  459. * @return void
  460. */
  461. public function updatePendingTasks(): void
  462. {
  463. dLog('generate')->info('更新图片状态ing');
  464. // 1. 处理火山API的pending任务(批量提交)
  465. $volcPendingTasks = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PENDING)
  466. ->whereIn('model', BaseConst::VOLC_PIC_MODELS)
  467. ->orderBy('created_at', 'asc')
  468. ->limit(50) // 每次最多处理50个任务,避免超时
  469. ->get();
  470. foreach ($volcPendingTasks as $task) {
  471. dLog('generate')->info('开始处理火山API任务: ' . $task->task_id);
  472. $this->submitTaskToVolcApi($task);
  473. }
  474. // 2. 获取所有处理中的任务(仅即梦AI需要查询状态)
  475. $processingTasks = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PROCESSING)
  476. ->where('model', self::MODEL_JIMENG_4)
  477. ->get();
  478. foreach ($processingTasks as $task) {
  479. $statusInfo = $this->queryTaskStatus($task);
  480. if ($statusInfo['status'] === 'success') {
  481. $task->updateStatus(MpGeneratePicTask::STATUS_SUCCESS, [
  482. 'result_url' => $statusInfo['result_url'],
  483. 'result_json' => $statusInfo['result_json'] ?? []
  484. ]);
  485. // 同步调整分镜图片状态和结果
  486. $segment_id = getProp($task, 'alias_segment_id');
  487. if ($segment_id) {
  488. DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([
  489. 'img_url' => $statusInfo['result_url'][0],
  490. 'pic_task_status' => '已完成',
  491. ]);
  492. // 如果是首帧图ID则更新anime表
  493. if (Redis::sismember('anime_first_frame_urls', $segment_id)) {
  494. $anime_id = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->value('anime_id');
  495. 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')]);
  496. Redis::srem('anime_first_frame_urls', $segment_id);
  497. }
  498. }
  499. } elseif ($statusInfo['status'] === 'failed') {
  500. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  501. 'error_message' => $statusInfo['error_message'],
  502. 'result_json' => $statusInfo['result_json'] ?? []
  503. ]);
  504. if (getProp($task, 'alias_segment_id')) {
  505. DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([
  506. 'pic_task_status' => '失败',
  507. ]);
  508. }
  509. }
  510. // 如果仍然是处理中状态,不做任何操作
  511. //处理:如果任务处理超过12小时,标记为失败
  512. $processingTime = now()->diffInHours($task->created_at);
  513. if ($processingTime > 12) {
  514. $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
  515. 'error_message' => '任务处理超时(超过12小时)'
  516. ]);
  517. }
  518. }
  519. // 3. 检查是否有任务完成,如果有则处理下一个排队任务(仅即梦AI)
  520. $this->processNextQueuedTask();
  521. }
  522. /**
  523. * 处理下一个排队的任务
  524. *
  525. * @return void
  526. */
  527. public function processNextQueuedTask(): void
  528. {
  529. // 检查是否还有正在处理的任务(仅即梦AI)
  530. if ($this->hasProcessingTask()) {
  531. return; // 如果还有处理中的任务,不处理新任务
  532. }
  533. // 获取最早的待处理任务(仅即梦AI)
  534. $nextTask = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PENDING)
  535. ->where('model', self::MODEL_JIMENG_4)
  536. ->orderBy('created_at', 'asc')
  537. ->first();
  538. if ($nextTask) {
  539. dLog('generate')->info('开始处理排队任务: ' . $nextTask->task_id);
  540. $this->submitTaskToJimengApi($nextTask);
  541. }
  542. }
  543. /**
  544. * 获取任务队列状态
  545. *
  546. * @return array
  547. */
  548. public function getQueueStatus(): array
  549. {
  550. $processingCount = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PROCESSING)->count();
  551. $pendingCount = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PENDING)->count();
  552. return [
  553. 'processing_count' => $processingCount,
  554. 'pending_count' => $pendingCount,
  555. 'can_submit_new_task' => $processingCount === 0
  556. ];
  557. }
  558. }