AnimeCheckImgUrlCommand.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. <?php
  2. namespace App\Console\Anime;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Support\Facades\DB;
  5. use App\Services\AIGeneration\AIImageGenerationService;
  6. use App\Services\DeepSeek\DeepSeekService;
  7. class AnimeCheckImgUrlCommand extends Command
  8. {
  9. /**
  10. * The name and signature of the console command.
  11. *
  12. * @var string
  13. */
  14. protected $signature = 'Anime:checkImgUrl';
  15. /**
  16. * The console command description.
  17. *
  18. * @var string
  19. */
  20. protected $description = '检查动漫对话角色和场景图片生成状态';
  21. protected $aiImageGenerationService;
  22. protected $DeepSeekService;
  23. public function __construct(AIImageGenerationService $aiImageGenerationService, DeepSeekService $DeepSeekService)
  24. {
  25. parent::__construct();
  26. $this->aiImageGenerationService = $aiImageGenerationService;
  27. $this->DeepSeekService = $DeepSeekService;
  28. }
  29. /**
  30. * Execute the console command.
  31. *
  32. * @return int
  33. */
  34. public function handle()
  35. {
  36. dLog('command')->info('====================开始检查动漫对话角色和场景图片生成状态====================');
  37. $time_start = microtime(true);
  38. $timeout = 50; // 超时时间50秒
  39. $check_interval = 3; // 每3秒检查一次
  40. while (true) {
  41. // 检查是否超时
  42. $elapsed_time = microtime(true) - $time_start;
  43. if ($elapsed_time >= $timeout) {
  44. dLog('command')->info('检查超时,退出循环');
  45. break;
  46. }
  47. // 检查是否有需要处理的数据
  48. $has_pending_animes = $this->hasPendingAnimes();
  49. $has_pending_episodes = $this->hasPendingEpisodes();
  50. // 如果没有需要检查的内容,直接跳出循环
  51. if (!$has_pending_animes && !$has_pending_episodes) {
  52. dLog('command')->info('没有需要检查的内容,退出循环');
  53. break;
  54. }
  55. // 检查动漫全局角色和场景
  56. if ($has_pending_animes) {
  57. $this->checkAnimes();
  58. }
  59. // 检查剧集角色和场景
  60. if ($has_pending_episodes) {
  61. $this->checkEpisodes();
  62. }
  63. // 等待3秒后继续下一次检查
  64. sleep($check_interval);
  65. }
  66. $time_end = microtime(true);
  67. dLog('command')->info('脚本执行时间: '.round($time_end-$time_start, 2).'秒');
  68. dLog('command')->info('====================结束检查动漫对话角色和场景图片生成状态====================');
  69. }
  70. // 检查动漫全局执行中的任务
  71. private function hasPendingAnimes() {
  72. $time_expire_datetime = date('Y-m-d H:i:s', strtotime('-12 hours'));
  73. DB::table('mp_animes')->where('generate_status', '执行中')->where('generate_at', '<', $time_expire_datetime)->update([
  74. 'generate_status' => '失败',
  75. 'updated_at'=>date('Y-m-d H:i:s'),
  76. ]);
  77. return DB::table('mp_animes')->where('generate_status', '执行中')->exists();
  78. }
  79. // 检查剧集执行中的任务
  80. private function hasPendingEpisodes() {
  81. $time_expire_datetime = date('Y-m-d H:i:s', strtotime('-12 hours'));
  82. DB::table('mp_anime_episodes')->whereIn('generate_status', ['执行中'])->where('generate_at', '<', $time_expire_datetime)->update([
  83. 'generate_status' => '失败',
  84. 'updated_at'=>date('Y-m-d H:i:s'),
  85. ]);
  86. return DB::table('mp_anime_episodes')->whereIn('generate_status', ['执行中', '待执行'])->exists();
  87. }
  88. // 检查全局
  89. private function checkAnimes() {
  90. dLog('command')->info('====================开始检查动漫全局====================');
  91. // 获取待执行的动漫对话数据
  92. $data = DB::table('mp_animes')->where('generate_status', '执行中')->select('id', 'roles', 'scenes')->get();
  93. foreach ($data as $item) {
  94. $id = getProp($item, 'id');
  95. $roles = json_decode(getProp($item, 'roles'), true);
  96. $scenes = json_decode(getProp($item, 'scenes'), true);
  97. $role_count = count($roles);
  98. $scene_count = count($scenes);
  99. // 处理角色图片状态
  100. $role_result = $this->checkRoles($id, $roles);
  101. // 处理场景图片状态
  102. $scene_result = $this->checkScenes($id, $scenes);
  103. if ($role_result['success_count'] == $role_count && $scene_result['success_count'] == $scene_count) {
  104. // 同步更新该动漫对话剧集的所有角色或场景
  105. $this->syncRolesAndScenes($id, $role_result['roles'], $scene_result['scenes']);
  106. }
  107. }
  108. dLog('command')->info('====================结束检查动漫全局====================');
  109. }
  110. // 检查全局角色
  111. private function checkRoles($id, $roles) {
  112. $role_success_count = 0;
  113. foreach ($roles as &$role) {
  114. $task_id = getProp($role, 'task_id');
  115. $task_status = getProp($role, 'task_status');
  116. if (!$task_id || in_array($task_status, ['success', 'failed']) || getProp($role, 'role') == '旁白') {
  117. $role_success_count++;
  118. continue;
  119. }
  120. $task = DB::table('mp_generate_pic_tasks')->where('id', $task_id)->select('result_url', 'status')->first();
  121. $status = getProp($task, 'status');
  122. if (in_array($status, ['success', 'failed'])) {
  123. $role['task_status'] = $status; // 使用实际的任务状态
  124. $role['url'] = '';
  125. $result_url = getProp($task, 'result_url');
  126. if ($result_url && is_json($result_url) && $status == 'success') {
  127. $role['url'] = json_decode($result_url, true)[0];
  128. }
  129. $role_success_count++;
  130. }
  131. }
  132. if ($role_success_count > 0) {
  133. DB::table('mp_animes')->where('id', $id)->update(['roles' => json_encode($roles, 256)]);
  134. }
  135. return [
  136. 'roles' => $roles,
  137. 'success_count' => $role_success_count,
  138. ];
  139. }
  140. // 检查全局场景
  141. private function checkScenes($id, $scenes) {
  142. $scene_success_count = 0;
  143. foreach ($scenes as &$scene) {
  144. $task_id = getProp($scene, 'task_id');
  145. $task_status = getProp($scene, 'task_status');
  146. if (!$task_id || in_array($task_status, ['success', 'failed'])) {
  147. $scene_success_count++;
  148. continue;
  149. }
  150. $task = DB::table('mp_generate_pic_tasks')->where('id', $task_id)->select('result_url', 'status')->first();
  151. $status = getProp($task, 'status');
  152. if (in_array($status, ['success', 'failed'])) {
  153. $scene['task_status'] = $status; // 使用实际的任务状态
  154. $scene['url'] = '';
  155. $result_url = getProp($task, 'result_url');
  156. if ($result_url && is_json($result_url) && $status == 'success') {
  157. $scene['url'] = json_decode($result_url, true)[0];
  158. }
  159. $scene_success_count++;
  160. }
  161. }
  162. if ($scene_success_count > 0) {
  163. DB::table('mp_animes')->where('id', $id)->update(['scenes' => json_encode($scenes, 256)]);
  164. }
  165. return [
  166. 'scenes' => $scenes,
  167. 'success_count' => $scene_success_count,
  168. ];
  169. }
  170. // 剧集中同步继承全局角色和场景
  171. private function syncRolesAndScenes($id, $roles, $scenes) {
  172. // 获取该动漫对话对应的所有剧集
  173. $episodes = DB::table('mp_anime_episodes')->where('anime_id', $id)->get();
  174. $anime = DB::table('mp_animes')->where('id', $id)->first();
  175. $art_style_type = getProp($anime, 'art_style_type');
  176. $character_prefix = '';
  177. $scene_prefix = '';
  178. if ($art_style_type) {
  179. $art_style_arr = $this->DeepSeekService->getArtStyleShortPromptByInput($art_style_type);
  180. $character_prefix = isset($art_style_arr['character_prefix']) ? $art_style_arr['character_prefix'] : '';
  181. $scene_prefix = isset($art_style_arr['scene_prefix']) ? $art_style_arr['scene_prefix'] : '';
  182. }
  183. if ($episodes->isEmpty()) {
  184. dLog('command')->info("动漫对话ID:{$id} 没有关联的剧集");
  185. return;
  186. }
  187. foreach ($episodes as $episode) {
  188. $episode_id = getProp($episode, 'id');
  189. $episode_roles = json_decode(getProp($episode, 'roles'), true) ?: [];
  190. $episode_scenes = json_decode(getProp($episode, 'scenes'), true) ?: [];
  191. $roles_updated = false;
  192. $scenes_updated = false;
  193. // 更新剧集中的角色URL
  194. foreach ($episode_roles as &$episode_role) {
  195. $episode_role_name = getProp($episode_role, 'role');
  196. $episode_description = getProp($episode_role, 'description');
  197. $episode_pic_prompt = getProp($episode_role, 'pic_prompt');
  198. $episode_url = getProp($episode_role, 'url');
  199. $episode_task_id = getProp($episode_role, 'task_id');
  200. // 如果已有URL或已有任务ID,跳过
  201. if (!empty($episode_url) || !empty($episode_task_id)) {
  202. continue;
  203. }
  204. $url_inherited = false;
  205. // 遍历方法中的角色数据
  206. foreach ($roles as $role) {
  207. $role_name = getProp($role, 'role');
  208. $role_description = getProp($role, 'description');
  209. $role_pic_prompt = getProp($role, 'pic_prompt');
  210. $role_url = getProp($role, 'url');
  211. $role_task_id = getProp($role, 'task_id');
  212. $role_task_status = getProp($role, 'task_status');
  213. // 满足条件:1.角色名相同 2.描述相同 3.pic_prompt相同 4.剧集中URL为空且方法中URL不为空
  214. if ($episode_role_name == $role_name
  215. && $episode_description == $role_description
  216. && $episode_pic_prompt == $role_pic_prompt
  217. && empty($episode_url)
  218. && !empty($role_url)) {
  219. // 只继承 task_id、task_status 和 url,不继承其他创建图片的参数
  220. $episode_role['task_id'] = $role_task_id;
  221. $episode_role['task_status'] = $role_task_status;
  222. $episode_role['url'] = $role_url;
  223. $roles_updated = true;
  224. $url_inherited = true;
  225. dLog('command')->info("剧集ID:{$episode_id} 角色:{$role_name} 继承全局数据 (task_id:{$role_task_id}, task_status:{$role_task_status}, url:{$role_url})");
  226. break;
  227. }
  228. }
  229. // 如果无法继承URL,则创建图片生成任务
  230. if (!$url_inherited && $episode_role_name != '旁白') {
  231. try {
  232. // 获取剧集的美术风格
  233. $art_style = getProp($episode, 'art_style');
  234. // 优先使用 pic_prompt,如果没有则使用 description
  235. $description = !empty($episode_pic_prompt) ? trim((string)$episode_pic_prompt) : $episode_description;
  236. // if ($art_style) {
  237. // // $description = "美术风格:\n{$art_style}\n主体描述:{$description}\n图片要求:图片侧重点是人物,不要出现场景或背景,并且只能生成一个人物";
  238. // $description = "主体描述:{$description}\n图片要求:图片侧重点是人物,不要出现场景或背景,并且只能生成一个人物";
  239. // }
  240. $description = "{$character_prefix}。\n主体描述:{$description}\n超高清,高细节,主体突出,构图合理,画面干净,无场景或背景,单人物,无文字,无水印,无字幕,电影级光影,视觉焦点明确";
  241. $params = [
  242. 'prompt' => $description,
  243. 'ref_img_urls' => []
  244. ];
  245. $task = $this->aiImageGenerationService->createImageGenerationTask($params);
  246. $task_id = $task->id;
  247. if ($task_id) {
  248. $episode_role['task_id'] = $task_id;
  249. $episode_role['task_status'] = 'processing';
  250. $roles_updated = true;
  251. dLog('command')->info("剧集ID:{$episode_id} 角色:{$episode_role_name} 创建图片生成任务,任务ID:{$task_id}");
  252. }
  253. } catch (\Exception $e) {
  254. dLog('command')->error("剧集ID:{$episode_id} 角色:{$episode_role_name} 创建图片生成任务失败: " . $e->getMessage());
  255. }
  256. }
  257. }
  258. // 更新剧集中的场景URL
  259. foreach ($episode_scenes as &$episode_scene) {
  260. $episode_scene_name = getProp($episode_scene, 'scene');
  261. $episode_description = getProp($episode_scene, 'description');
  262. $episode_pic_prompt = getProp($episode_scene, 'pic_prompt');
  263. $episode_url = getProp($episode_scene, 'url');
  264. $episode_task_id = getProp($episode_scene, 'task_id');
  265. // 如果已有URL或已有任务ID,跳过
  266. if (!empty($episode_url) || !empty($episode_task_id)) {
  267. continue;
  268. }
  269. $url_inherited = false;
  270. // 遍历方法中的场景数据
  271. foreach ($scenes as $scene) {
  272. $scene_name = getProp($scene, 'scene');
  273. $scene_description = getProp($scene, 'description');
  274. $scene_pic_prompt = getProp($scene, 'pic_prompt');
  275. $scene_url = getProp($scene, 'url');
  276. $scene_task_id = getProp($scene, 'task_id');
  277. $scene_task_status = getProp($scene, 'task_status');
  278. // 满足条件:1.场景名相同 2.描述相同 3.pic_prompt相同 4.剧集中URL为空且方法中URL不为空
  279. if ($episode_scene_name == $scene_name
  280. && $episode_description == $scene_description
  281. && $episode_pic_prompt == $scene_pic_prompt
  282. && empty($episode_url)
  283. && !empty($scene_url)) {
  284. // 只继承 task_id、task_status 和 url,不继承其他创建图片的参数
  285. $episode_scene['task_id'] = $scene_task_id;
  286. $episode_scene['task_status'] = $scene_task_status;
  287. $episode_scene['url'] = $scene_url;
  288. $scenes_updated = true;
  289. $url_inherited = true;
  290. dLog('command')->info("剧集ID:{$episode_id} 场景:{$scene_name} 继承全局数据 (task_id:{$scene_task_id}, task_status:{$scene_task_status}, url:{$scene_url})");
  291. break;
  292. }
  293. }
  294. // 如果无法继承URL,则创建图片生成任务
  295. if (!$url_inherited) {
  296. try {
  297. // 获取剧集的美术风格
  298. $art_style = getProp($episode, 'art_style');
  299. // 优先使用 pic_prompt,如果没有则使用 description
  300. $description = !empty($episode_pic_prompt) ? trim((string)$episode_pic_prompt) : $episode_description;
  301. // if ($art_style) {
  302. // // $description = "美术风格:\n{$art_style}\n场景描述:{$description}\n图片要求: 图片侧重点是场景,生成的是场景环境图片,不要出现人物(此为第一优先要求)";
  303. // $description = "场景描述:{$description}\n图片要求: 图片侧重点是场景,生成的是场景环境图片,不要出现人物(此为第一优先要求)";
  304. // }
  305. $description = "{$scene_prefix}。\n场景描述:{$description}\n无人物,超高清,高细节,主体突出,构图合理,画面干净,无文字,无水印,无字幕,电影级光影,视觉焦点明确";
  306. $params = [
  307. 'prompt' => $description,
  308. 'ref_img_urls' => []
  309. ];
  310. $task = $this->aiImageGenerationService->createImageGenerationTask($params);
  311. $task_id = $task->id;
  312. if ($task_id) {
  313. $episode_scene['task_id'] = $task_id;
  314. $episode_scene['task_status'] = 'processing';
  315. $scenes_updated = true;
  316. dLog('command')->info("剧集ID:{$episode_id} 场景:{$episode_scene_name} 创建图片生成任务,任务ID:{$task_id}");
  317. }
  318. } catch (\Exception $e) {
  319. dLog('command')->error("剧集ID:{$episode_id} 场景:{$episode_scene_name} 创建图片生成任务失败: " . $e->getMessage());
  320. }
  321. }
  322. }
  323. // 如果有更新,则保存到数据库
  324. if ($roles_updated || $scenes_updated) {
  325. $update_data = ['updated_at' => date('Y-m-d H:i:s')];
  326. if ($roles_updated) {
  327. $update_data['roles'] = json_encode($episode_roles, 256);
  328. }
  329. if ($scenes_updated) {
  330. $update_data['scenes'] = json_encode($episode_scenes, 256);
  331. }
  332. // 继承URL或创建任务后,将状态改为待执行,等后续检查分集图片成功后再改为成功状态
  333. $update_data['generate_status'] = '待执行';
  334. $update_data['generate_at'] = date('Y-m-d H:i:s');
  335. DB::table('mp_anime_episodes')->where('id', $episode_id)->update($update_data);
  336. dLog('command')->info("剧集ID:{$episode_id} 数据已同步更新,状态改为待执行");
  337. }
  338. }
  339. // 更新状态为已完成
  340. DB::table('mp_animes')->where('id', $id)->update(['generate_status' => '已完成', 'updated_at'=>date('Y-m-d H:i:s')]);
  341. dLog('command')->info('动漫对话图片已全部生成完毕');
  342. }
  343. // 检查剧集
  344. private function checkEpisodes() {
  345. dLog('command')->info('====================开始检查剧集====================');
  346. // 获取待执行和执行中的剧集数据
  347. $data = DB::table('mp_anime_episodes')->whereIn('generate_status', ['待执行', '执行中'])->select('id', 'roles', 'scenes', 'generate_status')->get();
  348. foreach ($data as $item) {
  349. $id = getProp($item, 'id');
  350. $roles = json_decode(getProp($item, 'roles'), true);
  351. $scenes = json_decode(getProp($item, 'scenes'), true);
  352. $role_count = count($roles);
  353. $scene_count = count($scenes);
  354. $current_status = getProp($item, 'generate_status');
  355. // 如果状态是待执行,先改为执行中
  356. if ($current_status == '待执行') {
  357. DB::table('mp_anime_episodes')->where('id', $id)->update([
  358. 'generate_status' => '执行中',
  359. 'generate_at' => date('Y-m-d H:i:s'),
  360. 'updated_at' => date('Y-m-d H:i:s')
  361. ]);
  362. dLog('command')->info("剧集ID:{$id} 状态从待执行改为执行中");
  363. }
  364. // 处理角色图片状态
  365. $role_result = $this->checkEpisodeRoles($id, $roles);
  366. // 处理场景图片状态
  367. $scene_result = $this->checkEpisodeScenes($id, $scenes);
  368. if ($role_result['success_count'] == $role_count && $scene_result['success_count'] == $scene_count) {
  369. // 更新状态为已完成
  370. DB::table('mp_anime_episodes')->where('id', $id)->update(['generate_status' => '已完成', 'updated_at'=>date('Y-m-d H:i:s')]);
  371. dLog('command')->info("剧集ID:{$id} 图片已全部生成完毕,状态改为已完成");
  372. }
  373. }
  374. dLog('command')->info('====================结束检查剧集====================');
  375. }
  376. // 检查剧集角色
  377. private function checkEpisodeRoles($id, $roles) {
  378. $role_success_count = 0;
  379. foreach ($roles as &$role) {
  380. $task_id = getProp($role, 'task_id');
  381. $task_status = getProp($role, 'task_status');
  382. if (!$task_id || in_array($task_status, ['success', 'failed']) || getProp($role, 'role') == '旁白') {
  383. $role_success_count++;
  384. continue;
  385. }
  386. $task = DB::table('mp_generate_pic_tasks')->where('id', $task_id)->select('result_url', 'status')->first();
  387. $status = getProp($task, 'status');
  388. if (in_array($status, ['success', 'failed'])) {
  389. $role['task_status'] = $status; // 使用实际的任务状态
  390. $role['url'] = '';
  391. $result_url = getProp($task, 'result_url');
  392. if ($result_url && is_json($result_url) && $status == 'success') {
  393. $role['url'] = json_decode($result_url, true)[0];
  394. }
  395. $role_success_count++;
  396. }
  397. }
  398. if ($role_success_count > 0) {
  399. DB::table('mp_anime_episodes')->where('id', $id)->update(['roles' => json_encode($roles, 256)]);
  400. }
  401. return [
  402. 'roles' => $roles,
  403. 'success_count' => $role_success_count,
  404. ];
  405. }
  406. // 检查剧集场景
  407. private function checkEpisodeScenes($id, $scenes) {
  408. $scene_success_count = 0;
  409. foreach ($scenes as &$scene) {
  410. $task_id = getProp($scene, 'task_id');
  411. $task_status = getProp($scene, 'task_status');
  412. if (!$task_id || in_array($task_status, ['success', 'failed'])) {
  413. $scene_success_count++;
  414. continue;
  415. }
  416. $task = DB::table('mp_generate_pic_tasks')->where('id', $task_id)->select('result_url', 'status')->first();
  417. $status = getProp($task, 'status');
  418. if (in_array($status, ['success', 'failed'])) {
  419. $scene['task_status'] = $status; // 使用实际的任务状态
  420. $scene['url'] = '';
  421. $result_url = getProp($task, 'result_url');
  422. if ($result_url && is_json($result_url) && $status == 'success') {
  423. $scene['url'] = json_decode($result_url, true)[0];
  424. }
  425. $scene_success_count++;
  426. }
  427. }
  428. if ($scene_success_count > 0) {
  429. DB::table('mp_anime_episodes')->where('id', $id)->update(['scenes' => json_encode($scenes, 256)]);
  430. }
  431. return [
  432. 'scenes' => $scenes,
  433. 'success_count' => $scene_success_count,
  434. ];
  435. }
  436. }