AnimeController.php 34 KB

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