lh 1 месяц назад
Родитель
Сommit
e4063eea77
3 измененных файлов с 461 добавлено и 8 удалено
  1. 240 5
      app/Http/Controllers/Anime/AnimeController.php
  2. 219 3
      app/Services/Anime/AnimeService.php
  3. 2 0
      routes/api.php

+ 240 - 5
app/Http/Controllers/Anime/AnimeController.php

@@ -685,6 +685,241 @@ class AnimeController extends BaseController
         ]);
     }
 
+    public function createActVideoTask(Request $request) {
+        $uid = Site::getUid();
+        // 忽略所有超时限制
+        set_time_limit(0);
+        ini_set('max_execution_time', '0');
+
+        $data = $request->all();
+        
+        // 创建视频生成任务
+        $result = $this->AnimeService->createSegmentVideoTask($data);
+        $taskId = $result['task_id'];
+        
+        // 设置 SSE 响应头
+        return response()->stream(function () use ($taskId, $result, $uid) {
+            // 设置 SSE 响应头
+            echo "data: " . json_encode([
+                'type' => 'init',
+                'data' => $result
+            ]) . "\n\n";
+            ob_flush();
+            flush();
+            
+            $startTime = time();
+            $maxDuration = 300; // 5分钟超时
+            $checkInterval = 5; // 每5秒检查一次
+            
+            while (time() - $startTime < $maxDuration) {
+                try {
+                    // 查询任务状态
+                    $task = \App\Models\MpGenerateVideoTask::find($taskId);
+                    if (!$task) {
+                        echo "data: " . json_encode([
+                            'type' => 'error',
+                            'message' => '任务不存在'
+                        ]) . "\n\n";
+                        ob_flush();
+                        flush();
+                        break;
+                    }
+                    
+                    // 如果任务还在处理中,查询最新状态
+                    if ($task->status === 'processing') {
+                        // 根据任务类型查询不同的状态
+                        if ($task->api_type === 'jimeng') {
+                            $statusResult = $this->AIVideoGenerationService->queryJimengTaskStatus($task);
+                        } elseif ($task->api_type === 'keling') {
+                            $statusResult = $this->AIVideoGenerationService->queryKelingOmniTaskStatus($task);
+                        } elseif ($task->api_type === 'zzengine') {
+                            $statusResult = $this->AIVideoGenerationService->queryUnifiedApiTaskStatus($task);
+                        } else {
+                            $statusResult = $this->AIVideoGenerationService->querySeedanceTaskStatus($task);
+                        }
+                        if (isset($statusResult['status'])) {
+                            // 更新任务状态
+                            $task->update([
+                                'status' => $statusResult['status'],
+                                'result_url' => $statusResult['result_url'] ?? null,
+                                'compressed_url' => $statusResult['compressed_url'] ?? null,
+                                'last_frame_url' => $statusResult['last_frame_url'] ?? '',
+                                'error_message' => $statusResult['error_message'] ?? null,
+                                'completed_at' => in_array($statusResult['status'], [
+                                    'success',
+                                    'failed'
+                                ]) ? now() : null
+                            ]);
+                            
+                            // 如果任务成功,更新分镜表
+                            if ($statusResult['status'] === 'success' && isset($statusResult['result_url'])) {
+                                try {
+                                    DB::beginTransaction();
+                                    
+                                    $now = date('Y-m-d H:i:s');
+                                    
+                                    // 获取分镜ID
+                                    $act = DB::table('mp_episode_segments')
+                                        ->where('video_task_id', $taskId)
+                                        ->first();
+                                    
+                                    if (!$act) {
+                                        throw new \Exception('未找到对应的片段记录');
+                                    }
+                                    
+                                    $act_id = $act->id;
+                                    $anime_id = $act->anime_id;
+                                    $episode_number = $act->episode_number;
+                                    $act_content = $act->act_content;
+                                    
+                                    // 更新分镜表
+                                    $segmentUpdateData = [
+                                        'origin_video_url' => $statusResult['result_url'],
+                                        'video_task_status' => '已完成',
+                                        'last_frame_url' => $statusResult['last_frame_url'] ?? '',
+                                        'current_type' => 2,
+                                        'updated_at' => $now
+                                    ];
+                                    $compressed_video_url = compressVideo($statusResult['result_url']);
+                                    $segmentUpdateData['video_url'] = $compressed_video_url ?: $statusResult['result_url'];
+                                    
+                                    // 只有当video_duration存在且大于0时才更新
+                                    if (isset($statusResult['video_duration']) && $statusResult['video_duration'] > 0) {
+                                        $segmentUpdateData['video_duration'] = $statusResult['video_duration'];
+                                        $segmentUpdateData['video_time_point_start'] = 0;
+                                        // $segmentUpdateData['video_time_point_end'] = $statusResult['video_duration'];
+                                        $segmentUpdateData['video_time_point_end'] = $segment->audio_duration ?? 0;
+                                    }
+                                    
+                                    $updateResult = DB::table('mp_episode_segments')
+                                        ->where('id', $act_id)
+                                        ->update($segmentUpdateData);
+                                    
+                                    if (!$updateResult) {
+                                        Utils::throwError('20003:更新分镜表失败');
+                                    }
+
+                                    // 更新分镜视频成功后新增对话记录
+                                    // 保存对话记录
+                                    $records = [
+                                        [
+                                            'uid'           => $uid,
+                                            'anime_id'      => $anime_id,
+                                            'sequence'      => $episode_number,
+                                            'role'          => 'user',
+                                            'content'       => '片段转视频',
+                                            'act_id'        => $act_id,
+                                            'video_url'     => '',
+                                            'created_at'    => $now,
+                                            'updated_at'    => $now
+                                        ],
+                                        [
+                                            'uid'           => $uid,
+                                            'anime_id'      => $anime_id,
+                                            'sequence'      => $episode_number,
+                                            'role'          => 'assistant',
+                                            'content'       => $act_content,
+                                            'act_id'        => $act_id,
+                                            'video_url'     => $statusResult['result_url'],
+                                            'created_at'    => $now,
+                                            'updated_at'    => $now
+                                        ]
+                                    ];
+
+                                    DB::table('mp_anime_records')->insert($records);
+                                    
+                                    DB::commit();
+                                    
+                                } catch (\Exception $e) {
+                                    DB::rollBack();
+                                    
+                                    $logData = [
+                                        'task_id' => $taskId,
+                                        'error' => $e->getMessage()
+                                    ];
+                                    dLog('anime')->error('视频任务处理失败', $logData);
+                                    logDB('anime', 'error', '视频任务处理失败', $logData);
+                                }
+                            } elseif ($statusResult['status'] === 'failed') {
+                                DB::table('mp_episode_segments')
+                                    ->where('video_task_id', $taskId)
+                                    ->update([
+                                        'video_task_status' => '失败',
+                                        'updated_at' => date('Y-m-d H:i:s')
+                                    ]);
+                            }
+                        }
+                    }
+                    
+                    // 发送当前状态
+                    echo "data: " . json_encode([
+                        'type' => 'update',
+                        'data' => [
+                            'task_id' => $task->id,
+                            'status' => $task->status,
+                            'result_url' => $task->result_url,
+                            'error_message' => $task->error_message,
+                            'elapsed_time' => time() - $startTime
+                        ]
+                    ]) . "\n\n";
+                    ob_flush();
+                    flush();
+                    
+                    // 如果任务完成(成功或失败),结束连接
+                    if (in_array($task->status, [
+                        'success',
+                        'failed'
+                    ])) {
+                        echo "data: " . json_encode([
+                            'type' => 'completed',
+                            'data' => [
+                                'task_id' => $task->id,
+                                'status' => $task->status,
+                                'video_url' => $task->compressed_url ?: $task->result_url,
+                                'origin_video_url' => $task->result_url,
+                                'last_frame_url' => $task->last_frame_url,
+                                'error_message' => $task->error_message
+                            ]
+                        ]) . "\n\n";
+                        ob_flush();
+                        flush();
+                        break;
+                    }
+                    
+                    sleep($checkInterval);
+                    
+                } catch (\Exception $e) {
+                    echo "data: " . json_encode([
+                        'type' => 'error',
+                        'message' => '查询任务状态失败: ' . $e->getMessage()
+                    ]) . "\n\n";
+                    ob_flush();
+                    flush();
+                    sleep($checkInterval);
+                }
+            }
+            
+            // 超时处理
+            if (time() - $startTime >= $maxDuration) {
+                echo "data: " . json_encode([
+                    'type' => 'time_out',
+                    'message' => '任务执行超时,请稍后查询任务状态',
+                    'data' => [
+                        'task_id' => $taskId
+                    ]
+                ]) . "\n\n";
+                ob_flush();
+                flush();
+            }
+            
+        }, 200, [
+            'Content-Type' => 'text/event-stream',
+            'Cache-Control' => 'no-cache',
+            'Connection' => 'keep-alive',
+            'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
+        ]);
+    }
+
     /**
      * 批量生成分镜视频
      */
