Sfoglia il codice sorgente

1.新增创建视频配音合成任务的逻辑2.优化解析分镜内容的方法,以适配视频配音参数

lh 4 giorni fa
parent
commit
44efe1a761

+ 103 - 27
app/Http/Controllers/Anime/AnimeController.php

@@ -668,38 +668,114 @@ class AnimeController extends BaseController
                                     ]) ? now() : null
                                 ]);
                                 
-                                // 如果任务成功,更新分镜表
+                                // 如果任务成功,使用事务更新分镜表并创建配音任务
                                 if ($statusResult['status'] === 'success' && isset($statusResult['result_url'])) {
-                                    DB::table('mp_episode_segments')
-                                        ->where('segment_id', $segmentId)
-                                        ->update([
-                                            'video_url' => $statusResult['result_url'],
-                                            'video_task_status' => '已完成',
-                                            'last_frame_url' => $statusResult['last_frame_url'] ?? '',
-                                            'updated_at' => date('Y-m-d H:i:s')
-                                        ]);
-                                    
-                                    $completedTasks[] = $taskId;
-                                    
-                                    // 发送单个任务完成通知
-                                    echo "data: " . json_encode([
-                                        'type' => 'task_completed',
-                                        'data' => [
+                                    try {
+                                        DB::beginTransaction();
+                                        
+                                        $now = date('Y-m-d H:i:s');
+                                        // 更新分镜表
+                                        $updateResult = DB::table('mp_episode_segments')
+                                            ->where('segment_id', $segmentId)
+                                            ->update([
+                                                'video_url' => $statusResult['result_url'],
+                                                'video_task_status' => '已完成',
+                                                'last_frame_url' => $statusResult['last_frame_url'] ?? '',
+                                                'updated_at' => $now
+                                            ]);
+                                        
+                                        if (!$updateResult) {
+                                            if (!$dubTaskId) {
+                                                Utils::throwError('20003:更新分镜表失败');
+                                            }
+                                        }
+                                        
+                                        // 获取分镜信息用于创建配音任务
+                                        $segment = DB::table('mp_episode_segments')
+                                            ->where('segment_id', $segmentId)
+                                            ->select('voice_actor', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
+                                            ->first();
+                                        
+                                        // 如果分镜有对话内容,创建视频配音合成任务
+                                        $dubTaskId = null;
+                                        if ($segment && !empty($segment->dialogue)) {
+                                            $generate_json = [
+                                                'text' => $segment->dialogue,
+                                                'role' => $segment->voice_actor,
+                                                'voice_type' => $segment->voice_type,
+                                                'voice_name' => $segment->voice_name,
+                                                'emotion' => $segment->emotion,
+                                                'emotion_type' => $segment->emotion_type,
+                                                'gender' => $segment->gender,
+                                                'speed_ratio' => $segment->speed_ratio ?? 0,
+                                                'loudness_ratio' => $segment->loudness_ratio ?? 0,
+                                                'emotion_scale' => $segment->emotion_scale ?? 0,
+                                                'pitch' => $segment->pitch ?? 0,
+                                            ];
+                                            
+                                            // 插入视频配音合成任务
+                                            $dubTaskId = DB::table('mp_dub_video_tasks')->insertGetId([
+                                                'alias_segment_id' => $segmentId,
+                                                'video_url' => $statusResult['result_url'],
+                                                'generate_status' => '执行中',
+                                                'dub_video_url' => '',
+                                                'generate_json' => json_encode($generate_json, 256),
+                                                'created_at' => $now,
+                                                'updated_at' => $now,
+                                            ]);
+                                            
+                                            if (!$dubTaskId) {
+                                                Utils::throwError('20003:创建配音任务失败');
+                                            }
+                                        }
+                                        
+                                        DB::commit();
+                                        
+                                        $completedTasks[] = $taskId;
+                                        
+                                        // 发送单个任务完成通知
+                                        echo "data: " . json_encode([
+                                            'type' => 'task_completed',
+                                            'data' => [
+                                                'segment_id' => $segmentId,
+                                                'task_id' => $taskId,
+                                                'segment_number' => $segmentTask['segment_number'],
+                                                'status' => 'success',
+                                                'video_url' => $statusResult['result_url'],
+                                                'last_frame_url' => $statusResult['last_frame_url'] ?? '',
+                                                'completed_count' => count($completedTasks),
+                                                'total_count' => count($segmentTasks),
+                                                'has_dub_task' => $dubTaskId > 0
+                                            ]
+                                        ]) . "\n\n";
+                                        ob_flush();
+                                        flush();
+                                        
+                                    } catch (\Exception $e) {
+                                        DB::rollBack();
+                                        
+                                        dLog('anime')->error('视频任务处理失败', [
                                             'segment_id' => $segmentId,
                                             'task_id' => $taskId,
-                                            'segment_number' => $segmentTask['segment_number'],
-                                            'status' => 'success',
-                                            'video_url' => $statusResult['result_url'],
-                                            'last_frame_url' => $statusResult['last_frame_url'] ?? '',
-                                            'completed_count' => count($completedTasks),
-                                            'total_count' => count($segmentTasks)
-                                        ]
-                                    ]) . "\n\n";
-                                    ob_flush();
-                                    flush();
+                                            'error' => $e->getMessage()
+                                        ]);
+                                        
+                                        // 发送任务处理失败通知
+                                        echo "data: " . json_encode([
+                                            'type' => 'task_process_error',
+                                            'data' => [
+                                                'segment_id' => $segmentId,
+                                                'task_id' => $taskId,
+                                                'segment_number' => $segmentTask['segment_number'],
+                                                'error_message' => '任务处理失败: ' . $e->getMessage()
+                                            ]
+                                        ]) . "\n\n";
+                                        ob_flush();
+                                        flush();
+                                    }
                                     
                                 } elseif ($statusResult['status'] === 'failed') {
-                                    DB::table('mp_episode_segments')
+                                    $updateResult = DB::table('mp_episode_segments')
                                         ->where('segment_id', $segmentId)
                                         ->update([
                                             'video_task_status' => '失败',

+ 184 - 12
app/Services/Anime/AnimeService.php

@@ -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;
     }