9
0

2 Коммиты 44efe1a761 ... 3abba67b71

Автор SHA1 Сообщение Дата
  lh 3abba67b71 新增生成主体或场景图片接口 4 дней назад
  lh 05e2225e6f 定时任务脚本也新增对分镜表的更新和视频配音任务的创建 4 дней назад

+ 1 - 1
app/Console/Commands/CheckImageGenerationTasksCommand.php

@@ -32,7 +32,7 @@ class CheckImageGenerationTasksCommand extends Command
     {
         dLog('generate')->info('开始检查图片生成任务状态...');
 
-        // 执行40s
+        // 执行50s
         $time_start = time();
         try {
             $count = DB::table('mp_generate_pic_tasks')->where('status', 'processing')->count('id');

+ 16 - 7
app/Console/Commands/CheckVideoGenerationTasksCommand.php

@@ -4,6 +4,7 @@ namespace App\Console\Commands;
 
 use App\Services\AIGeneration\AIVideoGenerationService;
 use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
 
 class CheckVideoGenerationTasksCommand extends Command
 {
@@ -24,19 +25,27 @@ class CheckVideoGenerationTasksCommand extends Command
     /**
      * Execute the console command.
      *
+     * @param AIVideoGenerationService $videoGenerationService
      * @return int
      */
     public function handle(AIVideoGenerationService $videoGenerationService)
     {
-        $this->info('开始检查视频生成任务状态...');
-        
+        dLog('generate')->info('开始检查视频生成任务状态...');
+
+        // 执行50s
+        $time_start = time();
         try {
-            // 更新所有待处理的视频生成任务状态
-            $videoGenerationService->updatePendingTasks();
-            
-            $this->info('视频生成任务状态检查完成');
+            $count = DB::table('mp_generate_video_tasks')->where('status', 'processing')->count('id');
+            while ($count > 0) {
+                $time_diff = time() - $time_start;
+                sleep(3);
+                if ($time_diff > 50) break;
+                $videoGenerationService->updatePendingTasks();
+                $count = DB::table('mp_generate_video_tasks')->where('status', 'processing')->count('id');
+            }
+            dLog('generate')->info('视频任务状态检查完成');
         } catch (\Exception $e) {
-            $this->error('视频生成任务状态检查失败: ' . $e->getMessage());
+            dLog('generate')->error('视频任务状态检查失败: ' . $e->getMessage());
             return 1;
         }
 

+ 121 - 28
app/Http/Controllers/Anime/AnimeController.php

@@ -355,16 +355,87 @@ class AnimeController extends BaseController
                                 ]) ? now() : null
                             ]);
                             
-                            // 如果任务成功,更新分镜表
+                            // 如果任务成功,使用事务更新分镜表并创建配音任务
                             if ($statusResult['status'] === 'success' && isset($statusResult['result_url'])) {
-                                DB::table('mp_episode_segments')
-                                    ->where('video_task_id', $taskId)
-                                    ->update([
-                                        'video_url' => $statusResult['result_url'],
-                                        'video_task_status' => '已完成',
-                                        'last_frame_url' => $statusResult['last_frame_url'] ?? '',
-                                        'updated_at' => date('Y-m-d H:i:s')
+                                try {
+                                    DB::beginTransaction();
+                                    
+                                    $now = date('Y-m-d H:i:s');
+                                    
+                                    // 获取分镜ID
+                                    $segment = DB::table('mp_episode_segments')
+                                        ->where('video_task_id', $taskId)
+                                        ->first();
+                                    
+                                    if (!$segment) {
+                                        throw new \Exception('未找到对应的分镜记录');
+                                    }
+                                    
+                                    $segmentId = $segment->segment_id;
+                                    
+                                    // 更新分镜表
+                                    $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) {
+                                        Utils::throwError('20003:更新分镜表失败');
+                                    }
+                                    
+                                    // 获取分镜信息用于创建配音任务
+                                    $segmentInfo = 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 ($segmentInfo && !empty($segmentInfo->dialogue)) {
+                                        $generate_json = [
+                                            'text' => $segmentInfo->dialogue,
+                                            'role' => $segmentInfo->voice_actor,
+                                            'voice_type' => $segmentInfo->voice_type,
+                                            'voice_name' => $segmentInfo->voice_name,
+                                            'emotion' => $segmentInfo->emotion,
+                                            'emotion_type' => $segmentInfo->emotion_type,
+                                            'gender' => $segmentInfo->gender,
+                                            'speed_ratio' => $segmentInfo->speed_ratio ?? 0,
+                                            'loudness_ratio' => $segmentInfo->loudness_ratio ?? 0,
+                                            'emotion_scale' => $segmentInfo->emotion_scale ?? 0,
+                                            'pitch' => $segmentInfo->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();
+                                    
+                                } catch (\Exception $e) {
+                                    DB::rollBack();
+                                    
+                                    dLog('anime')->error('视频任务处理失败', [
+                                        'task_id' => $taskId,
+                                        'error' => $e->getMessage()
                                     ]);
+                                }
                             } elseif ($statusResult['status'] === 'failed') {
                                 DB::table('mp_episode_segments')
                                     ->where('video_task_id', $taskId)
@@ -384,7 +455,6 @@ class AnimeController extends BaseController
                             'status' => $task->status,
                             'result_url' => $task->result_url,
                             'error_message' => $task->error_message,
-                            'progress' => $this->getTaskProgress($task->status),
                             'elapsed_time' => time() - $startTime
                         ]
                     ]) . "\n\n";
@@ -444,24 +514,6 @@ class AnimeController extends BaseController
             'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
         ]);
     }
