Forráskód Böngészése

Merge branch 'test' into check_points

lh 1 hónapja
szülő
commit
f5a770983f

+ 117 - 0
app/Http/Controllers/Anime/AnimeController.php

@@ -2418,6 +2418,123 @@ class AnimeController extends BaseController
     }
 
     /**
+     * 文生视频(通用)
+     * 支持传入视频参数、提示词、参考图等信息,不支持products入参
+     */
+    public function generateVideo(Request $request) {
+        // 忽略所有超时限制
+        set_time_limit(0);
+        ini_set('max_execution_time', '0');
+
+        $data = $request->all();
+
+        // 创建视频生成任务
+        $result = $this->AnimeService->generateVideo($data);
+        $taskId = $result['task_id'];
+
+        // 设置 SSE 响应头
+        return response()->stream(function () use ($taskId, $result) {
+            // 立即发送 init 消息
+            echo "data: " . json_encode([
+                'type' => 'init',
+                'data' => $result
+            ]) . "\n\n";
+            if (ob_get_level() > 0) ob_flush();
+            flush();
+
+            $startTime = time();
+            $maxDuration = 3600; // 超时设置
+            $checkInterval = 5; // 检查间隔
+            $lastQueueMessageTime = time(); // 记录上次发送队列消息的时间
+            $queueMessageInterval = 60; // 队列消息发送间隔(秒)
+
+            while (time() - $startTime < $maxDuration) {
+                try {
+                    // 查询任务状态
+                    $task = \App\Models\MpGenerateVideoTask::find($taskId);
+                    if (!$task) {
+                        echo "data: " . json_encode([
+                            'type' => 'error',
+                            'message' => '任务不存在'
+                        ]) . "\n\n";
+                        if (ob_get_level() > 0) ob_flush();
+                        flush();
+                        break;
+                    }
+
+                    // 如果任务完成(成功或失败),结束连接
+                    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 ? mapErrorMessage($task->error_message) : ''
+                            ]
+                        ]) . "\n\n";
+                        if (ob_get_level() > 0) ob_flush();
+                        flush();
+                        break;
+                    }
+
+                    // 每分钟发送一次队列状态消息
+                    $currentTime = time();
+                    if ($currentTime - $lastQueueMessageTime >= $queueMessageInterval) {
+                        echo "data: " . json_encode([
+                            'type' => 'queue',
+                            'data' => [
+                                'task_id' => $task->id,
+                                'status' => $task->status,
+                                'elapsed_time' => $currentTime - $startTime,
+                                'message' => '任务正在队列中执行...'
+                            ]
+                        ]) . "\n\n";
+                        if (ob_get_level() > 0) ob_flush();
+                        flush();
+                        $lastQueueMessageTime = $currentTime;
+                    }
+
+                    sleep($checkInterval);
+
+                } catch (\Exception $e) {
+                    echo "data: " . json_encode([
+                        'type' => 'error',
+                        'message' => '查询任务状态失败: ' . $e->getMessage()
+                    ]) . "\n\n";
+                    if (ob_get_level() > 0) ob_flush();
+                    flush();
+                    sleep($checkInterval);
+                }
+            }
+
+            // 超时处理
+            if (time() - $startTime >= $maxDuration) {
+                echo "data: " . json_encode([
+                    'type' => 'time_out',
+                    'message' => '任务执行超时,请稍后查询任务状态',
+                    'data' => [
+                        'task_id' => $taskId
+                    ]
+                ]) . "\n\n";
+                if (ob_get_level() > 0) ob_flush();
+                flush();
+            }
+
+        }, 200, [
+            'Content-Type' => 'text/event-stream',
+            'Cache-Control' => 'no-cache',
+            'Connection' => 'keep-alive',
+            'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
+        ]);
+    }
+
+    /**
      * 预估视频生成所需积分(单一/批量)
      * 传参与创建接口一致,批量场景需额外传 type=batch_segment / batch_act
      *

+ 11 - 0
app/Http/Controllers/DeepSeek/DeepSeekController.php

@@ -546,6 +546,17 @@ class DeepSeekController extends BaseController
         return $this->success($result);
     }
 
+    /**
+     * 获取剧本资产生成任务列表
+     * @param Request $request
+     * @return mixed
+     */
+    public function getScriptGenerateTasks(Request $request) {
+        $data = $request->all();
+        $result = $this->deepseekService->getScriptGenerateTasks($data);
+        return $this->success($result, [new DeepSeekTransformer(), 'newBuildScriptGenerateTaskList']);
+    }
+
     // 保存剧本沟通记录
     public function saveScriptChatHistory(Request $request) {
         $data = $request->all();

+ 28 - 0
app/Models/MpScriptGenerateTask.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class MpScriptGenerateTask extends Model
+{
+    protected $table = 'mp_script_generate_tasks';
+
+    const STATUS_PENDING = 'pending';
+    const STATUS_PROCESSING = 'processing';
+    const STATUS_SUCCESS = 'success';
+    const STATUS_FAILED = 'failed';
+
+    protected $fillable = [
+        'id',
+        'uid',
+        'script_id',
+        'sequence',
+        'status',
+        'prompt',
+        'result',
+        'error_message',
+        'started_at',
+        'completed_at',
+    ];
+}

+ 235 - 16
app/Services/Anime/AnimeService.php

@@ -636,6 +636,14 @@ class AnimeService
         $episode['acts'] = $segmentsStructure['acts'];
         $episode['total_duration'] = $segmentsStructure['total_duration'];
 
+        // 获取mp_animes表中的视频参数
+        $anime = DB::table('mp_animes')->where('id', $anime_id)->first(['video_model', 'video_resolution', 'video_ratio']);
+        $episode['video_params'] = [
+            'video_model' => getProp($anime, 'video_model'),
+            'video_resolution' => getProp($anime, 'video_resolution'),
+            'ratio' => getProp($anime, 'video_ratio'),
+        ];
+
         return $episode;
     }
 
