AnimeController.php 34 KB

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