AnimeService = $AnimeService; $this->AIImageGenerationService = $AIImageGenerationService; $this->AIVideoGenerationService = $AIVideoGenerationService; } public function textModel() { $models = DB::table('mp_text_models')->where('is_enabled', 1)->select('model', 'name')->get()->map(function ($value) { return (array)$value; })->toArray(); return $this->success($models); } public function artStyleList() { $result = [ [ 'art_style' => '日系动漫风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/日系动漫风格.jpg" ], [ 'art_style' => '国漫风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/国漫风格.jpg" ], [ 'art_style' => 'Q版卡通风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/Q版卡通风格.jpg" ], [ 'art_style' => '简约扁平风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/简约扁平风格.jpg" ], [ 'art_style' => '古风仙侠风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/古风仙侠风格.jpg" ], [ 'art_style' => '武侠风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/武侠风格.jpg" ], [ 'art_style' => '新中式水墨风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/新中式水墨风格.jpg" ], [ 'art_style' => '写实插画风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/写实插画风格.jpg" ], [ 'art_style' => '3D卡通风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/3D卡通风格.jpg" ], [ 'art_style' => '条漫风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/条漫风格.jpg" ], [ 'art_style' => '赛博朋克风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/赛博朋克风格.jpg" ], [ 'art_style' => '暗黑悬疑风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/暗黑悬疑风格.jpg" ], [ 'art_style' => '治愈清新风格', 'pic_url' => "https://cdn-zwai.ycsd.cn/mp_audio/art_styles/治愈清新风格.jpg" ], ]; return $this->success($result); } // 批量生成主体图片 public function batchSetRoleImg(Request $request) { // 忽略所有超时限制 set_time_limit(0); ini_set('max_execution_time', '0'); $data = $request->all(); $result = $this->AnimeService->batchSetRoleImg($data); return $this->success($result); } // 批量生成场景图片 public function batchSetSceneImg(Request $request) { // 忽略所有超时限制 set_time_limit(0); ini_set('max_execution_time', '0'); $data = $request->all(); $result = $this->AnimeService->batchSetSceneImg($data); return $this->success($result); } // 编辑动漫 public function editAnime(Request $request) { $data = $request->all(); $result = $this->AnimeService->editAnime($data); return $this->success(['success'=>$result ? 1 : 0]); } // 修改分集主体列表 public function changeEpisodeRoles(Request $request) { $data = $request->all(); $result = $this->AnimeService->changeEpisodeRoles($data); return $this->success(['success'=>$result ? 1 : 0]); } // 修改分集场景列表 public function changeEpisodeScenes(Request $request) { $data = $request->all(); $result = $this->AnimeService->changeEpisodeScenes($data); return $this->success(['success'=>$result ? 1 : 0]); } // 编辑分镜剧本 public function editSegment(Request $request) { $data = $request->all(); $result = $this->AnimeService->editSegment($data); return $this->success(['success'=>$result ? 1 : 0]); } // 动漫对话列表 public function chatList(Request $request) { $data = $request->all(); $result = $this->AnimeService->chatList($data); return $this->success($result, [new AnimeTransformer(), 'newBuildChatList']); } // 动漫对话历史记录 public function chatHistory(Request $request) { $data = $request->all(); $result = $this->AnimeService->chatHistory($data); return $this->success($result); } // 动漫大纲 public function animeDetail(Request $request) { $data = $request->all(); $result = $this->AnimeService->animeDetail($data); return $this->success($result); } // 动漫剧集 public function episodeInfo(Request $request) { $data = $request->all(); $result = $this->AnimeService->episodeInfo($data); return $this->success($result); } // 分镜信息 public function segmentInfo(Request $request) { $data = $request->all(); $result = $this->AnimeService->segmentInfo($data); return $this->success($result); } // 复制剧集副本 public function copyEpisodeVersion(Request $request) { $data = $request->all(); $result = $this->AnimeService->copyEpisodeVersion($data); return $this->success(['success' => $result ? 1 : 0]); } // 剧集副本列表 public function episodeVersions(Request $request) { $data = $request->all(); $result = $this->AnimeService->episodeVersions($data); return $this->success($result); } // 绑定剧集副本 public function bindEpisodeVersion(Request $request) { $data = $request->all(); $result = $this->AnimeService->bindEpisodeVersion($data); return $this->success(['success' => $result ? 1 : 0]); } // 对话改图 public function chatChangeImg(Request $request) { $data = $request->all(); $result = $this->AnimeService->chatChangeImg($data); return $this->success(['img_url' => $result]); } // 一键生成分镜 public function batchSetSegmentPics(Request $request) { // 忽略所有超时限制 set_time_limit(0); ini_set('max_execution_time', '0'); $data = $request->all(); $result = $this->AnimeService->batchSetSegmentPics($data); return $this->success($result); } // 重新生成分镜 public function reGenerateSegment(Request $request) { // 忽略所有超时限制 set_time_limit(0); ini_set('max_execution_time', '0'); $data = $request->all(); $result = $this->AnimeService->reGenerateSegment($data); return $this->success(['img_url'=>$result]); } // 添加分镜 public function addSegment(Request $request) { // 忽略所有超时限制 set_time_limit(0); ini_set('max_execution_time', '0'); $data = $request->all(); $result = $this->AnimeService->addSegment($data); return $this->success($result); } // 复制分镜 public function copySegment(Request $request) { $data = $request->all(); $result = $this->AnimeService->copySegment($data); return $this->success($result); } // 移动分镜 public function moveSegment(Request $request) { $data = $request->all(); $result = $this->AnimeService->moveSegment($data); return $this->success(['success'=>$result ? 1: 0]); } // 删除分镜 public function delSegment(Request $request) { $data = $request->all(); $result = $this->AnimeService->delSegment($data); return $this->success(['success'=>$result ? 1: 0]); } // 分镜历史图片|视频 public function segmentHistory(Request $request) { $data = $request->all(); $result = $this->AnimeService->segmentHistory($data); return $this->success($result); } public function createSegmentVideoTask(Request $request) { // 忽略所有超时限制 set_time_limit(0); ini_set('max_execution_time', '0'); $data = $request->all(); // 验证参数 $validator = Validator::make($data, [ 'segment_id' => 'required|string', 'tail_frame' => 'nullable|string|max:500', ], [ 'segment_id.required' => '分镜ID不能为空', 'tail_frame.max' => '尾帧描述不能超过500个字符', ]); if ($validator->fails()) { Utils::throwError('1002:' . $validator->errors()->first()); } // 创建视频生成任务 $result = $this->AnimeService->createSegmentVideoTask($data); $taskId = $result['task_id']; // 设置 SSE 响应头 return response()->stream(function () use ($taskId, $result) { // 设置 SSE 响应头 echo "data: " . json_encode([ 'type' => 'task_created', 'data' => $result ]) . "\n\n"; ob_flush(); flush(); $startTime = time(); $maxDuration = 300; // 5分钟超时 $checkInterval = 5; // 每5秒检查一次 while (time() - $startTime < $maxDuration) { try { // 查询任务状态 $task = \App\Models\MpGenerateVideoTask::find($taskId); if (!$task) { echo "data: " . json_encode([ 'type' => 'error', 'message' => '任务不存在' ]) . "\n\n"; ob_flush(); flush(); break; } // 如果任务还在处理中,查询最新状态 if ($task->status === 'processing') { $statusResult = $this->AIVideoGenerationService->querySeedanceTaskStatus($task); if (isset($statusResult['status'])) { // 更新任务状态 $task->update([ 'status' => $statusResult['status'], 'result_url' => $statusResult['result_url'] ?? null, 'last_frame_url' => $statusResult['last_frame_url'] ?? '', 'error_message' => $statusResult['error_message'] ?? null, 'completed_at' => in_array($statusResult['status'], [ 'success', 'failed' ]) ? now() : null ]); // 如果任务成功,更新分镜表 if ($statusResult['status'] === 'success' && isset($statusResult['result_url'])) { DB::table('mp_episode_segments') ->where('video_task_id', $taskId) ->update([ 'video_url' => $statusResult['result_url'], 'video_task_status' => '已完成', 'last_frame_url' => $statusResult['last_frame_url'] ?? '', 'updated_at' => date('Y-m-d H:i:s') ]); } elseif ($statusResult['status'] === 'failed') { DB::table('mp_episode_segments') ->where('video_task_id', $taskId) ->update([ 'video_task_status' => '失败', 'updated_at' => date('Y-m-d H:i:s') ]); } } } // 发送当前状态 echo "data: " . json_encode([ 'type' => 'status_update', 'data' => [ 'task_id' => $task->id, 'status' => $task->status, 'result_url' => $task->result_url, 'error_message' => $task->error_message, 'progress' => $this->getTaskProgress($task->status), 'elapsed_time' => time() - $startTime ] ]) . "\n\n"; ob_flush(); flush(); // 如果任务完成(成功或失败),结束连接 if (in_array($task->status, [ 'success', 'failed' ])) { echo "data: " . json_encode([ 'type' => 'completed', 'data' => [ 'task_id' => $task->id, 'status' => $task->status, 'video_url' => $task->result_url, 'last_frame_url' => $task->last_frame_url, 'error_message' => $task->error_message ] ]) . "\n\n"; ob_flush(); flush(); break; } sleep($checkInterval); } catch (\Exception $e) { echo "data: " . json_encode([ 'type' => 'error', 'message' => '查询任务状态失败: ' . $e->getMessage() ]) . "\n\n"; ob_flush(); flush(); sleep($checkInterval); } } // 超时处理 if (time() - $startTime >= $maxDuration) { echo "data: " . json_encode([ 'type' => 'timeout', 'message' => '任务执行超时,请稍后查询任务状态', 'data' => [ 'task_id' => $taskId ] ]) . "\n\n"; ob_flush(); flush(); } }, 200, [ 'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive', 'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲 ]); } /** * 获取任务进度百分比 */ private function getTaskProgress($status) { switch ($status) { case \App\Models\MpGenerateVideoTask::STATUS_PENDING: return 10; case \App\Models\MpGenerateVideoTask::STATUS_PROCESSING: return 50; case \App\Models\MpGenerateVideoTask::STATUS_SUCCESS: return 100; case \App\Models\MpGenerateVideoTask::STATUS_FAILED: return 0; default: return 0; } } /** * 批量生成分镜视频 */ public function batchSetSegmentVideos(Request $request) { // 忽略所有超时限制 set_time_limit(0); ini_set('max_execution_time', '0'); $data = $request->all(); // 验证参数 $validator = Validator::make($data, [ 'anime_id' => 'required|string', 'episode_number' => 'required|integer', ], [ 'anime_id.required' => '动漫对话ID不能为空', 'episode_number.required' => '剧集序号不能为空', 'episode_number.integer' => '剧集序号必须是整数', ]); if ($validator->fails()) { Utils::throwError('1002:' . $validator->errors()->first()); } $animeId = $data['anime_id']; $episodeId = $data['episode_id']; $episodeNumber = $data['episode_number']; // 获取所有分镜信息 $segments = DB::table('mp_episode_segments') ->where('anime_id', $animeId) ->where('episode_id', $episodeId) ->where('video_url', '') ->orderBy('segment_number') ->limit(2) ->get(); if ($segments->isEmpty()) { Utils::throwError('20003:未找到分镜数据'); } // 批量创建视频任务 $taskIds = []; $segmentTasks = []; foreach ($segments as $segment) { try { // 检查是否已有视频或正在生成中 if (!empty($segment->video_url) || $segment->video_task_status === '生成中') { continue; } // 获取分镜内容 $segmentContent = $segment->segment_content ?: ''; $tailFrame = $segment->tail_frame ?: ''; // 构建完整的提示词 $fullPrompt = $segmentContent; if ($tailFrame) { $fullPrompt .= "\n尾帧描述:$tailFrame"; } // 智能选择视频时长 // $videoDuration = $this->AnimeService->calculateOptimalVideoDuration($segmentContent, $tailFrame); $videoDuration = -1; // 构建视频生成参数 $videoParams = [ 'model' => 'doubao-seedance-1-0-pro-250528', 'alias_segment_id' => $segment->segment_id, 'prompt' => $fullPrompt, 'video_duration' => $videoDuration, 'video_resolution' => '720P', 'seed' => -1, 'ratio' => '16:9', 'generate_audio' => false, 'draft' => false, 'watermark' => false, 'camera_fixed' => false, // 'callback_url' => 'http://mpaudio.yqsd.cn/api/video/seedanceCallback' ]; // 如果分镜有图片,作为首帧 if (!empty($segment->img_url)) { $videoParams['first_frame_url'] = $segment->img_url; } // 构建content数组 $videoParams['content'] = [ [ 'type' => 'text', 'text' => $videoParams['prompt'], ] ]; // 如果有首帧图片,添加到content中 if (isset($videoParams['first_frame_url'])) { $videoParams['content'][] = [ 'type' => 'image_url', 'image_url' => [ 'url' => $videoParams['first_frame_url'], ], 'role' => 'first_frame', ]; } // 创建视频任务 $task = $this->AIVideoGenerationService->createSeedanceTask($videoParams); $taskIds[] = $task->id; // 更新分镜表的视频任务信息 DB::table('mp_episode_segments') ->where('segment_id', $segment->segment_id) ->update([ 'video_task_id' => $task->id, 'video_task_status' => '生成中', 'updated_at' => date('Y-m-d H:i:s') ]); $segmentTasks[] = [ 'segment_id' => $segment->segment_id, 'task_id' => $task->id, 'segment_number' => $segment->segment_number, ]; } catch (\Exception $e) { dLog('anime')->error('创建分镜视频任务失败: ' . $e->getMessage(), [ 'segment_id' => $segment->segment_id, 'anime_id' => $animeId, 'episode_number' => $episodeNumber ]); continue; } } if (empty($taskIds)) { Utils::throwError('20003:没有需要生成视频的分镜'); } // 记录开始时间(包含创建任务的时间) $startTime = time(); $maxDuration = 1800; // 30分钟超时 // 设置 SSE 响应头并开始长连接 return response()->stream(function () use ($taskIds, $segmentTasks, $animeId, $episodeNumber, $episodeId, $startTime, $maxDuration) { // 发送初始任务创建信息 echo "data: " . json_encode([ 'type' => 'tasks_created', 'data' => [ 'anime_id' => $animeId, 'episode_id' => $episodeId, 'episode_number' => $episodeNumber, 'total_tasks' => count($taskIds), 'task_ids' => $taskIds, 'segments' => $segmentTasks, 'start_time' => $startTime, 'max_duration' => $maxDuration ] ]) . "\n\n"; ob_flush(); flush(); $checkInterval = 10; // 每10秒检查一次 $completedTasks = []; $failedTasks = []; // 检查是否已经超时(包含创建任务的时间) while (time() - $startTime < $maxDuration) { try { $allCompleted = true; $currentStatus = []; foreach ($segmentTasks as $segmentTask) { $taskId = $segmentTask['task_id']; $segmentId = $segmentTask['segment_id']; // 跳过已完成或失败的任务 if (in_array($taskId, $completedTasks) || in_array($taskId, $failedTasks)) { continue; } // 查询任务状态 $task = \App\Models\MpGenerateVideoTask::find($taskId); if (!$task) { $failedTasks[] = $taskId; continue; } // 如果任务还在处理中,查询最新状态 if ($task->status === 'processing') { $statusResult = $this->AIVideoGenerationService->querySeedanceTaskStatus($task); if (isset($statusResult['status'])) { // 更新任务状态 $task->update([ 'status' => $statusResult['status'], 'result_json' => $statusResult['result_json'] ?? [], 'result_url' => $statusResult['result_url'] ?? null, 'last_frame_url' => $statusResult['last_frame_url'] ?? '', 'error_message' => $statusResult['error_message'] ?? null, 'completed_at' => in_array($statusResult['status'], [ 'success', 'failed' ]) ? now() : null ]); // 如果任务成功,更新分镜表 if ($statusResult['status'] === 'success' && isset($statusResult['result_url'])) { DB::table('mp_episode_segments') ->where('segment_id', $segmentId) ->update([ 'video_url' => $statusResult['result_url'], 'video_task_status' => '已完成', 'last_frame_url' => $statusResult['last_frame_url'] ?? '', 'updated_at' => date('Y-m-d H:i:s') ]); $completedTasks[] = $taskId; // 发送单个任务完成通知 echo "data: " . json_encode([ 'type' => 'task_completed', 'data' => [ 'segment_id' => $segmentId, 'task_id' => $taskId, 'segment_number' => $segmentTask['segment_number'], 'status' => 'success', 'video_url' => $statusResult['result_url'], 'last_frame_url' => $statusResult['last_frame_url'] ?? '', 'completed_count' => count($completedTasks), 'total_count' => count($segmentTasks) ] ]) . "\n\n"; ob_flush(); flush(); } elseif ($statusResult['status'] === 'failed') { DB::table('mp_episode_segments') ->where('segment_id', $segmentId) ->update([ 'video_task_status' => '失败', 'updated_at' => date('Y-m-d H:i:s') ]); $failedTasks[] = $taskId; // 发送任务失败通知 echo "data: " . json_encode([ 'type' => 'task_failed', 'data' => [ 'segment_id' => $segmentId, 'task_id' => $taskId, 'segment_number' => $segmentTask['segment_number'], 'status' => 'failed', 'error_message' => $statusResult['error_message'] ?? '视频生成失败', 'failed_count' => count($failedTasks), 'total_count' => count($segmentTasks) ] ]) . "\n\n"; ob_flush(); flush(); } } } // 收集当前状态 $currentStatus[] = [ 'segment_id' => $segmentId, 'task_id' => $taskId, 'segment_number' => $segmentTask['segment_number'], 'status' => $task->status, 'progress' => $this->getTaskProgress($task->status), 'video_url' => $task->result_url, 'error_message' => $task->error_message ]; // 检查是否还有未完成的任务 if (!in_array($task->status, ['success', 'failed'])) { $allCompleted = false; } } // 发送整体进度更新 echo "data: " . json_encode([ 'type' => 'progress_update', 'data' => [ 'anime_id' => $animeId, 'episode_id' => $episodeId, 'episode_number' => $episodeNumber, 'completed_count' => count($completedTasks), 'failed_count' => count($failedTasks), 'total_count' => count($segmentTasks), 'elapsed_time' => time() - $startTime, 'remaining_time' => max(0, $maxDuration - (time() - $startTime)), 'segments_status' => $currentStatus ] ]) . "\n\n"; ob_flush(); flush(); // 如果所有任务都完成了,结束连接 if ($allCompleted) { // 获取最终结果 $finalSegments = DB::table('mp_episode_segments') ->where('anime_id', $animeId) ->where('episode_id', $episodeId) ->orderBy('segment_number') ->select('segment_id', 'segment_number', 'video_url', 'video_task_status', 'last_frame_url') ->get(); echo "data: " . json_encode([ 'type' => 'all_completed', 'data' => [ 'anime_id' => $animeId, 'episode_id' => $episodeId, 'episode_number' => $episodeNumber, 'completed_count' => count($completedTasks), 'failed_count' => count($failedTasks), 'total_count' => count($segmentTasks), 'total_elapsed_time' => time() - $startTime, 'segments' => $finalSegments->map(function($segment) { return [ 'segment_id' => $segment->segment_id, 'segment_number' => $segment->segment_number, 'video_url' => $segment->video_url, 'status' => $segment->video_task_status, 'last_frame_url' => $segment->last_frame_url ]; })->toArray() ] ]) . "\n\n"; ob_flush(); flush(); break; } // 在睡眠前再次检查是否超时,避免不必要的等待 if (time() - $startTime >= $maxDuration) { break; } sleep($checkInterval); } catch (\Exception $e) { echo "data: " . json_encode([ 'type' => 'error', 'message' => '查询任务状态失败: ' . $e->getMessage(), 'elapsed_time' => time() - $startTime, 'remaining_time' => max(0, $maxDuration - (time() - $startTime)) ]) . "\n\n"; ob_flush(); flush(); // 检查是否超时,如果超时则停止循环 if (time() - $startTime >= $maxDuration) { break; } sleep($checkInterval); } } // 超时处理 - 只有在真正超时时才执行 if (time() - $startTime >= $maxDuration) { // 获取当前所有任务状态 $timeoutSegments = DB::table('mp_episode_segments') ->where('anime_id', $animeId) ->where('episode_id', $episodeId) ->orderBy('segment_number') ->select('segment_id', 'segment_number', 'video_task_id', 'video_task_status', 'video_url') ->get(); echo "data: " . json_encode([ 'type' => 'timeout', 'message' => '批量视频生成已超时30分钟,停止状态查询', 'data' => [ 'anime_id' => $animeId, 'episode_id' => $episodeId, 'episode_number' => $episodeNumber, 'completed_count' => count($completedTasks), 'failed_count' => count($failedTasks), 'total_count' => count($segmentTasks), 'total_elapsed_time' => time() - $startTime, 'timeout_duration' => $maxDuration, 'segments' => $timeoutSegments->map(function($segment) { return [ 'segment_id' => $segment->segment_id, 'segment_number' => $segment->segment_number, 'task_id' => $segment->video_task_id, 'status' => $segment->video_task_status, 'video_url' => $segment->video_url ]; })->toArray() ] ]) . "\n\n"; ob_flush(); flush(); } }, 200, [ 'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive', 'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲 ]); } // 应用有声制作参数 public function applyAudioData(Request $request) { $data = $request->all(); $result = $this->AnimeService->applyAudioData($data); return $this->success($result); } }