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

1.去除图片数量限制
2.多图时增加图片压缩,优化多图模式大模型入参的结果

lh 19 часов назад
Родитель
Сommit
bef90134c8
1 измененных файлов с 156 добавлено и 19 удалено
  1. 156 19
      app/Services/AIGeneration/AIImageGenerationService.php

+ 156 - 19
app/Services/AIGeneration/AIImageGenerationService.php

@@ -430,11 +430,14 @@ class AIImageGenerationService
                 // 使用原生 cURL 处理 multipart/form-data 格式
                 $imageUrls = is_array($task->ref_img_url) ? $task->ref_img_url : [$task->ref_img_url];
                 
-                // 限制最多4张参考图
-                $imageUrls = array_slice($imageUrls, 0, 4);
+                // 多图上限:暂不限制张数(如需恢复限制,放开下面这行)
+                // $imageUrls = array_slice($imageUrls, 0, 4);
                 
                 dLog('generate')->info('下载参考图片', ['image_urls' => $imageUrls, 'count' => count($imageUrls)]);
                 
+                // 单张参考图超过该大小时压缩,避免请求体过大被网关拒绝
+                $maxRefBytes = 5 * 1024 * 1024;
+
                 // 下载所有参考图片并保存为临时文件
                 $tempFiles = [];
                 foreach ($imageUrls as $index => $imageUrl) {
@@ -446,8 +449,36 @@ class AIImageGenerationService
                     
                     dLog('generate')->info('参考图片下载完成', ['index' => $index, 'size' => strlen($imageContent)]);
                     
-                    // 保存为临时文件(cURL 需要真实文件路径)
                     $extension = pathinfo(parse_url($imageUrl, PHP_URL_PATH), PATHINFO_EXTENSION) ?: 'png';
+
+                    // 超过 5MB 时压缩(公共方法保持原图比例与原格式)
+                    if (strlen($imageContent) > $maxRefBytes) {
+                        $rawSize = strlen($imageContent);
+                        $compressedData = compressRemoteImageUrlToSize($imageUrl, $maxRefBytes);
+
+                        if (!empty($compressedData)) {
+                            $imageContent = $compressedData;
+                            $imgInfo = @getimagesizefromstring($imageContent);
+                            $mime = strtolower($imgInfo['mime'] ?? '');
+                            $extension = $mime === 'image/jpeg' ? 'jpg' : ($mime === 'image/webp' ? 'webp' : 'png');
+                        } else {
+                            // 公共方法对 JPEG 不做有损压缩,回退到本地缩放
+                            $fallbackData = $this->compressReferenceImageForEdit($imageContent, 2048, 90);
+                            if ($fallbackData !== null) {
+                                $imageContent = $fallbackData;
+                                $extension = 'jpg';
+                            }
+                        }
+
+                        dLog('generate')->info('参考图片压缩完成', [
+                            'index' => $index,
+                            'before' => $rawSize,
+                            'after' => strlen($imageContent),
+                            'extension' => $extension,
+                        ]);
+                    }
+
+                    // 保存为临时文件(cURL 需要真实文件路径)
                     $tempFile = tempnam(sys_get_temp_dir(), 'gpt_img_') . '.' . $extension;
                     file_put_contents($tempFile, $imageContent);
                     
@@ -463,8 +494,8 @@ class AIImageGenerationService
                     throw new \Exception('所有参考图片下载失败');
                 }
                 