-    
-    /**
-     * 获取任务进度百分比
-     */
-    private function getTaskProgress($status) {
-        switch ($status) {
-            case \App\Models\MpGenerateVideoTask::STATUS_PENDING:
-                return 10;
-            case \App\Models\MpGenerateVideoTask::STATUS_PROCESSING:
-                return 50;
-            case \App\Models\MpGenerateVideoTask::STATUS_SUCCESS:
-                return 100;
-            case \App\Models\MpGenerateVideoTask::STATUS_FAILED:
-                return 0;
-            default:
-                return 0;
-        }
-    }
 
     /**
      * 批量生成分镜视频
@@ -809,7 +861,6 @@ class AnimeController extends BaseController
                             'task_id' => $taskId,
                             'segment_number' => $segmentTask['segment_number'],
                             'status' => $task->status,
-                            'progress' => $this->getTaskProgress($task->status),
                             'video_url' => $task->result_url,
                             'error_message' => $task->error_message
                         ];
@@ -973,4 +1024,46 @@ class AnimeController extends BaseController
         ]);
     }
 
+    /**
+     * 创建完整视频合成任务
+     * 
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function createCompleteVideoTask(Request $request)
+    {
+        $data = $request->all();
+        
+        // 验证参数
+        $validator = Validator::make($data, [
+            'anime_id' => 'required|string',
+            'episode_id' => 'required|string',
+        ], [
+            'anime_id.required' => '请选择动漫',
+            'episode_id.required' => '请选择分集',
+        ]);
+        
+        if ($validator->fails()) {
+            Utils::throwError('1002:' . $validator->errors()->first());
+        }
+        
+        try {
+            $result = $this->AnimeService->createCompleteVideoTask($data);
+            return $this->success($result);
+        } catch (\Exception $e) {
+            dLog('anime')->error('创建完整视频合成任务失败', [
+                'anime_id' => $data['anime_id'] ?? '',
+                'episode_id' => $data['episode_id'] ?? '',
+                'error' => $e->getMessage()
+            ]);
+            return $this->error('20003:'.$e->getMessage());
+        }
+    }
+
+    public function setRoleOrScene(Request $request) {
+        $data = $request->all();
+
+        $result = $this->AnimeService->setRoleOrScene($data);
+        return $this->success($result);
+    }
 }

+ 30 - 0
app/Models/MpCompleteVideoTask.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class MpCompleteVideoTask extends Model
+{
+    protected $table = 'mp_complete_video_tasks';
+    
+    protected $fillable = [
+        'anime_id',
+        'episode_id',
+        'generate_json',
+        'generate_status',
+        'complete_video_url',
+        'error_message',
+        'created_at',
+        'updated_at'
+    ];
+
+    protected $casts = [
+        'generate_json' => 'array',
+    ];
+
+    // 状态常量
+    const STATUS_PROCESSING = '执行中';
+    const STATUS_SUCCESS = '执行成功';
+    const STATUS_FAILED = '执行失败';
+}

+ 241 - 1
app/Services/AIGeneration/AIVideoGenerationService.php

@@ -286,8 +286,79 @@ class AIVideoGenerationService
 
         if ($statusInfo['status'] === 'success') {
             $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
+            
+            // 同步调整分镜视频状态和结果
+            $segment_id = getProp($task, 'alias_segment_id');
+            if ($segment_id && isset($statusInfo['result_url'])) {
+                try {
+                    DB::beginTransaction();
+                    
+                    $now = date('Y-m-d H:i:s');
+                    
+                    // 更新分镜表
+                    DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
+                        'video_url' => $statusInfo['result_url'],
+                        'video_task_status' => '已完成',
+                        'last_frame_url' => $statusInfo['last_frame_url'] ?? '',
+                        'updated_at' => $now
+                    ]);
+                    
+                    // 获取分镜信息用于创建配音任务
+                    $segment = DB::table('mp_episode_segments')
+                        ->where('segment_id', $segment_id)
+                        ->select('voice_actor', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
+                        ->first();
+                    
+                    // 如果分镜有对话内容,创建视频配音合成任务
+                    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,
+                        ];
+                        
+                        // 插入视频配音合成任务
+                        DB::table('mp_dub_video_tasks')->insert([
+                            'alias_segment_id' => $segment_id,
+                            'video_url' => $statusInfo['result_url'],
+                            'generate_status' => '执行中',
+                            'dub_video_url' => '',
+                            'generate_json' => json_encode($generate_json, 256),
+                            'created_at' => $now,
+                            'updated_at' => $now,
+                        ]);
+                    }
+                    
+                    DB::commit();
+                    
+                } catch (\Exception $e) {
+                    DB::rollBack();
+                    dLog('generate')->error('即梦视频任务处理失败', [
+                        'segment_id' => $segment_id,
+                        'task_id' => $task->id,
+                        'error' => $e->getMessage()
+                    ]);
+                }
+            }
         } elseif ($statusInfo['status'] === 'failed') {
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, $statusInfo);
+            
+            // 同步更新分镜表状态为失败
+            $segment_id = getProp($task, 'alias_segment_id');
+            if ($segment_id) {
+                DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
+                    'video_task_status' => '失败',
+                    'updated_at' => date('Y-m-d H:i:s')
+                ]);
+            }
         }
         // 如果仍然是处理中状态,不做任何操作
         
@@ -297,6 +368,15 @@ class AIVideoGenerationService
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
                 'error_message' => '任务处理超时(超过12小时)'
             ]);
+            
+            // 同步更新分镜表状态为失败
+            $segment_id = getProp($task, 'alias_segment_id');
+            if ($segment_id) {
+                DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
+                    'video_task_status' => '失败',
+                    'updated_at' => date('Y-m-d H:i:s')
+                ]);
+            }
         }
     }
     
@@ -480,20 +560,100 @@ class AIVideoGenerationService
 
         if ($statusInfo['status'] === 'success') {
             $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
+            
+            // 同步调整分镜视频状态和结果
+            $segment_id = getProp($task, 'alias_segment_id');
+            if ($segment_id && isset($statusInfo['result_url'])) {
+                try {
+                    DB::beginTransaction();
+                    
+                    $now = date('Y-m-d H:i:s');
+                    
+                    // 更新分镜表
+                    DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
+                        'video_url' => $statusInfo['result_url'],
+                        'video_task_status' => '已完成',
+                        'last_frame_url' => $statusInfo['last_frame_url'] ?? '',
+                        'updated_at' => $now
+                    ]);
+                    
+                    // 获取分镜信息用于创建配音任务
+                    $segment = DB::table('mp_episode_segments')
+                        ->where('segment_id', $segment_id)
+                        ->select('voice_actor', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
+                        ->first();
+                    
+                    // 如果分镜有对话内容,创建视频配音合成任务
+                    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,
+                        ];
+                        
+                        // 插入视频配音合成任务
+                        DB::table('mp_dub_video_tasks')->insert([
+                            'alias_segment_id' => $segment_id,
+                            'video_url' => $statusInfo['result_url'],
+                            'generate_status' => '执行中',
+                            'dub_video_url' => '',
+                            'generate_json' => json_encode($generate_json, 256),
+                            'created_at' => $now,
+                            'updated_at' => $now,
+                        ]);
+                    }
+                    
+                    DB::commit();
+                    
+                } catch (\Exception $e) {
+                    DB::rollBack();
+                    dLog('generate')->error('Seedance视频任务处理失败', [
+                        'segment_id' => $segment_id,
+                        'task_id' => $task->id,
+                        'error' => $e->getMessage()
+                    ]);
+                }
+            }
         } elseif ($statusInfo['status'] === 'failed') {
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
                 'error_message' => $statusInfo['error_message'],
                 'result_json'   => $statusInfo['result_json'] ?? []
             ]);
+            
+            // 同步更新分镜表状态为失败
+            $segment_id = getProp($task, 'alias_segment_id');
+            if ($segment_id) {
+                DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
+                    'video_task_status' => '失败',
+                    'updated_at' => date('Y-m-d H:i:s')
+                ]);
+            }
         }
         // 如果仍然是处理中状态,不做任何操作
         
-        // 处理:如果任务处理超过12小时,标记为失败
+        // 处理:如果任务处理超过48小时,标记为失败
         $processingTime = now()->diffInHours($task->created_at);
         if ($processingTime > 48) {
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
                 'error_message' => '任务处理超时(超过48小时)'
             ]);
+            
+            // 同步更新分镜表状态为失败
+            $segment_id = getProp($task, 'alias_segment_id');
+            if ($segment_id) {
+                DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
+                    'video_task_status' => '失败',
+                    'updated_at' => date('Y-m-d H:i:s')
+                ]);
+            }
         }
     }
 
@@ -1122,11 +1282,82 @@ class AIVideoGenerationService
 
         if ($statusInfo['status'] === 'success') {
             $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
+            
+            // 同步调整分镜视频状态和结果
+            $segment_id = getProp($task, 'alias_segment_id');
+            if ($segment_id && isset($statusInfo['result_url'])) {
+                try {
+                    DB::beginTransaction();
+                    
+                    $now = date('Y-m-d H:i:s');
+                    
+                    // 更新分镜表
+                    DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
+                        'video_url' => $statusInfo['result_url'],
+                        'video_task_status' => '已完成',
+                        'last_frame_url' => $statusInfo['last_frame_url'] ?? '',
+                        'updated_at' => $now
+                    ]);
+                    
+                    // 获取分镜信息用于创建配音任务
+                    $segment = DB::table('mp_episode_segments')
+                        ->where('segment_id', $segment_id)
+                        ->select('voice_actor', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
+                        ->first();
+                    
+                    // 如果分镜有对话内容,创建视频配音合成任务
+                    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,
+                        ];
+                        
+                        // 插入视频配音合成任务
+                        DB::table('mp_dub_video_tasks')->insert([
+                            'alias_segment_id' => $segment_id,
+                            'video_url' => $statusInfo['result_url'],
+                            'generate_status' => '执行中',
+                            'dub_video_url' => '',
+                            'generate_json' => json_encode($generate_json, 256),
+                            'created_at' => $now,
+                            'updated_at' => $now,
+                        ]);
+                    }
+                    
+                    DB::commit();
+                    
+                } catch (\Exception $e) {
+                    DB::rollBack();
+                    dLog('generate')->error('可灵视频任务处理失败', [
+                        'segment_id' => $segment_id,
+                        'task_id' => $task->id,
+                        'error' => $e->getMessage()
+                    ]);
+                }
+            }
         } elseif ($statusInfo['status'] === 'failed') {
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
                 'error_message' => $statusInfo['error_message'],
                 'result_json' => $statusInfo['result_json'] ?? []
             ]);
+            
+            // 同步更新分镜表状态为失败
+            $segment_id = getProp($task, 'alias_segment_id');
+            if ($segment_id) {
+                DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
+                    'video_task_status' => '失败',
+                    'updated_at' => date('Y-m-d H:i:s')
+                ]);
+            }
         }
         // 如果仍然是处理中状态,不做任何操作
         
@@ -1136,6 +1367,15 @@ class AIVideoGenerationService
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
                 'error_message' => '任务处理超时(超过24小时)'
             ]);
+            
+            // 同步更新分镜表状态为失败
+            $segment_id = getProp($task, 'alias_segment_id');
+            if ($segment_id) {
+                DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
+                    'video_task_status' => '失败',
+                    'updated_at' => date('Y-m-d H:i:s')
+                ]);
+            }
         }
     }
 

+ 267 - 17
app/Services/Anime/AnimeService.php

@@ -67,12 +67,11 @@ class AnimeService
     }
 
     public function batchSetRoleImg($data) {
-        $anime_id = getProp($data, 'anime_id');
-        $episode_number = getProp($data, 'episode_number');
-        if (!$anime_id) Utils::throwError('1002:请选择对话');
-        $anime = DB::table('mp_animes')->where('id', $anime_id)->where('is_deleted', 0)->first();
-        $roles = json_decode(getProp($anime, 'roles'), true);
-        $art_style = getProp($anime, 'art_style');
+        $episode_id = getProp($data, 'episode_id');
+        if (!$episode_id) Utils::throwError('1002:请选择分集');
+        $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first();
+        $roles = json_decode(getProp($data, 'roles'), true);
+        $art_style = getProp($episode, 'art_style');
         
         foreach ($roles as &$role) {
             $role_name = getProp($role, 'role');
@@ -104,8 +103,7 @@ class AnimeService
         }
         
         // 保存角色信息
-        // $boolen = DB::table('mp_animes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->update([
-        $boolen = DB::table('mp_animes')->where('id', $anime_id)->update([
+        $boolen = DB::table('mp_anime_episodes')->where('id', $episode_id)->update([
             'roles'             => json_encode($roles, 256),
             'generate_status'   => '执行中',
             'generate_at'       => date('Y-m-d H:i:s'),
@@ -120,15 +118,16 @@ class AnimeService
 
 
     public function batchSetSceneImg($data) {
-        $anime_id = getProp($data, 'anime_id');
-        if (!$anime_id) Utils::throwError('1002:请选择对话');
-        $episode_number = getProp($data, 'episode_number');
-        $anime = DB::table('mp_animes')->where('id', $anime_id)->where('is_deleted', 0)->first();
-        $scenes = json_decode(getProp($anime, 'scenes'), true);
-        $art_style = getProp($anime, 'art_style');
+        $episode_id = getProp($data, 'episode_id');
+        if (!$episode_id) Utils::throwError('1002:请选择分集');
+        $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first();
+        $scenes = json_decode(getProp($episode, 'scenes'), true);
+        $art_style = getProp($episode, 'art_style');
+        $target_scene = getProp($data, 'target_scene');
         
         foreach ($scenes as &$scene) {
             $scene_name = getProp($scene, 'scene');
+            if ($scene_name != $target_scene) continue;
             $description = getProp($scene, 'description');
             if ($art_style) $description = "美术风格:\n$art_style\n\n场景描述:$description";
             // 参考图地址
@@ -156,8 +155,7 @@ class AnimeService
         }
         
         // 保存角色信息
-        // $boolen = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->update([
-        $boolen = DB::table('mp_animes')->where('id', $anime_id)->update([
+        $boolen = DB::table('mp_anime_episodes')->where('id', $episode_id)->update([
             'scenes'             => json_encode($scenes, 256),
             'generate_status'   => '执行中',
             'generate_at'       => date('Y-m-d H:i:s'),
@@ -512,7 +510,7 @@ class AnimeService
         $segment_id = getProp($data, 'segment_id');
         $prompt = getProp($data, 'prompt');
         if (!$prompt || !$segment_id) {
-            Utils::throwError('1002:请输入提示词,并且选择需修改的图片');
+            Utils::throwError('1002:请输入提示词,并且选择分镜');
         }
         $segment = DB::table("mp_episode_segments")->where('segment_id', $segment_id)->first();
         if (!$segment) Utils::throwError('20003:该分镜不存在!');
@@ -2492,4 +2490,256 @@ class AnimeService
         // 限制调整范围
         return max(-1, min(4, $score));
     }
+
+    /**
+     * 创建完整视频合成任务
+     * 
+     * @param array $data
+     * @return array
+     */
+    public function createCompleteVideoTask($data)
+    {
+        $animeId = getProp($data, 'anime_id');
+        $episodeId = getProp($data, 'episode_id');
+
+        // 验证参数
+        if (!$animeId || !$episodeId) {
+            Utils::throwError('20003:anime_id 和 episode_id 不能为空');
+        }
+
+        // 检查是否已存在处理中的任务
+        $existingTask = DB::table('mp_complete_video_tasks')
+            ->where('anime_id', $animeId)
+            ->where('episode_id', $episodeId)
+            ->whereIn('generate_status', ['执行中'])
+            ->first();
+
+        if ($existingTask) {
+            Utils::throwError('20003:该剧集已有正在处理的视频合成任务');
+        }
+
+        // 获取所有分镜的配音视频,按 segment_number 排序
+        $segments = DB::table('mp_episode_segments')
+            ->where('anime_id', $animeId)
+            ->where('episode_id', $episodeId)
+            ->orderBy('segment_number')
+            ->select('segment_id', 'segment_number', 'dub_video_url')
+            ->get();
+
+        if ($segments->isEmpty()) {
+            Utils::throwError('20003:未找到该剧集的分镜数据');
+        }
+
+        // 检查是否所有分镜都有配音视频
+        $missingVideos = $segments->filter(function($segment) {
+            return empty($segment->dub_video_url);
+        });
+
+        if ($missingVideos->isNotEmpty()) {
+            Utils::throwError('20003:存在未完成配音的分镜,请确保所有分镜都已完成配音');
+        }
+
+        // 构建 generate_json
+        $generateJson = [];
+        $sequence = 1;
+        foreach ($segments as $segment) {
+            $generateJson[] = [
+                'sequence' => $sequence++,
+                'dub_video_url' => $segment->dub_video_url,
+            ];
+        }
+
+        // 创建任务
+        $now = date('Y-m-d H:i:s');
+        $taskId = DB::table('mp_complete_video_tasks')->insertGetId([
+            'anime_id' => $animeId,
+            'episode_id' => $episodeId,
+            'generate_json' => json_encode($generateJson, 256),
+            'generate_status' => '执行中',
+            'complete_video_url' => '',
+            'created_at' => $now,
+            'updated_at' => $now
+        ]);
+
+        dLog('anime')->info('创建完整视频合成任务', [
+            'task_id' => $taskId,
+            'anime_id' => $animeId,
+            'episode_id' => $episodeId,
+            'segment_count' => count($generateJson)
+        ]);
+
+        return [
+            'task_id' => $taskId,
+            'anime_id' => $animeId,
+            'episode_id' => $episodeId,
+            'segment_count' => count($generateJson),
+            'generate_status' => '执行中'
+        ];
+    }
+
+    public function setRoleOrScene($data) {
+        $anime_id = getProp($data, 'anime_id');
+        $episode_id = getProp($data, 'episode_id');
+        $json_data = getProp($data, 'json_data');
+
+        // 参数验证
+        if (!$anime_id) Utils::throwError('20003:请提供动漫ID');
+        if (!$episode_id) Utils::throwError('20003:请提供剧集ID');
+        if (!$json_data) Utils::throwError('20003:请提供json_data数据');
+
+        // 验证剧集是否存在
+        $episode = DB::table('mp_anime_episodes')
+            ->where('anime_id', $anime_id)
+            ->where('id', $episode_id)
+            ->first();
+        
+        if (!$episode) Utils::throwError('20003:该剧集不存在');
+
+        // 判断是 role 还是 scene
+        $isRole = isset($json_data['role']);
+        $isScene = isset($json_data['scene']);
+        
+        if (!$isRole && !$isScene) {
+            Utils::throwError('20003:json_data必须包含role或scene字段');
+        }
+        
+        if ($isRole && $isScene) {
+            Utils::throwError('20003:json_data只能包含role或scene其中一个');
+        }
+
+        try {
+            // 获取当前的 roles 或 scenes
+            $fieldName = $isRole ? 'roles' : 'scenes';
+            $currentData = json_decode($episode->$fieldName, true) ?: [];
+            
+            // 查找匹配的项
+            $targetName = $isRole ? $json_data['role'] : $json_data['scene'];
+            $foundIndex = -1;
+            
+            foreach ($currentData as $index => $item) {
+                $itemName = $isRole ? ($item['role'] ?? '') : ($item['scene'] ?? '');
+                if ($itemName === $targetName) {
+                    $foundIndex = $index;
+                    break;
+                }
+            }
+            
+            if ($foundIndex === -1) {
+                Utils::throwError('20003:未找到匹配的' . ($isRole ? '角色' : '场景'));
+            }
+
+            // 检查是否有 url
+            $url = getProp($json_data, 'url', '');
+            
+            if (empty($url)) {
+                // 没有 url,需要生成图片
+                $description = getProp($json_data, 'description', '');
+                if (empty($description)) {
+                    Utils::throwError('20003:缺少description字段,无法生成图片');
+                }
+                
+                // 获取艺术风格
+                $art_style = $episode->art_style ?? '';
+                
+                // 构建提示词
+                $prompt = $description;
+                if ($art_style) {
+                    $prompt = $art_style . ',' . $prompt;
+                }
+                
+                // 创建图片生成任务
+                $params = [
+                    'prompt' => $prompt,
+                    'ref_img_urls' => [],
+                    'width' => 2048,
+                    'height' => 2048
+                ];
+                
+                $task = $this->aiImageGenerationService->createImageGenerationTask($params);
+                $task_id = $task->id;
+                
+                if (!$task_id) {
+                    Utils::throwError('20003:创建图片生成任务失败');
+                }
+                
+                // 轮询获取结果(3分钟超时,每3秒查询一次)
+                $start_time = time();
+                $timeout = 180; // 3分钟
+                $img_url = '';
+                
+                while (time() - $start_time < $timeout) {
+                    sleep(3);
+                    
+                    $task = MpGeneratePicTask::where('id', $task_id)->first();
+                    $statusInfo = $this->aiImageGenerationService->queryTaskStatus($task);
+                    
+                    if ($statusInfo['status'] === 'success') {
+                        $task->updateStatus('success', [
+                            'result_url' => $statusInfo['result_url'],
+                            'result_json' => $statusInfo['result_json'] ?? []
+                        ]);
+                        
+                        $img_url = $statusInfo['result_url'][0];
+                        break;
+                    } elseif ($statusInfo['status'] === 'failed') {
+                        $task->updateStatus('failed', [
+                            'error_message' => $statusInfo['error_message'],
+                            'result_json' => $statusInfo['result_json'] ?? []
+                        ]);
+                        
+                        Utils::throwError('20003:图片生成失败: ' . ($statusInfo['error_message'] ?? '未知错误'));
+                    }
+                }
+                
+                if (empty($img_url)) {
+                    Utils::throwError('20003:图片生成超时,请稍后重试');
+                }
+                
+                $url = $img_url;
+            }
+            
+            // 更新数据
+            $json_data['url'] = $url;
+            $currentData[$foundIndex] = $json_data;
+            
+            // 保存到数据库
+            $result = DB::table('mp_anime_episodes')
+                ->where('anime_id', $anime_id)
+                ->where('id', $episode_id)
+                ->update([
+                    $fieldName => json_encode($currentData, 256),
+                    'updated_at' => date('Y-m-d H:i:s')
+                ]);
+            
+            if ($result === false) {
+                Utils::throwError('20003:更新' . ($isRole ? '角色' : '场景') . '失败');
+            }
+            
+            dLog('anime')->info('更新' . ($isRole ? '角色' : '场景') . '成功', [
+                'anime_id' => $anime_id,
+                'episode_id' => $episode_id,
+                'type' => $isRole ? 'role' : 'scene',
+                'name' => $targetName,
+                'url' => $url
+            ]);
+            
+            return [
+                'json_data' => $currentData[$foundIndex]
+            ];
+            
+        } catch (\Exception $e) {
+            dLog('anime')->error('更新角色或场景失败', [
+                'anime_id' => $anime_id,
+                'episode_id' => $episode_id,
+                'error' => $e->getMessage()
+            ]);
+            
+            // 如果是已知错误,直接抛出
+            if (strpos($e->getMessage(), '20003:') === 0) {
+                throw $e;
+            }
+            
+            Utils::throwError('20003:更新失败: ' . $e->getMessage());
+        }
+    }
 }

+ 6 - 0
routes/api.php

@@ -160,6 +160,12 @@ Route::group(['middleware' => ['bindToken', 'bindExportToken', 'checkLogin']], f
         Route::post('batchSetSegmentVideos', [AnimeController::class, 'batchSetSegmentVideos']);
         Route::post('applyAudioData', [AnimeController::class, 'applyAudioData']);
         Route::get('episodePicsInfo', [AnimeController::class, 'episodePicsInfo']);
+        
+        // 完整视频合成任务
+        Route::post('createCompleteVideoTask', [AnimeController::class, 'createCompleteVideoTask']);
+        
+        // 设置角色或场景
+        Route::post('setRoleOrScene', [AnimeController::class, 'setRoleOrScene']);
     });
     
 });