@@ -1067,6 +1075,9 @@ class AnimeService
 
         try {
             $target_role_name = getProp($target_roles, 'role');
+            if (empty($target_role_name)) {
+                Utils::throwError('20003:角色名称不能为空');
+            }
             
             // 如果是删除操作
             if ($is_deleted == 1) {
@@ -1297,6 +1308,9 @@ class AnimeService
 
         try {
             $target_scene_name = getProp($target_scenes, 'scene');
+            if (empty($target_scene_name)) {
+                Utils::throwError('20003:场景名称不能为空');
+            }
             
             // 如果是删除操作
             if ($is_deleted == 1) {
@@ -1511,6 +1525,9 @@ class AnimeService
 
         try {
             $target_prop_name = getProp($target_props, 'prop');
+            if (empty($target_prop_name)) {
+                Utils::throwError('20003:道具名称不能为空');
+            }
             
             // 如果是删除操作
             if ($is_deleted == 1) {
@@ -3364,31 +3381,44 @@ class AnimeService
             if ($episode) {
                 $roles = json_decode(getProp($episode, 'roles', '[]'), true) ?: [];
                 $scenes = json_decode(getProp($episode, 'scenes', '[]'), true) ?: [];
+                $props = json_decode(getProp($episode, 'props', '[]'), true) ?: [];
                 $extra_products = json_decode(getProp($episode, 'extra_products', '[]'), true) ?: [];
                 
-                // 先合并roles和scenes
+                // 先合并roles、scenes和props
                 // 处理角色数组
                 foreach ($roles as $role) {
                     $product = $role;
                     // 将role字段重命名为product_name
-                    if (isset($product['role'])) {
+                    if (is_array($product) && !empty($product['role'] ?? null)) {
                         $product['product_name'] = $product['role'];
                         unset($product['role']);
+                        $product['product'] = 1; // 标记类型为角色
+                        $products[$product['product_name']] = $product; // 使用product_name作为key便于后续覆盖
                     }
-                    $product['product'] = 1; // 标记类型为角色
-                    $products[$product['product_name']] = $product; // 使用product_name作为key便于后续覆盖
                 }
                 
                 // 处理场景数组
                 foreach ($scenes as $scene) {
                     $product = $scene;
                     // 将scene字段重命名为product_name
-                    if (isset($product['scene'])) {
+                    if (is_array($product) && !empty($product['scene'] ?? null)) {
                         $product['product_name'] = $product['scene'];
                         unset($product['scene']);
+                        $product['product'] = 2; // 标记类型为场景
+                        $products[$product['product_name']] = $product; // 使用product_name作为key便于后续覆盖
+                    }
+                }
+
+                // 处理道具数组
+                foreach ($props as $prop) {
+                    $product = $prop;
+                    // 将prop字段重命名为product_name
+                    if (is_array($product) && !empty($product['prop'] ?? null)) {
+                        $product['product_name'] = $product['prop'];
+                        unset($product['prop']);
+                        $product['product'] = 3; // 标记类型为道具
+                        $products[$product['product_name']] = $product; // 使用product_name作为key便于后续覆盖
                     }
-                    $product['product'] = 2; // 标记类型为场景
-                    $products[$product['product_name']] = $product; // 使用product_name作为key便于后续覆盖
                 }
                 
                 // extra_products优先级更高,直接覆盖同名的roles和scenes
@@ -5095,6 +5125,146 @@ class AnimeService
     }
 
     /**
+     * 文生视频通用方法
+     * 支持传入视频参数、提示词、参考图等信息,不支持products入参
+     * 用户提示词不做任何处理,直接组装参数后根据模型调用
+     *
+     * @param array $data 请求参数
+     * @return array
+     */
+    public function generateVideo($data) {
+        $prompt = getProp($data, 'prompt');
+        if (empty($prompt)) {
+            Utils::throwError('20003:提示词不能为空');
+        }
+
+        $model = getProp($data, 'model');
+        if (!$model) {
+            $model = 'zhizhen-20';
+        }
+        // 验证模型是否在可用模型表中
+        if (!DB::table('mp_video_models')->where('model', $model)->where('is_enabled', 1)->exists()) {
+            Utils::throwError('20003:该模型已不可用,请更换!');
+        }
+
+        $video_duration = getProp($data, 'video_duration', 5);
+        $video_resolution = getProp($data, 'video_resolution', '480p');
+        $ratio = getProp($data, 'ratio', '9:16');
+        $generate_audio = getProp($data, 'generate_audio', 1);
+        $seed = getProp($data, 'seed', -1);
+        $first_frame_url = getProp($data, 'first_frame_url');
+        $tail_frame_url = getProp($data, 'tail_frame_url');
+
+        // 参考图(支持数组或JSON字符串,最多9张)
+        $reference_images = getProp($data, 'reference_images', []);
+        if (is_string($reference_images)) {
+            $reference_images = json_decode($reference_images, true) ?: [];
+        }
+        if (!is_array($reference_images)) {
+            Utils::throwError('20003:参考图格式不正确');
+        }
+        $reference_images = array_values(array_unique(array_filter($reference_images)));
+        $reference_images = array_slice($reference_images, 0, 9);
+
+        // 构建视频生成参数(用户参数直接透传,提示词不做处理)
+        $videoParams = [
+            'model' => $model,
+            'prompt' => $prompt,
+            'video_duration' => $video_duration,
+            'video_resolution' => $video_resolution,
+            'seed' => $seed,
+            'ratio' => $ratio,
+            'generate_audio' => (int)$generate_audio === 1 ? true : false,
+            'draft' => getProp($data, 'draft', false),
+            'watermark' => getProp($data, 'watermark', false),
+            'camera_fixed' => getProp($data, 'camera_fixed', false),
+        ];
+
+        if ($first_frame_url) $videoParams['first_frame_url'] = $first_frame_url;
+        if ($tail_frame_url) $videoParams['tail_frame_url'] = $tail_frame_url;
+
+        // 构建content数组
+        $videoParams['content'] = [
+            [
+                'type' => 'text',
+                'text' => $prompt,
+            ]
+        ];
+
+        // 根据模型选择不同的视频生成方法
+        if (strpos($model, 'jimeng') !== false) {
+            // 即梦模型参数调整
+            unset($videoParams['content']); // 即梦不使用content格式
+            $task = $this->aiVideoGenerationService->createJimengTask($videoParams);
+        } elseif (strpos($model, 'kling') !== false) {
+            // 可灵模型参数调整
+            $videoParams['aspect_ratio'] = $videoParams['ratio']; // 可灵使用aspect_ratio
+            unset($videoParams['ratio']);
+            unset($videoParams['content']); // 可灵不使用content格式
+            $task = $this->aiVideoGenerationService->createKelingOmniTask($videoParams);
+        } elseif (strpos($model, 'zhizhen') !== false) {
+            // 智帧等新模型使用统一API
+            $unifiedParams = [
+                'model_code' => $model,
+                'prompt' => $prompt,
+                'video_duration' => $video_duration,
+                'video_resolution' => strtolower($video_resolution),
+                'video_ratio' => $ratio,
+                'seed' => $seed,
+            ];
+
+            // 构建parameters参数
+            $parameters = [
+                'seconds' => $unifiedParams['video_duration'],
+                'resolution' => $unifiedParams['video_resolution'],
+                'ratio' => $ratio,
+                'generate_audio' => (int)$generate_audio === 1 ? true : false,
+            ];
+
+            // 首帧和尾帧
+            if ($first_frame_url) $parameters['first_frame_url'] = $first_frame_url;
+            if ($tail_frame_url) $parameters['last_frame_url'] = $tail_frame_url;
+
+            // 参考图处理
+            if ($reference_images) {
+                $unifiedParams['ref_image_url'] = json_encode($reference_images, 256);
+                $reference_images = $this->aiImageGenerationService->processReferenceImagesToAssets($reference_images, $unifiedParams['prompt']);
+                $parameters['reference_images'] = $reference_images;
+                $parameters['video_mode'] = 'multi_image';
+                $parameters['mode'] = 'multi_image';
+            }
+
+            $unifiedParams['parameters'] = $parameters;
+
+            // 调用统一API创建任务
+            $task = $this->aiVideoGenerationService->createUnifiedApiTask($unifiedParams);
+        } elseif (strpos($model, 'doubao-seedance-2.0') !== false) {
+            // 豆包Seedance 2.0(百度AI网关):图片需要先上传为素材
+            $videoParams['reference_images'] = $reference_images;
+
+            if ($reference_images) {
+                // 参考图上传为百度网关素材
+                $reference_image_assets = $this->aiVideoGenerationService->processReferenceImagesToSeedance20Assets($reference_images, $videoParams['prompt']);
+                $videoParams['content'][0]['text'] = $videoParams['prompt'];
+                $videoParams['reference_image_assets'] = $reference_image_assets;
+            }
+
+            $task = $this->aiVideoGenerationService->createSeedance20Task($videoParams);
+        } else {
+            $task = $this->aiVideoGenerationService->createSeedanceTask($videoParams);
+        }
+
+        return [
+            'task_id' => $task->id,
+            'status' => $task->status,
+            'model' => $model,
+            'video_duration' => $video_duration,
+            'video_resolution' => $video_resolution,
+            'ratio' => $ratio
+        ];
+    }
+
+    /**
      * 预估生成所需积分(mode=chat/image/video,支持多数量总额)
      *
      * chat  : count(默认1,AI对话单次10积分);批量分集可传 anime_id + generate_episode_number 按实际待创建集数估算
@@ -7905,7 +8075,7 @@ class AnimeService
                     }
                     
                     // 根据动态查找的列索引提取数据
-                    $product_name = isset($row[$nameColumnIndex]) ? trim($row[$nameColumnIndex]) : '';
+                    $product_name = isset($row[$nameColumnIndex]) ? trim((string)$row[$nameColumnIndex]) : '';
                     $sequence_str = isset($row[$sequenceColumnIndex]) ? trim($row[$sequenceColumnIndex]) : '';
                     $pic_prompt = '';
                     
@@ -7931,8 +8101,15 @@ class AnimeService
 
                     // 处理product_name两边的符号
                     $product_name = preg_replace('/^[\s*\[\]【】[]{}{}\x{3000}]+|[\s*\[\]【】[]{}{}\x{3000}]+$/u', '', $product_name);
-                    // 如果名称不存在则跳过
-                    if (!$product_name) continue;
+                    // 如果名称不存在或仅剩占位符号,则跳过
+                    if (!$product_name || preg_match('/^[-—_]+$/u', $product_name)) {
+                        dLog('anime')->warning('资产名称无效,跳过保存', [
+                            'script_id' => $script_id,
+                            'type' => $type,
+                            'product_name' => $product_name
+                        ]);
+                        continue;
+                    }
                     
                     // 直接插入新资产到 mp_products 表(parent_id 指向对应类型的根文件夹)
                     $product_id = DB::table('mp_products')->insertGetId([
@@ -7983,6 +8160,12 @@ class AnimeService
 
             // 批量插入映射数据(使用 insertOrIgnore 避免重复插入)
             $insertedCount = DB::table('mp_script_product_mappings')->insertOrIgnore($mappingRecords);
+
+            // 剧本已有资产,同步更新is_products标记
+            DB::table('mp_scripts')->where('id', $script_id)->update([
+                'is_products' => 1,
+                'updated_at' => $now
+            ]);
             
             // 如果需要自动生成图片,在事务中创建图片生成任务
             $productTaskMap = [];
@@ -8683,12 +8866,43 @@ class AnimeService
         $ratio = getProp($data, 'ratio');
         if (!$ratio) $ratio = '9:16';
 
-        return DB::table('mp_anime_episodes')->where('id', $episode_id)->update([
-            'video_model' => $model,
-            'video_resolution' => $video_resolution,
-            'ratio' => $ratio,
-            'updated_at' => date('Y-m-d H:i:s')
-        ]);
+        $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first(['anime_id']);
+        if (!$episode) {
+            Utils::throwError('20003:分集不存在');
+        }
+
+        try {
+            DB::beginTransaction();
+
+            $result = DB::table('mp_anime_episodes')->where('id', $episode_id)->update([
+                'video_model' => $model,
+                'video_resolution' => $video_resolution,
+                'ratio' => $ratio,
+                'updated_at' => date('Y-m-d H:i:s')
+            ]);
+            if ($result === false) {
+                Utils::throwError('20003:分集视频参数保存失败');
+            }
+
+            // 同步更新mp_animes表
+            $anime_result = DB::table('mp_animes')->where('id', $episode->anime_id)->update([
+                'video_model' => $model,
+                'video_resolution' => $video_resolution,
+                'video_ratio' => $ratio,
+                'updated_at' => date('Y-m-d H:i:s')
+            ]);
+            if ($anime_result === false) {
+                Utils::throwError('20003:动漫视频参数保存失败');
+            }
+
+        } catch (\Exception $e) {
+            DB::rollBack();
+            Utils::throwError('20003:'.$e->getMessage());
+        }
+
+        DB::commit();
+
+        return $result;
         
     }
 
@@ -8894,6 +9108,11 @@ class AnimeService
             'title' => getProp($episode, 'title'),
             'episode_number' => getProp($episode, 'episode_number'),
             'is_generated'  => getProp($episode, 'is_generated'),
+            'video_params' => [
+                'video_model' => getProp($episode, 'video_model'),
+                'video_resolution' => getProp($episode, 'video_resolution'),
+                'ratio' => getProp($episode, 'ratio'),
+            ],
             'products' => $products,
             'acts' => $return_acts
         ];

+ 138 - 54
app/Services/DeepSeek/DeepSeekService.php

@@ -285,12 +285,12 @@ class DeepSeekService
         $script_name = pathinfo($originalName, PATHINFO_FILENAME);
         
         if (!$script_name) {
-            $script_name = '剧本_' . date('YmdHis');
+            Utils::throwError('20003:未识别到剧本名,请联系管理员');
         }
         
-        // 如果剧本名称已存在,添加时间戳后缀
-        if (DB::table('mp_scripts')->where('script_name', $script_name)->where('is_deleted', 0)->exists()) {
-            $script_name .= '_' . date('YmdHis');
+        // 如果同一用户的剧本名存在则提示
+        if (DB::table('mp_scripts')->where('script_name', $script_name)->where('user_id', $uid)->where('is_deleted', 0)->exists()) {
+            Utils::throwError('20003:你已上传过该剧本,请修改文件名后上传');
         }
         
         // 插入数据到mp_scripts表
@@ -350,8 +350,23 @@ class DeepSeekService
         $templateId = getProp($data, 'template_id', 0); // 新增: 模板ID
         $script_id = getProp($data, 'script_id', 0); // 新增: 剧本ID
         $sequence = getProp($data, 'sequence', 0); // 新增: 剧集序号
-        $uid = Site::getUid();
         
+        // 新增: 如果提供了script_id,提前校验是否已有生成中的任务
+        $generateTaskId = 0;
+        $uid = Site::getUid();
+        if ($script_id > 0) {
+            $existingTask = DB::table('mp_script_generate_tasks')
+                ->where('uid', $uid)
+                ->where('script_id', $script_id)
+                ->orderByDesc('id')
+                ->first();
+
+            // 如果已有生成中的任务,直接提示,避免前端长时间等待
+            if ($existingTask && in_array(getProp($existingTask, 'status'), ['pending', 'processing'])) {
+                Utils::throwError('20003:该剧本资产正在生成中,请稍后重试');
+            }
+        }
+
         // 新增: 如果提供了script_id,从数据库获取剧本内容作为原文
         $originalContent = '';
         $hasValidScript = false; // 是否已加载到有效剧本(存在且内容非空)
@@ -567,62 +582,102 @@ class DeepSeekService
             }
         }
         
+        // 新增: 如果提供了script_id,创建剧本资产生成任务(所有参数校验通过后创建,避免校验失败留下卡在生成中的任务)
+        if ($script_id > 0) {
+            $uid = Site::getUid();
+            // 创建新的生成任务
+            $generateTaskId = DB::table('mp_script_generate_tasks')->insertGetId([
+                'uid'        => $uid,
+                'script_id'  => $script_id,
+                'sequence'   => $sequence,
+                'status'     => 'processing',
+                'prompt'     => getProp($data, 'prompt', ''),
+                'started_at' => date('Y-m-d H:i:s'),
+                'created_at' => date('Y-m-d H:i:s'),
+                'updated_at' => date('Y-m-d H:i:s')
+            ]);
+        }
+
         // 积分余额预检(仅存在有效剧本时按 chat 类型计费,不足直接报错)
         if ($hasValidScript) {
             $this->pointsService->checkUserPointsEnough(PointsService::CHAT_CHARGE_POINTS, $uid);
         }
 
         // 根据模型类型选择调用方法(复用现有的chatOnly方法)
-        $aiResult = null;
-        if (in_array($model, ['deepseek-reasoner', 'deepseek-chat', 'deepseek-v4-flash', 'deepseek-v4-pro'])) {
-            // DeepSeek 官方模型使用现有的 chatOnly 方法
-            $result = $this->chatOnly($post_data);
-            
-            // 记录实际使用的模型
-            dLog('deepseek')->info('newGenerateText使用DeepSeek模型', ['requested_model' => $model, 'actual_model' => $model]);
-            
-            $aiResult = [
-                'content' => $result['fullContent'],
-                'reasoning_content' => $result['fullReasoningContent'],
-                'usage' => $result['usage'],
-                'model' => $model,
-                'finish_reason' => 'stop'
-            ];
-        } else if (in_array($model, $this->gpt_text_models)) {
-            // GPT 模型使用现有的 gpt54ChatOnly 方法
-            $result = $this->gpt54ChatOnly($post_data);
-            
-            dLog('deepseek')->info('newGenerateText使用GPT模型', ['requested_model' => $model]);
-            
-            $aiResult = [
-                'content' => $result['fullContent'],
-                'reasoning_content' => '',
-                'usage' => $result['usage'],
-                'model' => $model,
-                'finish_reason' => 'stop'
-            ];
-        } else if (in_array($model, $this->valid_text_models)) {
-            // 火山引擎支持的模型使用火山引擎 API(非流式)
-            $post_data['stream'] = false;
-            $result = $this->volcEngineChatCompletion($post_data);
-            
-            dLog('deepseek')->info('newGenerateText使用火山引擎模型', [
-                'requested_model' => $model,
-                'result_keys' => array_keys($result),
-                'has_fullContent' => isset($result['fullContent']),
-                'has_content' => isset($result['content'])
+        try {
+            $aiResult = null;
+            if (in_array($model, ['deepseek-reasoner', 'deepseek-chat', 'deepseek-v4-flash', 'deepseek-v4-pro'])) {
+                // DeepSeek 官方模型使用现有的 chatOnly 方法
+                $result = $this->chatOnly($post_data);
+                
+                // 记录实际使用的模型
+                dLog('deepseek')->info('newGenerateText使用DeepSeek模型', ['requested_model' => $model, 'actual_model' => $model]);
+                
+                $aiResult = [
+                    'content' => $result['fullContent'],
+                    'reasoning_content' => $result['fullReasoningContent'],
+                    'usage' => $result['usage'],
+                    'model' => $model,
+                    'finish_reason' => 'stop'
+                ];
+            } else if (in_array($model, $this->gpt_text_models)) {
+                // GPT 模型使用现有的 gpt54ChatOnly 方法
+                $result = $this->gpt54ChatOnly($post_data);
+                
+                dLog('deepseek')->info('newGenerateText使用GPT模型', ['requested_model' => $model]);
+                
+                $aiResult = [
+                    'content' => $result['fullContent'],
+                    'reasoning_content' => '',
+                    'usage' => $result['usage'],
+                    'model' => $model,
+                    'finish_reason' => 'stop'
+                ];
+            } else if (in_array($model, $this->valid_text_models)) {
+                // 火山引擎支持的模型使用火山引擎 API(非流式)
+                $post_data['stream'] = false;
+                $result = $this->volcEngineChatCompletion($post_data);
+                
+                dLog('deepseek')->info('newGenerateText使用火山引擎模型', [
+                    'requested_model' => $model,
+                    'result_keys' => array_keys($result),
+                    'has_fullContent' => isset($result['fullContent']),
+                    'has_content' => isset($result['content'])
+                ]);
+                
+                // 火山引擎返回的数据结构使用 fullContent 和 fullReasoningContent
+                $aiResult = [
+                    'content' => $result['fullContent'] ?? $result['content'] ?? '',
+                    'reasoning_content' => $result['fullReasoningContent'] ?? $result['reasoning_content'] ?? '',
+                    'usage' => $result['usage'] ?? [],
+                    'model' => $model,
+                    'finish_reason' => $result['finish_reason'] ?? 'stop'
+                ];
+            } else {
+                Utils::throwError('20003:不支持的模型: ' . $model);
+            }
+        } catch (\Exception $e) {
+            // 生成失败,更新任务状态
+            if ($generateTaskId) {
+                DB::table('mp_script_generate_tasks')->where('id', $generateTaskId)->update([
+                    'status'         => 'failed',
+                    'error_message'  => $e->getMessage(),
+                    'completed_at'   => date('Y-m-d H:i:s'),
+                    'updated_at'     => date('Y-m-d H:i:s')
+                ]);
+            }
+            Utils::throwError('20003:' . str_replace(':',':',$e->getMessage()));
+        }
+
+        // 生成成功,更新任务状态
+        if ($generateTaskId && $aiResult) {
+            DB::table('mp_script_generate_tasks')->where('id', $generateTaskId)->update([
+                'status'       => 'success',
+                'result'       => getProp($aiResult, 'content', ''),
+                'completed_at' => date('Y-m-d H:i:s'),
+                'updated_at'   => date('Y-m-d H:i:s')
             ]);
-            
-            // 火山引擎返回的数据结构使用 fullContent 和 fullReasoningContent
-            $aiResult = [
-                'content' => $result['fullContent'] ?? $result['content'] ?? '',
-                'reasoning_content' => $result['fullReasoningContent'] ?? $result['reasoning_content'] ?? '',
-                'usage' => $result['usage'] ?? [],
-                'model' => $model,
-                'finish_reason' => $result['finish_reason'] ?? 'stop'
-            ];
-        } else {
-            Utils::throwError('20003:不支持的模型: ' . $model);
+            $aiResult['generate_task_id'] = $generateTaskId;
         }
 
         // 调用成功且存在有效剧本时,返回结果前扣除积分并记录明细(chat类型10积分)
@@ -702,6 +757,23 @@ class DeepSeekService
         return $aiResult;
     }
 
+    /**
+     * 获取剧本资产生成任务列表(按用户分页,每页15条)
+     *
+     * @param array $data
+     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
+     */
+    public function getScriptGenerateTasks($data) {
+        $uid = Site::getUid();
+
+        return DB::table('mp_script_generate_tasks as t')
+            ->leftJoin('mp_scripts as s', 't.script_id', '=', 's.id')
+            ->where('t.uid', $uid)
+            ->select('t.id', 't.script_id', 's.script_name', 't.sequence', 't.status', 't.error_message', 't.started_at', 't.completed_at')
+            ->orderByDesc('t.id')
+            ->paginate(15);
+    }
+
     public function saveScriptChatHistory($data) {
         $uid = Site::getUid();
         $rid = getProp($data, 'rid');
@@ -2386,11 +2458,17 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
         $uid = Site::getUid();
         $script_id = getProp($data, 'script_id');
         $script_name = getProp($data, 'script_name');
+        $is_products = getProp($data, 'is_products');
         
         $query = DB::table('mp_scripts')->where('is_deleted', 0)->where('user_id', $uid)->select('id as script_id', 'script_name');
         if ($script_id) {
             $query->where('id', $script_id);
         }
+
+        if ($is_products !== '') {
+            $query->where('is_products', $is_products);
+        }
+
         if ($script_name) {
             $query->where('script_name', 'like', "%{$script_name}%");
         }
@@ -9115,6 +9193,12 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
         $episode['scenes'] = $merged_scenes;
         $episode['props'] = $merged_props;
         $episode['end_episode_sequence'] = $end_episode_sequence;
+        // 获取mp_animes表中的视频参数
+        $episode['video_params'] = [
+            'video_model' => getProp($anime, 'video_model'),
+            'video_resolution' => getProp($anime, 'video_resolution'),
+            'ratio' => getProp($anime, 'video_ratio'),
+        ];
         
         // $episode['acts'] = isset($episode_arr['acts']) && is_array($episode_arr['acts']) ? $episode_arr['acts'] : [];
         yield [

+ 31 - 0
app/Transformer/DeepSeek/DeepSeekTransformer.php

@@ -34,6 +34,7 @@ class DeepSeekTransformer
                 'status'                => getProp($item, 'status'),
                 'created_at'            => transDate(getProp($item, 'created_at')),
                 'updated_at'            => transDate(getProp($item, 'updated_at')),
+                'is_products'           => getProp($item, 'is_products'),
                 'episode_content'       => $episode_content,
             ];
         }
@@ -41,5 +42,35 @@ class DeepSeekTransformer
         return $result;
     }
 
+    // 剧本资产生成任务列表
+    public function newBuildScriptGenerateTaskList($data): array
+    {
+        return [
+            'meta'      => getMeta($data),
+            'list'      => $this->newEachScriptGenerateTaskList($data),
+        ];
+    }
+
+    private function newEachScriptGenerateTaskList($list): array
+    {
+        $result = [];
+        if (empty($list)) return $result;
+
+        foreach ($list as $item) {
+            $result[] = [
+                'task_id'       => getProp($item, 'id'),
+                'script_id'     => getProp($item, 'script_id'),
+                'script_name'   => getProp($item, 'script_name'),
+                'sequence'      => getProp($item, 'sequence'),
+                'status'        => getProp($item, 'status'),
+                'error_message' => getProp($item, 'error_message'),
+                'started_at'    => transDate(getProp($item, 'started_at')),
+                'completed_at'  => transDate(getProp($item, 'completed_at')),
+            ];
+        }
+
+        return $result;
+    }
+
     
 }

+ 0 - 32
database/migrations/2026_08_05_000001_add_source_type_to_mp_assets_table.php

@@ -1,32 +0,0 @@
-<?php
-
-use Illuminate\Database\Migrations\Migration;
-use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Support\Facades\Schema;
-
-class AddSourceTypeToMpAssetsTable extends Migration
-{
-    /**
-     * Run the migrations.
-     *
-     * @return void
-     */
-    public function up()
-    {
-        Schema::table('mp_assets', function (Blueprint $table) {
-            $table->string('source_type', 32)->default('zhizhen')->comment('素材来源:zhizhen-智真AI, baidu-百度AI网关')->index()->after('asset_type');
-        });
-    }
-
-    /**
-     * Reverse the migrations.
-     *
-     * @return void
-     */
-    public function down()
-    {
-        Schema::table('mp_assets', function (Blueprint $table) {
-            $table->dropColumn('source_type');
-        });
-    }
-}

+ 2 - 0
routes/api.php

@@ -213,6 +213,7 @@ Route::group(['middleware' => ['bindToken', 'bindExportToken', 'checkLogin']], f
             Route::get('confirmActs', [AnimeController::class, 'confirmActs']);                         // 确认片段分镜
             Route::get('saveActVideoGenerateParams', [AnimeController::class, 'saveActVideoGenerateParams']);   // 保存生成视频参数
             Route::post('createActVideoTask', [AnimeController::class, 'createActVideoTask']);          // 片段转视频
+            Route::post('generateVideo', [AnimeController::class, 'generateVideo']);                     // 文生视频(通用)
             Route::post('batchSetActVideos', [AnimeController::class, 'batchSetActVideos']);            // 片段一键转视频
             Route::post('previewCharge', [AnimeController::class, 'previewCharge']);          // 预估视频生成积分(单一/批量)
             
@@ -223,6 +224,7 @@ Route::group(['middleware' => ['bindToken', 'bindExportToken', 'checkLogin']], f
             Route::post('generateText', [DeepSeekController::class, 'generateText']);    // 文生文(通用)
             Route::post('createGenerateText', [DeepSeekController::class, 'createGenerateText']);   // 新建剧本对话
             Route::post('newGenerateText', [DeepSeekController::class, 'newGenerateText']);    // 文生文(通用-非流式)
+            Route::get('getScriptGenerateTasks', [DeepSeekController::class, 'getScriptGenerateTasks']); // 获取剧本资产生成任务列表
             
             // 监控分镜图片和音频生成任务进度(SSE)
             Route::get('segmentPicsInfo', [AnimeController::class, 'monitorSegmentTasks']);