@@ -896,7 +1131,7 @@ class AnimeController extends BaseController
                     unset($videoParams['content']); // Keling不使用content格式
                     $task = $this->AIVideoGenerationService->createKelingOmniTask($videoParams);
                 } elseif (strpos($model, 'zhizhen') !== false) {
-                    // 智20等新模型使用统一API
+                    // 智20等新模型使用统一API
                     // 构建统一API参数
                     $unifiedParams = [
                         'model_code' => $model,
@@ -918,7 +1153,7 @@ class AnimeController extends BaseController
                         'generate_audio' => (int)$current_generate_audio === 1 ? true : false,
                     ];
                     
-                    // 处理首帧和尾帧(智20模型必须要有首帧图片)
+                    // 处理首帧和尾帧(智20模型必须要有首帧图片)
                     if ($hasImage) {
                         $parameters['first_frame_url'] = $videoParams['first_frame_url'];
                         
@@ -931,10 +1166,10 @@ class AnimeController extends BaseController
                         $logData = [
                             'segment_id' => $segment->segment_id,
                             'model' => $model,
-                            'error' => '智20模型需要提供首帧图片'
+                            'error' => '智20模型需要提供首帧图片'
                         ];
-                        dLog('anime')->error('智20模型缺少首帧图片', $logData);
-                        logDB('anime', 'error', '智20模型缺少首帧图片', $logData);
+                        dLog('anime')->error('智20模型缺少首帧图片', $logData);
+                        logDB('anime', 'error', '智20模型缺少首帧图片', $logData);
                         continue;
                     }
                     

+ 219 - 3
app/Services/Anime/AnimeService.php

@@ -3793,12 +3793,12 @@ class AnimeService
             $model = getProp($episode, 'video_model');
             if (!$model) {
                 // episode表也没有,使用默认值
-                $model = 'doubao-seedance-1-0-pro-250528';
+                $model = 'doubao-seedance-1-5-pro-251215';
             }
         }
         // 验证模型是否在可用模型表中
         if (!DB::table('mp_video_models')->where('model', $model)->where('is_enabled', 1)->exists()) {
-            $model = 'doubao-seedance-1-0-pro-250528';
+            $model = 'doubao-seedance-1-5-pro-251215';
         }
         // 保存到episode表
         $episode_id = getProp($segment, 'episode_id');
