AnimeController.php 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  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 = [
  35. [
  36. 'model' => 'v3',
  37. 'name' => 'DeepSeek 推理',
  38. ],
  39. [
  40. 'model' => 'r1',
  41. 'name' => 'DeepSeek 思考'
  42. ],
  43. ];
  44. return $this->success($models);
  45. }
  46. // 批量生成主体图片
  47. public function batchSetRoleImg(Request $request) {
  48. // 忽略所有超时限制
  49. set_time_limit(0);
  50. ini_set('max_execution_time', '0');
  51. $data = $request->all();
  52. $result = $this->AnimeService->batchSetRoleImg($data);
  53. return $this->success($result);
  54. }
  55. // 批量生成场景图片
  56. public function batchSetSceneImg(Request $request) {
  57. // 忽略所有超时限制
  58. set_time_limit(0);
  59. ini_set('max_execution_time', '0');
  60. $data = $request->all();
  61. $result = $this->AnimeService->batchSetSceneImg($data);
  62. return $this->success($result);
  63. }
  64. // 编辑动漫
  65. public function editAnime(Request $request) {
  66. $data = $request->all();
  67. $result = $this->AnimeService->editAnime($data);
  68. return $this->success(['success'=>$result ? 1 : 0]);
  69. }
  70. // 修改分集主体列表
  71. public function changeEpisodeRoles(Request $request) {
  72. $data = $request->all();
  73. $result = $this->AnimeService->changeEpisodeRoles($data);
  74. return $this->success(['success'=>$result ? 1 : 0]);
  75. }
  76. // 修改分集场景列表
  77. public function changeEpisodeScenes(Request $request) {
  78. $data = $request->all();
  79. $result = $this->AnimeService->changeEpisodeScenes($data);
  80. return $this->success(['success'=>$result ? 1 : 0]);
  81. }
  82. // 编辑分镜剧本
  83. public function editSegment(Request $request) {
  84. $data = $request->all();
  85. $result = $this->AnimeService->editSegment($data);
  86. return $this->success(['success'=>$result ? 1 : 0]);
  87. }
  88. // 动漫对话列表
  89. public function chatList(Request $request) {
  90. $data = $request->all();
  91. $result = $this->AnimeService->chatList($data);
  92. return $this->success($result, [new AnimeTransformer(), 'newBuildChatList']);
  93. }
  94. // 动漫对话历史记录
  95. public function chatHistory(Request $request) {
  96. $data = $request->all();
  97. $result = $this->AnimeService->chatHistory($data);
  98. return $this->success($result);
  99. }
  100. // 动漫大纲
  101. public function animeDetail(Request $request) {
  102. $data = $request->all();
  103. $result = $this->AnimeService->animeDetail($data);
  104. return $this->success($result);
  105. }
  106. // 动漫剧集
  107. public function episodeInfo(Request $request) {
  108. $data = $request->all();
  109. $result = $this->AnimeService->episodeInfo($data);
  110. return $this->success($result);
  111. }
  112. // 复制剧集副本
  113. public function copyEpisodeVersion(Request $request) {
  114. $data = $request->all();
  115. $result = $this->AnimeService->copyEpisodeVersion($data);
  116. return $this->success(['success' => $result ? 1 : 0]);
  117. }
  118. // 剧集副本列表
  119. public function episodeVersions(Request $request) {
  120. $data = $request->all();
  121. $result = $this->AnimeService->episodeVersions($data);
  122. return $this->success($result);
  123. }
  124. // 绑定剧集副本
  125. public function bindEpisodeVersion(Request $request) {
  126. $data = $request->all();
  127. $result = $this->AnimeService->bindEpisodeVersion($data);
  128. return $this->success(['success' => $result ? 1 : 0]);
  129. }
  130. // 对话改图
  131. public function chatChangeImg(Request $request) {
  132. $data = $request->all();
  133. $result = $this->AnimeService->chatChangeImg($data);
  134. return $this->success(['img_url' => $result]);
  135. }
  136. // 一键生成分镜
  137. public function batchSetSegmentPics(Request $request) {
  138. // 忽略所有超时限制
  139. set_time_limit(0);
  140. ini_set('max_execution_time', '0');
  141. $data = $request->all();
  142. $result = $this->AnimeService->batchSetSegmentPics($data);
  143. return $this->success($result);
  144. }
  145. // 重新生成分镜
  146. public function reGenerateSegment(Request $request) {
  147. // 忽略所有超时限制
  148. set_time_limit(0);
  149. ini_set('max_execution_time', '0');
  150. $data = $request->all();
  151. $result = $this->AnimeService->reGenerateSegment($data);
  152. return $this->success(['img_url'=>$result]);
  153. }
  154. // 添加分镜
  155. public function addSegment(Request $request) {
  156. // 忽略所有超时限制
  157. set_time_limit(0);
  158. ini_set('max_execution_time', '0');
  159. $data = $request->all();
  160. $result = $this->AnimeService->addSegment($data);
  161. return $this->success($result);
  162. }
  163. // 复制分镜
  164. public function copySegment(Request $request) {
  165. $data = $request->all();
  166. $result = $this->AnimeService->copySegment($data);
  167. return $this->success($result);
  168. }
  169. // 移动分镜
  170. public function moveSegment(Request $request) {
  171. $data = $request->all();
  172. $result = $this->AnimeService->moveSegment($data);
  173. return $this->success(['success'=>$result ? 1: 0]);
  174. }
  175. // 删除分镜
  176. public function delSegment(Request $request) {
  177. $data = $request->all();
  178. $result = $this->AnimeService->delSegment($data);
  179. return $this->success(['success'=>$result ? 1: 0]);
  180. }
  181. // 分镜历史图片|视频
  182. public function segmentHistory(Request $request) {
  183. $data = $request->all();
  184. $result = $this->AnimeService->segmentHistory($data);
  185. return $this->success($result);
  186. }
  187. public function createSegmentVideoTask(Request $request) {
  188. // 忽略所有超时限制
  189. set_time_limit(0);
  190. ini_set('max_execution_time', '0');
  191. $data = $request->all();
  192. // 验证参数
  193. $validator = Validator::make($data, [
  194. 'segment_id' => 'required|string',
  195. 'tail_frame' => 'nullable|string|max:500',
  196. ], [
  197. 'segment_id.required' => '分镜ID不能为空',
  198. 'tail_frame.max' => '尾帧描述不能超过500个字符',
  199. ]);
  200. if ($validator->fails()) {
  201. Utils::throwError('1002:' . $validator->errors()->first());
  202. }
  203. // 创建视频生成任务
  204. $result = $this->AnimeService->createSegmentVideoTask($data);
  205. $taskId = $result['task_id'];
  206. // 设置 SSE 响应头
  207. return response()->stream(function () use ($taskId, $result) {
  208. // 设置 SSE 响应头
  209. echo "data: " . json_encode([
  210. 'type' => 'task_created',
  211. 'data' => $result
  212. ]) . "\n\n";
  213. ob_flush();
  214. flush();
  215. $startTime = time();
  216. $maxDuration = 300; // 5分钟超时
  217. $checkInterval = 5; // 每5秒检查一次
  218. while (time() - $startTime < $maxDuration) {
  219. try {
  220. // 查询任务状态
  221. $task = \App\Models\MpGenerateVideoTask::find($taskId);
  222. if (!$task) {
  223. echo "data: " . json_encode([
  224. 'type' => 'error',
  225. 'message' => '任务不存在'
  226. ]) . "\n\n";
  227. ob_flush();
  228. flush();
  229. break;
  230. }
  231. // 如果任务还在处理中,查询最新状态
  232. if ($task->status === 'processing') {
  233. $statusResult = $this->AIVideoGenerationService->querySeedanceTaskStatus($task);
  234. if (isset($statusResult['status'])) {
  235. // 更新任务状态
  236. $task->update([
  237. 'status' => $statusResult['status'],
  238. 'result_url' => $statusResult['result_url'] ?? null,
  239. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  240. 'error_message' => $statusResult['error_message'] ?? null,
  241. 'completed_at' => in_array($statusResult['status'], [
  242. 'success',
  243. 'failed'
  244. ]) ? now() : null
  245. ]);
  246. // 如果任务成功,更新分镜表
  247. if ($statusResult['status'] === 'success' && isset($statusResult['result_url'])) {
  248. DB::table('mp_episode_segments')
  249. ->where('video_task_id', $taskId)
  250. ->update([
  251. 'video_url' => $statusResult['result_url'],
  252. 'video_task_status' => '已完成',
  253. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  254. 'updated_at' => date('Y-m-d H:i:s')
  255. ]);
  256. } elseif ($statusResult['status'] === 'failed') {
  257. DB::table('mp_episode_segments')
  258. ->where('video_task_id', $taskId)
  259. ->update([
  260. 'video_task_status' => '失败',
  261. 'updated_at' => date('Y-m-d H:i:s')
  262. ]);
  263. }
  264. }
  265. }
  266. // 发送当前状态
  267. echo "data: " . json_encode([
  268. 'type' => 'status_update',
  269. 'data' => [
  270. 'task_id' => $task->id,
  271. 'status' => $task->status,
  272. 'result_url' => $task->result_url,
  273. 'error_message' => $task->error_message,
  274. 'progress' => $this->getTaskProgress($task->status),
  275. 'elapsed_time' => time() - $startTime
  276. ]
  277. ]) . "\n\n";
  278. ob_flush();
  279. flush();
  280. // 如果任务完成(成功或失败),结束连接
  281. if (in_array($task->status, [
  282. 'success',
  283. 'failed'
  284. ])) {
  285. echo "data: " . json_encode([
  286. 'type' => 'completed',
  287. 'data' => [
  288. 'task_id' => $task->id,
  289. 'status' => $task->status,
  290. 'video_url' => $task->result_url,
  291. 'last_frame_url' => $task->last_frame_url,
  292. 'error_message' => $task->error_message
  293. ]
  294. ]) . "\n\n";
  295. ob_flush();
  296. flush();
  297. break;
  298. }
  299. sleep($checkInterval);
  300. } catch (\Exception $e) {
  301. echo "data: " . json_encode([
  302. 'type' => 'error',
  303. 'message' => '查询任务状态失败: ' . $e->getMessage()
  304. ]) . "\n\n";
  305. ob_flush();
  306. flush();
  307. sleep($checkInterval);
  308. }
  309. }
  310. // 超时处理
  311. if (time() - $startTime >= $maxDuration) {
  312. echo "data: " . json_encode([
  313. 'type' => 'timeout',
  314. 'message' => '任务执行超时,请稍后查询任务状态',
  315. 'data' => [
  316. 'task_id' => $taskId
  317. ]
  318. ]) . "\n\n";
  319. ob_flush();
  320. flush();
  321. }
  322. }, 200, [
  323. 'Content-Type' => 'text/event-stream',
  324. 'Cache-Control' => 'no-cache',
  325. 'Connection' => 'keep-alive',
  326. 'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
  327. ]);
  328. }
  329. /**
  330. * 获取任务进度百分比
  331. */
  332. private function getTaskProgress($status) {
  333. switch ($status) {
  334. case \App\Models\MpGenerateVideoTask::STATUS_PENDING:
  335. return 10;
  336. case \App\Models\MpGenerateVideoTask::STATUS_PROCESSING:
  337. return 50;
  338. case \App\Models\MpGenerateVideoTask::STATUS_SUCCESS:
  339. return 100;
  340. case \App\Models\MpGenerateVideoTask::STATUS_FAILED:
  341. return 0;
  342. default:
  343. return 0;
  344. }
  345. }
  346. /**
  347. * 批量生成分镜视频
  348. */
  349. public function batchSetSegmentVideos(Request $request) {
  350. // 忽略所有超时限制
  351. set_time_limit(0);
  352. ini_set('max_execution_time', '0');
  353. $data = $request->all();
  354. // 验证参数
  355. $validator = Validator::make($data, [
  356. 'anime_id' => 'required|string',
  357. 'episode_number' => 'required|integer',
  358. ], [
  359. 'anime_id.required' => '动漫对话ID不能为空',
  360. 'episode_number.required' => '剧集序号不能为空',
  361. 'episode_number.integer' => '剧集序号必须是整数',
  362. ]);
  363. if ($validator->fails()) {
  364. Utils::throwError('1002:' . $validator->errors()->first());
  365. }
  366. $animeId = $data['anime_id'];
  367. $episodeId = $data['episode_id'];
  368. $episodeNumber = $data['episode_number'];
  369. // 获取所有分镜信息
  370. $segments = DB::table('mp_episode_segments')
  371. ->where('anime_id', $animeId)
  372. ->where('episode_id', $episodeId)
  373. ->where('video_url', '')
  374. ->orderBy('segment_number')
  375. ->limit(2)
  376. ->get();
  377. if ($segments->isEmpty()) {
  378. Utils::throwError('20003:未找到分镜数据');
  379. }
  380. // 批量创建视频任务
  381. $taskIds = [];
  382. $segmentTasks = [];
  383. foreach ($segments as $segment) {
  384. try {
  385. // 检查是否已有视频或正在生成中
  386. if (!empty($segment->video_url) || $segment->video_task_status === '生成中') {
  387. continue;
  388. }
  389. // 获取分镜内容
  390. $segmentContent = $segment->segment_content ?: '';
  391. $tailFrame = $segment->tail_frame ?: '';
  392. // 构建完整的提示词
  393. $fullPrompt = $segmentContent;
  394. if ($tailFrame) {
  395. $fullPrompt .= "\n尾帧描述:$tailFrame";
  396. }
  397. // 智能选择视频时长
  398. $videoDuration = $this->calculateOptimalVideoDuration($segmentContent, $tailFrame);
  399. // 构建视频生成参数
  400. $videoParams = [
  401. 'model' => 'doubao-seedance-1-0-pro-250528',
  402. 'alias_segment_id' => $segment->segment_id,
  403. 'prompt' => $fullPrompt,
  404. 'video_duration' => $videoDuration,
  405. 'video_resolution' => '720P',
  406. 'seed' => -1,
  407. 'ratio' => '16:9',
  408. 'generate_audio' => false,
  409. 'draft' => false,
  410. 'watermark' => false,
  411. 'camera_fixed' => false,
  412. // 'callback_url' => 'http://mpaudio.yqsd.cn/api/video/seedanceCallback'
  413. ];
  414. // 如果分镜有图片,作为首帧
  415. if (!empty($segment->img_url)) {
  416. $videoParams['first_frame_url'] = $segment->img_url;
  417. }
  418. // 构建content数组
  419. $videoParams['content'] = [
  420. [
  421. 'type' => 'text',
  422. 'text' => $videoParams['prompt'],
  423. ]
  424. ];
  425. // 如果有首帧图片,添加到content中
  426. if (isset($videoParams['first_frame_url'])) {
  427. $videoParams['content'][] = [
  428. 'type' => 'image_url',
  429. 'image_url' => [
  430. 'url' => $videoParams['first_frame_url'],
  431. ],
  432. 'role' => 'first_frame',
  433. ];
  434. }
  435. // 创建视频任务
  436. $task = $this->AIVideoGenerationService->createSeedanceTask($videoParams);
  437. $taskIds[] = $task->id;
  438. // 更新分镜表的视频任务信息
  439. DB::table('mp_episode_segments')
  440. ->where('segment_id', $segment->segment_id)
  441. ->update([
  442. 'video_task_id' => $task->id,
  443. 'video_task_status' => '生成中',
  444. 'updated_at' => date('Y-m-d H:i:s')
  445. ]);
  446. $segmentTasks[] = [
  447. 'segment_id' => $segment->segment_id,
  448. 'task_id' => $task->id,
  449. 'segment_number' => $segment->segment_number,
  450. ];
  451. } catch (\Exception $e) {
  452. dLog('anime')->error('创建分镜视频任务失败: ' . $e->getMessage(), [
  453. 'segment_id' => $segment->segment_id,
  454. 'anime_id' => $animeId,
  455. 'episode_number' => $episodeNumber
  456. ]);
  457. continue;
  458. }
  459. }
  460. if (empty($taskIds)) {
  461. Utils::throwError('20003:没有需要生成视频的分镜');
  462. }
  463. // 记录开始时间(包含创建任务的时间)
  464. $startTime = time();
  465. $maxDuration = 1800; // 30分钟超时
  466. // 设置 SSE 响应头并开始长连接
  467. return response()->stream(function () use ($taskIds, $segmentTasks, $animeId, $episodeNumber, $episodeId, $startTime, $maxDuration) {
  468. // 发送初始任务创建信息
  469. echo "data: " . json_encode([
  470. 'type' => 'tasks_created',
  471. 'data' => [
  472. 'anime_id' => $animeId,
  473. 'episode_id' => $episodeId,
  474. 'episode_number' => $episodeNumber,
  475. 'total_tasks' => count($taskIds),
  476. 'task_ids' => $taskIds,
  477. 'segments' => $segmentTasks,
  478. 'start_time' => $startTime,
  479. 'max_duration' => $maxDuration
  480. ]
  481. ]) . "\n\n";
  482. ob_flush();
  483. flush();
  484. $checkInterval = 10; // 每10秒检查一次
  485. $completedTasks = [];
  486. $failedTasks = [];
  487. // 检查是否已经超时(包含创建任务的时间)
  488. while (time() - $startTime < $maxDuration) {
  489. try {
  490. $allCompleted = true;
  491. $currentStatus = [];
  492. foreach ($segmentTasks as $segmentTask) {
  493. $taskId = $segmentTask['task_id'];
  494. $segmentId = $segmentTask['segment_id'];
  495. // 跳过已完成或失败的任务
  496. if (in_array($taskId, $completedTasks) || in_array($taskId, $failedTasks)) {
  497. continue;
  498. }
  499. // 查询任务状态
  500. $task = \App\Models\MpGenerateVideoTask::find($taskId);
  501. if (!$task) {
  502. $failedTasks[] = $taskId;
  503. continue;
  504. }
  505. // 如果任务还在处理中,查询最新状态
  506. if ($task->status === 'processing') {
  507. $statusResult = $this->AIVideoGenerationService->querySeedanceTaskStatus($task);
  508. if (isset($statusResult['status'])) {
  509. // 更新任务状态
  510. $task->update([
  511. 'status' => $statusResult['status'],
  512. 'result_json' => $statusResult['result_json'] ?? [],
  513. 'result_url' => $statusResult['result_url'] ?? null,
  514. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  515. 'error_message' => $statusResult['error_message'] ?? null,
  516. 'completed_at' => in_array($statusResult['status'], [
  517. 'success',
  518. 'failed'
  519. ]) ? now() : null
  520. ]);
  521. // 如果任务成功,更新分镜表
  522. if ($statusResult['status'] === 'success' && isset($statusResult['result_url'])) {
  523. DB::table('mp_episode_segments')
  524. ->where('segment_id', $segmentId)
  525. ->update([
  526. 'video_url' => $statusResult['result_url'],
  527. 'video_task_status' => '已完成',
  528. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  529. 'updated_at' => date('Y-m-d H:i:s')
  530. ]);
  531. $completedTasks[] = $taskId;
  532. // 发送单个任务完成通知
  533. echo "data: " . json_encode([
  534. 'type' => 'task_completed',
  535. 'data' => [
  536. 'segment_id' => $segmentId,
  537. 'task_id' => $taskId,
  538. 'segment_number' => $segmentTask['segment_number'],
  539. 'status' => 'success',
  540. 'video_url' => $statusResult['result_url'],
  541. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  542. 'completed_count' => count($completedTasks),
  543. 'total_count' => count($segmentTasks)
  544. ]
  545. ]) . "\n\n";
  546. ob_flush();
  547. flush();
  548. } elseif ($statusResult['status'] === 'failed') {
  549. DB::table('mp_episode_segments')
  550. ->where('segment_id', $segmentId)
  551. ->update([
  552. 'video_task_status' => '失败',
  553. 'updated_at' => date('Y-m-d H:i:s')
  554. ]);
  555. $failedTasks[] = $taskId;
  556. // 发送任务失败通知
  557. echo "data: " . json_encode([
  558. 'type' => 'task_failed',
  559. 'data' => [
  560. 'segment_id' => $segmentId,
  561. 'task_id' => $taskId,
  562. 'segment_number' => $segmentTask['segment_number'],
  563. 'status' => 'failed',
  564. 'error_message' => $statusResult['error_message'] ?? '视频生成失败',
  565. 'failed_count' => count($failedTasks),
  566. 'total_count' => count($segmentTasks)
  567. ]
  568. ]) . "\n\n";
  569. ob_flush();
  570. flush();
  571. }
  572. }
  573. }
  574. // 收集当前状态
  575. $currentStatus[] = [
  576. 'segment_id' => $segmentId,
  577. 'task_id' => $taskId,
  578. 'segment_number' => $segmentTask['segment_number'],
  579. 'status' => $task->status,
  580. 'progress' => $this->getTaskProgress($task->status),
  581. 'video_url' => $task->result_url,
  582. 'error_message' => $task->error_message
  583. ];
  584. // 检查是否还有未完成的任务
  585. if (!in_array($task->status, ['success', 'failed'])) {
  586. $allCompleted = false;
  587. }
  588. }
  589. // 发送整体进度更新
  590. echo "data: " . json_encode([
  591. 'type' => 'progress_update',
  592. 'data' => [
  593. 'anime_id' => $animeId,
  594. 'episode_id' => $episodeId,
  595. 'episode_number' => $episodeNumber,
  596. 'completed_count' => count($completedTasks),
  597. 'failed_count' => count($failedTasks),
  598. 'total_count' => count($segmentTasks),
  599. 'elapsed_time' => time() - $startTime,
  600. 'remaining_time' => max(0, $maxDuration - (time() - $startTime)),
  601. 'segments_status' => $currentStatus
  602. ]
  603. ]) . "\n\n";
  604. ob_flush();
  605. flush();
  606. // 如果所有任务都完成了,结束连接
  607. if ($allCompleted) {
  608. // 获取最终结果
  609. $finalSegments = DB::table('mp_episode_segments')
  610. ->where('anime_id', $animeId)
  611. ->where('episode_id', $episodeId)
  612. ->orderBy('segment_number')
  613. ->select('segment_id', 'segment_number', 'video_url', 'video_task_status', 'last_frame_url')
  614. ->get();
  615. echo "data: " . json_encode([
  616. 'type' => 'all_completed',
  617. 'data' => [
  618. 'anime_id' => $animeId,
  619. 'episode_id' => $episodeId,
  620. 'episode_number' => $episodeNumber,
  621. 'completed_count' => count($completedTasks),
  622. 'failed_count' => count($failedTasks),
  623. 'total_count' => count($segmentTasks),
  624. 'total_elapsed_time' => time() - $startTime,
  625. 'segments' => $finalSegments->map(function($segment) {
  626. return [
  627. 'segment_id' => $segment->segment_id,
  628. 'segment_number' => $segment->segment_number,
  629. 'video_url' => $segment->video_url,
  630. 'status' => $segment->video_task_status,
  631. 'last_frame_url' => $segment->last_frame_url
  632. ];
  633. })->toArray()
  634. ]
  635. ]) . "\n\n";
  636. ob_flush();
  637. flush();
  638. break;
  639. }
  640. // 在睡眠前再次检查是否超时,避免不必要的等待
  641. if (time() - $startTime >= $maxDuration) {
  642. break;
  643. }
  644. sleep($checkInterval);
  645. } catch (\Exception $e) {
  646. echo "data: " . json_encode([
  647. 'type' => 'error',
  648. 'message' => '查询任务状态失败: ' . $e->getMessage(),
  649. 'elapsed_time' => time() - $startTime,
  650. 'remaining_time' => max(0, $maxDuration - (time() - $startTime))
  651. ]) . "\n\n";
  652. ob_flush();
  653. flush();
  654. // 检查是否超时,如果超时则停止循环
  655. if (time() - $startTime >= $maxDuration) {
  656. break;
  657. }
  658. sleep($checkInterval);
  659. }
  660. }
  661. // 超时处理 - 只有在真正超时时才执行
  662. if (time() - $startTime >= $maxDuration) {
  663. // 获取当前所有任务状态
  664. $timeoutSegments = DB::table('mp_episode_segments')
  665. ->where('anime_id', $animeId)
  666. ->where('episode_id', $episodeId)
  667. ->orderBy('segment_number')
  668. ->select('segment_id', 'segment_number', 'video_task_id', 'video_task_status', 'video_url')
  669. ->get();
  670. echo "data: " . json_encode([
  671. 'type' => 'timeout',
  672. 'message' => '批量视频生成已超时30分钟,停止状态查询',
  673. 'data' => [
  674. 'anime_id' => $animeId,
  675. 'episode_id' => $episodeId,
  676. 'episode_number' => $episodeNumber,
  677. 'completed_count' => count($completedTasks),
  678. 'failed_count' => count($failedTasks),
  679. 'total_count' => count($segmentTasks),
  680. 'total_elapsed_time' => time() - $startTime,
  681. 'timeout_duration' => $maxDuration,
  682. 'segments' => $timeoutSegments->map(function($segment) {
  683. return [
  684. 'segment_id' => $segment->segment_id,
  685. 'segment_number' => $segment->segment_number,
  686. 'task_id' => $segment->video_task_id,
  687. 'status' => $segment->video_task_status,
  688. 'video_url' => $segment->video_url
  689. ];
  690. })->toArray()
  691. ]
  692. ]) . "\n\n";
  693. ob_flush();
  694. flush();
  695. }
  696. }, 200, [
  697. 'Content-Type' => 'text/event-stream',
  698. 'Cache-Control' => 'no-cache',
  699. 'Connection' => 'keep-alive',
  700. 'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
  701. ]);
  702. }
  703. /**
  704. * 计算最优视频时长(从 AnimeService 复制)
  705. */
  706. private function calculateOptimalVideoDuration($segmentContent, $tailFrame = '') {
  707. // 基础时长
  708. $baseDuration = 4;
  709. // 根据内容长度调整
  710. $contentLength = mb_strlen($segmentContent);
  711. if ($contentLength > 200) {
  712. $baseDuration += 2;
  713. } elseif ($contentLength > 100) {
  714. $baseDuration += 1;
  715. }
  716. // 根据内容复杂度调整
  717. $complexity = $this->analyzeContentComplexity($segmentContent);
  718. if ($complexity > 0.7) {
  719. $baseDuration += 2;
  720. } elseif ($complexity > 0.5) {
  721. $baseDuration += 1;
  722. }
  723. // 如果有尾帧描述,增加时长
  724. if (!empty($tailFrame)) {
  725. $baseDuration += 1;
  726. }
  727. // 限制在合理范围内
  728. return min(max($baseDuration, 4), 12);
  729. }
  730. /**
  731. * 分析内容复杂度(从 AnimeService 复制)
  732. */
  733. private function analyzeContentComplexity($text) {
  734. $complexity = 0;
  735. // 动作词汇
  736. $actionWords = ['跑', '跳', '飞', '战斗', '攻击', '移动', '转身', '冲刺', '闪避', '追逐'];
  737. foreach ($actionWords as $word) {
  738. if (strpos($text, $word) !== false) {
  739. $complexity += 0.1;
  740. }
  741. }
  742. // 场景变化词汇
  743. $sceneWords = ['切换', '转场', '变化', '移动', '远景', '近景', '特写'];
  744. foreach ($sceneWords as $word) {
  745. if (strpos($text, $word) !== false) {
  746. $complexity += 0.15;
  747. }
  748. }
  749. // 情感词汇
  750. $emotionWords = ['激动', '愤怒', '悲伤', '惊讶', '恐惧', '兴奋'];
  751. foreach ($emotionWords as $word) {
  752. if (strpos($text, $word) !== false) {
  753. $complexity += 0.05;
  754. }
  755. }
  756. return min($complexity, 1.0);
  757. }
  758. // public function scriptList(Request $request) {
  759. // $data = $request->all();
  760. // $result = $this->AnimeService->scriptList($data);
  761. // return $this->success($result, [new AnimeTransformer(), 'newBuildScriptList']);
  762. // }
  763. }