-                // 构建 POST 字段
-                $postFields = [
+                // 构建 POST 字段(文本字段)
+                $textFields = [
                     'prompt' => $task->prompt,
                     'model' => 'gpt-image-2',
                     'n' => (string)$task->image_num, // 新接口要求字符串类型
@@ -473,27 +504,26 @@ class AIImageGenerationService
                 
                 // 尺寸参数(必填)
                 if ($task->width && $task->height) {
-                    $postFields['size'] = $task->width . 'x' . $task->height;
+                    $textFields['size'] = $task->width . 'x' . $task->height;
                 } else {
-                    $postFields['size'] = '1024x1024'; // 默认尺寸
+                    $textFields['size'] = '1024x1024'; // 默认尺寸
                 }
                 
-                // 添加参考图片文件(支持多图,新接口要求使用 image 字段名)
-                foreach ($tempFiles as $index => $tempFile) {
-                    $fieldName = 'image'; // 新接口统一使用 image 字段名
-                    $postFields[$fieldName] = new \CURLFile(
-                        $tempFile['path'], 
-                        'image/' . $tempFile['extension'], 
-                        'image' . ($index + 1) . '.' . $tempFile['extension']
-                    );
-                }
+                // 多张参考图需要在 multipart 中出现重复的 image[] 字段;
+                // PHP 数组无法承载重复 key(同名会覆盖),因此手动拼装请求体。
+                // 单张参考图沿用 image 字段,避免影响已有单图任务。
+                $fileFieldName = count($tempFiles) > 1 ? 'image[]' : 'image';
+                $boundary = '----MpGptImage' . md5(uniqid('', true));
+                $postFields = $this->buildMultipartBody($textFields, $tempFiles, $boundary, $fileFieldName);
                 
                 dLog('generate')->info('GPT-Image2 API参数(编辑模式-cURL)', [
                     'api_url' => $api_url, 
                     'prompt' => $task->prompt, 
-                    'size' => $postFields['size'], 
-                    'n' => $postFields['n'],
-                    'response_format' => $postFields['response_format'],
+                    'size' => $textFields['size'], 
+                    'n' => $textFields['n'],
+                    'response_format' => $textFields['response_format'],
+                    'file_field' => $fileFieldName,
+                    'body_size' => strlen($postFields),
                     'image_count' => count($tempFiles)
                 ]);
                 logDB('generate', 'info', 'GPT-Image2任务提交(编辑模式)', [
@@ -515,6 +545,8 @@ class AIImageGenerationService
                     CURLOPT_HTTPHEADER => [
                         'Authorization: Bearer ' . $apiKey,
                         'Accept: application/json',
+                        'Content-Type: multipart/form-data; boundary=' . $boundary,
+                        'Content-Length: ' . strlen($postFields),
                         'Expect:', // 移除 Expect: 100-continue,避免等待
                     ],
                     CURLOPT_TIMEOUT => 600,
@@ -635,6 +667,111 @@ class AIImageGenerationService
     }
 
     /**
+     * 压缩参考图片(兜底方案,用于公共压缩方法不处理的场景,如大尺寸 JPEG)
+     *
+     * 缩放到长边不超过 $maxSide,并统一输出 JPEG(透明背景填白)。
+     *
+     * @param string $imageStream 图片二进制数据
+     * @param int    $maxSide     长边像素上限
+     * @param int    $quality     JPEG 质量
+     * @return string|null 压缩后的二进制数据,失败返回 null
+     */
+    private function compressReferenceImageForEdit(string $imageStream, int $maxSide = 1536, int $quality = 90): ?string
+    {
+        try {
+            $imgInfo = @getimagesizefromstring($imageStream);
+            if (!$imgInfo) {
+                return null;
+            }
+
+            $srcImg = @imagecreatefromstring($imageStream);
+            if (!$srcImg) {
+                return null;
+            }
+
+            $origW = imagesx($srcImg);
+            $origH = imagesy($srcImg);
+            $scale = $maxSide / max($origW, $origH);
+
+            if ($scale < 1) {
+                $newW = max(1, (int)round($origW * $scale));
+                $newH = max(1, (int)round($origH * $scale));
+                $resizedImg = imagecreatetruecolor($newW, $newH);
+                // JPEG 不支持透明通道,用白底填充
+                $white = imagecolorallocate($resizedImg, 255, 255, 255);
+                imagefill($resizedImg, 0, 0, $white);
+                imagecopyresampled($resizedImg, $srcImg, 0, 0, 0, 0, $newW, $newH, $origW, $origH);
+                imagedestroy($srcImg);
+                $srcImg = $resizedImg;
+            } elseif ($imgInfo['mime'] !== 'image/jpeg') {
+                // 未缩放但非 JPEG:铺白底再输出,避免透明区域变黑
+                $flatImg = imagecreatetruecolor($origW, $origH);
+                $white = imagecolorallocate($flatImg, 255, 255, 255);
+                imagefill($flatImg, 0, 0, $white);
+                imagecopy($flatImg, $srcImg, 0, 0, 0, 0, $origW, $origH);
+                imagedestroy($srcImg);
+                $srcImg = $flatImg;
+            }
+
+            ob_start();
+            imagejpeg($srcImg, null, $quality);
+            $compressed = ob_get_clean();
+            imagedestroy($srcImg);
+
+            return ($compressed === false || $compressed === '') ? null : $compressed;
+        } catch (\Throwable $e) {
+            dLog('generate')->warning('参考图片压缩失败,使用原图', ['error' => $e->getMessage()]);
+            return null;
+        }
+    }
+
+    /**
+     * 构建 multipart/form-data 请求体
+     *
+     * 参考图字段需要在请求体中出现多次(image[]),而 PHP 数组同一个 key 只能保留最后一个值,
+     * 用 CURLOPT_POSTFIELDS 传数组会导致多张参考图互相覆盖,所以这里手动拼装请求体字符串。
+     *
+     * @param array  $textFields    普通文本字段 [name => value]
+     * @param array  $files         文件字段,每项含 path、extension
+     * @param string $boundary      multipart 分隔符
+     * @param string $fileFieldName 文件字段名,多图时传 image[],单图传 image
+     * @return string
+     */
+    private function buildMultipartBody(array $textFields, array $files, string $boundary, string $fileFieldName = 'image[]'): string
+    {
+        $eol = "\r\n";
+        $body = '';
+
+        foreach ($textFields as $name => $value) {
+            $body .= '--' . $boundary . $eol;
+            $body .= 'Content-Disposition: form-data; name="' . $name . '"' . $eol . $eol;
+            $body .= (string)$value . $eol;
+        }
+
+        foreach ($files as $index => $file) {
+            $extension = $file['extension'];
+            $filename = 'image' . ($index + 1) . '.' . $extension;
+            $mime = strtolower($extension) === 'jpg' ? 'image/jpeg' : 'image/' . $extension;
+
+            $content = @file_get_contents($file['path']);
+            if ($content === false) {
+                throw new \Exception('读取参考图片临时文件失败: ' . $file['path']);
+            }
+
+            $body .= '--' . $boundary . $eol;
+            $body .= 'Content-Disposition: form-data; name="' . $fileFieldName . '"; filename="' . $filename . '"' . $eol;
+            $body .= 'Content-Type: ' . $mime . $eol . $eol;
+            $body .= $content . $eol;
+
+            unset($content);
+        }
+
+        $body .= '--' . $boundary . '--' . $eol;
+
+        return $body;
+    }
+
+    /**
      * 处理GPT-Image2 API响应结果
      *
      * @param MpGeneratePicTask $task