AnimeController.php 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069
  1. <?php
  2. namespace App\Http\Controllers\Anime;
  3. use App\Transformer\Anime\AnimeTransformer;
  4. use App\Facade\Site;
  5. use App\Consts\ErrorConst;
  6. use App\Exceptions\ApiException;
  7. use App\Libs\ApiResponse;
  8. use App\Libs\Utils;
  9. use App\Models\MpGenerateVideoTask;
  10. use App\Services\AIGeneration\AIImageGenerationService;
  11. use App\Services\AIGeneration\AIVideoGenerationService;
  12. use App\Services\Anime\AnimeService;
  13. use Illuminate\Http\Request;
  14. use Illuminate\Routing\Controller as BaseController;
  15. use Illuminate\Support\Facades\DB;
  16. use Illuminate\Support\Facades\Redis;
  17. use Illuminate\Support\Facades\Validator;
  18. class AnimeController extends BaseController
  19. {
  20. use ApiResponse;
  21. protected $AnimeService;
  22. protected $AIImageGenerationService;
  23. protected $AIVideoGenerationService;
  24. public function __construct(
  25. AnimeService $AnimeService,
  26. AIImageGenerationService $AIImageGenerationService,
  27. AIVideoGenerationService $AIVideoGenerationService
  28. ) {
  29. $this->AnimeService = $AnimeService;
  30. $this->AIImageGenerationService = $AIImageGenerationService;
  31. $this->AIVideoGenerationService = $AIVideoGenerationService;
  32. }
  33. public function textModel() {
  34. $models = DB::table('mp_text_models')->where('is_enabled', 1)->select('model', 'name')->get()->map(function ($value) {
  35. return (array)$value;
  36. })->toArray();
  37. return $this->success($models);
  38. }
  39. public function artStyleList()
  40. {
  41. $result = [
  42. [
  43. 'art_style' => '日系动漫风格',
  44. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/日系动漫风格.jpg"
  45. ],
  46. [
  47. 'art_style' => '国漫风格',
  48. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/国漫风格.jpg"
  49. ],
  50. [
  51. 'art_style' => 'Q版卡通风格',
  52. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/Q版卡通风格.jpg"
  53. ],
  54. [
  55. 'art_style' => '简约扁平风格',
  56. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/简约扁平风格.jpg"
  57. ],
  58. [
  59. 'art_style' => '古风仙侠风格',
  60. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/古风仙侠风格.jpg"
  61. ],
  62. [
  63. 'art_style' => '武侠风格',
  64. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/武侠风格.jpg"
  65. ],
  66. [
  67. 'art_style' => '新中式水墨风格',
  68. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/新中式水墨风格.jpg"
  69. ],
  70. [
  71. 'art_style' => '写实插画风格',
  72. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/写实插画风格.jpg"
  73. ],
  74. [
  75. 'art_style' => '3D卡通风格',
  76. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/3D卡通风格.jpg"
  77. ],
  78. [
  79. 'art_style' => '条漫风格',
  80. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/条漫风格.jpg"
  81. ],
  82. [
  83. 'art_style' => '赛博朋克风格',
  84. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/赛博朋克风格.jpg"
  85. ],
  86. [
  87. 'art_style' => '暗黑悬疑风格',
  88. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/暗黑悬疑风格.jpg"
  89. ],
  90. [
  91. 'art_style' => '治愈清新风格',
  92. 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/治愈清新风格.jpg"
  93. ],
  94. ];
  95. return $this->success($result);
  96. }
  97. // 批量生成主体图片
  98. public function batchSetRoleImg(Request $request) {
  99. // 忽略所有超时限制
  100. set_time_limit(0);
  101. ini_set('max_execution_time', '0');
  102. $data = $request->all();
  103. $result = $this->AnimeService->batchSetRoleImg($data);
  104. return $this->success($result);
  105. }
  106. // 批量生成场景图片
  107. public function batchSetSceneImg(Request $request) {
  108. // 忽略所有超时限制
  109. set_time_limit(0);
  110. ini_set('max_execution_time', '0');
  111. $data = $request->all();
  112. $result = $this->AnimeService->batchSetSceneImg($data);
  113. return $this->success($result);
  114. }
  115. // 编辑动漫
  116. public function editAnime(Request $request) {
  117. $data = $request->all();
  118. $result = $this->AnimeService->editAnime($data);
  119. return $this->success(['success'=>$result ? 1 : 0]);
  120. }
  121. // 修改分集主体列表
  122. public function changeEpisodeRoles(Request $request) {
  123. $data = $request->all();
  124. $result = $this->AnimeService->changeEpisodeRoles($data);
  125. return $this->success(['success'=>$result ? 1 : 0]);
  126. }
  127. // 修改分集场景列表
  128. public function changeEpisodeScenes(Request $request) {
  129. $data = $request->all();
  130. $result = $this->AnimeService->changeEpisodeScenes($data);
  131. return $this->success(['success'=>$result ? 1 : 0]);
  132. }
  133. // 编辑分镜剧本
  134. public function editSegment(Request $request) {
  135. $data = $request->all();
  136. $result = $this->AnimeService->editSegment($data);
  137. return $this->success(['success'=>$result ? 1 : 0]);
  138. }
  139. // 动漫对话列表
  140. public function chatList(Request $request) {
  141. $data = $request->all();
  142. $result = $this->AnimeService->chatList($data);
  143. return $this->success($result, [new AnimeTransformer(), 'newBuildChatList']);
  144. }
  145. // 动漫对话历史记录
  146. public function chatHistory(Request $request) {
  147. $data = $request->all();
  148. $result = $this->AnimeService->chatHistory($data);
  149. return $this->success($result);
  150. }
  151. // 动漫大纲
  152. public function animeDetail(Request $request) {
  153. $data = $request->all();
  154. $result = $this->AnimeService->animeDetail($data);
  155. return $this->success($result);
  156. }
  157. // 动漫剧集
  158. public function episodeInfo(Request $request) {
  159. $data = $request->all();
  160. $result = $this->AnimeService->episodeInfo($data);
  161. return $this->success($result);
  162. }
  163. // 分镜信息
  164. public function segmentInfo(Request $request) {
  165. $data = $request->all();
  166. $result = $this->AnimeService->segmentInfo($data);
  167. return $this->success($result);
  168. }
  169. // 复制剧集副本
  170. public function copyEpisodeVersion(Request $request) {
  171. $data = $request->all();
  172. $result = $this->AnimeService->copyEpisodeVersion($data);
  173. return $this->success(['success' => $result ? 1 : 0]);
  174. }
  175. // 剧集副本列表
  176. public function episodeVersions(Request $request) {
  177. $data = $request->all();
  178. $result = $this->AnimeService->episodeVersions($data);
  179. return $this->success($result);
  180. }
  181. // 绑定剧集副本
  182. public function bindEpisodeVersion(Request $request) {
  183. $data = $request->all();
  184. $result = $this->AnimeService->bindEpisodeVersion($data);
  185. return $this->success(['success' => $result ? 1 : 0]);
  186. }
  187. // 对话改图
  188. public function chatChangeImg(Request $request) {
  189. $data = $request->all();
  190. $result = $this->AnimeService->chatChangeImg($data);
  191. return $this->success(['img_url' => $result]);
  192. }
  193. // 一键生成分镜
  194. public function batchSetSegmentPics(Request $request) {
  195. // 忽略所有超时限制
  196. set_time_limit(0);
  197. ini_set('max_execution_time', '0');
  198. $data = $request->all();
  199. $result = $this->AnimeService->batchSetSegmentPics($data);
  200. return $this->success($result);
  201. }
  202. // 重新生成分镜
  203. public function reGenerateSegment(Request $request) {
  204. // 忽略所有超时限制
  205. set_time_limit(0);
  206. ini_set('max_execution_time', '0');
  207. $data = $request->all();
  208. $result = $this->AnimeService->reGenerateSegment($data);
  209. return $this->success(['img_url'=>$result]);
  210. }
  211. // 添加分镜
  212. public function addSegment(Request $request) {
  213. // 忽略所有超时限制
  214. set_time_limit(0);
  215. ini_set('max_execution_time', '0');
  216. $data = $request->all();
  217. $result = $this->AnimeService->addSegment($data);
  218. return $this->success($result);
  219. }
  220. // 复制分镜
  221. public function copySegment(Request $request) {
  222. $data = $request->all();
  223. $result = $this->AnimeService->copySegment($data);
  224. return $this->success($result);
  225. }
  226. // 移动分镜
  227. public function moveSegment(Request $request) {
  228. $data = $request->all();
  229. $result = $this->AnimeService->moveSegment($data);
  230. return $this->success(['success'=>$result ? 1: 0]);
  231. }
  232. // 删除分镜
  233. public function delSegment(Request $request) {
  234. $data = $request->all();
  235. $result = $this->AnimeService->delSegment($data);
  236. return $this->success(['success'=>$result ? 1: 0]);
  237. }
  238. // 分镜历史图片|视频
  239. public function segmentHistory(Request $request) {
  240. $data = $request->all();
  241. $result = $this->AnimeService->segmentHistory($data);
  242. return $this->success($result);
  243. }
  244. public function createSegmentVideoTask(Request $request) {
  245. // 忽略所有超时限制
  246. set_time_limit(0);
  247. ini_set('max_execution_time', '0');
  248. $data = $request->all();
  249. // 验证参数
  250. $validator = Validator::make($data, [
  251. 'segment_id' => 'required|string',
  252. 'tail_frame' => 'nullable|string|max:500',
  253. ], [
  254. 'segment_id.required' => '分镜ID不能为空',
  255. 'tail_frame.max' => '尾帧描述不能超过500个字符',
  256. ]);
  257. if ($validator->fails()) {
  258. Utils::throwError('1002:' . $validator->errors()->first());
  259. }
  260. // 创建视频生成任务
  261. $result = $this->AnimeService->createSegmentVideoTask($data);
  262. $taskId = $result['task_id'];
  263. // 设置 SSE 响应头
  264. return response()->stream(function () use ($taskId, $result) {
  265. // 设置 SSE 响应头
  266. echo "data: " . json_encode([
  267. 'type' => 'task_created',
  268. 'data' => $result
  269. ]) . "\n\n";
  270. ob_flush();
  271. flush();
  272. $startTime = time();
  273. $maxDuration = 300; // 5分钟超时
  274. $checkInterval = 5; // 每5秒检查一次
  275. while (time() - $startTime < $maxDuration) {
  276. try {
  277. // 查询任务状态
  278. $task = \App\Models\MpGenerateVideoTask::find($taskId);
  279. if (!$task) {
  280. echo "data: " . json_encode([
  281. 'type' => 'error',
  282. 'message' => '任务不存在'
  283. ]) . "\n\n";
  284. ob_flush();
  285. flush();
  286. break;
  287. }
  288. // 如果任务还在处理中,查询最新状态
  289. if ($task->status === 'processing') {
  290. $statusResult = $this->AIVideoGenerationService->querySeedanceTaskStatus($task);
  291. if (isset($statusResult['status'])) {
  292. // 更新任务状态
  293. $task->update([
  294. 'status' => $statusResult['status'],
  295. 'result_url' => $statusResult['result_url'] ?? null,
  296. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  297. 'error_message' => $statusResult['error_message'] ?? null,
  298. 'completed_at' => in_array($statusResult['status'], [
  299. 'success',
  300. 'failed'
  301. ]) ? now() : null
  302. ]);
  303. // 如果任务成功,使用事务更新分镜表并创建配音任务
  304. if ($statusResult['status'] === 'success' && isset($statusResult['result_url'])) {
  305. try {
  306. DB::beginTransaction();
  307. $now = date('Y-m-d H:i:s');
  308. // 获取分镜ID
  309. $segment = DB::table('mp_episode_segments')
  310. ->where('video_task_id', $taskId)
  311. ->first();
  312. if (!$segment) {
  313. throw new \Exception('未找到对应的分镜记录');
  314. }
  315. $segmentId = $segment->segment_id;
  316. // 更新分镜表
  317. $updateResult = DB::table('mp_episode_segments')
  318. ->where('segment_id', $segmentId)
  319. ->update([
  320. 'video_url' => $statusResult['result_url'],
  321. 'video_task_status' => '已完成',
  322. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  323. 'updated_at' => $now
  324. ]);
  325. if (!$updateResult) {
  326. Utils::throwError('20003:更新分镜表失败');
  327. }
  328. // 获取分镜信息用于创建配音任务
  329. $segmentInfo = DB::table('mp_episode_segments')
  330. ->where('segment_id', $segmentId)
  331. ->select('voice_actor', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
  332. ->first();
  333. // 如果分镜有对话内容,创建视频配音合成任务
  334. $dubTaskId = null;
  335. if ($segmentInfo && !empty($segmentInfo->dialogue)) {
  336. $generate_json = [
  337. 'text' => $segmentInfo->dialogue,
  338. 'role' => $segmentInfo->voice_actor,
  339. 'voice_type' => $segmentInfo->voice_type,
  340. 'voice_name' => $segmentInfo->voice_name,
  341. 'emotion' => $segmentInfo->emotion,
  342. 'emotion_type' => $segmentInfo->emotion_type,
  343. 'gender' => $segmentInfo->gender,
  344. 'speed_ratio' => $segmentInfo->speed_ratio ?? 0,
  345. 'loudness_ratio' => $segmentInfo->loudness_ratio ?? 0,
  346. 'emotion_scale' => $segmentInfo->emotion_scale ?? 0,
  347. 'pitch' => $segmentInfo->pitch ?? 0,
  348. ];
  349. // 插入视频配音合成任务
  350. $dubTaskId = DB::table('mp_dub_video_tasks')->insertGetId([
  351. 'alias_segment_id' => $segmentId,
  352. 'video_url' => $statusResult['result_url'],
  353. 'generate_status' => '执行中',
  354. 'dub_video_url' => '',
  355. 'generate_json' => json_encode($generate_json, 256),
  356. 'created_at' => $now,
  357. 'updated_at' => $now,
  358. ]);
  359. if (!$dubTaskId) {
  360. Utils::throwError('20003:创建配音任务失败');
  361. }
  362. }
  363. DB::commit();
  364. } catch (\Exception $e) {
  365. DB::rollBack();
  366. dLog('anime')->error('视频任务处理失败', [
  367. 'task_id' => $taskId,
  368. 'error' => $e->getMessage()
  369. ]);
  370. }
  371. } elseif ($statusResult['status'] === 'failed') {
  372. DB::table('mp_episode_segments')
  373. ->where('video_task_id', $taskId)
  374. ->update([
  375. 'video_task_status' => '失败',
  376. 'updated_at' => date('Y-m-d H:i:s')
  377. ]);
  378. }
  379. }
  380. }
  381. // 发送当前状态
  382. echo "data: " . json_encode([
  383. 'type' => 'status_update',
  384. 'data' => [
  385. 'task_id' => $task->id,
  386. 'status' => $task->status,
  387. 'result_url' => $task->result_url,
  388. 'error_message' => $task->error_message,
  389. 'elapsed_time' => time() - $startTime
  390. ]
  391. ]) . "\n\n";
  392. ob_flush();
  393. flush();
  394. // 如果任务完成(成功或失败),结束连接
  395. if (in_array($task->status, [
  396. 'success',
  397. 'failed'
  398. ])) {
  399. echo "data: " . json_encode([
  400. 'type' => 'completed',
  401. 'data' => [
  402. 'task_id' => $task->id,
  403. 'status' => $task->status,
  404. 'video_url' => $task->result_url,
  405. 'last_frame_url' => $task->last_frame_url,
  406. 'error_message' => $task->error_message
  407. ]
  408. ]) . "\n\n";
  409. ob_flush();
  410. flush();
  411. break;
  412. }
  413. sleep($checkInterval);
  414. } catch (\Exception $e) {
  415. echo "data: " . json_encode([
  416. 'type' => 'error',
  417. 'message' => '查询任务状态失败: ' . $e->getMessage()
  418. ]) . "\n\n";
  419. ob_flush();
  420. flush();
  421. sleep($checkInterval);
  422. }
  423. }
  424. // 超时处理
  425. if (time() - $startTime >= $maxDuration) {
  426. echo "data: " . json_encode([
  427. 'type' => 'timeout',
  428. 'message' => '任务执行超时,请稍后查询任务状态',
  429. 'data' => [
  430. 'task_id' => $taskId
  431. ]
  432. ]) . "\n\n";
  433. ob_flush();
  434. flush();
  435. }
  436. }, 200, [
  437. 'Content-Type' => 'text/event-stream',
  438. 'Cache-Control' => 'no-cache',
  439. 'Connection' => 'keep-alive',
  440. 'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
  441. ]);
  442. }
  443. /**
  444. * 批量生成分镜视频
  445. */
  446. public function batchSetSegmentVideos(Request $request) {
  447. // 忽略所有超时限制
  448. set_time_limit(0);
  449. ini_set('max_execution_time', '0');
  450. $data = $request->all();
  451. // 验证参数
  452. $validator = Validator::make($data, [
  453. 'anime_id' => 'required|string',
  454. 'episode_number' => 'required|integer',
  455. ], [
  456. 'anime_id.required' => '动漫对话ID不能为空',
  457. 'episode_number.required' => '剧集序号不能为空',
  458. 'episode_number.integer' => '剧集序号必须是整数',
  459. ]);
  460. if ($validator->fails()) {
  461. Utils::throwError('1002:' . $validator->errors()->first());
  462. }
  463. $animeId = $data['anime_id'];
  464. $episodeId = $data['episode_id'];
  465. $episodeNumber = $data['episode_number'];
  466. // 获取所有分镜信息
  467. $segments = DB::table('mp_episode_segments')
  468. ->where('anime_id', $animeId)
  469. ->where('episode_id', $episodeId)
  470. ->where('video_url', '')
  471. ->orderBy('segment_number')
  472. ->limit(2)
  473. ->get();
  474. if ($segments->isEmpty()) {
  475. Utils::throwError('20003:未找到分镜数据');
  476. }
  477. // 批量创建视频任务
  478. $taskIds = [];
  479. $segmentTasks = [];
  480. foreach ($segments as $segment) {
  481. try {
  482. // 检查是否已有视频或正在生成中
  483. if (!empty($segment->video_url) || $segment->video_task_status === '生成中') {
  484. continue;
  485. }
  486. // 获取分镜内容
  487. $segmentContent = $segment->segment_content ?: '';
  488. $tailFrame = $segment->tail_frame ?: '';
  489. // 构建完整的提示词
  490. $fullPrompt = $segmentContent;
  491. if ($tailFrame) {
  492. $fullPrompt .= "\n尾帧描述:$tailFrame";
  493. }
  494. // 智能选择视频时长
  495. // $videoDuration = $this->AnimeService->calculateOptimalVideoDuration($segmentContent, $tailFrame);
  496. $videoDuration = -1;
  497. // 构建视频生成参数
  498. $videoParams = [
  499. 'model' => 'doubao-seedance-1-0-pro-250528',
  500. 'alias_segment_id' => $segment->segment_id,
  501. 'prompt' => $fullPrompt,
  502. 'video_duration' => $videoDuration,
  503. 'video_resolution' => '720P',
  504. 'seed' => -1,
  505. 'ratio' => '16:9',
  506. 'generate_audio' => false,
  507. 'draft' => false,
  508. 'watermark' => false,
  509. 'camera_fixed' => false,
  510. // 'callback_url' => 'http://mpaudio.yqsd.cn/api/video/seedanceCallback'
  511. ];
  512. // 如果分镜有图片,作为首帧
  513. if (!empty($segment->img_url)) {
  514. $videoParams['first_frame_url'] = $segment->img_url;
  515. }
  516. // 构建content数组
  517. $videoParams['content'] = [
  518. [
  519. 'type' => 'text',
  520. 'text' => $videoParams['prompt'],
  521. ]
  522. ];
  523. // 如果有首帧图片,添加到content中
  524. if (isset($videoParams['first_frame_url'])) {
  525. $videoParams['content'][] = [
  526. 'type' => 'image_url',
  527. 'image_url' => [
  528. 'url' => $videoParams['first_frame_url'],
  529. ],
  530. 'role' => 'first_frame',
  531. ];
  532. }
  533. // 创建视频任务
  534. $task = $this->AIVideoGenerationService->createSeedanceTask($videoParams);
  535. $taskIds[] = $task->id;
  536. // 更新分镜表的视频任务信息
  537. DB::table('mp_episode_segments')
  538. ->where('segment_id', $segment->segment_id)
  539. ->update([
  540. 'video_task_id' => $task->id,
  541. 'video_task_status' => '生成中',
  542. 'updated_at' => date('Y-m-d H:i:s')
  543. ]);
  544. $segmentTasks[] = [
  545. 'segment_id' => $segment->segment_id,
  546. 'task_id' => $task->id,
  547. 'segment_number' => $segment->segment_number,
  548. ];
  549. } catch (\Exception $e) {
  550. dLog('anime')->error('创建分镜视频任务失败: ' . $e->getMessage(), [
  551. 'segment_id' => $segment->segment_id,
  552. 'anime_id' => $animeId,
  553. 'episode_number' => $episodeNumber
  554. ]);
  555. continue;
  556. }
  557. }
  558. if (empty($taskIds)) {
  559. Utils::throwError('20003:没有需要生成视频的分镜');
  560. }
  561. // 记录开始时间(包含创建任务的时间)
  562. $startTime = time();
  563. $maxDuration = 1800; // 30分钟超时
  564. // 设置 SSE 响应头并开始长连接
  565. return response()->stream(function () use ($taskIds, $segmentTasks, $animeId, $episodeNumber, $episodeId, $startTime, $maxDuration) {
  566. // 发送初始任务创建信息
  567. echo "data: " . json_encode([
  568. 'type' => 'tasks_created',
  569. 'data' => [
  570. 'anime_id' => $animeId,
  571. 'episode_id' => $episodeId,
  572. 'episode_number' => $episodeNumber,
  573. 'total_tasks' => count($taskIds),
  574. 'task_ids' => $taskIds,
  575. 'segments' => $segmentTasks,
  576. 'start_time' => $startTime,
  577. 'max_duration' => $maxDuration
  578. ]
  579. ]) . "\n\n";
  580. ob_flush();
  581. flush();
  582. $checkInterval = 10; // 每10秒检查一次
  583. $completedTasks = [];
  584. $failedTasks = [];
  585. // 检查是否已经超时(包含创建任务的时间)
  586. while (time() - $startTime < $maxDuration) {
  587. try {
  588. $allCompleted = true;
  589. $currentStatus = [];
  590. foreach ($segmentTasks as $segmentTask) {
  591. $taskId = $segmentTask['task_id'];
  592. $segmentId = $segmentTask['segment_id'];
  593. // 跳过已完成或失败的任务
  594. if (in_array($taskId, $completedTasks) || in_array($taskId, $failedTasks)) {
  595. continue;
  596. }
  597. // 查询任务状态
  598. $task = \App\Models\MpGenerateVideoTask::find($taskId);
  599. if (!$task) {
  600. $failedTasks[] = $taskId;
  601. continue;
  602. }
  603. // 如果任务还在处理中,查询最新状态
  604. if ($task->status === 'processing') {
  605. $statusResult = $this->AIVideoGenerationService->querySeedanceTaskStatus($task);
  606. if (isset($statusResult['status'])) {
  607. // 更新任务状态
  608. $task->update([
  609. 'status' => $statusResult['status'],
  610. 'result_json' => $statusResult['result_json'] ?? [],
  611. 'result_url' => $statusResult['result_url'] ?? null,
  612. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  613. 'error_message' => $statusResult['error_message'] ?? null,
  614. 'completed_at' => in_array($statusResult['status'], [
  615. 'success',
  616. 'failed'
  617. ]) ? now() : null
  618. ]);
  619. // 如果任务成功,使用事务更新分镜表并创建配音任务
  620. if ($statusResult['status'] === 'success' && isset($statusResult['result_url'])) {
  621. try {
  622. DB::beginTransaction();
  623. $now = date('Y-m-d H:i:s');
  624. // 更新分镜表
  625. $updateResult = DB::table('mp_episode_segments')
  626. ->where('segment_id', $segmentId)
  627. ->update([
  628. 'video_url' => $statusResult['result_url'],
  629. 'video_task_status' => '已完成',
  630. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  631. 'updated_at' => $now
  632. ]);
  633. if (!$updateResult) {
  634. if (!$dubTaskId) {
  635. Utils::throwError('20003:更新分镜表失败');
  636. }
  637. }
  638. // 获取分镜信息用于创建配音任务
  639. $segment = DB::table('mp_episode_segments')
  640. ->where('segment_id', $segmentId)
  641. ->select('voice_actor', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
  642. ->first();
  643. // 如果分镜有对话内容,创建视频配音合成任务
  644. $dubTaskId = null;
  645. if ($segment && !empty($segment->dialogue)) {
  646. $generate_json = [
  647. 'text' => $segment->dialogue,
  648. 'role' => $segment->voice_actor,
  649. 'voice_type' => $segment->voice_type,
  650. 'voice_name' => $segment->voice_name,
  651. 'emotion' => $segment->emotion,
  652. 'emotion_type' => $segment->emotion_type,
  653. 'gender' => $segment->gender,
  654. 'speed_ratio' => $segment->speed_ratio ?? 0,
  655. 'loudness_ratio' => $segment->loudness_ratio ?? 0,
  656. 'emotion_scale' => $segment->emotion_scale ?? 0,
  657. 'pitch' => $segment->pitch ?? 0,
  658. ];
  659. // 插入视频配音合成任务
  660. $dubTaskId = DB::table('mp_dub_video_tasks')->insertGetId([
  661. 'alias_segment_id' => $segmentId,
  662. 'video_url' => $statusResult['result_url'],
  663. 'generate_status' => '执行中',
  664. 'dub_video_url' => '',
  665. 'generate_json' => json_encode($generate_json, 256),
  666. 'created_at' => $now,
  667. 'updated_at' => $now,
  668. ]);
  669. if (!$dubTaskId) {
  670. Utils::throwError('20003:创建配音任务失败');
  671. }
  672. }
  673. DB::commit();
  674. $completedTasks[] = $taskId;
  675. // 发送单个任务完成通知
  676. echo "data: " . json_encode([
  677. 'type' => 'task_completed',
  678. 'data' => [
  679. 'segment_id' => $segmentId,
  680. 'task_id' => $taskId,
  681. 'segment_number' => $segmentTask['segment_number'],
  682. 'status' => 'success',
  683. 'video_url' => $statusResult['result_url'],
  684. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  685. 'completed_count' => count($completedTasks),
  686. 'total_count' => count($segmentTasks),
  687. 'has_dub_task' => $dubTaskId > 0
  688. ]
  689. ]) . "\n\n";
  690. ob_flush();
  691. flush();
  692. } catch (\Exception $e) {
  693. DB::rollBack();
  694. dLog('anime')->error('视频任务处理失败', [
  695. 'segment_id' => $segmentId,
  696. 'task_id' => $taskId,
  697. 'error' => $e->getMessage()
  698. ]);
  699. // 发送任务处理失败通知
  700. echo "data: " . json_encode([
  701. 'type' => 'task_process_error',
  702. 'data' => [
  703. 'segment_id' => $segmentId,
  704. 'task_id' => $taskId,
  705. 'segment_number' => $segmentTask['segment_number'],
  706. 'error_message' => '任务处理失败: ' . $e->getMessage()
  707. ]
  708. ]) . "\n\n";
  709. ob_flush();
  710. flush();
  711. }
  712. } elseif ($statusResult['status'] === 'failed') {
  713. $updateResult = DB::table('mp_episode_segments')
  714. ->where('segment_id', $segmentId)
  715. ->update([
  716. 'video_task_status' => '失败',
  717. 'updated_at' => date('Y-m-d H:i:s')
  718. ]);
  719. $failedTasks[] = $taskId;
  720. // 发送任务失败通知
  721. echo "data: " . json_encode([
  722. 'type' => 'task_failed',
  723. 'data' => [
  724. 'segment_id' => $segmentId,
  725. 'task_id' => $taskId,
  726. 'segment_number' => $segmentTask['segment_number'],
  727. 'status' => 'failed',
  728. 'error_message' => $statusResult['error_message'] ?? '视频生成失败',
  729. 'failed_count' => count($failedTasks),
  730. 'total_count' => count($segmentTasks)
  731. ]
  732. ]) . "\n\n";
  733. ob_flush();
  734. flush();
  735. }
  736. }
  737. }
  738. // 收集当前状态
  739. $currentStatus[] = [
  740. 'segment_id' => $segmentId,
  741. 'task_id' => $taskId,
  742. 'segment_number' => $segmentTask['segment_number'],
  743. 'status' => $task->status,
  744. 'video_url' => $task->result_url,
  745. 'error_message' => $task->error_message
  746. ];
  747. // 检查是否还有未完成的任务
  748. if (!in_array($task->status, ['success', 'failed'])) {
  749. $allCompleted = false;
  750. }
  751. }
  752. // 发送整体进度更新
  753. echo "data: " . json_encode([
  754. 'type' => 'progress_update',
  755. 'data' => [
  756. 'anime_id' => $animeId,
  757. 'episode_id' => $episodeId,
  758. 'episode_number' => $episodeNumber,
  759. 'completed_count' => count($completedTasks),
  760. 'failed_count' => count($failedTasks),
  761. 'total_count' => count($segmentTasks),
  762. 'elapsed_time' => time() - $startTime,
  763. 'remaining_time' => max(0, $maxDuration - (time() - $startTime)),
  764. 'segments_status' => $currentStatus
  765. ]
  766. ]) . "\n\n";
  767. ob_flush();
  768. flush();
  769. // 如果所有任务都完成了,结束连接
  770. if ($allCompleted) {
  771. // 获取最终结果
  772. $finalSegments = DB::table('mp_episode_segments')
  773. ->where('anime_id', $animeId)
  774. ->where('episode_id', $episodeId)
  775. ->orderBy('segment_number')
  776. ->select('segment_id', 'segment_number', 'video_url', 'video_task_status', 'last_frame_url')
  777. ->get();
  778. echo "data: " . json_encode([
  779. 'type' => 'all_completed',
  780. 'data' => [
  781. 'anime_id' => $animeId,
  782. 'episode_id' => $episodeId,
  783. 'episode_number' => $episodeNumber,
  784. 'completed_count' => count($completedTasks),
  785. 'failed_count' => count($failedTasks),
  786. 'total_count' => count($segmentTasks),
  787. 'total_elapsed_time' => time() - $startTime,
  788. 'segments' => $finalSegments->map(function($segment) {
  789. return [
  790. 'segment_id' => $segment->segment_id,
  791. 'segment_number' => $segment->segment_number,
  792. 'video_url' => $segment->video_url,
  793. 'status' => $segment->video_task_status,
  794. 'last_frame_url' => $segment->last_frame_url
  795. ];
  796. })->toArray()
  797. ]
  798. ]) . "\n\n";
  799. ob_flush();
  800. flush();
  801. break;
  802. }
  803. // 在睡眠前再次检查是否超时,避免不必要的等待
  804. if (time() - $startTime >= $maxDuration) {
  805. break;
  806. }
  807. sleep($checkInterval);
  808. } catch (\Exception $e) {
  809. echo "data: " . json_encode([
  810. 'type' => 'error',
  811. 'message' => '查询任务状态失败: ' . $e->getMessage(),
  812. 'elapsed_time' => time() - $startTime,
  813. 'remaining_time' => max(0, $maxDuration - (time() - $startTime))
  814. ]) . "\n\n";
  815. ob_flush();
  816. flush();
  817. // 检查是否超时,如果超时则停止循环
  818. if (time() - $startTime >= $maxDuration) {
  819. break;
  820. }
  821. sleep($checkInterval);
  822. }
  823. }
  824. // 超时处理 - 只有在真正超时时才执行
  825. if (time() - $startTime >= $maxDuration) {
  826. // 获取当前所有任务状态
  827. $timeoutSegments = DB::table('mp_episode_segments')
  828. ->where('anime_id', $animeId)
  829. ->where('episode_id', $episodeId)
  830. ->orderBy('segment_number')
  831. ->select('segment_id', 'segment_number', 'video_task_id', 'video_task_status', 'video_url')
  832. ->get();
  833. echo "data: " . json_encode([
  834. 'type' => 'timeout',
  835. 'message' => '批量视频生成已超时30分钟,停止状态查询',
  836. 'data' => [
  837. 'anime_id' => $animeId,
  838. 'episode_id' => $episodeId,
  839. 'episode_number' => $episodeNumber,
  840. 'completed_count' => count($completedTasks),
  841. 'failed_count' => count($failedTasks),
  842. 'total_count' => count($segmentTasks),
  843. 'total_elapsed_time' => time() - $startTime,
  844. 'timeout_duration' => $maxDuration,
  845. 'segments' => $timeoutSegments->map(function($segment) {
  846. return [
  847. 'segment_id' => $segment->segment_id,
  848. 'segment_number' => $segment->segment_number,
  849. 'task_id' => $segment->video_task_id,
  850. 'status' => $segment->video_task_status,
  851. 'video_url' => $segment->video_url
  852. ];
  853. })->toArray()
  854. ]
  855. ]) . "\n\n";
  856. ob_flush();
  857. flush();
  858. }
  859. }, 200, [
  860. 'Content-Type' => 'text/event-stream',
  861. 'Cache-Control' => 'no-cache',
  862. 'Connection' => 'keep-alive',
  863. 'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
  864. ]);
  865. }
  866. // 应用有声制作参数
  867. public function applyAudioData(Request $request) {
  868. $data = $request->all();
  869. $result = $this->AnimeService->applyAudioData($data);
  870. return $this->success(['success' => $result ? 1 : 0]);
  871. }
  872. // 获取分集生成图片信息
  873. public function episodePicsInfo(Request $request) {
  874. // 忽略所有超时限制
  875. set_time_limit(0);
  876. ini_set('max_execution_time', '0');
  877. $data = $request->all();
  878. // 获取生成器函数
  879. $streamGenerator = $this->AnimeService->episodePicsInfo($data);
  880. // 返回 SSE 响应
  881. return response()->stream($streamGenerator, 200, [
  882. 'Content-Type' => 'text/event-stream',
  883. 'Cache-Control' => 'no-cache',
  884. 'Connection' => 'keep-alive',
  885. 'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
  886. ]);
  887. }
  888. /**
  889. * 创建完整视频合成任务
  890. *
  891. * @param Request $request
  892. * @return \Illuminate\Http\JsonResponse
  893. */
  894. public function createCompleteVideoTask(Request $request)
  895. {
  896. $data = $request->all();
  897. // 验证参数
  898. $validator = Validator::make($data, [
  899. 'anime_id' => 'required|string',
  900. 'episode_id' => 'required|string',
  901. ], [
  902. 'anime_id.required' => '请选择动漫',
  903. 'episode_id.required' => '请选择分集',
  904. ]);
  905. if ($validator->fails()) {
  906. Utils::throwError('1002:' . $validator->errors()->first());
  907. }
  908. try {
  909. $result = $this->AnimeService->createCompleteVideoTask($data);
  910. return $this->success($result);
  911. } catch (\Exception $e) {
  912. dLog('anime')->error('创建完整视频合成任务失败', [
  913. 'anime_id' => $data['anime_id'] ?? '',
  914. 'episode_id' => $data['episode_id'] ?? '',
  915. 'error' => $e->getMessage()
  916. ]);
  917. return $this->error('20003:'.$e->getMessage());
  918. }
  919. }
  920. public function setRoleOrScene(Request $request) {
  921. $data = $request->all();
  922. $result = $this->AnimeService->setRoleOrScene($data);
  923. return $this->success($result);
  924. }
  925. }