@@ -3943,7 +3943,7 @@ class AnimeService
                 }
             } else {
                 // 没有图片时,记录错误日志
-                logDB('anime', 'error', '智20模型缺少首帧图片', [
+                logDB('anime', 'error', '智20模型缺少首帧图片', [
                     'segment_id' => $segment_id,
                     'model' => $model
                 ]);
@@ -3991,6 +3991,222 @@ class AnimeService
             'video_duration' => $videoDuration
         ];
     }
+    
+    public function createActVideoTask($data) {
+        $act_id = getProp($data, 'act_id');
+        $first_frame_url = getProp($data, 'first_frame_url');
+        $tail_frame_url = getProp($data, 'tail_frame_url');
+        $generate_audio = getProp($data, 'generate_audio', 1);
+        $reference_images = getProp($data, 'reference_images', []);
+        
+        if (!$act_id) {
+            Utils::throwError('1002:片段ID不能为空');
+        }
+        
+        // 获取片段信息
+        $act = DB::table('mp_episode_segments')->where('id', $act_id)->first();
+        if (!$act) {
+            Utils::throwError('20003:片段不存在');
+        }
+        
+        $act = (array)$act;
+
+        $episode_id = getProp($act, 'episode_id');
+        $ratio = DB::table('mp_anime_episodes')->where('id', $episode_id)->value('ratio');
+        if (!$ratio) $ratio = '9:16';
+        
+        // 获取分镜内容
+        $actContent = getProp($act, 'act_content', '');
+
+        // 音效同出(默认使用)
+        $current_generate_audio = $generate_audio ? 1 : 0;
+
+        // 构建完整的提示词
+        $fullPrompt = $actContent;
+        
+        // 去除多余的换行符(将连续多个换行符替换为单个换行符)
+        $fullPrompt = preg_replace('/\n{2,}/', "\n", $fullPrompt);
+        $fullPrompt = trim($fullPrompt);
+        
+        $videoDuration = getProp($act, 'act_duration');
+        
+        // 处理视频模型
+        $model = getProp($data, 'model');
+        if (!$model) {
+            // 用户没有输入,从episode表获取
+            $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first();
+            $model = getProp($episode, 'video_model');
+            if (!$model) {
+                // episode表也没有,使用默认值
+                $model = 'zhizhen-20';
+            }
+        }
+        // 验证模型是否在可用模型表中
+        if (!DB::table('mp_video_models')->where('model', $model)->where('is_enabled', 1)->exists()) {
+            $model = 'zhizhen-20';
+        }
+        // 保存到episode表
+        DB::table('mp_anime_episodes')->where('id', $episode_id)->update([
+            'video_model' => $model,
+            'updated_at' => date('Y-m-d H:i:s')
+        ]);
+        
+        // 构建视频生成参数
+        $videoParams = [
+            'model' => $model,
+            'alias_act_id' => $act_id,
+            'prompt' => trim($fullPrompt),
+            'video_duration' => $videoDuration,
+            'video_resolution' => '720P',
+            'seed' => -1,
+            'ratio' => $ratio,
+            'generate_audio' => (int)$current_generate_audio === 0 ? false : true,
+            'draft' => false,
+            'watermark' => false,
+            'camera_fixed' => false,
+            // 'callback_url' => 'http://mpaudio.yqsd.cn/api/video/seedanceCallback'
+        ];
+        
+        // 如果分镜有图片,作为首帧
+        $hasImage = !empty(getProp($act, 'img_url')) || $first_frame_url;
+        $hasVideo = !empty(getProp($act, 'video_url'));
+        
+        if ($hasImage) {
+            if ($first_frame_url) {
+                $videoParams['first_frame_url'] = $first_frame_url;
+            }else {
+                $videoParams['first_frame_url'] = getProp($act, 'img_url');
+            }
+            if ($tail_frame_url) $videoParams['tail_frame_url'] = $tail_frame_url;
+        }
+        
+        // 构建content数组
+        $videoParams['content'] = [
+            [
+                'type' => 'text',
+                'text' => $videoParams['prompt'],
+            ]
+        ];
+        
+        // 如果有首帧图片,添加到content中
+        if ($hasImage) {
+            if (in_array($model, ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128'])) {
+                $videoParams['content'][] = [
+                    'type' => 'image_url',
+                    'image_url' => [
+                        'url' => $videoParams['first_frame_url'],
+                    ],
+                    'role' => 'reference_image',
+                ];
+                if (isset($videoParams['tail_frame_url'])) {
+                    $videoParams['content'][] = [
+                        'type' => 'image_url',
+                        'image_url' => [
+                            'url' => $videoParams['tail_frame_url'],
+                        ],
+                        'role' => 'reference_image',
+                    ];
+                }
+            }else {
+                $videoParams['content'][] = [
+                    'type' => 'image_url',
+                    'image_url' => [
+                        'url' => $videoParams['first_frame_url'],
+                    ],
+                    'role' => 'first_frame',
+                ];
+
+                if (isset($videoParams['tail_frame_url'])) {
+                    $videoParams['content'][] = [
+                        'type' => 'image_url',
+                        'image_url' => [
+                            'url' => $videoParams['tail_frame_url'],
+                        ],
+                        'role' => 'last_frame',
+                    ];
+                }
+            }
+        }
+
+        // dd($videoParams);
+        
+        // 根据模型选择不同的视频生成方法
+        if (strpos($model, 'jimeng') !== false) {
+            // 即梦模型参数调整
+            unset($videoParams['content']); // 即梦不使用content格式
+            $task = $this->aiVideoGenerationService->createJimengTask($videoParams);
+        } elseif (strpos($model, 'kling') !== false) {
+            // Keling模型参数调整
+            $videoParams['aspect_ratio'] = $videoParams['ratio']; // Keling使用aspect_ratio
+            unset($videoParams['ratio']);
+            unset($videoParams['content']); // Keling不使用content格式
+            $task = $this->aiVideoGenerationService->createKelingOmniTask($videoParams);
+        } elseif (strpos($model, 'zhizhen') !== false) {
+            // 智帧20等新模型使用统一API
+            // 构建统一API参数
+            $unifiedParams = [
+                'model_code' => $model,
+                'alias_act_id' => $act_id,
+                'prompt' => trim($fullPrompt),
+                'video_duration' => $videoDuration,
+                'video_resolution' => strtolower($videoParams['video_resolution']),
+                'video_ratio' => $ratio,
+                'seed' => -1,
+            ];
+            
+            // 构建parameters参数
+            $parameters = [
+                'seconds' => $unifiedParams['video_duration'],
+                'resolution' => $unifiedParams['video_resolution'],
+                'ratio' => $ratio,
+                // 'video_mode' => 'multi_image',
+                // 'mode' => 'multi_image',
+                'generate_audio' => (int)$current_generate_audio === 1 ? true : false,
+            ];
+            
+            $hasImage = false;
+            // 处理首帧和尾帧(智帧20模型必须要有首帧图片)
+            if ($hasImage) {
+                $parameters['first_frame_url'] = $videoParams['first_frame_url'];
+                if (isset($videoParams['tail_frame_url'])) {
+                    $parameters['last_frame_url'] = $videoParams['tail_frame_url'];
+                }
+            } else {
+                // 没有图片时,记录错误日志
+                logDB('anime', 'error', '智帧20模型缺少首帧图片', [
+                    'act_id' => $act_id,
+                    'model' => $model
+                ]);
+            }
+
+            if ($reference_images) {
+                $parameters['reference_images'] = $reference_images;
+            }
+            
+            $unifiedParams['parameters'] = $parameters;
+            dd($unifiedParams);
+            
+            // 调用统一API创建任务
+            $task = $this->aiVideoGenerationService->createUnifiedApiTask($unifiedParams);
+        } else {
+            $task = $this->aiVideoGenerationService->createSeedanceTask($videoParams);
+        }
+        
+        // 更新分镜表的视频任务信息
+        DB::table('mp_episode_segments')->where('id', $act_id)->update([
+            'video_task_id' => $task->id,
+            'video_task_status' => '生成中',
+            'generate_audio' => $generate_audio,
+            'updated_at' => date('Y-m-d H:i:s')
+        ]);
+        
+        return [
+            'task_id' => $task->id,
+            'status' => $task->status,
+            'act_id' => $act_id,
+            'video_duration' => $videoDuration
+        ];
+    }
 
     /**
      * 根据提示词内容智能计算最优视频时长

+ 2 - 0
routes/api.php

@@ -188,6 +188,8 @@ Route::group(['middleware' => ['bindToken', 'bindExportToken', 'checkLogin']], f
             Route::get('segmentChatHistory', [AnimeController::class, 'segmentChatHistory']);           // 分镜对话历史记录
             Route::post('createSegmentVideoTask', [AnimeController::class, 'createSegmentVideoTask']);  // 分镜图片转视频
             Route::post('batchSetSegmentVideos', [AnimeController::class, 'batchSetSegmentVideos']);    // 一键转视频
+            Route::post('createActVideoTask', [AnimeController::class, 'createActVideoTask']);          // 片段转视频
+            Route::post('batchSetActVideos', [AnimeController::class, 'batchSetActVideos']);            // 片段一键转视频
             Route::post('applyAudioData', [AnimeController::class, 'applyAudioData']);                  // 修改分镜音频参数
             Route::post('previewAudio', [AnimeController::class, 'previewAudio']);                      // 音频试听
             Route::get('episodePicsInfo', [AnimeController::class, 'episodePicsInfo']);                 // 获取主体和场景图片生成信息