|
|
@@ -1322,6 +1322,39 @@ class AnimeService
|
|
|
Utils::throwError('20003:新增分镜失败!');
|
|
|
}
|
|
|
|
|
|
+ // 如果有视频URL且有对话内容,创建视频配音合成任务
|
|
|
+ if ($video_url && !empty($insert_data['dialogue'])) {
|
|
|
+ $now = date('Y-m-d H:i:s');
|
|
|
+ $generate_json = [
|
|
|
+ 'text' => $insert_data['dialogue'],
|
|
|
+ 'role' => getProp($insert_data, 'voice_actor'),
|
|
|
+ 'voice_type' => getProp($insert_data, 'voice_type'),
|
|
|
+ 'voice_name' => getProp($insert_data, 'voice_name'),
|
|
|
+ 'emotion' => getProp($insert_data, 'emotion'),
|
|
|
+ 'emotion_type' => getProp($insert_data, 'emotion_type'),
|
|
|
+ 'gender' => getProp($insert_data, 'gender'),
|
|
|
+ 'speed_ratio' => getProp($insert_data, 'speed_ratio', 0),
|
|
|
+ 'loudness_ratio' => getProp($insert_data, 'loudness_ratio', 0),
|
|
|
+ 'emotion_scale' => getProp($insert_data, 'emotion_scale', 0),
|
|
|
+ 'pitch' => getProp($insert_data, 'pitch', 0),
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 插入视频配音合成任务
|
|
|
+ $dubTaskId = DB::table('mp_dub_video_tasks')->insertGetId([
|
|
|
+ 'alias_segment_id' => $segment_id,
|
|
|
+ 'video_url' => $video_url,
|
|
|
+ 'generate_status' => '执行中',
|
|
|
+ 'dub_video_url' => '',
|
|
|
+ 'generate_json' => json_encode($generate_json, 256),
|
|
|
+ 'created_at' => $now,
|
|
|
+ 'updated_at' => $now,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (!$dubTaskId) {
|
|
|
+ Utils::throwError('20003:创建配音任务失败!');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 如果是第一集的首帧图片,则存入待更新数组
|
|
|
if ($boolen && (int)$episode_number === 1 && (int)$new_segment_number === 1) {
|
|
|
Redis::sadd('anime_first_frame_urls', $segment_id);
|
|
|
@@ -1645,7 +1678,7 @@ class AnimeService
|
|
|
$affected_segments = DB::table('mp_episode_segments')
|
|
|
->where('episode_id', $episode_id)
|
|
|
->where('voice_actor', $target_voice_actor)
|
|
|
- ->select('segment_id', 'video_url', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
|
|
|
+ ->select('segment_id', 'video_url', 'voice_actor', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
|
|
|
->get();
|
|
|
|
|
|
foreach ($affected_segments as $seg) {
|
|
|
@@ -1664,7 +1697,7 @@ class AnimeService
|
|
|
if (!isset($segments_data[$segment_id])) {
|
|
|
$segment = DB::table('mp_episode_segments')
|
|
|
->where('segment_id', $segment_id)
|
|
|
- ->select('segment_id', 'video_url', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
|
|
|
+ ->select('segment_id', 'video_url', 'voice_actor', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
|
|
|
->first();
|
|
|
|
|
|
if ($segment) {
|
|
|
@@ -1689,6 +1722,7 @@ class AnimeService
|
|
|
// 构建音频生成参数
|
|
|
$generate_json = [
|
|
|
'text' => getProp($segment, 'dialogue'),
|
|
|
+ 'role' => getProp($segment, 'voice_actor'),
|
|
|
'voice_type' => getProp($segment, 'voice_type'),
|
|
|
'voice_name' => getProp($segment, 'voice_name'),
|
|
|
'emotion' => getProp($segment, 'emotion'),
|
|
|
@@ -1700,12 +1734,12 @@ class AnimeService
|
|
|
'pitch' => getProp($segment, 'pitch', 0),
|
|
|
];
|
|
|
|
|
|
- // 插入音频合成任务
|
|
|
+ // 插入视频配音合成任务
|
|
|
$task_id = DB::table('mp_dub_video_tasks')->insertGetId([
|
|
|
'alias_segment_id' => $sid,
|
|
|
'video_url' => $video_url,
|
|
|
'generate_status' => '执行中',
|
|
|
- 'audio_url' => '',
|
|
|
+ 'dub_video_url' => '',
|
|
|
'generate_json' => json_encode($generate_json, 256),
|
|
|
'created_at' => $now,
|
|
|
'updated_at' => $now,
|
|
|
@@ -2025,7 +2059,7 @@ class AnimeService
|
|
|
// 从分镜剧本中提取关键字段
|
|
|
private function handleSegmentContent(&$data) {
|
|
|
$segmentContent = getProp($data, 'segment_content');
|
|
|
- // 解析分镜详细信息
|
|
|
+ // 解析分镜详细信息 - 与 Helpers.php 中 handleEpisodeContent 的 segmentData 结构保持一致
|
|
|
$segmentData = [
|
|
|
'description' => '',
|
|
|
'composition' => '',
|
|
|
@@ -2033,11 +2067,20 @@ class AnimeService
|
|
|
'voice_actor' => '',
|
|
|
'dialogue' => '',
|
|
|
'frame_type' => '',
|
|
|
- 'scene' => '', // 新增:场景
|
|
|
- 'characters' => '', // 新增:出镜角色
|
|
|
- 'tail_frame' => '', // 新增:尾帧描述
|
|
|
+ 'scene' => '', // 场景
|
|
|
+ 'characters' => '', // 出镜角色
|
|
|
+ 'tail_frame' => '', // 尾帧描述
|
|
|
+ 'emotion' => '中性', // 情感
|
|
|
+ 'gender' => '0', // 性别(0未知,1男,2女)
|
|
|
+ 'speed_ratio' => 0, // 语速
|
|
|
+ 'loudness_ratio' => 0, // 音量
|
|
|
+ 'emotion_scale' => 4, // 情感强度
|
|
|
+ 'pitch' => 0, // 音调
|
|
|
];
|
|
|
|
|
|
+ // 用于存储需要从 segment_content 中移除的匹配项
|
|
|
+ $replaceEmptyArr = [];
|
|
|
+
|
|
|
// 提取各个字段 - 兼容中文冒号和英文冒号,支持多种表达方式
|
|
|
if (preg_match('/(?:画面描述|镜头描述|场景描述)[::]\s*([^\n]+)/u', $segmentContent, $descMatch)) {
|
|
|
$segmentData['description'] = trim($descMatch[1]);
|
|
|
@@ -2063,22 +2106,151 @@ class AnimeService
|
|
|
$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]);
|
|
|
}
|
|
|
+
|
|
|
+ // 情感字段
|
|
|
+ if (preg_match('/(?:情感|情绪|感情)[::]\s*([^\n]+)/u', $segmentContent, $emotionMatch)) {
|
|
|
+ $replaceEmptyArr[] = trim($emotionMatch[0]);
|
|
|
+ $segmentData['emotion'] = trim($emotionMatch[1]);
|
|
|
+ $data['emotion'] = trim($emotionMatch[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 性别字段
|
|
|
+ if (preg_match('/(?:性别)[::]\s*([^\n]+)/u', $segmentContent, $genderMatch)) {
|
|
|
+ $replaceEmptyArr[] = trim($genderMatch[0]);
|
|
|
+ $genderStr = trim($genderMatch[1]);
|
|
|
+ if (strpos($genderStr, '男') !== false || $genderStr === '1') {
|
|
|
+ $segmentData['gender'] = '1';
|
|
|
+ $data['gender'] = '1';
|
|
|
+ } elseif (strpos($genderStr, '女') !== false || $genderStr === '2') {
|
|
|
+ $segmentData['gender'] = '2';
|
|
|
+ $data['gender'] = '2';
|
|
|
+ } else {
|
|
|
+ $segmentData['gender'] = '0';
|
|
|
+ $data['gender'] = '0';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 语速字段
|
|
|
+ if (preg_match('/(?:语速|说话速度)[::]\s*([-+]?[0-9]*\.?[0-9]+)/u', $segmentContent, $speedMatch)) {
|
|
|
+ $replaceEmptyArr[] = trim($speedMatch[0]);
|
|
|
+ $segmentData['speed_ratio'] = (float)trim($speedMatch[1]);
|
|
|
+ $data['speed_ratio'] = (float)trim($speedMatch[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 音量字段
|
|
|
+ if (preg_match('/(?:音量|声音大小)[::]\s*([-+]?[0-9]*\.?[0-9]+)/u', $segmentContent, $loudnessMatch)) {
|
|
|
+ $replaceEmptyArr[] = trim($loudnessMatch[0]);
|
|
|
+ $segmentData['loudness_ratio'] = (float)trim($loudnessMatch[1]);
|
|
|
+ $data['loudness_ratio'] = (float)trim($loudnessMatch[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 情感强度字段
|
|
|
+ if (preg_match('/(?:情感强度|情绪强度)[::]\s*([0-9]+)/u', $segmentContent, $scaleMatch)) {
|
|
|
+ $replaceEmptyArr[] = trim($scaleMatch[0]);
|
|
|
+ $segmentData['emotion_scale'] = (int)trim($scaleMatch[1]);
|
|
|
+ $data['emotion_scale'] = (int)trim($scaleMatch[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 音调字段
|
|
|
+ if (preg_match('/(?:音调|音高)[::]\s*([-+]?[0-9]+)/u', $segmentContent, $pitchMatch)) {
|
|
|
+ $replaceEmptyArr[] = trim($pitchMatch[0]);
|
|
|
+ $segmentData['pitch'] = (int)trim($pitchMatch[1]);
|
|
|
+ $data['pitch'] = (int)trim($pitchMatch[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从 segment_content 中移除已提取的配音参数字段
|
|
|
+ if (!empty($replaceEmptyArr)) {
|
|
|
+ $data['segment_content'] = str_replace($replaceEmptyArr, '', $segmentContent);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有配音角色,查询音色信息并验证情感
|
|
|
+ $voice_actor = $segmentData['voice_actor'];
|
|
|
+ if (!empty($voice_actor) && $voice_actor !== '旁白') {
|
|
|
+ // 从当前分镜所属的剧集或动漫中查找角色音色信息
|
|
|
+ $anime_id = getProp($data, 'anime_id');
|
|
|
+ $episode_id = getProp($data, 'episode_id');
|
|
|
+
|
|
|
+ // 优先从剧集的角色列表中查找
|
|
|
+ $roles = [];
|
|
|
+ if ($episode_id) {
|
|
|
+ $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first();
|
|
|
+ if ($episode && getProp($episode, 'roles')) {
|
|
|
+ $roles = json_decode(getProp($episode, 'roles'), true) ?: [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果剧集中没有,从动漫的角色列表中查找
|
|
|
+ if (empty($roles) && $anime_id) {
|
|
|
+ $anime = DB::table('mp_animes')->where('id', $anime_id)->first();
|
|
|
+ if ($anime && getProp($anime, 'roles')) {
|
|
|
+ $roles = json_decode(getProp($anime, 'roles'), true) ?: [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找匹配的角色音色信息
|
|
|
+ $timbre_info = null;
|
|
|
+ foreach ($roles as $role) {
|
|
|
+ if (getProp($role, 'role') === $voice_actor) {
|
|
|
+ $timbre_info = $role;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果找到音色信息,添加到数据中
|
|
|
+ if ($timbre_info) {
|
|
|
+ $voice_type = getProp($timbre_info, 'voice_type');
|
|
|
+ $voice_name = getProp($timbre_info, 'voice_name');
|
|
|
+ $voice_audio_url = getProp($timbre_info, 'voice_audio_url');
|
|
|
+
|
|
|
+ if ($voice_type) {
|
|
|
+ $data['voice_type'] = $voice_type;
|
|
|
+ $segmentData['voice_type'] = $voice_type;
|
|
|
+ }
|
|
|
+ if ($voice_name) {
|
|
|
+ $data['voice_name'] = $voice_name;
|
|
|
+ $segmentData['voice_name'] = $voice_name;
|
|
|
+ }
|
|
|
+ if ($voice_audio_url) {
|
|
|
+ $data['voice_audio_url'] = $voice_audio_url;
|
|
|
+ $segmentData['voice_audio_url'] = $voice_audio_url;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证情感是否在音色支持的情感列表中
|
|
|
+ if ($voice_type) {
|
|
|
+ $timbre_emotion = DB::table('mp_timbres')->where('timbre_type', $voice_type)->value('emotion');
|
|
|
+ if ($timbre_emotion) {
|
|
|
+ $timbre_emotion = explode(',', $timbre_emotion);
|
|
|
+ } else {
|
|
|
+ $timbre_emotion = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $emotion = getProp($segmentData, 'emotion', '中性');
|
|
|
+ if (!in_array($emotion, $timbre_emotion)) {
|
|
|
+ $emotion = '中性';
|
|
|
+ }
|
|
|
+ $emotion_list = DB::table('mp_emotion_list')->where('is_enabled', 1)->pluck('emotion_name', 'emotion_code')->toArray();
|
|
|
+ $emotion_list = array_flip($emotion_list);
|
|
|
+ $emotion_type = isset($emotion_list[$emotion]) ? $emotion_list[$emotion] : 'neutral';
|
|
|
+ $data['emotion_type'] = $emotion_type;
|
|
|
+ $segmentData['emotion_type'] = $emotion_type;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
return $segmentData;
|
|
|
}
|