aiImageGenerationService = $aiImageGenerationService; $this->aiVideoGenerationService = $aiVideoGenerationService; $this->url = 'https://api.deepseek.com/chat/completions'; $this->api_key = env('DEEPSEEK_API_KEY'); $this->headers = [ 'Authorization' => 'Bearer '.$this->api_key, 'Content-Type' => 'application/json; charset=UTF-8' ]; } public function chatList($data) { $anime_name = getProp($data, 'anime_name'); $query = DB::table('mp_animes')->where('is_deleted', 0); if ($anime_name) { $query->where('anime_name', 'like', "%$anime_name%"); } return $query->orderBy('id', 'desc')->paginate(); } public function chatHistory($data) { $anime_id = getProp($data, 'anime_id'); $sequence = getProp($data, 'sequence', 0); if (!$anime_id) Utils::throwError('20003:请选择对话!'); $query = DB::table('mp_anime_records as ar')->leftJoin('mp_anime_episodes as ae', 'ar.episode_id', '=', 'ae.id')->where('ar.anime_id', $anime_id) ->where('ar.sequence', $sequence)->select('ar.role', 'ar.content', 'ar.created_at', 'ar.episode_id', 'ae.title as episode_title', 'ae.created_at as episode_created_at'); return $query->orderBy('ar.id')->get()->map(function ($value) { $value = (array)$value; $value['created_at'] = transDate($value['created_at']); $value['episode_created_at'] = transDate($value['episode_created_at']); return $value; })->toArray(); } public function batchSetRoleImg($data) { $anime_id = getProp($data, 'anime_id'); $episode_number = getProp($data, 'episode_number'); if (!$anime_id) Utils::throwError('1002:请选择对话'); $anime = DB::table('mp_animes')->where('id', $anime_id)->where('is_deleted', 0)->first(); $roles = json_decode(getProp($anime, 'roles'), true); $art_style = getProp($anime, 'art_style'); foreach ($roles as &$role) { $role_name = getProp($role, 'role'); if ($role_name == '旁白') continue; $description = getProp($role, 'description'); if ($art_style) $description = "美术风格:\n$art_style\n主体描述:$description"; // 参考图地址 $ref_img_url = getProp($role, 'url'); // 此处调用AIImageGenerationService的生成图片任务接口: createImageGenerationTask(注意使用默认参数,prompt使用角色描述),并返回任务id try { $params = [ 'prompt' => $description, 'ref_img_urls' => !empty($ref_img_url) ? [$ref_img_url] : [] ]; $task = $this->aiImageGenerationService->createImageGenerationTask($params); $task_id = $task->id; if (!$task_id) { Utils::throwError("20003: 角色({$role_name})生成图片失败"); } $role['task_id'] = $task_id; $role['task_status'] = 'processing'; } catch (\Exception $e) { Utils::throwError("20003:" . $e->getMessage()); } } // 保存角色信息 // $boolen = DB::table('mp_animes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->update([ $boolen = DB::table('mp_animes')->where('id', $anime_id)->update([ 'roles' => json_encode($roles, 256), 'generate_status' => '执行中', 'generate_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]); if (!$boolen) { Utils::throwError('20003:保存角色信息失败'); } return $boolen; } public function batchSetSceneImg($data) { $anime_id = getProp($data, 'anime_id'); if (!$anime_id) Utils::throwError('1002:请选择对话'); $episode_number = getProp($data, 'episode_number'); $anime = DB::table('mp_animes')->where('id', $anime_id)->where('is_deleted', 0)->first(); $scenes = json_decode(getProp($anime, 'scenes'), true); $art_style = getProp($anime, 'art_style'); foreach ($scenes as &$scene) { $scene_name = getProp($scene, 'scene'); $description = getProp($scene, 'description'); if ($art_style) $description = "美术风格:\n$art_style\n\n场景描述:$description"; // 参考图地址 $ref_img_url = getProp($scene, 'url'); // 此处调用AIImageGenerationService的生成图片任务接口: createImageGenerationTask(注意使用默认参数,prompt使用角色描述),并返回任务id try { $params = [ 'prompt' => $description, 'ref_img_urls' => !empty($ref_img_url) ? [$ref_img_url] : [] ]; $task = $this->aiImageGenerationService->createImageGenerationTask($params); $task_id = $task->id; if (!$task_id) { Utils::throwError("20003: 角色({$scene_name})生成图片失败"); } $scene['task_id'] = $task_id; $scene['task_status'] = 'processing'; } catch (\Exception $e) { Utils::throwError("20003:" . $e->getMessage()); } } // 保存角色信息 // $boolen = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->update([ $boolen = DB::table('mp_animes')->where('id', $anime_id)->update([ 'scenes' => json_encode($scenes, 256), 'generate_status' => '执行中', 'generate_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]); if (!$boolen) { Utils::throwError('20003:保存角色信息失败'); } return $boolen; } public function editAnime($data) { $script_arr =getProp($data, 'script', []); if (!$script_arr) Utils::throwError('20003:剧本内容不能为空'); $start_episode_sequence = getProp($data, 'start_episode_sequence', 1); $anime_id = getProp($data, 'anime_id'); try { DB::beginTransaction(); $table_data = [ 'intro' => isset($script_arr['intro']) ? $script_arr['intro'] : '', 'highlights' => isset($script_arr['highlights']) ? $script_arr['highlights'] : '', 'role_relationship' => isset($script_arr['role_relationship']) ? $script_arr['role_relationship'] : '', 'core_contradiction' => isset($script_arr['core_contradiction']) ? $script_arr['core_contradiction'] : '', 'roles' => isset($script_arr['roles']) ? json_encode($script_arr['roles'], 256) : '', 'art_style' => isset($script_arr['art_style']) ? $script_arr['art_style'] : '', 'scenes' => isset($script_arr['scenes']) ? json_encode($script_arr['scenes'], 256) : '', 'status' => 3, 'start_episode_sequence' => $start_episode_sequence, 'end_episode_sequence' => $start_episode_sequence + count($script_arr['episodes']) - 1, 'episode_num' => count($script_arr['episodes']), 'updated_at' => date('Y-m-d H:i:s') ]; // 保存剧本内容 if (!empty($anime_id)) { $boolen = DB::table('mp_animes')->where('id', $anime_id)->update($table_data); if (!$boolen) { dLog('anime')->info('对话保存失败', $table_data); Utils::throwError('20003:对话保存失败'); } }else { $table['created_at'] = $table_data['updated_at']; $anime_id = DB::table('mp_animes')->insertGetId($table_data); if (!$anime_id) { dLog('anime')->info('对话保存失败', $table_data); Utils::throwError('20003:对话保存失败'); } } // 保存分集内容 // 删除历史分集内容 $boolen1 = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->delete(); $episodes = []; $sequence = 1; foreach ($script_arr['episodes'] as $episode) { $segment_number = 1; foreach($episode['segments'] as $segment) { $episodes[] = [ 'anime_id' => $anime_id, 'episode_number' => $episode['episode_number'], 'title' => $episode['title'], // 'content' => $episode['content'], 'content' => '', 'segment_number' => $segment_number, 'segment_content' => $segment['segment_content'], 'sequence' => $sequence, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]; $sequence++; $segment_number++; } } $boolen2 = DB::table('mp_anime_episodes')->insert($episodes); if (!$boolen2) { dLog('deepseek')->info('分集内容保存失败', $episodes); Utils::throwError('20003:分集内容保存失败'); } } catch (\Exception $e) { DB::rollBack(); Utils::throwError('20003:'.$e->getMessage()); } DB::commit(); return true; } public function animeDetail($data) { $anime_id = getProp($data, 'anime_id'); $anime = DB::table('mp_animes')->where('id', $anime_id)->where('is_deleted', 0) ->selectRaw('id as anime_id, anime_name, intro, highlights, roles, role_relationship, core_contradiction, art_style, scenes, status, remark, generate_status, created_at')->first(); if (!$anime) Utils::throwError('20003:该剧本不存在!'); $anime = (array)$anime; $anime['roles'] = json_decode($anime['roles']); $anime['scenes'] = json_decode($anime['scenes']); // 获取分集信息 $episodes = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('is_default', 1) ->select('id as episode_id', 'episode_number', 'title', DB::raw("(select es.img_url from mp_episode_segments as es where es.episode_id = mp_anime_episodes.id order by es.segment_number limit 1) as first_frame_url")) ->orderBy('episode_number')->get()->map(function ($value) { return (array)$value; })->toArray(); $anime['episodes'] = $episodes; return $anime; } public function episodeInfo($data) { $anime_id = getProp($data, 'anime_id'); $episode_id = getProp($data, 'episode_id'); if (!$anime_id || !$episode_id) { Utils::throwError('1002:请选择剧集'); } $episode = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('id', $episode_id)->where('is_default', 1)->select('id as episode_id', 'anime_id', 'episode_number', 'title', 'intro', 'art_style', 'roles', 'scenes')->first(); if (!$episode) Utils::throwError('20003: 该剧集不存在!'); $episode = (array)$episode; $episode['roles'] = json_decode($episode['roles'], true); $episode['scenes'] = json_decode($episode['scenes'], true); // 获取分镜信息 $segments = DB::table('mp_episode_segments')->where('anime_id', $anime_id)->where('episode_id', getProp($episode, 'episode_id'))->orderBy('segment_number') ->select('*')->get()->map(function ($value) { return (array)$value; })->toArray(); $acts = []; foreach($segments as $segment) { if (isset($acts[getProp($segment, 'act_number')])) { $acts[getProp($segment, 'act_number')]['segments'][] = [ 'segment_id' => getProp($segment, 'segment_id'), 'segment_number' => getProp($segment, 'segment_number'), 'segment_content' => getProp($segment, 'segment_content'), 'description' => getProp($segment, 'description'), 'composition' => getProp($segment, 'composition'), 'camera_movement' => getProp($segment, 'camera_movement'), 'voice_actor' => getProp($segment, 'voice_actor'), 'dialogue' => getProp($segment, 'dialogue'), 'frame_type' => getProp($segment, 'frame_type'), 'scene' => getProp($segment, 'scene'), 'characters' => getProp($segment, 'characters'), 'tail_frame' => getProp($segment, 'tail_frame'), 'emotion' => getProp($segment, 'emotion'), 'emotion_type' => getProp($segment, 'emotion_type'), 'gender' => getProp($segment, 'gender'), 'speed_ratio' => getProp($segment, 'speed_ratio'), 'loudness_ratio' => getProp($segment, 'loudness_ratio'), 'emotion_scale' => getProp($segment, 'emotion_scale'), 'pitch' => getProp($segment, 'pitch'), 'voice_name' => getProp($segment, 'voice_name'), 'voice_type' => getProp($segment, 'voice_type'), 'voice_audio_url' => getProp($segment, 'voice_audio_url'), 'img_url' => getProp($segment, 'img_url'), 'video_url' => getProp($segment, 'video_url'), 'last_frame_url' => getProp($segment, 'last_frame_url'), ]; }else { $acts[getProp($segment, 'act_number')]= [ 'act_number' => getProp($segment, 'act_number'), 'act_title' => getProp($segment, 'act_title'), 'segments' => [ [ 'segment_id' => getProp($segment, 'segment_id'), 'segment_number' => getProp($segment, 'segment_number'), 'segment_content' => getProp($segment, 'segment_content'), 'description' => getProp($segment, 'description'), 'composition' => getProp($segment, 'composition'), 'camera_movement' => getProp($segment, 'camera_movement'), 'voice_actor' => getProp($segment, 'voice_actor'), 'dialogue' => getProp($segment, 'dialogue'), 'frame_type' => getProp($segment, 'frame_type'), 'scene' => getProp($segment, 'scene'), 'characters' => getProp($segment, 'characters'), 'tail_frame' => getProp($segment, 'tail_frame'), 'emotion' => getProp($segment, 'emotion'), 'emotion_type' => getProp($segment, 'emotion_type'), 'gender' => getProp($segment, 'gender'), 'speed_ratio' => getProp($segment, 'speed_ratio'), 'loudness_ratio' => getProp($segment, 'loudness_ratio'), 'emotion_scale' => getProp($segment, 'emotion_scale'), 'pitch' => getProp($segment, 'pitch'), 'voice_name' => getProp($segment, 'voice_name'), 'voice_type' => getProp($segment, 'voice_type'), 'voice_audio_url' => getProp($segment, 'voice_audio_url'), 'img_url' => getProp($segment, 'img_url'), 'video_url' => getProp($segment, 'video_url'), 'last_frame_url' => getProp($segment, 'last_frame_url'), ] ] ]; } } $episode['acts'] = array_values($acts); return $episode; } public function segmentInfo($data) { $segment_id = getProp($data, 'segment_id'); if (!$segment_id) { Utils::throwError('1002:请选择分镜'); } // 获取分镜信息 $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first(); $result = [ 'segment_id' => getProp($segment, 'segment_id'), 'segment_number' => getProp($segment, 'segment_number'), 'segment_content' => getProp($segment, 'segment_content'), 'description' => getProp($segment, 'description'), 'composition' => getProp($segment, 'composition'), 'camera_movement' => getProp($segment, 'camera_movement'), 'voice_actor' => getProp($segment, 'voice_actor'), 'dialogue' => getProp($segment, 'dialogue'), 'frame_type' => getProp($segment, 'frame_type'), 'scene' => getProp($segment, 'scene'), 'characters' => getProp($segment, 'characters'), 'tail_frame' => getProp($segment, 'tail_frame'), 'emotion' => getProp($segment, 'emotion'), 'emotion_type' => getProp($segment, 'emotion_type'), 'gender' => getProp($segment, 'gender'), 'speed_ratio' => getProp($segment, 'speed_ratio'), 'loudness_ratio' => getProp($segment, 'loudness_ratio'), 'emotion_scale' => getProp($segment, 'emotion_scale'), 'pitch' => getProp($segment, 'pitch'), 'voice_name' => getProp($segment, 'voice_name'), 'voice_type' => getProp($segment, 'voice_type'), 'voice_audio_url' => getProp($segment, 'voice_audio_url'), 'img_url' => getProp($segment, 'img_url'), 'video_url' => getProp($segment, 'video_url'), 'last_frame_url' => getProp($segment, 'last_frame_url'), ]; return $result; } public function copyEpisodeVersion($data) { $episode_id = getProp($data, 'episode_id'); $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first(); if (!$episode) Utils::throwError('20003:该剧集不存在'); $episode = (array)$episode; $count = DB::table('mp_anime_episodes')->where('anime_id', $episode['anime_id'])->where('episode_number', $episode['episode_number'])->count('id'); $sequence = $count + 1; unset($episode['id']); $episode['sequence'] = $sequence; $episode['created_at'] = date('Y-m-d H:i:s'); $episode['updated_at'] = date('Y-m-d H:i:s'); $episode['is_default'] = 1; try { DB::beginTransaction(); // 移除is_default标志 $boolen = DB::table('mp_anime_episodes')->where('anime_id', $episode['anime_id'])->where('episode_number', $episode['episode_number'])->update(['is_default' => 0, 'updated_at'=>date('Y-m-d H:i:s')]); if (!$boolen) { Utils::throwError('20003:更新失败!'); } // 新增副本 $new_episode_id = DB::table('mp_anime_episodes')->insertGetId($episode); if (!$new_episode_id) { Utils::throwError('20003:创建副本失败!'); } $episode['episode_id'] = $new_episode_id; // 获取分镜数据 $segments = DB::table('mp_episode_segments')->where('episode_id', $episode_id)->get()->map(function($item) use ($new_episode_id) { $item = (array)$item; unset($item['id']); $item['episode_id'] = $new_episode_id; $item['segment_id'] = date('YmdHis').mt_rand(1000,9999).str_pad($item['segment_number'], 3, "0", STR_PAD_LEFT); $item['created_at'] = $item['updated_at'] = date('Y-m-d H:i:s'); return $item; })->toArray(); // 写入分镜数据 $boolen2 = DB::table('mp_episode_segments')->insert($segments); if (!$boolen2) { Utils::throwError('20003:写入分镜数据失败!'); } }catch (\Exception $e) { DB::rollBack(); Utils::throwError('20003:'.$e->getMessage()); } DB::commit(); return $episode; } public function episodeVersions($data) { $anime_id = getProp($data, 'anime_id'); $episode_id = getProp($data, 'episode_id'); if (!$anime_id || !$episode_id) { Utils::throwError('1002:请选择剧集'); } $episodes = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('id', $episode_id)->select('anime_id', 'episode_number', 'sequence', 'title', 'intro', 'art_style', 'roles', 'scenes')->get()->map(function($value) { return (array)$value; })->toArray(); if (!$episodes) Utils::throwError('20003: 该剧集不存在!'); foreach($episodes as &$episode) { $episode['version'] = 'V'.$episode['sequence']; $episode['roles'] = json_decode($episode['roles'], true); $episode['scenes'] = json_decode($episode['scenes'], true); } return $episodes; } public function bindEpisodeVersion($data) { $episode_id = getProp($data, 'episode_id'); $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first(); if (!$episode) Utils::throwError('20003:该剧集不存在'); $episode = (array)$episode; if ((int)$episode['is_default'] === 1) return true; try { DB::beginTransaction(); // 移除is_default标志 $boolen = DB::table('mp_anime_episodes')->where('anime_id', $episode['anime_id'])->where('episode_number', $episode['episode_number'])->update(['is_default' => 0, 'updated_at'=>date('Y-m-d H:i:s')]); if (!$boolen) { Utils::throwError('20003:更新失败!'); } // 绑定副本 $boolen2 = DB::table('mp_anime_episodes')->where('id', $episode_id)->update(['is_default' => 1, 'updated_at'=>date('Y-m-d H:i:s')]); if (!$boolen2) { Utils::throwError('20003:绑定失败!'); } }catch (\Exception $e) { DB::rollBack(); Utils::throwError('20003:'.$e->getMessage()); } DB::commit(); return true; } public function chatChangeImg($data) { $segment_id = getProp($data, 'segment_id'); $prompt = getProp($data, 'prompt'); if (!$prompt || !$segment_id) { Utils::throwError('1002:请输入提示词,并且选择需修改的图片'); } $segment = DB::table("mp_episode_segments")->where('segment_id', $segment_id)->first(); if (!$segment) Utils::throwError('20003:该分镜不存在!'); $ref_img_url = getProp($segment, 'img_url'); if (empty($ref_img_url)) { $prompt = getProp($segment, 'segment_content'); } $ratio = getProp($data, 'ratio'); if (!in_array($ratio, ['1:1', '4:3', '3:2', '16:9', '21:9'])) { Utils::throwError('1002:画面比例选择不正确,只支持1:1、4:3、3:2、16:9、21:9'); } // 根据画面比例给宽高赋值 switch ($ratio) { case '1:1': $width = 2048; $height = 2048; break; case '4:3': $width = 2304; $height = 1728; break; case '3:2': $width = 2496; $height = 1664; break; case '16:9': $width = 2560; $height = 1440; break; case '21:9': $width = 3024; $height = 1296; break; default: $width = 2048; $height = 2048; } try { $params = [ 'alias_segment_id' => $segment_id, 'prompt' => $prompt, 'ref_img_urls' => !empty($ref_img_url) ? [$ref_img_url] : [], 'width' => $width, 'height' => $height ]; $task = $this->aiImageGenerationService->createImageGenerationTask($params); $task_id = $task->id; if (!$task_id) { Utils::throwError("20003: 生成图片失败"); } // 更新任务ID $boolen = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update(['pic_task_id' => $task_id, 'pic_task_status' => '生成中', 'updated_at' => date('Y-m-d H:i:s')]); if (!$boolen) Utils::throwError('20003:更新任务ID失败'); } catch (\Exception $e) { Utils::throwError("20003:" . $e->getMessage()); } $img_url = $this->loopGetPicTaskResult($task_id); if (!$img_url) Utils::throwError('20003:图片生成中,请稍后刷新'); return $img_url; } public function changeEpisodeRoles($data) { $anime_id = getProp($data, 'anime_id'); $episode_id = getProp($data, 'episode_id'); $roles = getProp($data, 'roles'); // 参数验证 if (!$anime_id) Utils::throwError('20003:请提供动漫ID'); if (!$episode_id) Utils::throwError('20003:请提供剧集ID'); if (!is_array($roles)) Utils::throwError('20003:角色列表必须是数组格式'); // 验证剧集是否存在 $episode = DB::table('mp_anime_episodes') ->where('anime_id', $anime_id) ->where('id', $episode_id) ->first(); if (!$episode) Utils::throwError('20003:该剧集不存在'); try { // 更新角色列表 $result = DB::table('mp_anime_episodes') ->where('anime_id', $anime_id) ->where('id', $episode_id) ->update([ 'roles' => json_encode($roles, 256), 'updated_at' => date('Y-m-d H:i:s') ]); if ($result === false) { Utils::throwError('20003:更新角色列表失败'); } return ['success' => true, 'message' => '角色列表更新成功']; } catch (\Exception $e) { dLog('anime')->error('更新剧集角色失败', [ 'anime_id' => $anime_id, 'episode_id' => $episode_id, 'error' => $e->getMessage() ]); Utils::throwError('20003:更新角色列表失败: ' . $e->getMessage()); } } public function changeEpisodeScenes($data) { $anime_id = getProp($data, 'anime_id'); $episode_id = getProp($data, 'episode_id'); $scenes = getProp($data, 'scenes'); // 参数验证 if (!$anime_id) Utils::throwError('20003:请提供动漫ID'); if (!$episode_id) Utils::throwError('20003:请提供剧集ID'); if (!is_array($scenes)) Utils::throwError('20003:场景列表必须是数组格式'); // 验证剧集是否存在 $episode = DB::table('mp_anime_episodes') ->where('anime_id', $anime_id) ->where('id', $episode_id) ->first(); if (!$episode) Utils::throwError('20003:该剧集不存在'); try { // 更新场景列表 $result = DB::table('mp_anime_episodes') ->where('anime_id', $anime_id) ->where('id', $episode_id) ->update([ 'scenes' => json_encode($scenes, 256), 'updated_at' => date('Y-m-d H:i:s') ]); if ($result === false) { Utils::throwError('20003:更新场景列表失败'); } return ['success' => true, 'message' => '场景列表更新成功']; } catch (\Exception $e) { dLog('anime')->error('更新剧集场景失败', [ 'anime_id' => $anime_id, 'episode_id' => $episode_id, 'error' => $e->getMessage() ]); Utils::throwError('20003:更新场景列表失败: ' . $e->getMessage()); } } public function editSegment($data) { $segment_id = getProp($data, 'segment_id'); $segment_content = getProp($data, 'segment_content'); // 参数验证 if (!$segment_id) Utils::throwError('20003:请提供分镜ID'); if (!$segment_content) Utils::throwError('20003:请提供分镜内容'); // 验证分镜是否存在 $segment = DB::table('mp_episode_segments') ->where('segment_id', $segment_id) ->first(); if (!$segment) Utils::throwError('20003:该分镜不存在'); try { // 准备更新数据 $update_data = [ 'segment_content' => $segment_content, 'updated_at' => date('Y-m-d H:i:s') ]; // 使用现有方法分析segment_content,提取各个字段 $this->handleSegmentContent($update_data); // 更新分镜信息 $result = DB::table('mp_episode_segments') ->where('segment_id', $segment_id) ->update($update_data); if ($result === false) { Utils::throwError('20003:更新分镜剧本失败'); } return true; } catch (\Exception $e) { dLog('anime')->error('更新分镜剧本失败', [ 'segment_id' => $segment_id, 'error' => $e->getMessage() ]); Utils::throwError('20003:更新分镜剧本失败: ' . $e->getMessage()); } } public function batchSetSegmentPics($data) { $anime_id = getProp($data, 'anime_id'); $episode_id = getProp($data, 'episode_id'); $ratio = getProp($data, 'ratio'); if (!in_array($ratio, ['1:1', '4:3', '3:2', '16:9', '21:9'])) { Utils::throwError('1002:画面比例选择不正确,只支持1:1、4:3、3:2、16:9、21:9'); } if (!$anime_id || !$episode_id) { Utils::throwError('1002:请选择剧集'); } // 获取剧集信息 $episode = DB::table('mp_anime_episodes') ->where('anime_id', $anime_id) ->where('id', $episode_id) ->where('is_default', 1) ->select('id as episode_id', 'anime_id', 'episode_number') ->first(); if (!$episode) { Utils::throwError('20003:该剧集不存在'); } // 根据画面比例给宽高赋值 switch ($ratio) { case '1:1': $width = 2048; $height = 2048; break; case '4:3': $width = 2304; $height = 1728; break; case '3:2': $width = 2496; $height = 1664; break; case '16:9': $width = 2560; $height = 1440; break; case '21:9': $width = 3024; $height = 1296; break; default: $width = 2048; $height = 2048; } // 获取动漫的角色信息(包含参考图) $anime = DB::table('mp_animes') ->where('id', $anime_id) ->select('roles', 'scenes') ->first(); if (!$anime || !$anime->roles) { Utils::throwError('20003:未找到角色信息'); } $roles = json_decode($anime->roles, true); if (!$roles) { Utils::throwError('20003:角色信息格式错误'); } // 构建角色名称到参考图的映射 $role_map = []; foreach ($roles as $role) { $role_name = getProp($role, 'role'); $role_url = getProp($role, 'url'); if (!empty($role_name) && !empty($role_url)) { // // 处理角色名称,去除装扮后缀(如:许芸-校服装 -> 许芸)- 修复中文乱码问题(弃用,容易产生重复图片) // $base_name = preg_replace('/[--].*$/u', '', $role_name); // $role_map[$base_name] = $role_url; // 同时保存完整名称的映射 $role_map[$role_name] = $role_url; } } $scenes = json_decode($anime->scenes, true); if (!$scenes) { Utils::throwError('20003:角色信息格式错误'); } // 构建角色名称到参考图的映射 $scene_map = []; foreach ($scenes as $scene) { $scene_name = getProp($scene, 'scene'); $scene_url = getProp($scene, 'url'); if (!empty($scene_name) && !empty($scene_url)) { $scene_map[$scene_name] = $scene_url; } } // 获取所有分镜信息 $segments = DB::table('mp_episode_segments') ->where('anime_id', $anime_id) ->where('episode_id', $episode->episode_id) ->where('pic_task_status', '') ->orderBy('segment_number') ->select('episode_number', 'segment_id', 'segment_number', 'segment_content', 'scene', 'characters') ->limit(3) ->get() ->toArray(); if (empty($segments)) { Utils::throwError('20003:未找到分镜信息'); } $updated_segments = []; $failed_segments = []; // 批量为每个分镜生成图片任务 foreach ($segments as $segment) { $segment_id = $segment->segment_id; $segment_content = $segment->segment_content; if (empty($segment_content)) { $failed_segments[] = [ 'segment_id' => $segment_id, 'error' => '分镜内容为空' ]; continue; } try { // 解析分镜内容,提取画面描述作为主要提示词 $prompt = $segment_content; $ref_img_urls = []; // 分镜场景 $scene = getProp($segment, 'scene'); $matched_scene = ''; if (isset($scene_map[$scene])) { $img_index = count($ref_img_urls) + 1; $prompt = "场景:$scene(图$img_index)\n$prompt"; $ref_img_urls[] = $scene_map[$scene]; $matched_scene = $scene; } // 分镜角色 $characters = getProp($segment, 'characters'); $characters = str_replace(['、', ',', ';'], ',', $characters); $roles = explode(',', $characters); // 从分镜内容中提取涉及的角色 $segment_characters = $this->extractCharactersFromSegment($segment_content, array_keys($role_map), $roles); // 获取匹配角色的参考图 foreach ($segment_characters as $character) { if (isset($role_map[$character])) { // 将$prompt中出现的角色替换成角色(图1、2、3,序号是$ref_img_urls的个数加1) $img_index = count($ref_img_urls) + 1; $prompt = str_replace($character, $character . '(图' . $img_index . ')', $prompt); $ref_img_urls[] = $role_map[$character]; } } // 如果没有找到匹配的角色参考图,不使用参考图 if (empty($ref_img_urls)) { $ref_img_urls = []; } // 准备生成任务参数 $params = [ 'alias_segment_id' => $segment_id, 'prompt' => $prompt, 'ref_img_urls' => $ref_img_urls, 'width' => $width, 'height' => $height, ]; // $task_id = mt_rand(100000, 999999); // 调用AI图片生成服务 $task = $this->aiImageGenerationService->createImageGenerationTask($params); $task_id = $task->id; if (!$task_id) { $failed_segments[] = [ 'segment_id' => $segment_id, 'error' => '创建生成任务失败' ]; continue; } // 更新分镜的任务信息 $update_result = DB::table('mp_episode_segments') ->where('segment_id', $segment_id) ->update([ 'pic_task_id' => $task_id, 'pic_task_status' => '生成中', 'updated_at' => date('Y-m-d H:i:s') ]); // 如果是第一集的首帧图片,则存入待更新数组 if ($update_result && (int)getProp($segment, 'episode_number') === 1 && (int)getProp($segment, 'segment_number') === 1) { Redis::sadd('anime_first_frame_urls', $segment_id); } if ($update_result) { $updated_segments[] = [ 'segment_id' => $segment_id, 'task_id' => $task_id, 'status' => '生成中', 'matched_scenes' => $matched_scene, 'matched_roles' => $segment_characters, 'ref_urls_count' => count($ref_img_urls) ]; } else { $failed_segments[] = [ 'segment_id' => $segment_id, 'error' => '更新分镜任务状态失败' ]; } } catch (\Exception $e) { $failed_segments[] = [ 'segment_id' => $segment_id, 'error' => $e->getMessage() ]; } } // // 更新剧集生成状态 // if (!empty($updated_segments)) { // DB::table('mp_anime_episodes') // ->where('id', $episode->episode_id) // ->update([ // 'generate_status' => '执行中', // 'updated_at' => date('Y-m-d H:i:s') // ]); // } return [ 'success_count' => count($updated_segments), 'failed_count' => count($failed_segments), 'success_segments' => $updated_segments, 'failed_segments' => $failed_segments, 'total_segments' => count($segments) ]; } public function reGenerateSegment($data) { $segment_id = getProp($data, 'segment_id'); $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first(); $anime_id = getProp($segment, 'anime_id'); $episode_id = getProp($segment, 'episode_id'); $episode_number = getProp($segment, 'episode_number'); $segment_content = getProp($data, 'segment_content'); $ratio = getProp($data, 'ratio'); if (!in_array($ratio, ['1:1', '4:3', '3:2', '16:9', '21:9'])) { Utils::throwError('1002:画面比例选择不正确,只支持1:1、4:3、3:2、16:9、21:9'); } if (!$anime_id || !$episode_id) { Utils::throwError('1002:请选择分镜'); } // 根据画面比例给宽高赋值 switch ($ratio) { case '1:1': $width = 2048; $height = 2048; break; case '4:3': $width = 2304; $height = 1728; break; case '3:2': $width = 2496; $height = 1664; break; case '16:9': $width = 2560; $height = 1440; break; case '21:9': $width = 3024; $height = 1296; break; default: $width = 2048; $height = 2048; } // 获取动漫的角色信息(包含参考图) $anime = DB::table('mp_animes') ->where('id', $anime_id) ->select('roles', 'scenes') ->first(); if (!$anime || !$anime->roles) { Utils::throwError('20003:未找到角色信息'); } $roles = json_decode($anime->roles, true); if (!$roles) { Utils::throwError('20003:角色信息格式错误'); } // 构建角色名称到参考图的映射 $role_map = []; foreach ($roles as $role) { $role_name = getProp($role, 'role'); $role_url = getProp($role, 'url'); if (!empty($role_name) && !empty($role_url)) { $role_map[$role_name] = $role_url; } } $scenes = json_decode($anime->scenes, true); if (!$scenes) { Utils::throwError('20003:角色信息格式错误'); } // 构建角色名称到参考图的映射 $scene_map = []; foreach ($scenes as $scene) { $scene_name = getProp($scene, 'scene'); $scene_url = getProp($scene, 'url'); if (!empty($scene_name) && !empty($scene_url)) { $scene_map[$scene_name] = $scene_url; } } $segment_id = $segment->segment_id; $segment_content = $segment->segment_content; if (empty($segment_content)) { Utils::throwError('20003:分镜内容不存在,请填写内容后重试'); } try { // 分镜剧本数组 $update_data = [ 'segment_content' => $segment_content, 'pic_task_status' => '生成中', 'updated_at' => date('Y-m-d H:i:s') ]; // 解析分镜内容,提取画面描述作为主要提示词 $prompt = $segment_content; $ref_img_urls = []; // 分镜场景 $scene = getProp($data, 'scene'); if ($scene) { $matched_scene = ''; if (isset($scene_map[$scene])) { $img_index = count($ref_img_urls) + 1; $prompt = "场景:$scene(图$img_index)\n$prompt"; $ref_img_urls[] = $scene_map[$scene]; $matched_scene = $scene; } $update_data['scene'] = $scene; } // 分镜角色 $characters = getProp($data, 'characters'); if ($characters) { $update_data['characters'] = $characters; $characters = str_replace(['、', ',', ';'], ',', $characters); $characters = explode(',', $characters); // 从分镜内容中提取涉及的角色 $segment_characters = $this->extractCharactersFromSegment($segment_content, array_keys($role_map), $characters); // 获取匹配角色的参考图 foreach ($segment_characters as $character) { if (isset($role_map[$character])) { // 将$prompt中出现的角色替换成角色(图1、2、3,序号是$ref_img_urls的个数加1) $img_index = count($ref_img_urls) + 1; $prompt = str_replace($character, $character . '(图' . $img_index . ')', $prompt); $ref_img_urls[] = $role_map[$character]; } } } // 如果没有找到匹配的角色参考图,不使用参考图 if (empty($ref_img_urls)) { $ref_img_urls = []; } // 准备生成任务参数 $params = [ 'alias_segment_id' => $segment_id, 'prompt' => $prompt, 'ref_img_urls' => $ref_img_urls, 'width' => $width, 'height' => $height, ]; // 调用AI图片生成服务 $task = $this->aiImageGenerationService->createImageGenerationTask($params); $task_id = $task->id; if (!$task_id) { Utils::throwError('20003:创建生成任务失败!'); } $update_data['pic_task_id'] = $task_id; // 更新分镜剧本信息 $this->handleSegmentContent($update_data); $boolen = DB::table('mp_episode_segments') ->where('segment_id', $segment_id) ->update($update_data); // 如果是第一集的首帧图片,则存入待更新数组 if ($boolen && (int)getProp($segment, 'episode_number') === 1 && (int)getProp($segment, 'segment_number') === 1) { Redis::sadd('anime_first_frame_urls', $segment_id); } } catch (\Exception $e) { $failed_segments[] = [ 'segment_id' => $segment_id, 'error' => $e->getMessage() ]; } $img_url = $this->loopGetPicTaskResult($task_id); if (!$img_url) Utils::throwError('20003:图片生成中,请稍后刷新'); return $img_url; } public function addSegment($data) { $prev_segment_id = getProp($data, 'prev_segment_id'); $segment = DB::table('mp_episode_segments')->where('segment_id', $prev_segment_id)->first(); $anime_id = getProp($segment, 'anime_id'); $episode_id = getProp($segment, 'episode_id'); $episode_number = getProp($segment, 'episode_number'); $segment_number = getProp($segment, 'segment_number'); $segment_content = getProp($data, 'segment_content'); $img_url = getProp($data, 'img_url'); $video_url = getProp($data, 'video_url'); $ratio = getProp($data, 'ratio'); if (!in_array($ratio, ['1:1', '4:3', '3:2', '16:9', '21:9'])) { Utils::throwError('1002:画面比例选择不正确,只支持1:1、4:3、3:2、16:9、21:9'); } if (!$anime_id || !$episode_id) { Utils::throwError('1002:请选择分镜'); } // 根据画面比例给宽高赋值 switch ($ratio) { case '1:1': $width = 2048; $height = 2048; break; case '4:3': $width = 2304; $height = 1728; break; case '3:2': $width = 2496; $height = 1664; break; case '16:9': $width = 2560; $height = 1440; break; case '21:9': $width = 3024; $height = 1296; break; default: $width = 2048; $height = 2048; } // 获取动漫的角色信息(包含参考图) $anime = DB::table('mp_animes') ->where('id', $anime_id) ->select('roles', 'scenes') ->first(); if (!$anime || !$anime->roles) { Utils::throwError('20003:未找到角色信息'); } $roles = json_decode($anime->roles, true); if (!$roles) { Utils::throwError('20003:角色信息格式错误'); } // 构建角色名称到参考图的映射 $role_map = []; foreach ($roles as $role) { $role_name = getProp($role, 'role'); $role_url = getProp($role, 'url'); if (!empty($role_name) && !empty($role_url)) { $role_map[$role_name] = $role_url; } } $scenes = json_decode($anime->scenes, true); if (!$scenes) { Utils::throwError('20003:角色信息格式错误'); } // 构建角色名称到参考图的映射 $scene_map = []; foreach ($scenes as $scene) { $scene_name = getProp($scene, 'scene'); $scene_url = getProp($scene, 'url'); if (!empty($scene_name) && !empty($scene_url)) { $scene_map[$scene_name] = $scene_url; } } $segment_id = $segment->segment_id; $segment_content = $segment->segment_content; if (empty($segment_content)) { Utils::throwError('20003:分镜内容不存在,请填写内容后重试'); } try { DB::beginTransaction(); $new_segment_number = $segment_number + 1; $segment_id = date('YmdHis') . mt_rand(1000, 9999) . str_pad($new_segment_number, 3, "0", STR_PAD_LEFT); // 分镜剧本数组 $insert_data = [ 'anime_id' => $anime_id, 'episode_id' => $episode_id, 'episode_number' => $episode_number, 'act_number' => getProp($segment, 'act_number'), 'act_title' => getProp($segment, 'act_title'), 'segment_id' => $segment_id, 'segment_number' => $new_segment_number, 'segment_content' => $segment_content, 'img_url' => $img_url, 'video_url' => $video_url, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]; // 更新分镜剧本信息 $this->handleSegmentContent($insert_data); // 如果没有上传图片则使用当前描述进行生成 if (!$img_url) { // 解析分镜内容,提取画面描述作为主要提示词 $prompt = $segment_content; $ref_img_urls = []; // 分镜场景 $scene = getProp($insert_data, 'scene'); if ($scene) { $matched_scene = ''; if (isset($scene_map[$scene])) { $img_index = count($ref_img_urls) + 1; $prompt = "场景:$scene(图$img_index)\n$prompt"; $ref_img_urls[] = $scene_map[$scene]; $matched_scene = $scene; } $update_data['scene'] = $scene; } // 分镜角色 $characters = getProp($insert_data, 'characters'); if ($characters) { $update_data['characters'] = $characters; $characters = str_replace(['、', ',', ';'], ',', $characters); $characters = explode(',', $characters); // 从分镜内容中提取涉及的角色 $segment_characters = $this->extractCharactersFromSegment($segment_content, array_keys($role_map), $characters); // 获取匹配角色的参考图 foreach ($segment_characters as $character) { if (isset($role_map[$character])) { // 将$prompt中出现的角色替换成角色(图1、2、3,序号是$ref_img_urls的个数加1) $img_index = count($ref_img_urls) + 1; $prompt = str_replace($character, $character . '(图' . $img_index . ')', $prompt); $ref_img_urls[] = $role_map[$character]; } } } // 如果没有找到匹配的角色参考图,不使用参考图 if (empty($ref_img_urls)) { $ref_img_urls = []; } // 准备生成任务参数 $params = [ 'alias_segment_id' => $segment_id, 'prompt' => $prompt, 'ref_img_urls' => $ref_img_urls, 'width' => $width, 'height' => $height, ]; // 调用AI图片生成服务 $task = $this->aiImageGenerationService->createImageGenerationTask($params); $task_id = $task->id; // $task_id = 'whatever'; if (!$task_id) { Utils::throwError('20003:创建生成任务失败!'); } $insert_data['pic_task_status'] = '生成中'; $insert_data['pic_task_id'] = $task_id; } // 先更新序号 DB::table('mp_episode_segments')->where('anime_id', $anime_id)->where('episode_id', $episode_id)->where('segment_number', '>', $segment_number)->increment('segment_number'); $boolen = DB::table('mp_episode_segments')->insert($insert_data); if (!$boolen) { Utils::throwError('20003:新增分镜失败!'); } // 如果是第一集的首帧图片,则存入待更新数组 if ($boolen && (int)$episode_number === 1 && (int)$new_segment_number === 1) { Redis::sadd('anime_first_frame_urls', $segment_id); } } catch (\Exception $e) { DB::rollBack(); Utils::throwError('20003:'.$e->getMessage()); } DB::commit(); if (isset($task_id)) $img_url = $this->loopGetPicTaskResult($task_id); if (!$img_url) Utils::throwError('20003:图片生成中,请稍后刷新'); $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first(); $segment = $segment ? (array)$segment : []; return $segment; } public function copySegment($data) { $segment_id = getProp($data, 'segment_id'); $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first(); if (!$segment) Utils::throwError('20003:请选择分镜'); $anime_id = getProp($segment, 'anime_id'); $episode_id = getProp($segment, 'episode_id'); $episode_number = getProp($segment, 'episode_number'); $segment_number = getProp($segment, 'segment_number'); $new_segment_id = 0; try { DB::beginTransaction(); $segment = (array)$segment; unset($segment['id']); $segment['segment_number'] += 1; $segment['segment_id'] = date('YmdHis') . mt_rand(1000, 9999) . str_pad($segment['segment_number'], 3, "0", STR_PAD_LEFT); $segment['pic_task_id'] = $segment['pic_task_status'] = $segment['video_task_id'] = $segment['video_task_status'] = ''; $segment['created_at'] = $segment['updated_at'] = date('Y-m-d H:i:s'); // 更新其他分镜序号 DB::table('mp_episode_segments')->where('anime_id', $anime_id)->where('episode_id', $episode_id)->where('segment_number', '>', $segment_number)->increment('segment_number'); // 复制分镜 $new_segment_id = DB::table('mp_episode_segments')->insertGetId($segment); if (!$new_segment_id) { Utils::throwError('20003:复制分镜失败'); } }catch (\Exception $e) { DB::rollBack(); Utils::throwError('20003:'.$e->getMessage()); } DB::commit(); $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first(); $segment = $segment ? (array)$segment : []; return $segment; } public function moveSegment($data) { $segment_id = getProp($data, 'segment_id'); $target_segment_id = getProp($data, 'target_segment_id'); // 获取源分镜信息 $source_segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first(); if (!$source_segment) Utils::throwError('20003:请选择分镜'); // 获取目标分镜信息 $target_segment = DB::table('mp_episode_segments')->where('segment_id', $target_segment_id)->first(); if (!$target_segment) Utils::throwError('20003:目标分镜不存在'); $anime_id = getProp($source_segment, 'anime_id'); $episode_id = getProp($source_segment, 'episode_id'); $episode_number = getProp($source_segment, 'episode_number'); $source_segment_number = getProp($source_segment, 'segment_number'); $target_segment_number = getProp($target_segment, 'segment_number'); // 验证源分镜和目标分镜必须属于同一个动漫的同一集 $target_anime_id = getProp($target_segment, 'anime_id'); $target_episode_number = getProp($target_segment, 'episode_number'); if ($anime_id != $target_anime_id || $episode_number != $target_episode_number) { Utils::throwError('20003:只能在同一动漫的同一集内移动分镜'); } // 如果源分镜和目标分镜相同,无需移动 if ($source_segment_number == $target_segment_number) { return true; } try { DB::beginTransaction(); if ($source_segment_number < $target_segment_number) { // 向后移动:源分镜移动到目标分镜之后 // 1. 将源分镜和目标分镜之间的所有分镜序号减1 DB::table('mp_episode_segments') ->where('anime_id', $anime_id) ->where('episode_id', $episode_id) ->where('segment_number', '>', $source_segment_number) ->where('segment_number', '<=', $target_segment_number) ->decrement('segment_number'); // 2. 将源分镜移动到目标分镜之后 $new_segment_number = $target_segment_number; } else { // 向前移动:源分镜移动到目标分镜之后 // 1. 将目标分镜之后到源分镜之前的所有分镜序号加1 DB::table('mp_episode_segments') ->where('anime_id', $anime_id) ->where('episode_id', $episode_id) ->where('segment_number', '>', $target_segment_number) ->where('segment_number', '<', $source_segment_number) ->increment('segment_number'); // 2. 将源分镜移动到目标分镜之后 $new_segment_number = $target_segment_number + 1; } // 3. 更新源分镜的序号 $update_result = DB::table('mp_episode_segments') ->where('segment_id', $segment_id) ->update([ 'segment_number' => $new_segment_number, 'updated_at' => date('Y-m-d H:i:s') ]); if (!$update_result) { Utils::throwError('20003:更新分镜序号失败'); } DB::commit(); return true; } catch (\Exception $e) { DB::rollBack(); Utils::throwError('20003:' . $e->getMessage()); } } public function delSegment($data) { $segment_id = getProp($data, 'segment_id'); $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first(); if (!$segment) Utils::throwError('20003:请选择分镜'); $anime_id = getProp($segment, 'anime_id'); $episode_id = getProp($segment, 'episode_id'); $episode_number = getProp($segment, 'episode_number'); $segment_number = getProp($segment, 'segment_number'); try { DB::beginTransaction(); // 删除分镜 $boolen = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->delete(); if (!$boolen) { Utils::throwError('20003:删除分镜失败'); } // 更新其他分镜序号 DB::table('mp_episode_segments')->where('anime_id', $anime_id)->where('episode_id', $episode_id)->where('segment_number', '>', $segment_number)->decrement('segment_number'); }catch (\Exception $e) { DB::rollBack(); Utils::throwError('20003:'.$e->getMessage()); } DB::commit(); return true; } public function segmentHistory($data) { $segment_id = getProp($data, 'segment_id'); // 获取历史图片 $pics = []; $pic_history = DB::table('mp_generate_pic_tasks')->where('alias_segment_id', $segment_id)->where('status', 'success')->orderBy('created_at', 'desc')->get(); foreach($pic_history as $task) { $result_url = getProp($task, 'result_url'); if (is_json($result_url)) { $pics = array_merge($pics, json_decode($result_url, true)); }else { $pics[] = $result_url; } } // 获取历史视频 $videos = []; $video_history = DB::table('mp_generate_video_tasks')->where('alias_segment_id', $segment_id)->where('status', 'success')->orderBy('created_at', 'desc')->get(); foreach($video_history as $task) { $result_url = getProp($task, 'result_url'); if (is_json($result_url)) { $videos = array_merge($pics, json_decode($result_url, true)); }else { $videos[] = $result_url; } } return compact('pics', 'videos'); } public function applyAudioData($data) { $segment_id = getProp($data, 'segment_id'); $global_data = getProp($data, 'global_data'); $segment_data = getProp($data, 'segment_data'); // 解析数据 try { DB::beginTransaction(); // 更新全局数据 $boolen = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([ 'global_data' => json_encode($global_data), 'segment_data' => json_encode($segment_data), ]); if (!$boolen) { Utils::throwError('20003:更新分镜失败'); } DB::commit(); return true; } catch (\Exception $e) { DB::rollBack(); Utils::throwError('20003:'.$e->getMessage()); } } // 轮训获取图片任务结果 private function loopGetPicTaskResult($task_id) { $start_count = 0; $img_url = ''; while ($start_count <= 30) { sleep(3); $task = MpGeneratePicTask::where('id', $task_id)->first(); $statusInfo = $this->aiImageGenerationService->queryTaskStatus($task); if ($statusInfo['status'] === 'success') { $task->updateStatus('success', [ 'result_url' => $statusInfo['result_url'], 'result_json' => $statusInfo['result_json'] ?? [] ]); // 同步调整分镜图片状态和结果 if (getProp($task, 'alias_segment_id')) { DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([ 'img_url' => $statusInfo['result_url'][0], 'pic_task_status' => '已完成', ]); } $img_url = $statusInfo['result_url'][0]; break; } elseif ($statusInfo['status'] === 'failed') { $task->updateStatus('failed', [ 'error_message' => $statusInfo['error_message'], 'result_json' => $statusInfo['result_json'] ?? [] ]); if (getProp($task, 'alias_segment_id')) { DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([ 'pic_task_status' => '失败', ]); } break; } $start_count+=3; } return $img_url; } /** * 从分镜内容中提取涉及的角色 */ private function extractCharactersFromSegment($segment_content, $available_characters, $roles) { $found_characters = []; foreach ($available_characters as $character) { // 检查角色名称是否在分镜内容中出现 if (strpos($segment_content, $character) !== false) { $found_characters[] = $character; } } if (!$found_characters) { foreach ($roles as $character) { // 检查角色名称是否在分镜内容中出现 if (strpos($segment_content, $character) !== false) { $found_characters[] = $character; } } } return array_unique($found_characters); } // 从分镜剧本中提取关键字段 private function handleSegmentContent(&$data) { $segmentContent = getProp($data, 'segment_content'); // 解析分镜详细信息 $segmentData = [ 'description' => '', 'composition' => '', 'camera_movement' => '', 'voice_actor' => '', 'dialogue' => '', 'frame_type' => '', 'scene' => '', // 新增:场景 'characters' => '', // 新增:出镜角色 'tail_frame' => '', // 新增:尾帧描述 ]; // 提取各个字段 - 兼容中文冒号和英文冒号,支持多种表达方式 if (preg_match('/(?:画面描述|镜头描述|场景描述)[::]\s*([^\n]+)/u', $segmentContent, $descMatch)) { $segmentData['description'] = trim($descMatch[1]); $data['description'] = trim($descMatch[1]); } if (preg_match('/(?:构图设计|构图|镜头构图)[::]\s*([^\n]+)/u', $segmentContent, $compMatch)) { $segmentData['composition'] = trim($compMatch[1]); $data['composition'] = trim($compMatch[1]); } if (preg_match('/(?:运镜调度|运镜|镜头运动|摄影机运动)[::]\s*([^\n]+)/u', $segmentContent, $cameraMatch)) { $segmentData['camera_movement'] = trim($cameraMatch[1]); $data['camera_movement'] = trim($cameraMatch[1]); } if (preg_match('/(?:配音角色|配音|角色|声优)[::]\s*([^\n]+)/u', $segmentContent, $voiceMatch)) { $segmentData['voice_actor'] = trim($voiceMatch[1]); $data['voice_actor'] = trim($voiceMatch[1]); } if (preg_match('/(?:台词内容|台词|对白|对话)[::]\s*([^\n]+)/u', $segmentContent, $dialogueMatch)) { $segmentData['dialogue'] = trim($dialogueMatch[1]); $data['dialogue'] = trim($dialogueMatch[1]); } if (preg_match('/(?:画面类型|镜头类型|类型)[::]\s*([^\n]+)/u', $segmentContent, $frameMatch)) { $segmentData['frame_type'] = trim($frameMatch[1]); $data['frame_type'] = trim($frameMatch[1]); } // 新增:场景字段 if (preg_match('/(?:场景|拍摄场景|背景场景|环境)[::]\s*([^\n]+)/u', $segmentContent, $sceneMatch)) { $segmentData['scene'] = trim($sceneMatch[1]); $data['scene'] = trim($sceneMatch[1]); } // 新增:出镜角色字段 if (preg_match('/(?:出镜角色|角色出镜|登场角色|人物)[::]\s*([^\n]+)/u', $segmentContent, $charactersMatch)) { $segmentData['characters'] = trim($charactersMatch[1]); $data['characters'] = trim($charactersMatch[1]); } // 新增:尾帧描述字段 if (preg_match('/(?:尾帧描述|尾帧|结束帧|最后一帧|结尾画面|结束画面)[::]\s*([^\n]+)/u', $segmentContent, $tailFrameMatch)) { $segmentData['tail_frame'] = trim($tailFrameMatch[1]); $data['tail_frame'] = trim($tailFrameMatch[1]); } return $segmentData; } public function createSegmentVideoTask($data) { $segment_id = getProp($data, 'segment_id'); $tail_frame = getProp($data, 'tail_frame'); if (!$segment_id) { Utils::throwError('1002:分镜ID不能为空'); } // 获取分镜信息 $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first(); if (!$segment) { Utils::throwError('20003:分镜不存在'); } $segment = (array)$segment; // 获取分镜内容 $segmentContent = getProp($segment, 'segment_content', ''); // 如果没有传入尾帧描述,使用分镜表中的尾帧描述 if (!$tail_frame) { $tail_frame = getProp($segment, 'tail_frame', ''); } // 构建完整的提示词 $fullPrompt = $segmentContent; if ($tail_frame) { $fullPrompt .= "\n尾帧描述:$tail_frame"; } // 智能选择视频时长 // $videoDuration = $this->calculateOptimalVideoDuration($segmentContent, $tail_frame); $videoDuration = -1; // 构建视频生成参数 $videoParams = [ 'model' => 'doubao-seedance-1-0-pro-250528', 'alias_segment_id' => $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 (getProp($segment, 'img_url')) { $videoParams['first_frame_url'] = getProp($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', ]; } // dd($videoParams); // 创建视频生成任务 $task = $this->aiVideoGenerationService->createSeedanceTask($videoParams); // 更新分镜表的视频任务信息 DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([ 'video_task_id' => $task->id, 'video_task_status' => '生成中', 'updated_at' => date('Y-m-d H:i:s') ]); return [ 'task_id' => $task->id, 'status' => $task->status, 'segment_id' => $segment_id, 'video_duration' => $videoDuration ]; } /** * 根据提示词内容智能计算最优视频时长 * * @param string $segmentContent 分镜内容 * @param string $tailFrame 尾帧描述 * @return int 视频时长(2-12秒) */ public function calculateOptimalVideoDuration($segmentContent, $tailFrame = '') { // 合并所有文本内容 $fullText = trim($segmentContent . ' ' . $tailFrame); // 如果没有内容,返回默认时长 if (empty($fullText)) { return 5; } // 计算文本长度(中文字符按2个字符计算) $textLength = mb_strlen($fullText, 'UTF-8'); $chineseCharCount = preg_match_all('/[\x{4e00}-\x{9fff}]/u', $fullText); $adjustedLength = $textLength + $chineseCharCount; // 中文字符权重更高 // 分析内容复杂度 $complexityScore = $this->analyzeContentComplexity($fullText); // 基础时长计算(根据文本长度) $baseDuration = 3; // 默认2秒 // if ($adjustedLength <= 20) { // $baseDuration = 3; // } elseif ($adjustedLength <= 40) { // $baseDuration = 4; // } elseif ($adjustedLength <= 60) { // $baseDuration = 5; // } elseif ($adjustedLength <= 80) { // $baseDuration = 6; // } elseif ($adjustedLength <= 100) { // $baseDuration = 7; // } elseif ($adjustedLength <= 120) { // $baseDuration = 8; // } else { // $baseDuration = 9; // } // 根据内容复杂度调整时长 $finalDuration = $baseDuration + $complexityScore; // 确保时长在2-12秒范围内 return max(2, min(12, $finalDuration)); } /** * 分析内容复杂度,返回时长调整值 * * @param string $text 文本内容 * @return int 调整值(-2到+3) */ private function analyzeContentComplexity($text) { $score = 0; // 动作关键词(需要更长时间展现) $actionKeywords = [ '跑步', '奔跑', '飞行', '跳跃', '战斗', '打斗', '追逐', '逃跑', '舞蹈', '表演', '变化', '变身', '爆炸', '崩塌', '建造', '制作', '烹饪', '绘画', '写字', '移动', '旋转', '翻滚', '攀爬', '游泳', '潜水', '滑行', '飞翔' ]; // 静态关键词(可以用较短时间) $staticKeywords = [ '站立', '坐着', '躺着', '静止', '凝视', '思考', '沉思', '观察', '等待', '睡觉', '休息', '静坐', '冥想', '阅读', '听音乐' ]; // 复杂场景关键词(需要更长时间) $complexSceneKeywords = [ '人群', '聚会', '会议', '战争', '庆典', '仪式', '表演', '比赛', '竞技', '多人', '群体', '团队', '合作', '互动', '对话', '争论', '讨论' ]; // 情感关键词(需要适中时间表现) $emotionKeywords = [ '哭泣', '笑容', '愤怒', '惊讶', '恐惧', '悲伤', '喜悦', '兴奋', '紧张', '感动', '震惊', '困惑', '失望', '希望', '绝望', '温柔', '严肃' ]; // 检查动作关键词 foreach ($actionKeywords as $keyword) { if (strpos($text, $keyword) !== false) { $score += 1; break; // 避免重复加分 } } // 检查静态关键词 foreach ($staticKeywords as $keyword) { if (strpos($text, $keyword) !== false) { $score -= 1; break; } } // 检查复杂场景关键词 foreach ($complexSceneKeywords as $keyword) { if (strpos($text, $keyword) !== false) { $score += 1; break; } } // 检查情感关键词 foreach ($emotionKeywords as $keyword) { if (strpos($text, $keyword) !== false) { $score += 1; break; } } // 检查时间相关词汇 if (preg_match('/缓慢|慢慢|渐渐|逐渐|慢动作/', $text)) { $score += 1; } if (preg_match('/快速|迅速|急速|瞬间|立刻|马上/', $text)) { $score -= 1; } // 检查转场词汇 if (preg_match('/然后|接着|随后|紧接着|同时|与此同时/', $text)) { $score += 1; } // 检查环境描述(复杂环境需要更多时间) if (preg_match('/风景|景色|环境|背景|氛围|光线|阴影|色彩|细节/', $text)) { $score += 1; } // 检查对话内容(对话需要更多时间) if (preg_match('/说|讲|告诉|回答|询问|问|对话|交谈/', $text)) { $score += 1; } // 限制调整范围 return max(-1, min(4, $score)); } }