AnimeService.php 70 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774
  1. <?php
  2. namespace App\Services\Anime;
  3. use App\Consts\ErrorConst;
  4. use App\Facade\Site;
  5. use App\Libs\Utils;
  6. use App\Models\MpGeneratePicTask;
  7. use App\Services\AIGeneration\AIImageGenerationService;
  8. use App\Services\AIGeneration\AIVideoGenerationService;
  9. use Dflydev\DotAccessData\Util;
  10. use GuzzleHttp\Client;
  11. use Illuminate\Support\Facades\DB;
  12. use Illuminate\Support\Facades\Log;
  13. use Illuminate\Support\Facades\Redis;
  14. use OSS\Core\OssException;
  15. use OSS\OssClient;
  16. class AnimeService
  17. {
  18. protected $aiImageGenerationService;
  19. protected $aiVideoGenerationService;
  20. private $url;
  21. private $api_key;
  22. private $headers;
  23. public function __construct(
  24. AIImageGenerationService $aiImageGenerationService,
  25. AIVideoGenerationService $aiVideoGenerationService
  26. ) {
  27. $this->aiImageGenerationService = $aiImageGenerationService;
  28. $this->aiVideoGenerationService = $aiVideoGenerationService;
  29. $this->url = 'https://api.deepseek.com/chat/completions';
  30. $this->api_key = env('DEEPSEEK_API_KEY');
  31. $this->headers = [
  32. 'Authorization' => 'Bearer '.$this->api_key,
  33. 'Content-Type' => 'application/json; charset=UTF-8'
  34. ];
  35. }
  36. public function chatList($data) {
  37. $anime_name = getProp($data, 'anime_name');
  38. $query = DB::table('mp_animes')->where('is_deleted', 0);
  39. if ($anime_name) {
  40. $query->where('anime_name', 'like', "%$anime_name%");
  41. }
  42. return $query->orderBy('id', 'desc')->paginate();
  43. }
  44. public function chatHistory($data) {
  45. $anime_id = getProp($data, 'anime_id');
  46. $sequence = getProp($data, 'sequence', 0);
  47. if (!$anime_id) Utils::throwError('20003:请选择对话!');
  48. $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)
  49. ->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');
  50. return $query->orderBy('ar.id')->get()->map(function ($value) {
  51. $value = (array)$value;
  52. $value['created_at'] = transDate($value['created_at']);
  53. $value['episode_created_at'] = transDate($value['episode_created_at']);
  54. return $value;
  55. })->toArray();
  56. }
  57. public function batchSetRoleImg($data) {
  58. $anime_id = getProp($data, 'anime_id');
  59. $episode_number = getProp($data, 'episode_number');
  60. if (!$anime_id) Utils::throwError('1002:请选择对话');
  61. $anime = DB::table('mp_animes')->where('id', $anime_id)->where('is_deleted', 0)->first();
  62. $roles = json_decode(getProp($anime, 'roles'), true);
  63. $art_style = getProp($anime, 'art_style');
  64. foreach ($roles as &$role) {
  65. $role_name = getProp($role, 'role');
  66. if ($role_name == '旁白') continue;
  67. $description = getProp($role, 'description');
  68. if ($art_style) $description = "美术风格:\n$art_style\n主体描述:$description";
  69. // 参考图地址
  70. $ref_img_url = getProp($role, 'url');
  71. // 此处调用AIImageGenerationService的生成图片任务接口: createImageGenerationTask(注意使用默认参数,prompt使用角色描述),并返回任务id
  72. try {
  73. $params = [
  74. 'prompt' => $description,
  75. 'ref_img_urls' => !empty($ref_img_url) ? [$ref_img_url] : []
  76. ];
  77. $task = $this->aiImageGenerationService->createImageGenerationTask($params);
  78. $task_id = $task->id;
  79. if (!$task_id) {
  80. Utils::throwError("20003: 角色({$role_name})生成图片失败");
  81. }
  82. $role['task_id'] = $task_id;
  83. $role['task_status'] = 'processing';
  84. } catch (\Exception $e) {
  85. Utils::throwError("20003:" . $e->getMessage());
  86. }
  87. }
  88. // 保存角色信息
  89. // $boolen = DB::table('mp_animes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->update([
  90. $boolen = DB::table('mp_animes')->where('id', $anime_id)->update([
  91. 'roles' => json_encode($roles, 256),
  92. 'generate_status' => '执行中',
  93. 'generate_at' => date('Y-m-d H:i:s'),
  94. 'updated_at' => date('Y-m-d H:i:s')
  95. ]);
  96. if (!$boolen) {
  97. Utils::throwError('20003:保存角色信息失败');
  98. }
  99. return $boolen;
  100. }
  101. public function batchSetSceneImg($data) {
  102. $anime_id = getProp($data, 'anime_id');
  103. if (!$anime_id) Utils::throwError('1002:请选择对话');
  104. $episode_number = getProp($data, 'episode_number');
  105. $anime = DB::table('mp_animes')->where('id', $anime_id)->where('is_deleted', 0)->first();
  106. $scenes = json_decode(getProp($anime, 'scenes'), true);
  107. $art_style = getProp($anime, 'art_style');
  108. foreach ($scenes as &$scene) {
  109. $scene_name = getProp($scene, 'scene');
  110. $description = getProp($scene, 'description');
  111. if ($art_style) $description = "美术风格:\n$art_style\n\n场景描述:$description";
  112. // 参考图地址
  113. $ref_img_url = getProp($scene, 'url');
  114. // 此处调用AIImageGenerationService的生成图片任务接口: createImageGenerationTask(注意使用默认参数,prompt使用角色描述),并返回任务id
  115. try {
  116. $params = [
  117. 'prompt' => $description,
  118. 'ref_img_urls' => !empty($ref_img_url) ? [$ref_img_url] : []
  119. ];
  120. $task = $this->aiImageGenerationService->createImageGenerationTask($params);
  121. $task_id = $task->id;
  122. if (!$task_id) {
  123. Utils::throwError("20003: 角色({$scene_name})生成图片失败");
  124. }
  125. $scene['task_id'] = $task_id;
  126. $scene['task_status'] = 'processing';
  127. } catch (\Exception $e) {
  128. Utils::throwError("20003:" . $e->getMessage());
  129. }
  130. }
  131. // 保存角色信息
  132. // $boolen = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->update([
  133. $boolen = DB::table('mp_animes')->where('id', $anime_id)->update([
  134. 'scenes' => json_encode($scenes, 256),
  135. 'generate_status' => '执行中',
  136. 'generate_at' => date('Y-m-d H:i:s'),
  137. 'updated_at' => date('Y-m-d H:i:s')
  138. ]);
  139. if (!$boolen) {
  140. Utils::throwError('20003:保存角色信息失败');
  141. }
  142. return $boolen;
  143. }
  144. public function editAnime($data) {
  145. $script_arr =getProp($data, 'script', []);
  146. if (!$script_arr) Utils::throwError('20003:剧本内容不能为空');
  147. $start_episode_sequence = getProp($data, 'start_episode_sequence', 1);
  148. $anime_id = getProp($data, 'anime_id');
  149. try {
  150. DB::beginTransaction();
  151. $table_data = [
  152. 'intro' => isset($script_arr['intro']) ? $script_arr['intro'] : '',
  153. 'highlights' => isset($script_arr['highlights']) ? $script_arr['highlights'] : '',
  154. 'role_relationship' => isset($script_arr['role_relationship']) ? $script_arr['role_relationship'] : '',
  155. 'core_contradiction' => isset($script_arr['core_contradiction']) ? $script_arr['core_contradiction'] : '',
  156. 'roles' => isset($script_arr['roles']) ? json_encode($script_arr['roles'], 256) : '',
  157. 'art_style' => isset($script_arr['art_style']) ? $script_arr['art_style'] : '',
  158. 'scenes' => isset($script_arr['scenes']) ? json_encode($script_arr['scenes'], 256) : '',
  159. 'status' => 3,
  160. 'start_episode_sequence' => $start_episode_sequence,
  161. 'end_episode_sequence' => $start_episode_sequence + count($script_arr['episodes']) - 1,
  162. 'episode_num' => count($script_arr['episodes']),
  163. 'updated_at' => date('Y-m-d H:i:s')
  164. ];
  165. // 保存剧本内容
  166. if (!empty($anime_id)) {
  167. $boolen = DB::table('mp_animes')->where('id', $anime_id)->update($table_data);
  168. if (!$boolen) {
  169. dLog('anime')->info('对话保存失败', $table_data);
  170. Utils::throwError('20003:对话保存失败');
  171. }
  172. }else {
  173. $table['created_at'] = $table_data['updated_at'];
  174. $anime_id = DB::table('mp_animes')->insertGetId($table_data);
  175. if (!$anime_id) {
  176. dLog('anime')->info('对话保存失败', $table_data);
  177. Utils::throwError('20003:对话保存失败');
  178. }
  179. }
  180. // 保存分集内容
  181. // 删除历史分集内容
  182. $boolen1 = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->delete();
  183. $episodes = [];
  184. $sequence = 1;
  185. foreach ($script_arr['episodes'] as $episode) {
  186. $segment_number = 1;
  187. foreach($episode['segments'] as $segment) {
  188. $episodes[] = [
  189. 'anime_id' => $anime_id,
  190. 'episode_number' => $episode['episode_number'],
  191. 'title' => $episode['title'],
  192. // 'content' => $episode['content'],
  193. 'content' => '',
  194. 'segment_number' => $segment_number,
  195. 'segment_content' => $segment['segment_content'],
  196. 'sequence' => $sequence,
  197. 'created_at' => date('Y-m-d H:i:s'),
  198. 'updated_at' => date('Y-m-d H:i:s')
  199. ];
  200. $sequence++;
  201. $segment_number++;
  202. }
  203. }
  204. $boolen2 = DB::table('mp_anime_episodes')->insert($episodes);
  205. if (!$boolen2) {
  206. dLog('deepseek')->info('分集内容保存失败', $episodes);
  207. Utils::throwError('20003:分集内容保存失败');
  208. }
  209. } catch (\Exception $e) {
  210. DB::rollBack();
  211. Utils::throwError('20003:'.$e->getMessage());
  212. }
  213. DB::commit();
  214. return true;
  215. }
  216. public function animeDetail($data) {
  217. $anime_id = getProp($data, 'anime_id');
  218. $anime = DB::table('mp_animes')->where('id', $anime_id)->where('is_deleted', 0)
  219. ->selectRaw('id as anime_id, anime_name, intro, highlights, roles, role_relationship, core_contradiction, art_style, scenes, status, remark, generate_status, created_at')->first();
  220. if (!$anime) Utils::throwError('20003:该剧本不存在!');
  221. $anime = (array)$anime;
  222. $anime['roles'] = json_decode($anime['roles']);
  223. $anime['scenes'] = json_decode($anime['scenes']);
  224. // 获取分集信息
  225. $episodes = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('is_default', 1)
  226. ->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"))
  227. ->orderBy('episode_number')->get()->map(function ($value) {
  228. return (array)$value;
  229. })->toArray();
  230. $anime['episodes'] = $episodes;
  231. return $anime;
  232. }
  233. public function episodeInfo($data) {
  234. $anime_id = getProp($data, 'anime_id');
  235. $episode_id = getProp($data, 'episode_id');
  236. if (!$anime_id || !$episode_id) {
  237. Utils::throwError('1002:请选择剧集');
  238. }
  239. $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();
  240. if (!$episode) Utils::throwError('20003: 该剧集不存在!');
  241. $episode = (array)$episode;
  242. $episode['roles'] = json_decode($episode['roles'], true);
  243. $episode['scenes'] = json_decode($episode['scenes'], true);
  244. // 获取分镜信息
  245. $segments = DB::table('mp_episode_segments')->where('anime_id', $anime_id)->where('episode_id', getProp($episode, 'episode_id'))->orderBy('segment_number')->select('act_number', 'act_title', 'segment_id', 'segment_number', 'segment_content')->get()->map(function ($value) {
  246. return (array)$value;
  247. })->toArray();
  248. $acts = [];
  249. foreach($segments as $segment) {
  250. if (isset($acts[getProp($segment, 'act_number')])) {
  251. $acts[getProp($segment, 'act_number')]['segments'][] = [
  252. 'segment_id' => getProp($segment, 'segment_id'),
  253. 'segment_number' => getProp($segment, 'segment_number'),
  254. 'segment_content' => getProp($segment, 'segment_content'),
  255. ];
  256. }else {
  257. $acts[getProp($segment, 'act_number')]= [
  258. 'act_number' => getProp($segment, 'act_number'),
  259. 'act_title' => getProp($segment, 'act_title'),
  260. 'segments' => [
  261. [
  262. 'segment_id' => getProp($segment, 'segment_id'),
  263. 'segment_number' => getProp($segment, 'segment_number'),
  264. 'segment_content' => getProp($segment, 'segment_content'),
  265. ]
  266. ]
  267. ];
  268. }
  269. }
  270. $episode['acts'] = array_values($acts);
  271. return $episode;
  272. }
  273. public function copyEpisodeVersion($data) {
  274. $episode_id = getProp($data, 'episode_id');
  275. $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first();
  276. if (!$episode) Utils::throwError('20003:该剧集不存在');
  277. $episode = (array)$episode;
  278. $count = DB::table('mp_anime_episodes')->where('anime_id', $episode['anime_id'])->where('episode_number', $episode['episode_number'])->count('id');
  279. $sequence = $count + 1;
  280. unset($episode['id']);
  281. $episode['sequence'] = $sequence;
  282. $episode['created_at'] = date('Y-m-d H:i:s');
  283. $episode['updated_at'] = date('Y-m-d H:i:s');
  284. $episode['is_default'] = 1;
  285. try {
  286. DB::beginTransaction();
  287. // 移除is_default标志
  288. $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')]);
  289. if (!$boolen) {
  290. Utils::throwError('20003:更新失败!');
  291. }
  292. // 新增副本
  293. $new_episode_id = DB::table('mp_anime_episodes')->insertGetId($episode);
  294. if (!$new_episode_id) {
  295. Utils::throwError('20003:创建副本失败!');
  296. }
  297. $episode['episode_id'] = $new_episode_id;
  298. // 获取分镜数据
  299. $segments = DB::table('mp_episode_segments')->where('episode_id', $episode_id)->get()->map(function($item) use ($new_episode_id) {
  300. $item = (array)$item;
  301. unset($item['id']);
  302. $item['episode_id'] = $new_episode_id;
  303. $item['segment_id'] = date('YmdHis').mt_rand(1000,9999).str_pad($item['segment_number'], 3, "0", STR_PAD_LEFT);
  304. $item['created_at'] = $item['updated_at'] = date('Y-m-d H:i:s');
  305. return $item;
  306. })->toArray();
  307. // 写入分镜数据
  308. $boolen2 = DB::table('mp_episode_segments')->insert($segments);
  309. if (!$boolen2) {
  310. Utils::throwError('20003:写入分镜数据失败!');
  311. }
  312. }catch (\Exception $e) {
  313. DB::rollBack();
  314. Utils::throwError('20003:'.$e->getMessage());
  315. }
  316. DB::commit();
  317. return $episode;
  318. }
  319. public function episodeVersions($data) {
  320. $anime_id = getProp($data, 'anime_id');
  321. $episode_id = getProp($data, 'episode_id');
  322. if (!$anime_id || !$episode_id) {
  323. Utils::throwError('1002:请选择剧集');
  324. }
  325. $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) {
  326. return (array)$value;
  327. })->toArray();
  328. if (!$episodes) Utils::throwError('20003: 该剧集不存在!');
  329. foreach($episodes as &$episode) {
  330. $episode['version'] = 'V'.$episode['sequence'];
  331. $episode['roles'] = json_decode($episode['roles'], true);
  332. $episode['scenes'] = json_decode($episode['scenes'], true);
  333. }
  334. return $episodes;
  335. }
  336. public function bindEpisodeVersion($data) {
  337. $episode_id = getProp($data, 'episode_id');
  338. $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first();
  339. if (!$episode) Utils::throwError('20003:该剧集不存在');
  340. $episode = (array)$episode;
  341. if ((int)$episode['is_default'] === 1) return true;
  342. try {
  343. DB::beginTransaction();
  344. // 移除is_default标志
  345. $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')]);
  346. if (!$boolen) {
  347. Utils::throwError('20003:更新失败!');
  348. }
  349. // 绑定副本
  350. $boolen2 = DB::table('mp_anime_episodes')->where('id', $episode_id)->update(['is_default' => 1, 'updated_at'=>date('Y-m-d H:i:s')]);
  351. if (!$boolen2) {
  352. Utils::throwError('20003:绑定失败!');
  353. }
  354. }catch (\Exception $e) {
  355. DB::rollBack();
  356. Utils::throwError('20003:'.$e->getMessage());
  357. }
  358. DB::commit();
  359. return true;
  360. }
  361. public function chatChangeImg($data) {
  362. $segment_id = getProp($data, 'segment_id');
  363. $prompt = getProp($data, 'prompt');
  364. if (!$prompt || !$segment_id) {
  365. Utils::throwError('1002:请输入提示词,并且选择需修改的图片');
  366. }
  367. $segment = DB::table("mp_episode_segments")->where('segment_id', $segment_id)->first();
  368. if (!$segment) Utils::throwError('20003:该分镜不存在!');
  369. $ref_img_url = getProp($segment, 'img_url');
  370. if (empty($ref_img_url)) {
  371. $prompt = getProp($segment, 'segment_content');
  372. }
  373. try {
  374. $params = [
  375. 'alias_segment_id' => $segment_id,
  376. 'prompt' => $prompt,
  377. 'ref_img_urls' => !empty($ref_img_url) ? [$ref_img_url] : []
  378. ];
  379. $task = $this->aiImageGenerationService->createImageGenerationTask($params);
  380. $task_id = $task->id;
  381. if (!$task_id) {
  382. Utils::throwError("20003: 生成图片失败");
  383. }
  384. // 更新任务ID
  385. $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')]);
  386. if (!$boolen) Utils::throwError('20003:更新任务ID失败');
  387. } catch (\Exception $e) {
  388. Utils::throwError("20003:" . $e->getMessage());
  389. }
  390. $img_url = $this->loopGetPicTaskResult($task_id);
  391. if (!$img_url) Utils::throwError('20003:图片生成中,请稍后刷新');
  392. return $img_url;
  393. }
  394. public function changeEpisodeRoles($data) {
  395. $anime_id = getProp($data, 'anime_id');
  396. $episode_id = getProp($data, 'episode_id');
  397. $roles = getProp($data, 'roles');
  398. // 参数验证
  399. if (!$anime_id) Utils::throwError('20003:请提供动漫ID');
  400. if (!$episode_id) Utils::throwError('20003:请提供剧集ID');
  401. if (!is_array($roles)) Utils::throwError('20003:角色列表必须是数组格式');
  402. // 验证剧集是否存在
  403. $episode = DB::table('mp_anime_episodes')
  404. ->where('anime_id', $anime_id)
  405. ->where('id', $episode_id)
  406. ->first();
  407. if (!$episode) Utils::throwError('20003:该剧集不存在');
  408. try {
  409. // 更新角色列表
  410. $result = DB::table('mp_anime_episodes')
  411. ->where('anime_id', $anime_id)
  412. ->where('id', $episode_id)
  413. ->update([
  414. 'roles' => json_encode($roles, 256),
  415. 'updated_at' => date('Y-m-d H:i:s')
  416. ]);
  417. if ($result === false) {
  418. Utils::throwError('20003:更新角色列表失败');
  419. }
  420. return ['success' => true, 'message' => '角色列表更新成功'];
  421. } catch (\Exception $e) {
  422. dLog('anime')->error('更新剧集角色失败', [
  423. 'anime_id' => $anime_id,
  424. 'episode_id' => $episode_id,
  425. 'error' => $e->getMessage()
  426. ]);
  427. Utils::throwError('20003:更新角色列表失败: ' . $e->getMessage());
  428. }
  429. }
  430. public function changeEpisodeScenes($data) {
  431. $anime_id = getProp($data, 'anime_id');
  432. $episode_id = getProp($data, 'episode_id');
  433. $scenes = getProp($data, 'scenes');
  434. // 参数验证
  435. if (!$anime_id) Utils::throwError('20003:请提供动漫ID');
  436. if (!$episode_id) Utils::throwError('20003:请提供剧集ID');
  437. if (!is_array($scenes)) Utils::throwError('20003:场景列表必须是数组格式');
  438. // 验证剧集是否存在
  439. $episode = DB::table('mp_anime_episodes')
  440. ->where('anime_id', $anime_id)
  441. ->where('id', $episode_id)
  442. ->first();
  443. if (!$episode) Utils::throwError('20003:该剧集不存在');
  444. try {
  445. // 更新场景列表
  446. $result = DB::table('mp_anime_episodes')
  447. ->where('anime_id', $anime_id)
  448. ->where('id', $episode_id)
  449. ->update([
  450. 'scenes' => json_encode($scenes, 256),
  451. 'updated_at' => date('Y-m-d H:i:s')
  452. ]);
  453. if ($result === false) {
  454. Utils::throwError('20003:更新场景列表失败');
  455. }
  456. return ['success' => true, 'message' => '场景列表更新成功'];
  457. } catch (\Exception $e) {
  458. dLog('anime')->error('更新剧集场景失败', [
  459. 'anime_id' => $anime_id,
  460. 'episode_id' => $episode_id,
  461. 'error' => $e->getMessage()
  462. ]);
  463. Utils::throwError('20003:更新场景列表失败: ' . $e->getMessage());
  464. }
  465. }
  466. public function editSegment($data) {
  467. $segment_id = getProp($data, 'segment_id');
  468. $segment_content = getProp($data, 'segment_content');
  469. // 参数验证
  470. if (!$segment_id) Utils::throwError('20003:请提供分镜ID');
  471. if (!$segment_content) Utils::throwError('20003:请提供分镜内容');
  472. // 验证分镜是否存在
  473. $segment = DB::table('mp_episode_segments')
  474. ->where('segment_id', $segment_id)
  475. ->first();
  476. if (!$segment) Utils::throwError('20003:该分镜不存在');
  477. try {
  478. // 准备更新数据
  479. $update_data = [
  480. 'segment_content' => $segment_content,
  481. 'updated_at' => date('Y-m-d H:i:s')
  482. ];
  483. // 使用现有方法分析segment_content,提取各个字段
  484. $this->handleSegmentContent($update_data);
  485. // 更新分镜信息
  486. $result = DB::table('mp_episode_segments')
  487. ->where('segment_id', $segment_id)
  488. ->update($update_data);
  489. if ($result === false) {
  490. Utils::throwError('20003:更新分镜剧本失败');
  491. }
  492. return true;
  493. } catch (\Exception $e) {
  494. dLog('anime')->error('更新分镜剧本失败', [
  495. 'segment_id' => $segment_id,
  496. 'error' => $e->getMessage()
  497. ]);
  498. Utils::throwError('20003:更新分镜剧本失败: ' . $e->getMessage());
  499. }
  500. }
  501. public function batchSetSegmentPics($data) {
  502. $anime_id = getProp($data, 'anime_id');
  503. $episode_id = getProp($data, 'episode_id');
  504. $ratio = getProp($data, 'ratio');
  505. if (!in_array($ratio, ['1:1', '4:3', '3:2', '16:9', '21:9'])) {
  506. Utils::throwError('1002:画面比例选择不正确,只支持1:1、4:3、3:2、16:9、21:9');
  507. }
  508. if (!$anime_id || !$episode_id) {
  509. Utils::throwError('1002:请选择剧集');
  510. }
  511. // 获取剧集信息
  512. $episode = DB::table('mp_anime_episodes')
  513. ->where('anime_id', $anime_id)
  514. ->where('id', $episode_id)
  515. ->where('is_default', 1)
  516. ->select('id as episode_id', 'anime_id', 'episode_number')
  517. ->first();
  518. if (!$episode) {
  519. Utils::throwError('20003:该剧集不存在');
  520. }
  521. // 根据画面比例给宽高赋值
  522. switch ($ratio) {
  523. case '1:1':
  524. $width = 2048;
  525. $height = 2048;
  526. break;
  527. case '4:3':
  528. $width = 2304;
  529. $height = 1728;
  530. break;
  531. case '3:2':
  532. $width = 2496;
  533. $height = 1664;
  534. break;
  535. case '16:9':
  536. $width = 2560;
  537. $height = 1440;
  538. break;
  539. case '21:9':
  540. $width = 3024;
  541. $height = 1296;
  542. break;
  543. default:
  544. $width = 2048;
  545. $height = 2048;
  546. }
  547. // 获取动漫的角色信息(包含参考图)
  548. $anime = DB::table('mp_animes')
  549. ->where('id', $anime_id)
  550. ->select('roles', 'scenes')
  551. ->first();
  552. if (!$anime || !$anime->roles) {
  553. Utils::throwError('20003:未找到角色信息');
  554. }
  555. $roles = json_decode($anime->roles, true);
  556. if (!$roles) {
  557. Utils::throwError('20003:角色信息格式错误');
  558. }
  559. // 构建角色名称到参考图的映射
  560. $role_map = [];
  561. foreach ($roles as $role) {
  562. $role_name = getProp($role, 'role');
  563. $role_url = getProp($role, 'url');
  564. if (!empty($role_name) && !empty($role_url)) {
  565. // // 处理角色名称,去除装扮后缀(如:许芸-校服装 -> 许芸)- 修复中文乱码问题(弃用,容易产生重复图片)
  566. // $base_name = preg_replace('/[--].*$/u', '', $role_name);
  567. // $role_map[$base_name] = $role_url;
  568. // 同时保存完整名称的映射
  569. $role_map[$role_name] = $role_url;
  570. }
  571. }
  572. $scenes = json_decode($anime->scenes, true);
  573. if (!$scenes) {
  574. Utils::throwError('20003:角色信息格式错误');
  575. }
  576. // 构建角色名称到参考图的映射
  577. $scene_map = [];
  578. foreach ($scenes as $scene) {
  579. $scene_name = getProp($scene, 'scene');
  580. $scene_url = getProp($scene, 'url');
  581. if (!empty($scene_name) && !empty($scene_url)) {
  582. $scene_map[$scene_name] = $scene_url;
  583. }
  584. }
  585. // 获取所有分镜信息
  586. $segments = DB::table('mp_episode_segments')
  587. ->where('anime_id', $anime_id)
  588. ->where('episode_id', $episode->episode_id)
  589. ->where('pic_task_status', '')
  590. ->orderBy('segment_number')
  591. ->select('episode_number', 'segment_id', 'segment_number', 'segment_content', 'scene', 'characters')
  592. ->limit(3)
  593. ->get()
  594. ->toArray();
  595. if (empty($segments)) {
  596. Utils::throwError('20003:未找到分镜信息');
  597. }
  598. $updated_segments = [];
  599. $failed_segments = [];
  600. // 批量为每个分镜生成图片任务
  601. foreach ($segments as $segment) {
  602. $segment_id = $segment->segment_id;
  603. $segment_content = $segment->segment_content;
  604. if (empty($segment_content)) {
  605. $failed_segments[] = [
  606. 'segment_id' => $segment_id,
  607. 'error' => '分镜内容为空'
  608. ];
  609. continue;
  610. }
  611. try {
  612. // 解析分镜内容,提取画面描述作为主要提示词
  613. $prompt = $segment_content;
  614. $ref_img_urls = [];
  615. // 分镜场景
  616. $scene = getProp($segment, 'scene');
  617. $matched_scene = '';
  618. if (isset($scene_map[$scene])) {
  619. $img_index = count($ref_img_urls) + 1;
  620. $prompt = "场景:$scene(图$img_index)\n$prompt";
  621. $ref_img_urls[] = $scene_map[$scene];
  622. $matched_scene = $scene;
  623. }
  624. // 分镜角色
  625. $characters = getProp($segment, 'characters');
  626. $characters = str_replace(['、', ',', ';'], ',', $characters);
  627. $roles = explode(',', $characters);
  628. // 从分镜内容中提取涉及的角色
  629. $segment_characters = $this->extractCharactersFromSegment($segment_content, array_keys($role_map), $roles);
  630. // 获取匹配角色的参考图
  631. foreach ($segment_characters as $character) {
  632. if (isset($role_map[$character])) {
  633. // 将$prompt中出现的角色替换成角色(图1、2、3,序号是$ref_img_urls的个数加1)
  634. $img_index = count($ref_img_urls) + 1;
  635. $prompt = str_replace($character, $character . '(图' . $img_index . ')', $prompt);
  636. $ref_img_urls[] = $role_map[$character];
  637. }
  638. }
  639. // 如果没有找到匹配的角色参考图,不使用参考图
  640. if (empty($ref_img_urls)) {
  641. $ref_img_urls = [];
  642. }
  643. // 准备生成任务参数
  644. $params = [
  645. 'alias_segment_id' => $segment_id,
  646. 'prompt' => $prompt,
  647. 'ref_img_urls' => $ref_img_urls,
  648. 'width' => $width,
  649. 'height' => $height,
  650. ];
  651. // $task_id = mt_rand(100000, 999999);
  652. // 调用AI图片生成服务
  653. $task = $this->aiImageGenerationService->createImageGenerationTask($params);
  654. $task_id = $task->id;
  655. if (!$task_id) {
  656. $failed_segments[] = [
  657. 'segment_id' => $segment_id,
  658. 'error' => '创建生成任务失败'
  659. ];
  660. continue;
  661. }
  662. // 更新分镜的任务信息
  663. $update_result = DB::table('mp_episode_segments')
  664. ->where('segment_id', $segment_id)
  665. ->update([
  666. 'pic_task_id' => $task_id,
  667. 'pic_task_status' => '生成中',
  668. 'updated_at' => date('Y-m-d H:i:s')
  669. ]);
  670. // 如果是第一集的首帧图片,则存入待更新数组
  671. if ($update_result && (int)getProp($segment, 'episode_number') === 1 && (int)getProp($segment, 'segment_number') === 1) {
  672. Redis::sadd('anime_first_frame_urls', $segment_id);
  673. }
  674. if ($update_result) {
  675. $updated_segments[] = [
  676. 'segment_id' => $segment_id,
  677. 'task_id' => $task_id,
  678. 'status' => '生成中',
  679. 'matched_scenes' => $matched_scene,
  680. 'matched_roles' => $segment_characters,
  681. 'ref_urls_count' => count($ref_img_urls)
  682. ];
  683. } else {
  684. $failed_segments[] = [
  685. 'segment_id' => $segment_id,
  686. 'error' => '更新分镜任务状态失败'
  687. ];
  688. }
  689. } catch (\Exception $e) {
  690. $failed_segments[] = [
  691. 'segment_id' => $segment_id,
  692. 'error' => $e->getMessage()
  693. ];
  694. }
  695. }
  696. // // 更新剧集生成状态
  697. // if (!empty($updated_segments)) {
  698. // DB::table('mp_anime_episodes')
  699. // ->where('id', $episode->episode_id)
  700. // ->update([
  701. // 'generate_status' => '执行中',
  702. // 'updated_at' => date('Y-m-d H:i:s')
  703. // ]);
  704. // }
  705. return [
  706. 'success_count' => count($updated_segments),
  707. 'failed_count' => count($failed_segments),
  708. 'success_segments' => $updated_segments,
  709. 'failed_segments' => $failed_segments,
  710. 'total_segments' => count($segments)
  711. ];
  712. }
  713. public function reGenerateSegment($data) {
  714. $segment_id = getProp($data, 'segment_id');
  715. $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first();
  716. $anime_id = getProp($segment, 'anime_id');
  717. $episode_id = getProp($segment, 'episode_id');
  718. $episode_number = getProp($segment, 'episode_number');
  719. $segment_content = getProp($data, 'segment_content');
  720. $ratio = getProp($data, 'ratio');
  721. if (!in_array($ratio, ['1:1', '4:3', '3:2', '16:9', '21:9'])) {
  722. Utils::throwError('1002:画面比例选择不正确,只支持1:1、4:3、3:2、16:9、21:9');
  723. }
  724. if (!$anime_id || !$episode_id) {
  725. Utils::throwError('1002:请选择分镜');
  726. }
  727. // 根据画面比例给宽高赋值
  728. switch ($ratio) {
  729. case '1:1':
  730. $width = 2048;
  731. $height = 2048;
  732. break;
  733. case '4:3':
  734. $width = 2304;
  735. $height = 1728;
  736. break;
  737. case '3:2':
  738. $width = 2496;
  739. $height = 1664;
  740. break;
  741. case '16:9':
  742. $width = 2560;
  743. $height = 1440;
  744. break;
  745. case '21:9':
  746. $width = 3024;
  747. $height = 1296;
  748. break;
  749. default:
  750. $width = 2048;
  751. $height = 2048;
  752. }
  753. // 获取动漫的角色信息(包含参考图)
  754. $anime = DB::table('mp_animes')
  755. ->where('id', $anime_id)
  756. ->select('roles', 'scenes')
  757. ->first();
  758. if (!$anime || !$anime->roles) {
  759. Utils::throwError('20003:未找到角色信息');
  760. }
  761. $roles = json_decode($anime->roles, true);
  762. if (!$roles) {
  763. Utils::throwError('20003:角色信息格式错误');
  764. }
  765. // 构建角色名称到参考图的映射
  766. $role_map = [];
  767. foreach ($roles as $role) {
  768. $role_name = getProp($role, 'role');
  769. $role_url = getProp($role, 'url');
  770. if (!empty($role_name) && !empty($role_url)) {
  771. $role_map[$role_name] = $role_url;
  772. }
  773. }
  774. $scenes = json_decode($anime->scenes, true);
  775. if (!$scenes) {
  776. Utils::throwError('20003:角色信息格式错误');
  777. }
  778. // 构建角色名称到参考图的映射
  779. $scene_map = [];
  780. foreach ($scenes as $scene) {
  781. $scene_name = getProp($scene, 'scene');
  782. $scene_url = getProp($scene, 'url');
  783. if (!empty($scene_name) && !empty($scene_url)) {
  784. $scene_map[$scene_name] = $scene_url;
  785. }
  786. }
  787. $segment_id = $segment->segment_id;
  788. $segment_content = $segment->segment_content;
  789. if (empty($segment_content)) {
  790. Utils::throwError('20003:分镜内容不存在,请填写内容后重试');
  791. }
  792. try {
  793. // 分镜剧本数组
  794. $update_data = [
  795. 'segment_content' => $segment_content,
  796. 'pic_task_status' => '生成中',
  797. 'updated_at' => date('Y-m-d H:i:s')
  798. ];
  799. // 解析分镜内容,提取画面描述作为主要提示词
  800. $prompt = $segment_content;
  801. $ref_img_urls = [];
  802. // 分镜场景
  803. $scene = getProp($data, 'scene');
  804. if ($scene) {
  805. $matched_scene = '';
  806. if (isset($scene_map[$scene])) {
  807. $img_index = count($ref_img_urls) + 1;
  808. $prompt = "场景:$scene(图$img_index)\n$prompt";
  809. $ref_img_urls[] = $scene_map[$scene];
  810. $matched_scene = $scene;
  811. }
  812. $update_data['scene'] = $scene;
  813. }
  814. // 分镜角色
  815. $characters = getProp($data, 'characters');
  816. if ($characters) {
  817. $update_data['characters'] = $characters;
  818. $characters = str_replace(['、', ',', ';'], ',', $characters);
  819. $characters = explode(',', $characters);
  820. // 从分镜内容中提取涉及的角色
  821. $segment_characters = $this->extractCharactersFromSegment($segment_content, array_keys($role_map), $characters);
  822. // 获取匹配角色的参考图
  823. foreach ($segment_characters as $character) {
  824. if (isset($role_map[$character])) {
  825. // 将$prompt中出现的角色替换成角色(图1、2、3,序号是$ref_img_urls的个数加1)
  826. $img_index = count($ref_img_urls) + 1;
  827. $prompt = str_replace($character, $character . '(图' . $img_index . ')', $prompt);
  828. $ref_img_urls[] = $role_map[$character];
  829. }
  830. }
  831. }
  832. // 如果没有找到匹配的角色参考图,不使用参考图
  833. if (empty($ref_img_urls)) {
  834. $ref_img_urls = [];
  835. }
  836. // 准备生成任务参数
  837. $params = [
  838. 'alias_segment_id' => $segment_id,
  839. 'prompt' => $prompt,
  840. 'ref_img_urls' => $ref_img_urls,
  841. 'width' => $width,
  842. 'height' => $height,
  843. ];
  844. // 调用AI图片生成服务
  845. $task = $this->aiImageGenerationService->createImageGenerationTask($params);
  846. $task_id = $task->id;
  847. if (!$task_id) {
  848. Utils::throwError('20003:创建生成任务失败!');
  849. }
  850. $update_data['pic_task_id'] = $task_id;
  851. // 更新分镜剧本信息
  852. $this->handleSegmentContent($update_data);
  853. $boolen = DB::table('mp_episode_segments')
  854. ->where('segment_id', $segment_id)
  855. ->update($update_data);
  856. // 如果是第一集的首帧图片,则存入待更新数组
  857. if ($boolen && (int)getProp($segment, 'episode_number') === 1 && (int)getProp($segment, 'segment_number') === 1) {
  858. Redis::sadd('anime_first_frame_urls', $segment_id);
  859. }
  860. } catch (\Exception $e) {
  861. $failed_segments[] = [
  862. 'segment_id' => $segment_id,
  863. 'error' => $e->getMessage()
  864. ];
  865. }
  866. $img_url = $this->loopGetPicTaskResult($task_id);
  867. if (!$img_url) Utils::throwError('20003:图片生成中,请稍后刷新');
  868. return $img_url;
  869. }
  870. public function addSegment($data) {
  871. $prev_segment_id = getProp($data, 'prev_segment_id');
  872. $segment = DB::table('mp_episode_segments')->where('segment_id', $prev_segment_id)->first();
  873. $anime_id = getProp($segment, 'anime_id');
  874. $episode_id = getProp($segment, 'episode_id');
  875. $episode_number = getProp($segment, 'episode_number');
  876. $segment_number = getProp($segment, 'segment_number');
  877. $segment_content = getProp($data, 'segment_content');
  878. $img_url = getProp($data, 'img_url');
  879. $video_url = getProp($data, 'video_url');
  880. $ratio = getProp($data, 'ratio');
  881. if (!in_array($ratio, ['1:1', '4:3', '3:2', '16:9', '21:9'])) {
  882. Utils::throwError('1002:画面比例选择不正确,只支持1:1、4:3、3:2、16:9、21:9');
  883. }
  884. if (!$anime_id || !$episode_id) {
  885. Utils::throwError('1002:请选择分镜');
  886. }
  887. // 根据画面比例给宽高赋值
  888. switch ($ratio) {
  889. case '1:1':
  890. $width = 2048;
  891. $height = 2048;
  892. break;
  893. case '4:3':
  894. $width = 2304;
  895. $height = 1728;
  896. break;
  897. case '3:2':
  898. $width = 2496;
  899. $height = 1664;
  900. break;
  901. case '16:9':
  902. $width = 2560;
  903. $height = 1440;
  904. break;
  905. case '21:9':
  906. $width = 3024;
  907. $height = 1296;
  908. break;
  909. default:
  910. $width = 2048;
  911. $height = 2048;
  912. }
  913. // 获取动漫的角色信息(包含参考图)
  914. $anime = DB::table('mp_animes')
  915. ->where('id', $anime_id)
  916. ->select('roles', 'scenes')
  917. ->first();
  918. if (!$anime || !$anime->roles) {
  919. Utils::throwError('20003:未找到角色信息');
  920. }
  921. $roles = json_decode($anime->roles, true);
  922. if (!$roles) {
  923. Utils::throwError('20003:角色信息格式错误');
  924. }
  925. // 构建角色名称到参考图的映射
  926. $role_map = [];
  927. foreach ($roles as $role) {
  928. $role_name = getProp($role, 'role');
  929. $role_url = getProp($role, 'url');
  930. if (!empty($role_name) && !empty($role_url)) {
  931. $role_map[$role_name] = $role_url;
  932. }
  933. }
  934. $scenes = json_decode($anime->scenes, true);
  935. if (!$scenes) {
  936. Utils::throwError('20003:角色信息格式错误');
  937. }
  938. // 构建角色名称到参考图的映射
  939. $scene_map = [];
  940. foreach ($scenes as $scene) {
  941. $scene_name = getProp($scene, 'scene');
  942. $scene_url = getProp($scene, 'url');
  943. if (!empty($scene_name) && !empty($scene_url)) {
  944. $scene_map[$scene_name] = $scene_url;
  945. }
  946. }
  947. $segment_id = $segment->segment_id;
  948. $segment_content = $segment->segment_content;
  949. if (empty($segment_content)) {
  950. Utils::throwError('20003:分镜内容不存在,请填写内容后重试');
  951. }
  952. try {
  953. DB::beginTransaction();
  954. $new_segment_number = $segment_number + 1;
  955. $segment_id = date('YmdHis') . mt_rand(1000, 9999) . str_pad($new_segment_number, 3, "0", STR_PAD_LEFT);
  956. // 分镜剧本数组
  957. $insert_data = [
  958. 'anime_id' => $anime_id,
  959. 'episode_id' => $episode_id,
  960. 'episode_number' => $episode_number,
  961. 'act_number' => getProp($segment, 'act_number'),
  962. 'act_title' => getProp($segment, 'act_title'),
  963. 'segment_id' => $segment_id,
  964. 'segment_number' => $new_segment_number,
  965. 'segment_content' => $segment_content,
  966. 'img_url' => $img_url,
  967. 'video_url' => $video_url,
  968. 'created_at' => date('Y-m-d H:i:s'),
  969. 'updated_at' => date('Y-m-d H:i:s')
  970. ];
  971. // 更新分镜剧本信息
  972. $this->handleSegmentContent($insert_data);
  973. // 如果没有上传图片则使用当前描述进行生成
  974. if (!$img_url) {
  975. // 解析分镜内容,提取画面描述作为主要提示词
  976. $prompt = $segment_content;
  977. $ref_img_urls = [];
  978. // 分镜场景
  979. $scene = getProp($insert_data, 'scene');
  980. if ($scene) {
  981. $matched_scene = '';
  982. if (isset($scene_map[$scene])) {
  983. $img_index = count($ref_img_urls) + 1;
  984. $prompt = "场景:$scene(图$img_index)\n$prompt";
  985. $ref_img_urls[] = $scene_map[$scene];
  986. $matched_scene = $scene;
  987. }
  988. $update_data['scene'] = $scene;
  989. }
  990. // 分镜角色
  991. $characters = getProp($insert_data, 'characters');
  992. if ($characters) {
  993. $update_data['characters'] = $characters;
  994. $characters = str_replace(['、', ',', ';'], ',', $characters);
  995. $characters = explode(',', $characters);
  996. // 从分镜内容中提取涉及的角色
  997. $segment_characters = $this->extractCharactersFromSegment($segment_content, array_keys($role_map), $characters);
  998. // 获取匹配角色的参考图
  999. foreach ($segment_characters as $character) {
  1000. if (isset($role_map[$character])) {
  1001. // 将$prompt中出现的角色替换成角色(图1、2、3,序号是$ref_img_urls的个数加1)
  1002. $img_index = count($ref_img_urls) + 1;
  1003. $prompt = str_replace($character, $character . '(图' . $img_index . ')', $prompt);
  1004. $ref_img_urls[] = $role_map[$character];
  1005. }
  1006. }
  1007. }
  1008. // 如果没有找到匹配的角色参考图,不使用参考图
  1009. if (empty($ref_img_urls)) {
  1010. $ref_img_urls = [];
  1011. }
  1012. // 准备生成任务参数
  1013. $params = [
  1014. 'alias_segment_id' => $segment_id,
  1015. 'prompt' => $prompt,
  1016. 'ref_img_urls' => $ref_img_urls,
  1017. 'width' => $width,
  1018. 'height' => $height,
  1019. ];
  1020. // 调用AI图片生成服务
  1021. $task = $this->aiImageGenerationService->createImageGenerationTask($params);
  1022. $task_id = $task->id;
  1023. // $task_id = 'whatever';
  1024. if (!$task_id) {
  1025. Utils::throwError('20003:创建生成任务失败!');
  1026. }
  1027. $insert_data['pic_task_status'] = '生成中';
  1028. $insert_data['pic_task_id'] = $task_id;
  1029. }
  1030. // 先更新序号
  1031. DB::table('mp_episode_segments')->where('anime_id', $anime_id)->where('episode_id', $episode_id)->where('segment_number', '>', $segment_number)->increment('segment_number');
  1032. $boolen = DB::table('mp_episode_segments')->insert($insert_data);
  1033. if (!$boolen) {
  1034. Utils::throwError('20003:新增分镜失败!');
  1035. }
  1036. // 如果是第一集的首帧图片,则存入待更新数组
  1037. if ($boolen && (int)$episode_number === 1 && (int)$new_segment_number === 1) {
  1038. Redis::sadd('anime_first_frame_urls', $segment_id);
  1039. }
  1040. } catch (\Exception $e) {
  1041. DB::rollBack();
  1042. Utils::throwError('20003:'.$e->getMessage());
  1043. }
  1044. DB::commit();
  1045. if (isset($task_id)) $img_url = $this->loopGetPicTaskResult($task_id);
  1046. if (!$img_url) Utils::throwError('20003:图片生成中,请稍后刷新');
  1047. $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first();
  1048. $segment = $segment ? (array)$segment : [];
  1049. return $segment;
  1050. }
  1051. public function copySegment($data) {
  1052. $segment_id = getProp($data, 'segment_id');
  1053. $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first();
  1054. if (!$segment) Utils::throwError('20003:请选择分镜');
  1055. $anime_id = getProp($segment, 'anime_id');
  1056. $episode_id = getProp($segment, 'episode_id');
  1057. $episode_number = getProp($segment, 'episode_number');
  1058. $segment_number = getProp($segment, 'segment_number');
  1059. $new_segment_id = 0;
  1060. try {
  1061. DB::beginTransaction();
  1062. $segment = (array)$segment;
  1063. unset($segment['id']);
  1064. $segment['segment_number'] += 1;
  1065. $segment['segment_id'] = date('YmdHis') . mt_rand(1000, 9999) . str_pad($segment['segment_number'], 3, "0", STR_PAD_LEFT);
  1066. $segment['pic_task_id'] = $segment['pic_task_status'] = $segment['video_task_id'] = $segment['video_task_status'] = '';
  1067. $segment['created_at'] = $segment['updated_at'] = date('Y-m-d H:i:s');
  1068. // 更新其他分镜序号
  1069. DB::table('mp_episode_segments')->where('anime_id', $anime_id)->where('episode_id', $episode_id)->where('segment_number', '>', $segment_number)->increment('segment_number');
  1070. // 复制分镜
  1071. $new_segment_id = DB::table('mp_episode_segments')->insertGetId($segment);
  1072. if (!$new_segment_id) {
  1073. Utils::throwError('20003:复制分镜失败');
  1074. }
  1075. }catch (\Exception $e) {
  1076. DB::rollBack();
  1077. Utils::throwError('20003:'.$e->getMessage());
  1078. }
  1079. DB::commit();
  1080. $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first();
  1081. $segment = $segment ? (array)$segment : [];
  1082. return $segment;
  1083. }
  1084. public function moveSegment($data) {
  1085. $segment_id = getProp($data, 'segment_id');
  1086. $target_segment_id = getProp($data, 'target_segment_id');
  1087. // 获取源分镜信息
  1088. $source_segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first();
  1089. if (!$source_segment) Utils::throwError('20003:请选择分镜');
  1090. // 获取目标分镜信息
  1091. $target_segment = DB::table('mp_episode_segments')->where('segment_id', $target_segment_id)->first();
  1092. if (!$target_segment) Utils::throwError('20003:目标分镜不存在');
  1093. $anime_id = getProp($source_segment, 'anime_id');
  1094. $episode_id = getProp($source_segment, 'episode_id');
  1095. $episode_number = getProp($source_segment, 'episode_number');
  1096. $source_segment_number = getProp($source_segment, 'segment_number');
  1097. $target_segment_number = getProp($target_segment, 'segment_number');
  1098. // 验证源分镜和目标分镜必须属于同一个动漫的同一集
  1099. $target_anime_id = getProp($target_segment, 'anime_id');
  1100. $target_episode_number = getProp($target_segment, 'episode_number');
  1101. if ($anime_id != $target_anime_id || $episode_number != $target_episode_number) {
  1102. Utils::throwError('20003:只能在同一动漫的同一集内移动分镜');
  1103. }
  1104. // 如果源分镜和目标分镜相同,无需移动
  1105. if ($source_segment_number == $target_segment_number) {
  1106. return true;
  1107. }
  1108. try {
  1109. DB::beginTransaction();
  1110. if ($source_segment_number < $target_segment_number) {
  1111. // 向后移动:源分镜移动到目标分镜之后
  1112. // 1. 将源分镜和目标分镜之间的所有分镜序号减1
  1113. DB::table('mp_episode_segments')
  1114. ->where('anime_id', $anime_id)
  1115. ->where('episode_id', $episode_id)
  1116. ->where('segment_number', '>', $source_segment_number)
  1117. ->where('segment_number', '<=', $target_segment_number)
  1118. ->decrement('segment_number');
  1119. // 2. 将源分镜移动到目标分镜之后
  1120. $new_segment_number = $target_segment_number;
  1121. } else {
  1122. // 向前移动:源分镜移动到目标分镜之后
  1123. // 1. 将目标分镜之后到源分镜之前的所有分镜序号加1
  1124. DB::table('mp_episode_segments')
  1125. ->where('anime_id', $anime_id)
  1126. ->where('episode_id', $episode_id)
  1127. ->where('segment_number', '>', $target_segment_number)
  1128. ->where('segment_number', '<', $source_segment_number)
  1129. ->increment('segment_number');
  1130. // 2. 将源分镜移动到目标分镜之后
  1131. $new_segment_number = $target_segment_number + 1;
  1132. }
  1133. // 3. 更新源分镜的序号
  1134. $update_result = DB::table('mp_episode_segments')
  1135. ->where('segment_id', $segment_id)
  1136. ->update([
  1137. 'segment_number' => $new_segment_number,
  1138. 'updated_at' => date('Y-m-d H:i:s')
  1139. ]);
  1140. if (!$update_result) {
  1141. Utils::throwError('20003:更新分镜序号失败');
  1142. }
  1143. DB::commit();
  1144. return true;
  1145. } catch (\Exception $e) {
  1146. DB::rollBack();
  1147. Utils::throwError('20003:' . $e->getMessage());
  1148. }
  1149. }
  1150. public function delSegment($data) {
  1151. $segment_id = getProp($data, 'segment_id');
  1152. $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first();
  1153. if (!$segment) Utils::throwError('20003:请选择分镜');
  1154. $anime_id = getProp($segment, 'anime_id');
  1155. $episode_id = getProp($segment, 'episode_id');
  1156. $episode_number = getProp($segment, 'episode_number');
  1157. $segment_number = getProp($segment, 'segment_number');
  1158. try {
  1159. DB::beginTransaction();
  1160. // 删除分镜
  1161. $boolen = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->delete();
  1162. if (!$boolen) {
  1163. Utils::throwError('20003:删除分镜失败');
  1164. }
  1165. // 更新其他分镜序号
  1166. DB::table('mp_episode_segments')->where('anime_id', $anime_id)->where('episode_id', $episode_id)->where('segment_number', '>', $segment_number)->decrement('segment_number');
  1167. }catch (\Exception $e) {
  1168. DB::rollBack();
  1169. Utils::throwError('20003:'.$e->getMessage());
  1170. }
  1171. DB::commit();
  1172. return true;
  1173. }
  1174. public function segmentHistory($data) {
  1175. $segment_id = getProp($data, 'segment_id');
  1176. // 获取历史图片
  1177. $pics = [];
  1178. $pic_history = DB::table('mp_generate_pic_tasks')->where('alias_segment_id', $segment_id)->where('status', 'success')->orderBy('created_at', 'desc')->get();
  1179. foreach($pic_history as $task) {
  1180. $result_url = getProp($task, 'result_url');
  1181. if (is_json($result_url)) {
  1182. $pics = array_merge($pics, json_decode($result_url, true));
  1183. }else {
  1184. $pics[] = $result_url;
  1185. }
  1186. }
  1187. // 获取历史视频
  1188. $videos = [];
  1189. $video_history = DB::table('mp_generate_video_tasks')->where('alias_segment_id', $segment_id)->where('status', 'success')->orderBy('created_at', 'desc')->get();
  1190. foreach($video_history as $task) {
  1191. $result_url = getProp($task, 'result_url');
  1192. if (is_json($result_url)) {
  1193. $videos = array_merge($pics, json_decode($result_url, true));
  1194. }else {
  1195. $videos[] = $result_url;
  1196. }
  1197. }
  1198. return compact('pics', 'videos');
  1199. }
  1200. // 轮训获取图片任务结果
  1201. private function loopGetPicTaskResult($task_id) {
  1202. $start_count = 0;
  1203. $img_url = '';
  1204. while ($start_count <= 30) {
  1205. sleep(3);
  1206. $task = MpGeneratePicTask::where('id', $task_id)->first();
  1207. $statusInfo = $this->aiImageGenerationService->queryTaskStatus($task);
  1208. if ($statusInfo['status'] === 'success') {
  1209. $task->updateStatus('success', [
  1210. 'result_url' => $statusInfo['result_url'],
  1211. 'result_json' => $statusInfo['result_json'] ?? []
  1212. ]);
  1213. // 同步调整分镜图片状态和结果
  1214. if (getProp($task, 'alias_segment_id')) {
  1215. DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([
  1216. 'img_url' => $statusInfo['result_url'][0],
  1217. 'pic_task_status' => '已完成',
  1218. ]);
  1219. }
  1220. $img_url = $statusInfo['result_url'][0];
  1221. break;
  1222. } elseif ($statusInfo['status'] === 'failed') {
  1223. $task->updateStatus('failed', [
  1224. 'error_message' => $statusInfo['error_message'],
  1225. 'result_json' => $statusInfo['result_json'] ?? []
  1226. ]);
  1227. if (getProp($task, 'alias_segment_id')) {
  1228. DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([
  1229. 'pic_task_status' => '失败',
  1230. ]);
  1231. }
  1232. break;
  1233. }
  1234. $start_count+=3;
  1235. }
  1236. return $img_url;
  1237. }
  1238. /**
  1239. * 从分镜内容中提取涉及的角色
  1240. */
  1241. private function extractCharactersFromSegment($segment_content, $available_characters, $roles) {
  1242. $found_characters = [];
  1243. foreach ($available_characters as $character) {
  1244. // 检查角色名称是否在分镜内容中出现
  1245. if (strpos($segment_content, $character) !== false) {
  1246. $found_characters[] = $character;
  1247. }
  1248. }
  1249. if (!$found_characters) {
  1250. foreach ($roles as $character) {
  1251. // 检查角色名称是否在分镜内容中出现
  1252. if (strpos($segment_content, $character) !== false) {
  1253. $found_characters[] = $character;
  1254. }
  1255. }
  1256. }
  1257. return array_unique($found_characters);
  1258. }
  1259. // 从分镜剧本中提取关键字段
  1260. private function handleSegmentContent(&$data) {
  1261. $segmentContent = getProp($data, 'segment_content');
  1262. // 解析分镜详细信息
  1263. $segmentData = [
  1264. 'description' => '',
  1265. 'composition' => '',
  1266. 'camera_movement' => '',
  1267. 'voice_actor' => '',
  1268. 'dialogue' => '',
  1269. 'frame_type' => '',
  1270. 'scene' => '', // 新增:场景
  1271. 'characters' => '', // 新增:出镜角色
  1272. 'tail_frame' => '', // 新增:尾帧描述
  1273. ];
  1274. // 提取各个字段 - 兼容中文冒号和英文冒号,支持多种表达方式
  1275. if (preg_match('/(?:画面描述|镜头描述|场景描述)[::]\s*([^\n]+)/u', $segmentContent, $descMatch)) {
  1276. $segmentData['description'] = trim($descMatch[1]);
  1277. $data['description'] = trim($descMatch[1]);
  1278. }
  1279. if (preg_match('/(?:构图设计|构图|镜头构图)[::]\s*([^\n]+)/u', $segmentContent, $compMatch)) {
  1280. $segmentData['composition'] = trim($compMatch[1]);
  1281. $data['composition'] = trim($compMatch[1]);
  1282. }
  1283. if (preg_match('/(?:运镜调度|运镜|镜头运动|摄影机运动)[::]\s*([^\n]+)/u', $segmentContent, $cameraMatch)) {
  1284. $segmentData['camera_movement'] = trim($cameraMatch[1]);
  1285. $data['camera_movement'] = trim($cameraMatch[1]);
  1286. }
  1287. if (preg_match('/(?:配音角色|配音|角色|声优)[::]\s*([^\n]+)/u', $segmentContent, $voiceMatch)) {
  1288. $segmentData['voice_actor'] = trim($voiceMatch[1]);
  1289. $data['voice_actor'] = trim($voiceMatch[1]);
  1290. }
  1291. if (preg_match('/(?:台词内容|台词|对白|对话)[::]\s*([^\n]+)/u', $segmentContent, $dialogueMatch)) {
  1292. $segmentData['dialogue'] = trim($dialogueMatch[1]);
  1293. $data['dialogue'] = trim($dialogueMatch[1]);
  1294. }
  1295. if (preg_match('/(?:画面类型|镜头类型|类型)[::]\s*([^\n]+)/u', $segmentContent, $frameMatch)) {
  1296. $segmentData['frame_type'] = trim($frameMatch[1]);
  1297. $data['frame_type'] = trim($frameMatch[1]);
  1298. }
  1299. // 新增:场景字段
  1300. if (preg_match('/(?:场景|拍摄场景|背景场景|环境)[::]\s*([^\n]+)/u', $segmentContent, $sceneMatch)) {
  1301. $segmentData['scene'] = trim($sceneMatch[1]);
  1302. $data['scene'] = trim($sceneMatch[1]);
  1303. }
  1304. // 新增:出镜角色字段
  1305. if (preg_match('/(?:出镜角色|角色出镜|登场角色|人物)[::]\s*([^\n]+)/u', $segmentContent, $charactersMatch)) {
  1306. $segmentData['characters'] = trim($charactersMatch[1]);
  1307. $data['characters'] = trim($charactersMatch[1]);
  1308. }
  1309. // 新增:尾帧描述字段
  1310. if (preg_match('/(?:尾帧描述|尾帧|结束帧|最后一帧|结尾画面|结束画面)[::]\s*([^\n]+)/u', $segmentContent, $tailFrameMatch)) {
  1311. $segmentData['tail_frame'] = trim($tailFrameMatch[1]);
  1312. $data['tail_frame'] = trim($tailFrameMatch[1]);
  1313. }
  1314. return $segmentData;
  1315. }
  1316. public function createSegmentVideoTask($data) {
  1317. $segment_id = getProp($data, 'segment_id');
  1318. $tail_frame = getProp($data, 'tail_frame');
  1319. if (!$segment_id) {
  1320. Utils::throwError('1002:分镜ID不能为空');
  1321. }
  1322. // 获取分镜信息
  1323. $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first();
  1324. if (!$segment) {
  1325. Utils::throwError('20003:分镜不存在');
  1326. }
  1327. $segment = (array)$segment;
  1328. // 获取分镜内容
  1329. $segmentContent = getProp($segment, 'segment_content', '');
  1330. // 如果没有传入尾帧描述,使用分镜表中的尾帧描述
  1331. if (!$tail_frame) {
  1332. $tail_frame = getProp($segment, 'tail_frame', '');
  1333. }
  1334. // 构建完整的提示词
  1335. $fullPrompt = $segmentContent;
  1336. if ($tail_frame) {
  1337. $fullPrompt .= "\n尾帧描述:$tail_frame";
  1338. }
  1339. // 智能选择视频时长
  1340. // $videoDuration = $this->calculateOptimalVideoDuration($segmentContent, $tail_frame);
  1341. $videoDuration = -1;
  1342. // 构建视频生成参数
  1343. $videoParams = [
  1344. 'model' => 'doubao-seedance-1-0-pro-250528',
  1345. 'alias_segment_id' => $segment_id,
  1346. 'prompt' => $fullPrompt,
  1347. 'video_duration' => $videoDuration,
  1348. 'video_resolution' => '720P',
  1349. 'seed' => -1,
  1350. 'ratio' => '16:9',
  1351. 'generate_audio' => false,
  1352. 'draft' => false,
  1353. 'watermark' => false,
  1354. 'camera_fixed' => false,
  1355. 'callback_url' => 'http://mpaudio.yqsd.cn/api/video/seedanceCallback'
  1356. ];
  1357. // 如果分镜有图片,作为首帧
  1358. if (getProp($segment, 'img_url')) {
  1359. $videoParams['first_frame_url'] = getProp($segment, 'img_url');
  1360. }
  1361. // 构建content数组
  1362. $videoParams['content'] = [
  1363. [
  1364. 'type' => 'text',
  1365. 'text' => $videoParams['prompt'],
  1366. ]
  1367. ];
  1368. // 如果有首帧图片,添加到content中
  1369. if (isset($videoParams['first_frame_url'])) {
  1370. $videoParams['content'][] = [
  1371. 'type' => 'image_url',
  1372. 'image_url' => [
  1373. 'url' => $videoParams['first_frame_url'],
  1374. ],
  1375. 'role' => 'first_frame',
  1376. ];
  1377. }
  1378. // dd($videoParams);
  1379. // 创建视频生成任务
  1380. $task = $this->aiVideoGenerationService->createSeedanceTask($videoParams);
  1381. // 更新分镜表的视频任务信息
  1382. DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
  1383. 'video_task_id' => $task->id,
  1384. 'video_task_status' => '生成中',
  1385. 'updated_at' => date('Y-m-d H:i:s')
  1386. ]);
  1387. return [
  1388. 'task_id' => $task->id,
  1389. 'status' => $task->status,
  1390. 'segment_id' => $segment_id,
  1391. 'video_duration' => $videoDuration
  1392. ];
  1393. }
  1394. /**
  1395. * 根据提示词内容智能计算最优视频时长
  1396. *
  1397. * @param string $segmentContent 分镜内容
  1398. * @param string $tailFrame 尾帧描述
  1399. * @return int 视频时长(2-12秒)
  1400. */
  1401. public function calculateOptimalVideoDuration($segmentContent, $tailFrame = '') {
  1402. // 合并所有文本内容
  1403. $fullText = trim($segmentContent . ' ' . $tailFrame);
  1404. // 如果没有内容,返回默认时长
  1405. if (empty($fullText)) {
  1406. return 5;
  1407. }
  1408. // 计算文本长度(中文字符按2个字符计算)
  1409. $textLength = mb_strlen($fullText, 'UTF-8');
  1410. $chineseCharCount = preg_match_all('/[\x{4e00}-\x{9fff}]/u', $fullText);
  1411. $adjustedLength = $textLength + $chineseCharCount; // 中文字符权重更高
  1412. // 分析内容复杂度
  1413. $complexityScore = $this->analyzeContentComplexity($fullText);
  1414. // 基础时长计算(根据文本长度)
  1415. $baseDuration = 3; // 默认2秒
  1416. // if ($adjustedLength <= 20) {
  1417. // $baseDuration = 3;
  1418. // } elseif ($adjustedLength <= 40) {
  1419. // $baseDuration = 4;
  1420. // } elseif ($adjustedLength <= 60) {
  1421. // $baseDuration = 5;
  1422. // } elseif ($adjustedLength <= 80) {
  1423. // $baseDuration = 6;
  1424. // } elseif ($adjustedLength <= 100) {
  1425. // $baseDuration = 7;
  1426. // } elseif ($adjustedLength <= 120) {
  1427. // $baseDuration = 8;
  1428. // } else {
  1429. // $baseDuration = 9;
  1430. // }
  1431. // 根据内容复杂度调整时长
  1432. $finalDuration = $baseDuration + $complexityScore;
  1433. // 确保时长在2-12秒范围内
  1434. return max(2, min(12, $finalDuration));
  1435. }
  1436. /**
  1437. * 分析内容复杂度,返回时长调整值
  1438. *
  1439. * @param string $text 文本内容
  1440. * @return int 调整值(-2到+3)
  1441. */
  1442. private function analyzeContentComplexity($text) {
  1443. $score = 0;
  1444. // 动作关键词(需要更长时间展现)
  1445. $actionKeywords = [
  1446. '跑步', '奔跑', '飞行', '跳跃', '战斗', '打斗', '追逐', '逃跑', '舞蹈', '表演',
  1447. '变化', '变身', '爆炸', '崩塌', '建造', '制作', '烹饪', '绘画', '写字',
  1448. '移动', '旋转', '翻滚', '攀爬', '游泳', '潜水', '滑行', '飞翔'
  1449. ];
  1450. // 静态关键词(可以用较短时间)
  1451. $staticKeywords = [
  1452. '站立', '坐着', '躺着', '静止', '凝视', '思考', '沉思', '观察', '等待',
  1453. '睡觉', '休息', '静坐', '冥想', '阅读', '听音乐'
  1454. ];
  1455. // 复杂场景关键词(需要更长时间)
  1456. $complexSceneKeywords = [
  1457. '人群', '聚会', '会议', '战争', '庆典', '仪式', '表演', '比赛', '竞技',
  1458. '多人', '群体', '团队', '合作', '互动', '对话', '争论', '讨论'
  1459. ];
  1460. // 情感关键词(需要适中时间表现)
  1461. $emotionKeywords = [
  1462. '哭泣', '笑容', '愤怒', '惊讶', '恐惧', '悲伤', '喜悦', '兴奋', '紧张',
  1463. '感动', '震惊', '困惑', '失望', '希望', '绝望', '温柔', '严肃'
  1464. ];
  1465. // 检查动作关键词
  1466. foreach ($actionKeywords as $keyword) {
  1467. if (strpos($text, $keyword) !== false) {
  1468. $score += 1;
  1469. break; // 避免重复加分
  1470. }
  1471. }
  1472. // 检查静态关键词
  1473. foreach ($staticKeywords as $keyword) {
  1474. if (strpos($text, $keyword) !== false) {
  1475. $score -= 1;
  1476. break;
  1477. }
  1478. }
  1479. // 检查复杂场景关键词
  1480. foreach ($complexSceneKeywords as $keyword) {
  1481. if (strpos($text, $keyword) !== false) {
  1482. $score += 1;
  1483. break;
  1484. }
  1485. }
  1486. // 检查情感关键词
  1487. foreach ($emotionKeywords as $keyword) {
  1488. if (strpos($text, $keyword) !== false) {
  1489. $score += 1;
  1490. break;
  1491. }
  1492. }
  1493. // 检查时间相关词汇
  1494. if (preg_match('/缓慢|慢慢|渐渐|逐渐|慢动作/', $text)) {
  1495. $score += 1;
  1496. }
  1497. if (preg_match('/快速|迅速|急速|瞬间|立刻|马上/', $text)) {
  1498. $score -= 1;
  1499. }
  1500. // 检查转场词汇
  1501. if (preg_match('/然后|接着|随后|紧接着|同时|与此同时/', $text)) {
  1502. $score += 1;
  1503. }
  1504. // 检查环境描述(复杂环境需要更多时间)
  1505. if (preg_match('/风景|景色|环境|背景|氛围|光线|阴影|色彩|细节/', $text)) {
  1506. $score += 1;
  1507. }
  1508. // 检查对话内容(对话需要更多时间)
  1509. if (preg_match('/说|讲|告诉|回答|询问|问|对话|交谈/', $text)) {
  1510. $score += 1;
  1511. }
  1512. // 限制调整范围
  1513. return max(-1, min(4, $score));
  1514. }
  1515. }