Просмотр исходного кода

Merge branch 'test' into check_points

lh 1 месяц назад
Родитель
Сommit
e6255fa64e

+ 29 - 3
app/Console/Commands/CheckVideoGenerationTasksCommand.php

@@ -8,6 +8,9 @@ use Illuminate\Support\Facades\DB;
 
 class CheckVideoGenerationTasksCommand extends Command
 {
+    /** @var AIVideoGenerationService */
+    private $videoGenerationService;
+
     /**
      * The name and signature of the console command.
      *
@@ -30,6 +33,8 @@ class CheckVideoGenerationTasksCommand extends Command
      */
     public function handle(AIVideoGenerationService $videoGenerationService)
     {
+        $this->videoGenerationService = $videoGenerationService;
+
         dLog('command')->info('开始检查视频生成任务状态...');
 
         // 执行50s
@@ -169,7 +174,7 @@ class CheckVideoGenerationTasksCommand extends Command
                     
                     $successCount++;
                     
-                    // 更新分镜视频成功后新增对话记录
+                    // 更新分镜视频成功后新增对话记录(addVideoGenerationRecords 内部按 video_task_id 幂等)
                     $this->addVideoGenerationRecords($videoTask, $segmentId, $videoTask->result_url);
                     
                     dLog('command')->info('分镜视频生成成功', [
@@ -197,6 +202,7 @@ class CheckVideoGenerationTasksCommand extends Command
                         'video_task_id' => $videoTaskId,
                         'error' => $videoTask->error_message
                     ]);
+                    $this->videoGenerationService->addVideoGenerationFailedRecord($videoTask, $videoTask->error_message);
                     
                 } else {
                     // 仍在处理中,不更新
@@ -280,7 +286,25 @@ class CheckVideoGenerationTasksCommand extends Command
                 $assistantContent = $segment->act_show_content ? $segment->act_show_content : $segment->act_content;
                 if (!$assistantContent) $assistantContent = '';
             }
-            
+
+            // 同一视频任务只写一次对话记录(仅在有任务ID时按任务幂等,允许多个任务对应同一 act)
+            $videoTaskId = getProp($task, 'id');
+            if (!empty($videoTaskId)) {
+                $exists = DB::table('mp_anime_records')
+                    ->where('video_task_id', $videoTaskId)
+                    ->where('role', 'assistant')
+                    ->exists();
+
+                if ($exists) {
+                    dLog('generate')->info('该视频任务的对话记录已存在,跳过重复写入', [
+                        'video_task_id' => $videoTaskId,
+                        'segment_id_or_act_id' => $segmentIdOrActId,
+                        'video_url' => $videoUrl
+                    ]);
+                    return;
+                }
+            }
+
             // 保存对话记录
             $records = [
                 [
@@ -289,6 +313,7 @@ class CheckVideoGenerationTasksCommand extends Command
                     'sequence'      => $segment->episode_number,
                     'role'          => 'user',
                     'content'       => $userContent,
+                    'video_task_id' => $videoTaskId,
                     $recordField    => $recordValue,
                     'video_url'     => '',
                     'reference_images' => json_encode([], 256),
@@ -301,6 +326,7 @@ class CheckVideoGenerationTasksCommand extends Command
                     'sequence'      => $segment->episode_number,
                     'role'          => 'assistant',
                     'content'       => $assistantContent,
+                    'video_task_id' => $videoTaskId,
                     $recordField    => $recordValue,
                     'video_url'     => $videoUrl,
                     'reference_images' => $reference_images,
@@ -326,4 +352,4 @@ class CheckVideoGenerationTasksCommand extends Command
             ]);
         }
     }
-}
+}

+ 1 - 1
app/Console/Kernel.php

@@ -32,7 +32,7 @@ class Kernel extends ConsoleKernel
         $schedule->command('AIGeneration:checkImgTasks')->everyMinute();
         
         // 检查视频生成任务状态
-        $schedule->command('AIGeneration:checkVideoTasks')->everyMinute();
+        $schedule->command('AIGeneration:checkVideoTasks')->everyMinute()->withoutOverlapping(5);
 
         // 检查动漫对话图片生成状态
         $schedule->command('Anime:checkImgUrl')->everyMinute();

+ 7 - 0
app/Consts/BaseConst.php

@@ -86,6 +86,13 @@ class BaseConst
     const GPT_IMAGE2_MODELS = ['gpt-image-2'];
     const NANO_BANANA_MODELS = ['NanoBanana2', 'NanoBananaPro', 'GptImage2'];
     const GPT_TEXT_MODELS = ['gpt-5.4', 'gpt-5.6-terra'];
