AnimeController.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  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. 'video_url' => $task->result_url,
  270. 'last_frame_url' => $task->last_frame_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. 'alias_segment_id' => $segment->segment_id,
  377. 'prompt' => $fullPrompt,
  378. 'video_duration' => $videoDuration,
  379. 'video_resolution' => '720P',
  380. 'seed' => -1,
  381. 'ratio' => '16:9',
  382. 'generate_audio' => false,
  383. 'draft' => false,
  384. 'watermark' => false,
  385. 'camera_fixed' => false,
  386. 'callback_url' => 'http://mpaudio.yqsd.cn'
  387. ];
  388. // 如果分镜有图片,作为首帧
  389. if (!empty($segment->img_url)) {
  390. $videoParams['first_frame_url'] = $segment->img_url;
  391. }
  392. // 构建content数组
  393. $videoParams['content'] = [
  394. [
  395. 'type' => 'text',
  396. 'text' => $videoParams['prompt'],
  397. ]
  398. ];
  399. // 如果有首帧图片,添加到content中
  400. if (isset($videoParams['first_frame_url'])) {
  401. $videoParams['content'][] = [
  402. 'type' => 'image_url',
  403. 'image_url' => [
  404. 'url' => $videoParams['first_frame_url'],
  405. ],
  406. 'role' => 'first_frame',
  407. ];
  408. }
  409. // 创建视频任务
  410. $task = $this->AIVideoGenerationService->createSeedanceTask($videoParams);
  411. $taskIds[] = $task->id;
  412. // 更新分镜表的视频任务信息
  413. DB::table('mp_episode_segments')
  414. ->where('segment_id', $segment->segment_id)
  415. ->update([
  416. 'video_task_id' => $task->id,
  417. 'video_task_status' => '生成中',
  418. 'updated_at' => date('Y-m-d H:i:s')
  419. ]);
  420. $segmentTasks[] = [
  421. 'segment_id' => $segment->segment_id,
  422. 'task_id' => $task->id,
  423. 'segment_number' => $segment->segment_number,
  424. ];
  425. } catch (\Exception $e) {
  426. dLog('anime')->error('创建分镜视频任务失败: ' . $e->getMessage(), [
  427. 'segment_id' => $segment->segment_id,
  428. 'anime_id' => $animeId,
  429. 'episode_number' => $episodeNumber
  430. ]);
  431. continue;
  432. }
  433. }
  434. if (empty($taskIds)) {
  435. Utils::throwError('20003:没有需要生成视频的分镜');
  436. }
  437. // 记录开始时间(包含创建任务的时间)
  438. $startTime = time();
  439. $maxDuration = 1800; // 30分钟超时
  440. // 设置 SSE 响应头并开始长连接
  441. return response()->stream(function () use ($taskIds, $segmentTasks, $animeId, $episodeNumber, $episodeId, $startTime, $maxDuration) {
  442. // 发送初始任务创建信息
  443. echo "data: " . json_encode([
  444. 'type' => 'tasks_created',
  445. 'data' => [
  446. 'anime_id' => $animeId,
  447. 'episode_id' => $episodeId,
  448. 'episode_number' => $episodeNumber,
  449. 'total_tasks' => count($taskIds),
  450. 'task_ids' => $taskIds,
  451. 'segments' => $segmentTasks,
  452. 'start_time' => $startTime,
  453. 'max_duration' => $maxDuration
  454. ]
  455. ]) . "\n\n";
  456. ob_flush();
  457. flush();
  458. $checkInterval = 10; // 每10秒检查一次
  459. $completedTasks = [];
  460. $failedTasks = [];
  461. // 检查是否已经超时(包含创建任务的时间)
  462. while (time() - $startTime < $maxDuration) {
  463. try {
  464. $allCompleted = true;
  465. $currentStatus = [];
  466. foreach ($segmentTasks as $segmentTask) {
  467. $taskId = $segmentTask['task_id'];
  468. $segmentId = $segmentTask['segment_id'];
  469. // 跳过已完成或失败的任务
  470. if (in_array($taskId, $completedTasks) || in_array($taskId, $failedTasks)) {
  471. continue;
  472. }
  473. // 查询任务状态
  474. $task = \App\Models\MpGenerateVideoTask::find($taskId);
  475. if (!$task) {
  476. $failedTasks[] = $taskId;
  477. continue;
  478. }
  479. // 如果任务还在处理中,查询最新状态
  480. if ($task->status === 'processing') {
  481. $statusResult = $this->AIVideoGenerationService->querySeedanceTaskStatus($task);
  482. if (isset($statusResult['status'])) {
  483. // 更新任务状态
  484. $task->update([
  485. 'status' => $statusResult['status'],
  486. 'result_url' => $statusResult['result_url'] ?? null,
  487. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  488. 'error_message' => $statusResult['error_message'] ?? null,
  489. 'completed_at' => in_array($statusResult['status'], [
  490. 'success',
  491. 'failed'
  492. ]) ? now() : null
  493. ]);
  494. // 如果任务成功,更新分镜表
  495. if ($statusResult['status'] === 'success' && isset($statusResult['result_url'])) {
  496. DB::table('mp_episode_segments')
  497. ->where('segment_id', $segmentId)
  498. ->update([
  499. 'video_url' => $statusResult['result_url'],
  500. 'video_task_status' => '已完成',
  501. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  502. 'updated_at' => date('Y-m-d H:i:s')
  503. ]);
  504. $completedTasks[] = $taskId;
  505. // 发送单个任务完成通知
  506. echo "data: " . json_encode([
  507. 'type' => 'task_completed',
  508. 'data' => [
  509. 'segment_id' => $segmentId,
  510. 'task_id' => $taskId,
  511. 'segment_number' => $segmentTask['segment_number'],
  512. 'status' => 'success',
  513. 'video_url' => $statusResult['result_url'],
  514. 'last_frame_url' => $statusResult['last_frame_url'] ?? '',
  515. 'completed_count' => count($completedTasks),
  516. 'total_count' => count($segmentTasks)
  517. ]
  518. ]) . "\n\n";
  519. ob_flush();
  520. flush();
  521. } elseif ($statusResult['status'] === 'failed') {
  522. DB::table('mp_episode_segments')
  523. ->where('segment_id', $segmentId)
  524. ->update([
  525. 'video_task_status' => '失败',
  526. 'updated_at' => date('Y-m-d H:i:s')
  527. ]);
  528. $failedTasks[] = $taskId;
  529. // 发送任务失败通知
  530. echo "data: " . json_encode([
  531. 'type' => 'task_failed',
  532. 'data' => [
  533. 'segment_id' => $segmentId,
  534. 'task_id' => $taskId,
  535. 'segment_number' => $segmentTask['segment_number'],
  536. 'status' => 'failed',
  537. 'error_message' => $statusResult['error_message'] ?? '视频生成失败',
  538. 'failed_count' => count($failedTasks),
  539. 'total_count' => count($segmentTasks)
  540. ]
  541. ]) . "\n\n";
  542. ob_flush();
  543. flush();
  544. }
  545. }
  546. }
  547. // 收集当前状态
  548. $currentStatus[] = [
  549. 'segment_id' => $segmentId,
  550. 'task_id' => $taskId,
  551. 'segment_number' => $segmentTask['segment_number'],
  552. 'status' => $task->status,
  553. 'progress' => $this->getTaskProgress($task->status),
  554. 'video_url' => $task->result_url,
  555. 'error_message' => $task->error_message
  556. ];
  557. // 检查是否还有未完成的任务
  558. if (!in_array($task->status, ['success', 'failed'])) {
  559. $allCompleted = false;
  560. }
  561. }
  562. // 发送整体进度更新
  563. echo "data: " . json_encode([
  564. 'type' => 'progress_update',
  565. 'data' => [
  566. 'anime_id' => $animeId,
  567. 'episode_id' => $episodeId,
  568. 'episode_number' => $episodeNumber,
  569. 'completed_count' => count($completedTasks),
  570. 'failed_count' => count($failedTasks),
  571. 'total_count' => count($segmentTasks),
  572. 'elapsed_time' => time() - $startTime,
  573. 'remaining_time' => max(0, $maxDuration - (time() - $startTime)),
  574. 'segments_status' => $currentStatus
  575. ]
  576. ]) . "\n\n";
  577. ob_flush();
  578. flush();
  579. // 如果所有任务都完成了,结束连接
  580. if ($allCompleted) {
  581. // 获取最终结果
  582. $finalSegments = DB::table('mp_episode_segments')
  583. ->where('anime_id', $animeId)
  584. ->where('episode_id', $episodeId)
  585. ->orderBy('segment_number')
  586. ->select('segment_id', 'segment_number', 'video_url', 'video_task_status', 'last_frame_url')
  587. ->get();
  588. echo "data: " . json_encode([
  589. 'type' => 'all_completed',
  590. 'data' => [
  591. 'anime_id' => $animeId,
  592. 'episode_id' => $episodeId,
  593. 'episode_number' => $episodeNumber,
  594. 'completed_count' => count($completedTasks),
  595. 'failed_count' => count($failedTasks),
  596. 'total_count' => count($segmentTasks),
  597. 'total_elapsed_time' => time() - $startTime,
  598. 'segments' => $finalSegments->map(function($segment) {
  599. return [
  600. 'segment_id' => $segment->segment_id,
  601. 'segment_number' => $segment->segment_number,
  602. 'video_url' => $segment->video_url,
  603. 'status' => $segment->video_task_status,
  604. 'last_frame_url' => $segment->last_frame_url
  605. ];
  606. })->toArray()
  607. ]
  608. ]) . "\n\n";
  609. ob_flush();
  610. flush();
  611. break;
  612. }
  613. // 在睡眠前再次检查是否超时,避免不必要的等待
  614. if (time() - $startTime >= $maxDuration) {
  615. break;
  616. }
  617. sleep($checkInterval);
  618. } catch (\Exception $e) {
  619. echo "data: " . json_encode([
  620. 'type' => 'error',
  621. 'message' => '查询任务状态失败: ' . $e->getMessage(),
  622. 'elapsed_time' => time() - $startTime,
  623. 'remaining_time' => max(0, $maxDuration - (time() - $startTime))
  624. ]) . "\n\n";
  625. ob_flush();
  626. flush();
  627. // 检查是否超时,如果超时则停止循环
  628. if (time() - $startTime >= $maxDuration) {
  629. break;
  630. }
  631. sleep($checkInterval);
  632. }
  633. }
  634. // 超时处理 - 只有在真正超时时才执行
  635. if (time() - $startTime >= $maxDuration) {
  636. // 获取当前所有任务状态
  637. $timeoutSegments = DB::table('mp_episode_segments')
  638. ->where('anime_id', $animeId)
  639. ->where('episode_id', $episodeId)
  640. ->orderBy('segment_number')
  641. ->select('segment_id', 'segment_number', 'video_task_id', 'video_task_status', 'video_url')
  642. ->get();
  643. echo "data: " . json_encode([
  644. 'type' => 'timeout',
  645. 'message' => '批量视频生成已超时30分钟,停止状态查询',
  646. 'data' => [
  647. 'anime_id' => $animeId,
  648. 'episode_id' => $episodeId,
  649. 'episode_number' => $episodeNumber,
  650. 'completed_count' => count($completedTasks),
  651. 'failed_count' => count($failedTasks),
  652. 'total_count' => count($segmentTasks),
  653. 'total_elapsed_time' => time() - $startTime,
  654. 'timeout_duration' => $maxDuration,
  655. 'segments' => $timeoutSegments->map(function($segment) {
  656. return [
  657. 'segment_id' => $segment->segment_id,
  658. 'segment_number' => $segment->segment_number,
  659. 'task_id' => $segment->video_task_id,
  660. 'status' => $segment->video_task_status,
  661. 'video_url' => $segment->video_url
  662. ];
  663. })->toArray()
  664. ]
  665. ]) . "\n\n";
  666. ob_flush();
  667. flush();
  668. }
  669. }, 200, [
  670. 'Content-Type' => 'text/event-stream',
  671. 'Cache-Control' => 'no-cache',
  672. 'Connection' => 'keep-alive',
  673. 'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
  674. ]);
  675. }
  676. /**
  677. * 计算最优视频时长(从 AnimeService 复制)
  678. */
  679. private function calculateOptimalVideoDuration($segmentContent, $tailFrame = '') {
  680. // 基础时长
  681. $baseDuration = 4;
  682. // 根据内容长度调整
  683. $contentLength = mb_strlen($segmentContent);
  684. if ($contentLength > 200) {
  685. $baseDuration += 2;
  686. } elseif ($contentLength > 100) {
  687. $baseDuration += 1;
  688. }
  689. // 根据内容复杂度调整
  690. $complexity = $this->analyzeContentComplexity($segmentContent);
  691. if ($complexity > 0.7) {
  692. $baseDuration += 2;
  693. } elseif ($complexity > 0.5) {
  694. $baseDuration += 1;
  695. }
  696. // 如果有尾帧描述,增加时长
  697. if (!empty($tailFrame)) {
  698. $baseDuration += 1;
  699. }
  700. // 限制在合理范围内
  701. return min(max($baseDuration, 4), 12);
  702. }
  703. /**
  704. * 分析内容复杂度(从 AnimeService 复制)
  705. */
  706. private function analyzeContentComplexity($text) {
  707. $complexity = 0;
  708. // 动作词汇
  709. $actionWords = ['跑', '跳', '飞', '战斗', '攻击', '移动', '转身', '冲刺', '闪避', '追逐'];
  710. foreach ($actionWords as $word) {
  711. if (strpos($text, $word) !== false) {
  712. $complexity += 0.1;
  713. }
  714. }
  715. // 场景变化词汇
  716. $sceneWords = ['切换', '转场', '变化', '移动', '远景', '近景', '特写'];
  717. foreach ($sceneWords as $word) {
  718. if (strpos($text, $word) !== false) {
  719. $complexity += 0.15;
  720. }
  721. }
  722. // 情感词汇
  723. $emotionWords = ['激动', '愤怒', '悲伤', '惊讶', '恐惧', '兴奋'];
  724. foreach ($emotionWords as $word) {
  725. if (strpos($text, $word) !== false) {
  726. $complexity += 0.05;
  727. }
  728. }
  729. return min($complexity, 1.0);
  730. }
  731. // public function scriptList(Request $request) {
  732. // $data = $request->all();
  733. // $result = $this->AnimeService->scriptList($data);
  734. // return $this->success($result, [new AnimeTransformer(), 'newBuildScriptList']);
  735. // }
  736. }