+    const GEMINI_TEXT_MODELS = [
+        'gemini-3-pro-preview',
+        'gemini-3-flash-preview',
+        'gemini-3.1-pro-preview',
+        'gemini-3.1-flash-image-preview-t',
+        'gemini-3.6-flash'
+    ];
     // 火山写作奖励计划接入点(使用该接入点可在第二天根据前一天使用量获取免费额度)
     const MODELS_MAP = [
         'doubao-seed-2-0-pro-260215'        => 'ep-20260422093628-pv5vj',

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

@@ -1249,6 +1249,17 @@ class AnimeController extends BaseController
         $data = $request->all();
 
         $result = $this->AnimeService->generateImg($data);
+
+        // 多张图片:data为首图URL,image_urls为全部图片URL数组
+        if (is_array($result) && isset($result['img_url']) && isset($result['image_urls'])) {
+            return $this->respond([
+                'msg'        => '',
+                'code'       => 0,
+                'data'       => $result['img_url'],
+                'image_urls' => $result['image_urls']
+            ]);
+        }
+
         return $this->success($result);
     }
 

+ 16 - 34
app/Libs/Helpers.php

@@ -5245,7 +5245,7 @@ function mapErrorMessage($errorMessage)
 }
 
 /**
- * 将480p视频升级到720p(使用FFmpeg本地超分)
+ * 将480p视频升级到720p(使用FFmpeg本地超分,强制输出720*1280或1280*720分辨率
  * 
  * @param string|resource $videoSource 视频URL地址或视频内容(字符串)
  * @param string $prefix 上传文件夹前缀(如:'video')
@@ -5346,53 +5346,35 @@ function upgrade480pTo720p($videoSource, $prefix = 'video', $isContent = false)
             'json_parsed' => !empty($videoInfo)
         ]);
         
-        // 根据视频方向决定缩放参数,确保短边达到720
-        // 计算缩放比例,使短边至少为720
+        // 根据视频方向决定目标标准分辨率,强制720*1280(竖屏)或1280*720(横屏),不再按比例计算
         if ($width > 0 && $height > 0) {
-            $shortSide = min($width, $height);
-            $longSide = max($width, $height);
-            $targetShortSide = 720;
-            
-            // 计算缩放后的长边尺寸
-            $scale = $targetShortSide / $shortSide;
-            $targetLongSide = round($longSide * $scale);
-            
-            // 确保是偶数(视频编码要求)
-            if ($targetLongSide % 2 !== 0) {
-                $targetLongSide += 1;
-            }
-            
             dLog('video_upgrade')->info('缩放计算详情', [
                 'original' => "{$width}x{$height}",
-                'short_side' => $shortSide,
-                'long_side' => $longSide,
-                'scale_ratio' => $scale,
-                'target_short' => $targetShortSide,
-                'target_long' => $targetLongSide,
+                'target' => $width < $height ? '720x1280' : '1280x720',
                 'is_portrait' => $width < $height ? 'yes' : 'no'
             ]);
             
             if ($width < $height) {
-                // 竖屏:宽度是短边
-                $scaleFilter = "scale={$targetShortSide}:{$targetLongSide}:flags=lanczos";
-                dLog('video_upgrade')->info('检测到竖屏视频,短边(宽)设为720', [
+                // 竖屏:强制720*1280(scale保持宽高比 + pad补边)
+                $scaleFilter = "scale=720:1280:flags=lanczos:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2";
+                dLog('video_upgrade')->info('检测到竖屏视频,强制720*1280', [
                     'original_size' => "{$width}x{$height}",
-                    'target_size' => "{$targetShortSide}x{$targetLongSide}",
+                    'target_size' => '720x1280',
                     'scale_filter' => $scaleFilter
                 ]);
             } else {
-                // 横屏:高度是短边
-                $scaleFilter = "scale={$targetLongSide}:{$targetShortSide}:flags=lanczos";
-                dLog('video_upgrade')->info('检测到横屏视频,短边(高)设为720', [
+                // 横屏:强制1280*720(scale保持宽高比 + pad补边)
+                $scaleFilter = "scale=1280:720:flags=lanczos:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2";
+                dLog('video_upgrade')->info('检测到横屏视频,强制1280*720', [
                     'original_size' => "{$width}x{$height}",
-                    'target_size' => "{$targetLongSide}x{$targetShortSide}",
+                    'target_size' => '1280x720',
                     'scale_filter' => $scaleFilter
                 ]);
             }
         } else {
-            // 无法获取分辨率,使用默认的高度720
-            $scaleFilter = "scale=-2:720:flags=lanczos";
-            dLog('video_upgrade')->warning('无法获取视频分辨率,使用默认缩放');
+            // 无法获取分辨率,默认按横屏1280*720处理
+            $scaleFilter = "scale=1280:720:flags=lanczos:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2";
+            dLog('video_upgrade')->warning('无法获取视频分辨率,默认按1280*720处理');
         }
 
         // 输出文件路径
@@ -5400,7 +5382,7 @@ function upgrade480pTo720p($videoSource, $prefix = 'video', $isContent = false)
 
         // 使用FFmpeg将视频升级到720p,使用高质量的Lanczos算法和锐化滤镜
         // 滤镜说明:
-        // 1. scale: 使用Lanczos算法缩放,保持宽高比,短边720
+        // 1. scale: 使用Lanczos算法缩放并保持宽高比,pad补边到720*1280或1280*720标准分辨率
         // 2. unsharp=5:5:1.0:5:5:0.5: 锐化滤镜,增强细节
         //    - 5:5:1.0 表示亮度锐化(luma_msize_x:luma_msize_y:luma_amount)
         //    - 5:5:0.5 表示色度锐化(chroma_msize_x:chroma_msize_y:chroma_amount)
@@ -5469,7 +5451,7 @@ function upgrade480pTo720p($videoSource, $prefix = 'video', $isContent = false)
         
         dLog('video_upgrade')->info('输出视频分辨率验证', [
             'input_original' => "{$width}x{$height}",
-            'expected' => isset($targetShortSide) ? ($width < $height ? "{$targetShortSide}x{$targetLongSide}" : "{$targetLongSide}x{$targetShortSide}") : '未知',
+            'expected' => ($width > 0 && $height > 0) ? ($width < $height ? '720x1280' : '1280x720') : '1280x720',
             'actual_output' => "{$outputWidth}x{$outputHeight}",
             'success' => ($outputWidth > 0 && $outputHeight > 0)
         ]);

Разница между файлами не показана из-за своего большого размера
+ 669 - 69
app/Services/AIGeneration/AIVideoGenerationService.php


+ 36 - 7
app/Services/Anime/AnimeService.php

@@ -970,6 +970,7 @@ class AnimeService
                     'role'          => 'user',
                     'content'       => $prompt,
                     'segment_id'    => $segment_id,
+                    'pic_task_id'   => $task_id,
                     'image_url'     => '',
                     'created_at'    => $now,
                     'updated_at'    => $now
@@ -981,6 +982,7 @@ class AnimeService
                     'role'          => 'assistant',
                     'content'       => $pic_prompt,
                     'segment_id'    => $segment_id,
+                    'pic_task_id'   => $task_id,
                     'image_url'     => $img_url,
                     'created_at'    => $now,
                     'updated_at'    => $now
@@ -1010,7 +1012,7 @@ class AnimeService
 
         if (!$segment_id) Utils::throwError('20003:请选择分镜!');
 
-        $query = DB::table('mp_anime_records')->where('segment_id', $segment_id)->select('role', 'content', 'segment_id', 'image_url', 'video_url', 'created_at');
+        $query = DB::table('mp_anime_records')->where('segment_id', $segment_id)->select('role', 'content', 'segment_id', 'image_url', 'video_url', 'video_task_id', 'pic_task_id', 'created_at');
         
         $chat = $query->orderBy('id')->get()->map(function ($value) {
             $value = (array)$value;
@@ -2380,6 +2382,7 @@ class AnimeService
                     'role'          => 'user',
                     'content'       => '重新生成',
                     'segment_id'    => $segment_id,
+                    'pic_task_id'   => $task_id,
                     'image_url'     => '',
                     'created_at'    => $now,
                     'updated_at'    => $now
@@ -2391,6 +2394,7 @@ class AnimeService
                     'role'          => 'assistant',
                     'content'       => $pic_prompt,
                     'segment_id'    => $segment_id,
+                    'pic_task_id'   => $task_id,
                     'image_url'     => $img_url,
                     'created_at'    => $now,
                     'updated_at'    => $now
@@ -3335,7 +3339,7 @@ class AnimeService
 
         if (!$act_id) Utils::throwError('20003:请选择片段!');
 
-        $query = DB::table('mp_anime_records')->where('act_id', $act_id)->select('role', 'content', 'act_id', 'image_url', 'video_url', 'reference_images', 'created_at');
+        $query = DB::table('mp_anime_records')->where('act_id', $act_id)->select('role', 'content', 'act_id', 'image_url', 'video_url', 'video_task_id', 'pic_task_id', 'reference_images', 'created_at');
         
         $chat = $query->orderBy('id')->get()->map(function ($value) {
             $value = (array)$value;
@@ -5871,11 +5875,12 @@ class AnimeService
      * 根据 inner_prompt_type 获取生成图片数量
      *
      * @param int $innerPromptType 内置提示词类型
+     * @param int $imageNum 传入的图片张数
      * @return int 图片数量
      */
-    public static function getInnerImageNum($innerPromptType = 0)
+    public static function getInnerImageNum($innerPromptType = 0, $imageNum)
     {
-        return (int)$innerPromptType === 2 ? 9 : 1;
+        return (int)$innerPromptType === 2 ? 9 : $imageNum;
     }
 
     /**
@@ -5908,6 +5913,7 @@ class AnimeService
         $ratio = getProp($data, 'ratio', '9:16');
         $resolution = getProp($data, 'resolution', '2k');
         $inner_prompt_type = getProp($data, 'inner_prompt_type', 0);
+        $imageNum = (int)getProp($data, 'image_num', 1);
         
         // 定义分辨率和宽高比对应的尺寸映射表
         $sizeMap = [
@@ -5996,7 +6002,7 @@ class AnimeService
 
         // 根据 inner_prompt_type 附加内置提示词并确定生成图片数量
         $prompt = self::appendInnerPrompt($prompt, $inner_prompt_type);
-        $imageNum = self::getInnerImageNum($inner_prompt_type);
+        $imageNum = self::getInnerImageNum($inner_prompt_type, $imageNum);
 
         try {
             // 创建图片生成任务
@@ -6027,6 +6033,7 @@ class AnimeService
             $start_time = time();
             $timeout = 300; // 5分钟
             $img_url = '';
+            $img_urls = [];
             
             while (time() - $start_time < $timeout) {
                 sleep(3);
@@ -6037,7 +6044,14 @@ class AnimeService
                     $task = MpGeneratePicTask::where('id', $task_id)->first();
                     
                     if ($task->status === 'success' && $task->result_url) {
-                        $img_url = self::extractResultUrls($task->result_url, $inner_prompt_type);
+                        $resultUrls = is_array($task->result_url) ? $task->result_url : json_decode((string)$task->result_url, true);
+                        $resultUrls = is_array($resultUrls) ? array_values(array_filter($resultUrls)) : [];
+                        if (!empty($resultUrls)) {
+                            $img_urls = $resultUrls;
+                            $img_url = $resultUrls[0];
+                        } else {
+                            $img_url = (string)$task->result_url;
+                        }
                         break;
                     } elseif ($task->status === 'failed') {
                         $errorMsg = mapErrorMessage($task->error_message ?? '');
@@ -6056,7 +6070,14 @@ class AnimeService
                             'result_json' => $statusInfo['result_json'] ?? []
                         ]);
                         
-                        $img_url = self::extractResultUrls($statusInfo['result_url'], $inner_prompt_type);
+                        $resultUrls = is_array($statusInfo['result_url']) ? $statusInfo['result_url'] : json_decode((string)$statusInfo['result_url'], true);
+                        $resultUrls = is_array($resultUrls) ? array_values(array_filter($resultUrls)) : [];
+                        if (!empty($resultUrls)) {
+                            $img_urls = $resultUrls;
+                            $img_url = $resultUrls[0];
+                        } else {
+                            $img_url = (string)$statusInfo['result_url'];
+                        }
                         break;
                     } elseif ($statusInfo['status'] === 'failed') {
                         $task->updateStatus('failed', [
@@ -6076,6 +6097,14 @@ class AnimeService
                 Utils::throwError('20003:图片生成超时,请稍后重试');
             }
             
+            // 多张图片返回全部URL数组(配合控制器输出image_urls字段)
+            if (count($img_urls) > 1) {
+                return [
+                    'img_url'    => $img_url,
+                    'image_urls' => $img_urls
+                ];
+            }
+            
             return $img_url;
             
         } catch (\Exception $e) {

Разница между файлами не показана из-за своего большого размера
+ 970 - 33
app/Services/DeepSeek/DeepSeekService.php


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
resources/views/animeIndex.blade.php