|
|
@@ -1,689 +0,0 @@
|
|
|
-<?php
|
|
|
-
|
|
|
-namespace app\api\controller;
|
|
|
-
|
|
|
-use app\common\controller\Api;
|
|
|
-use app\job\SeedreamImageGenerationJob;
|
|
|
-use OSS\OssClient;
|
|
|
-use think\Config;
|
|
|
-use think\Log;
|
|
|
-use think\Queue;
|
|
|
-
|
|
|
-/**
|
|
|
- * 文生图 / 图生图 / 组图生组图(豆包 Seedream)
|
|
|
- */
|
|
|
-class ImageGeneration extends Api
|
|
|
-{
|
|
|
- protected $noNeedLogin = ['*'];
|
|
|
- protected $noNeedRight = ['*'];
|
|
|
-
|
|
|
- const DEFAULT_MODEL = 'doubao-seedream-5-0-260128';
|
|
|
- const DEFAULT_API_URL = 'https://ark.cn-beijing.volces.com/api/v3/images/generations';
|
|
|
-
|
|
|
- /**
|
|
|
- * 文生图 / 图生图 / 组图生组图
|
|
|
- * POST /api/image_generation/generate
|
|
|
- *
|
|
|
- * 参数:
|
|
|
- * - prompt(必填)提示词
|
|
|
- * - image / images / image_url / image_urls / reference_images(可选)参考图,支持字符串或数组,最多 14 张
|
|
|
- * - 上传 image / images 文件(可选,支持多文件;先上传 OSS,再以 OSS URL 作为参考图)
|
|
|
- * - max_images / generate_count / output_count 生成张数,默认 1;大于 1 时走异步队列(seedreamimage)
|
|
|
- * - model 模型,默认 doubao-seedream-5-0-260128
|
|
|
- * - size 尺寸,默认 2K
|
|
|
- * - sequential_image_generation 组图模式,不传时:生成 1 张为 disabled,多张为 auto
|
|
|
- * - stream 是否流式,默认 false(组图队列任务固定非流式)
|
|
|
- * - watermark 是否加水印,默认 false
|
|
|
- *
|
|
|
- * 组图(max_images>1)返回 task_id,请轮询 getStatus 接口获取结果
|
|
|
- *
|
|
|
- * 模式说明(由参考图数量 + 生成数量自动判定):
|
|
|
- * - 0 张参考 + 1 张输出 → text_to_image 文生单图
|
|
|
- * - 0 张参考 + N 张输出 → text_to_images 文生组图
|
|
|
- * - 1 张参考 + 1 张输出 → image_to_image 单图生单图
|
|
|
- * - 1 张参考 + N 张输出 → image_to_images 单图生组图
|
|
|
- * - M 张参考 + N 张输出 → images_to_images 组图生组图
|
|
|
- *
|
|
|
- * AI 调用成功后会写入 picture_storage(img_id、prompt、reference_img_url 等;generate_img_url 暂不写入)
|
|
|
- */
|
|
|
- public function generate()
|
|
|
- {
|
|
|
- set_time_limit(600);
|
|
|
- ini_set('max_execution_time', '600');
|
|
|
-
|
|
|
- try {
|
|
|
- $params = $this->mergeRequestParams();
|
|
|
-
|
|
|
- $prompt = trim((string)($params['prompt'] ?? ''));
|
|
|
- if ($prompt === '') {
|
|
|
- return json(['code' => 0, 'msg' => 'prompt 不能为空']);
|
|
|
- }
|
|
|
-
|
|
|
- $referenceImages = $this->resolveReferenceImages($params);
|
|
|
- $referenceCount = count($referenceImages);
|
|
|
- $genConfig = $this->buildGenerationConfig($params, $referenceCount);
|
|
|
-
|
|
|
- $model = trim((string)($params['model'] ?? self::DEFAULT_MODEL));
|
|
|
- $stream = filter_var($params['stream'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
|
|
- $watermark = filter_var($params['watermark'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
|
|
- $size = trim((string)($params['size'] ?? '2K'));
|
|
|
-
|
|
|
- $requestBody = [
|
|
|
- 'model' => $model,
|
|
|
- 'prompt' => $prompt,
|
|
|
- 'sequential_image_generation' => $genConfig['sequential_image_generation'],
|
|
|
- 'response_format' => 'url',
|
|
|
- 'size' => $size,
|
|
|
- 'stream' => $stream,
|
|
|
- 'watermark' => $watermark,
|
|
|
- ];
|
|
|
- if ($genConfig['sequential_image_generation'] !== 'disabled') {
|
|
|
- $requestBody['sequential_image_generation_options'] = [
|
|
|
- 'max_images' => $genConfig['max_images'],
|
|
|
- ];
|
|
|
- }
|
|
|
- if ($referenceCount > 0) {
|
|
|
- $requestBody['image'] = $referenceCount === 1 ? $referenceImages[0] : $referenceImages;
|
|
|
- }
|
|
|
-
|
|
|
- $taskPayload = [
|
|
|
- 'prompt' => $prompt,
|
|
|
- 'model' => $model,
|
|
|
- 'size' => $size,
|
|
|
- 'watermark' => $watermark,
|
|
|
- 'stream' => $stream,
|
|
|
- 'reference_images' => $referenceImages,
|
|
|
- 'gen_config' => $genConfig,
|
|
|
- 'request_body' => $requestBody,
|
|
|
- ];
|
|
|
-
|
|
|
- if ($genConfig['max_images'] > 1) {
|
|
|
- return $this->submitBatchTask($taskPayload, $genConfig, $referenceImages, $referenceCount);
|
|
|
- }
|
|
|
-
|
|
|
- $payload = SeedreamImageGenerationJob::execute($taskPayload);
|
|
|
- $dbError = (string)($payload['db_save_error'] ?? '');
|
|
|
- unset($payload['db_save_error']);
|
|
|
-
|
|
|
- return $this->jsonGenerateSuccess($payload, $dbError);
|
|
|
- } catch (\Exception $e) {
|
|
|
- $this->logError('[ImageGeneration/generate] ' . $e->getMessage());
|
|
|
- return json(['code' => 0, 'msg' => $e->getMessage()]);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 查询组图异步任务状态
|
|
|
- * GET/POST /api/image_generation/getStatus
|
|
|
- * 参数:task_id
|
|
|
- */
|
|
|
- public function getStatus()
|
|
|
- {
|
|
|
- $taskId = trim((string)$this->request->param('task_id', ''));
|
|
|
- if ($taskId === '') {
|
|
|
- return json(['code' => 0, 'msg' => 'task_id 不能为空']);
|
|
|
- }
|
|
|
-
|
|
|
- $redis = getTaskRedis();
|
|
|
- $raw = $redis->get(SeedreamImageGenerationJob::REDIS_KEY_PREFIX . $taskId);
|
|
|
- if (!$raw) {
|
|
|
- return json(['code' => 0, 'msg' => '任务不存在或已过期']);
|
|
|
- }
|
|
|
-
|
|
|
- $info = json_decode($raw, true);
|
|
|
- if (!is_array($info)) {
|
|
|
- return json(['code' => 0, 'msg' => '任务数据异常']);
|
|
|
- }
|
|
|
-
|
|
|
- return json([
|
|
|
- 'code' => 1,
|
|
|
- 'msg' => '查询成功',
|
|
|
- 'data' => $info,
|
|
|
- ]);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 组图任务提交到独立队列 seedreamimage
|
|
|
- */
|
|
|
- private function submitBatchTask(
|
|
|
- array $taskPayload,
|
|
|
- array $genConfig,
|
|
|
- array $referenceImages,
|
|
|
- int $referenceCount
|
|
|
- ): \think\response\Json {
|
|
|
- $taskId = 'sd' . date('YmdHis') . mt_rand(1000, 9999);
|
|
|
- $taskPayload['task_id'] = $taskId;
|
|
|
- $taskPayload['stream'] = false;
|
|
|
-
|
|
|
- $redis = getTaskRedis();
|
|
|
- $redis->set(
|
|
|
- SeedreamImageGenerationJob::REDIS_KEY_PREFIX . $taskId,
|
|
|
- json_encode([
|
|
|
- 'status' => 'pending',
|
|
|
- 'task_id' => $taskId,
|
|
|
- 'mode' => $genConfig['mode'],
|
|
|
- 'reference_count' => $referenceCount,
|
|
|
- 'reference_images' => $referenceImages,
|
|
|
- 'max_images' => $genConfig['max_images'],
|
|
|
- 'created_at' => date('Y-m-d H:i:s'),
|
|
|
- ], JSON_UNESCAPED_UNICODE),
|
|
|
- ['EX' => SeedreamImageGenerationJob::TASK_TTL]
|
|
|
- );
|
|
|
-
|
|
|
- Queue::push('app\job\SeedreamImageGenerationJob', $taskPayload, SeedreamImageGenerationJob::QUEUE_NAME);
|
|
|
-
|
|
|
- return json([
|
|
|
- 'code' => 1,
|
|
|
- 'msg' => '组图任务已提交,请稍后查询结果',
|
|
|
- 'data' => [
|
|
|
- 'async' => true,
|
|
|
- 'task_id' => $taskId,
|
|
|
- 'status' => 'pending',
|
|
|
- 'mode' => $genConfig['mode'],
|
|
|
- 'reference_count' => $referenceCount,
|
|
|
- 'reference_images' => $referenceImages,
|
|
|
- 'max_images' => $genConfig['max_images'],
|
|
|
- 'poll_url' => '/api/image_generation/getStatus',
|
|
|
- ],
|
|
|
- ]);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * AI 成功但数据库失败时,仍返回生成结果并附带警告
|
|
|
- */
|
|
|
- private function jsonGenerateSuccess(array $payload, string $dbError = ''): \think\response\Json
|
|
|
- {
|
|
|
- if ($dbError !== '') {
|
|
|
- $payload['db_save_error'] = $dbError;
|
|
|
- }
|
|
|
- return json([
|
|
|
- 'code' => 1,
|
|
|
- 'msg' => $dbError !== '' ? '生成成功,但数据库保存失败' : '生成成功',
|
|
|
- 'data' => $payload,
|
|
|
- ]);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 根据前端参数构建组图配置
|
|
|
- */
|
|
|
- private function buildGenerationConfig(array $params, int $referenceCount): array
|
|
|
- {
|
|
|
- $maxImages = (int)($params['max_images'] ?? $params['generate_count'] ?? $params['output_count'] ?? 1);
|
|
|
- $maxImages = max(1, min(15, $maxImages));
|
|
|
-
|
|
|
- $sequentialMode = trim((string)($params['sequential_image_generation'] ?? ''));
|
|
|
- if ($sequentialMode === '') {
|
|
|
- $sequentialMode = $maxImages > 1 ? 'auto' : 'disabled';
|
|
|
- }
|
|
|
-
|
|
|
- return [
|
|
|
- 'max_images' => $maxImages,
|
|
|
- 'sequential_image_generation' => $sequentialMode,
|
|
|
- 'mode' => $this->detectGenerationMode($referenceCount, $maxImages),
|
|
|
- ];
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 判定当前生成模式
|
|
|
- */
|
|
|
- private function detectGenerationMode(int $referenceCount, int $maxImages): string
|
|
|
- {
|
|
|
- if ($referenceCount === 0) {
|
|
|
- return $maxImages > 1 ? 'text_to_images' : 'text_to_image';
|
|
|
- }
|
|
|
- if ($referenceCount === 1) {
|
|
|
- return $maxImages > 1 ? 'image_to_images' : 'image_to_image';
|
|
|
- }
|
|
|
- return $maxImages > 1 ? 'images_to_images' : 'images_to_image';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 解析参考图列表:上传文件/base64 先落 OSS,再以公网 URL 传给 AI
|
|
|
- * @return string[]
|
|
|
- */
|
|
|
- private function resolveReferenceImages(array $params): array
|
|
|
- {
|
|
|
- $resolved = [];
|
|
|
-
|
|
|
- foreach (['image', 'images'] as $field) {
|
|
|
- $uploaded = $this->request->file($field);
|
|
|
- if (empty($uploaded)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- $fileList = is_array($uploaded) ? $uploaded : [$uploaded];
|
|
|
- foreach ($fileList as $file) {
|
|
|
- if (!$file) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- $ossUrl = $this->uploadUploadedFileToOss($file);
|
|
|
- if ($ossUrl !== '') {
|
|
|
- $resolved[] = $ossUrl;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- $rawList = [];
|
|
|
- foreach (['images', 'reference_images', 'image_urls'] as $key) {
|
|
|
- if (empty($params[$key]) || !is_array($params[$key])) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- foreach ($params[$key] as $item) {
|
|
|
- if ($item !== null && $item !== '') {
|
|
|
- $rawList[] = $item;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- foreach (['image', 'image_url'] as $key) {
|
|
|
- if (empty($params[$key])) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- if (is_array($params[$key])) {
|
|
|
- foreach ($params[$key] as $item) {
|
|
|
- if ($item !== null && $item !== '') {
|
|
|
- $rawList[] = $item;
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- $rawList[] = $params[$key];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- foreach ($rawList as $raw) {
|
|
|
- $item = $this->resolveSingleReferenceImage((string)$raw);
|
|
|
- if ($item !== '') {
|
|
|
- $resolved[] = $item;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return array_slice($resolved, 0, 14);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 解析单张参考图(URL/路径直接使用;base64 先上传 OSS)
|
|
|
- */
|
|
|
- private function resolveSingleReferenceImage(string $raw): string
|
|
|
- {
|
|
|
- $raw = trim($raw);
|
|
|
- if ($raw === '') {
|
|
|
- return '';
|
|
|
- }
|
|
|
-
|
|
|
- if (stripos($raw, 'data:image/') === 0) {
|
|
|
- return $this->uploadBase64ImageToOss($raw);
|
|
|
- }
|
|
|
-
|
|
|
- if (stripos($raw, 'http://') === 0 || stripos($raw, 'https://') === 0) {
|
|
|
- return $raw;
|
|
|
- }
|
|
|
-
|
|
|
- if (preg_match('/^[A-Za-z0-9+\/=\s]+$/', $raw) && strlen($raw) > 100) {
|
|
|
- $ossUrl = $this->uploadBase64ImageToOss($raw);
|
|
|
- if ($ossUrl !== '') {
|
|
|
- return $ossUrl;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- $normalizedPath = ltrim(str_replace('\\', '/', $raw), '/');
|
|
|
- $publicUrl = $this->buildOssPublicUrl($normalizedPath);
|
|
|
- if (stripos($publicUrl, 'http://') === 0 || stripos($publicUrl, 'https://') === 0) {
|
|
|
- return $publicUrl;
|
|
|
- }
|
|
|
-
|
|
|
- $localPath = ROOT_PATH . 'public' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $normalizedPath);
|
|
|
- if (is_file($localPath)) {
|
|
|
- return $this->uploadLocalFileToOss($localPath, $normalizedPath);
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- $loaded = $this->readImageBinary($raw);
|
|
|
- $dataUrl = 'data:' . $loaded['mimeType'] . ';base64,' . $loaded['base64Data'];
|
|
|
- return $this->uploadBase64ImageToOss($dataUrl);
|
|
|
- } catch (\Exception $e) {
|
|
|
- $this->logWarning('[ImageGeneration] 读取参考图失败: ' . $e->getMessage());
|
|
|
- return '';
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * form-data 上传文件:落盘 → OSS → 返回公网 URL
|
|
|
- */
|
|
|
- private function uploadUploadedFileToOss($file): string
|
|
|
- {
|
|
|
- if (!$this->isOssEnabled()) {
|
|
|
- throw new \Exception('OSS 未配置,无法上传参考图');
|
|
|
- }
|
|
|
-
|
|
|
- $ext = $this->resolveUploadedImageExt($file);
|
|
|
- if ($ext === '') {
|
|
|
- throw new \Exception('不支持的图片格式');
|
|
|
- }
|
|
|
-
|
|
|
- $dateDir = date('Ymd');
|
|
|
- $saveDir = ROOT_PATH . 'public' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'imagegen' . DIRECTORY_SEPARATOR . $dateDir . DIRECTORY_SEPARATOR;
|
|
|
- if (!is_dir($saveDir)) {
|
|
|
- mkdir($saveDir, 0755, true);
|
|
|
- }
|
|
|
-
|
|
|
- $fileName = 'ref_' . str_replace('.', '', uniqid('', true)) . '.' . $ext;
|
|
|
- $localFullPath = $saveDir . $fileName;
|
|
|
- $saved = false;
|
|
|
-
|
|
|
- if (method_exists($file, 'isValid') && $file->isValid()) {
|
|
|
- $info = $file->move($saveDir, $fileName);
|
|
|
- if ($info) {
|
|
|
- $localFullPath = $saveDir . $info->getFilename();
|
|
|
- $saved = true;
|
|
|
- }
|
|
|
- }
|
|
|
- if (!$saved) {
|
|
|
- $uploadInfo = $file->getInfo();
|
|
|
- $tmpPath = isset($uploadInfo['tmp_name']) ? (string)$uploadInfo['tmp_name'] : '';
|
|
|
- if ($tmpPath === '' || !is_file($tmpPath)) {
|
|
|
- $tmpPath = (string)($file->getRealPath() ?: '');
|
|
|
- }
|
|
|
- if ($tmpPath === '' || !is_file($tmpPath) || !@copy($tmpPath, $localFullPath)) {
|
|
|
- throw new \Exception('参考图保存失败');
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- $objectKey = 'uploads/imagegen/' . $dateDir . '/' . basename($localFullPath);
|
|
|
- return $this->uploadLocalFileToOss($localFullPath, $objectKey);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * base64 图片:落盘 → OSS → 返回公网 URL
|
|
|
- */
|
|
|
- private function uploadBase64ImageToOss(string $base64Input): string
|
|
|
- {
|
|
|
- if (!$this->isOssEnabled()) {
|
|
|
- throw new \Exception('OSS 未配置,无法上传参考图');
|
|
|
- }
|
|
|
-
|
|
|
- $parsed = $this->parseBase64Image($base64Input);
|
|
|
- if ($parsed === null) {
|
|
|
- return '';
|
|
|
- }
|
|
|
-
|
|
|
- [$ext, $imageData] = $parsed;
|
|
|
- $dateDir = date('Ymd');
|
|
|
- $saveDir = ROOT_PATH . 'public' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'imagegen' . DIRECTORY_SEPARATOR . $dateDir . DIRECTORY_SEPARATOR;
|
|
|
- if (!is_dir($saveDir)) {
|
|
|
- mkdir($saveDir, 0755, true);
|
|
|
- }
|
|
|
-
|
|
|
- $fileName = 'ref_' . str_replace('.', '', uniqid('', true)) . '.' . $ext;
|
|
|
- $localFullPath = $saveDir . $fileName;
|
|
|
- if (file_put_contents($localFullPath, $imageData) === false) {
|
|
|
- throw new \Exception('参考图本地保存失败');
|
|
|
- }
|
|
|
-
|
|
|
- $objectKey = 'uploads/imagegen/' . $dateDir . '/' . $fileName;
|
|
|
- return $this->uploadLocalFileToOss($localFullPath, $objectKey);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 本地文件上传 OSS 并返回公网 URL
|
|
|
- */
|
|
|
- private function uploadLocalFileToOss(string $localFullPath, string $objectKey): string
|
|
|
- {
|
|
|
- $objectKey = $this->normalizeOssObjectKey($objectKey);
|
|
|
- if (!$this->putFileToOss($localFullPath, $objectKey)) {
|
|
|
- throw new \Exception('参考图上传 OSS 失败');
|
|
|
- }
|
|
|
-
|
|
|
- $url = $this->buildOssPublicUrl($objectKey);
|
|
|
- if (stripos($url, 'http://') !== 0 && stripos($url, 'https://') !== 0) {
|
|
|
- throw new \Exception('无法获取参考图 OSS 访问地址,请检查 oss.host 配置');
|
|
|
- }
|
|
|
-
|
|
|
- return $url;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 解析 base64 图片
|
|
|
- * @return array{0:string,1:string}|null [扩展名, 二进制内容]
|
|
|
- */
|
|
|
- private function parseBase64Image(string $base64Input): ?array
|
|
|
- {
|
|
|
- $base64Input = trim($base64Input);
|
|
|
- if ($base64Input === '') {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- $ext = 'jpg';
|
|
|
- $rawBase64 = $base64Input;
|
|
|
- $prefix = 'data:image/';
|
|
|
- if (stripos($base64Input, $prefix) === 0) {
|
|
|
- $semi = stripos($base64Input, ';base64,');
|
|
|
- if ($semi === false) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- $mimePart = strtolower(substr($base64Input, strlen($prefix), $semi - strlen($prefix)));
|
|
|
- if ($mimePart === 'jpeg') {
|
|
|
- $mimePart = 'jpg';
|
|
|
- }
|
|
|
- if (in_array($mimePart, ['jpg', 'png', 'gif', 'webp', 'bmp'], true)) {
|
|
|
- $ext = $mimePart;
|
|
|
- }
|
|
|
- $rawBase64 = substr($base64Input, $semi + 8);
|
|
|
- }
|
|
|
-
|
|
|
- $rawBase64 = preg_replace('/\s+/', '', $rawBase64);
|
|
|
- if ($rawBase64 === '') {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- $imageData = base64_decode($rawBase64, true);
|
|
|
- if ($imageData === false || strlen($imageData) < 100) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- return [$ext, $imageData];
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 解析上传图片扩展名
|
|
|
- */
|
|
|
- private function resolveUploadedImageExt($file): string
|
|
|
- {
|
|
|
- $name = $file->getInfo('name') ?? '';
|
|
|
- $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
|
|
- if ($ext === 'jpeg') {
|
|
|
- $ext = 'jpg';
|
|
|
- }
|
|
|
- $allowed = ['jpg', 'png', 'gif', 'webp', 'bmp'];
|
|
|
- if ($ext && in_array($ext, $allowed, true)) {
|
|
|
- return $ext;
|
|
|
- }
|
|
|
-
|
|
|
- $mime = method_exists($file, 'getMime') ? strtolower((string)$file->getMime()) : '';
|
|
|
- $map = [
|
|
|
- 'image/jpeg' => 'jpg',
|
|
|
- 'image/png' => 'png',
|
|
|
- 'image/gif' => 'gif',
|
|
|
- 'image/webp' => 'webp',
|
|
|
- 'image/bmp' => 'bmp',
|
|
|
- ];
|
|
|
-
|
|
|
- return $map[$mime] ?? '';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 读取图片二进制(本地路径 / 完整 URL / OSS 相对路径)
|
|
|
- * @return array{base64Data:string,mimeType:string}
|
|
|
- */
|
|
|
- private function readImageBinary(string $imageUrl): array
|
|
|
- {
|
|
|
- $imageUrl = trim($imageUrl);
|
|
|
- if ($imageUrl === '') {
|
|
|
- throw new \Exception('图片路径不能为空');
|
|
|
- }
|
|
|
-
|
|
|
- $rootPath = str_replace('\\', '/', ROOT_PATH);
|
|
|
- $relativePath = ltrim($imageUrl, '/');
|
|
|
- $localPath = rtrim($rootPath, '/') . '/public/' . $relativePath;
|
|
|
- $localPathDecoded = rtrim($rootPath, '/') . '/public/' . urldecode($relativePath);
|
|
|
-
|
|
|
- $imageContent = false;
|
|
|
- $mimeType = '';
|
|
|
-
|
|
|
- if (preg_match('/^https?:\/\//i', $imageUrl)) {
|
|
|
- $imageContent = @file_get_contents($imageUrl);
|
|
|
- } elseif (file_exists($localPath)) {
|
|
|
- $imageContent = @file_get_contents($localPath);
|
|
|
- if ($imageContent !== false) {
|
|
|
- $finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
|
- $mimeType = finfo_file($finfo, $localPath);
|
|
|
- finfo_close($finfo);
|
|
|
- }
|
|
|
- } elseif (file_exists($localPathDecoded)) {
|
|
|
- $imageContent = @file_get_contents($localPathDecoded);
|
|
|
- if ($imageContent !== false) {
|
|
|
- $finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
|
- $mimeType = finfo_file($finfo, $localPathDecoded);
|
|
|
- finfo_close($finfo);
|
|
|
- }
|
|
|
- } else {
|
|
|
- $ossConfig = $this->getOssConfig();
|
|
|
- $ossHost = trim((string)($ossConfig['host'] ?? ''));
|
|
|
- if ($ossHost !== '') {
|
|
|
- if (stripos($ossHost, 'http://') !== 0 && stripos($ossHost, 'https://') !== 0) {
|
|
|
- $ossHost = 'https://' . $ossHost;
|
|
|
- }
|
|
|
- $remoteUrl = rtrim($ossHost, '/') . '/' . ltrim($relativePath, '/');
|
|
|
- $imageContent = @file_get_contents($remoteUrl);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if ($imageContent === false || $imageContent === '') {
|
|
|
- throw new \Exception('图片内容读取失败(本地/OSS均未读取到)');
|
|
|
- }
|
|
|
-
|
|
|
- if ($mimeType === '') {
|
|
|
- $finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
|
- $mimeType = finfo_buffer($finfo, $imageContent);
|
|
|
- finfo_close($finfo);
|
|
|
- }
|
|
|
- if (!$mimeType) {
|
|
|
- $mimeType = 'image/png';
|
|
|
- }
|
|
|
-
|
|
|
- return [
|
|
|
- 'base64Data' => base64_encode($imageContent),
|
|
|
- 'mimeType' => $mimeType,
|
|
|
- ];
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取 OSS 配置
|
|
|
- */
|
|
|
- private function getOssConfig(): array
|
|
|
- {
|
|
|
- $config = Config::get('oss');
|
|
|
- return is_array($config) ? $config : [];
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * OSS 是否可用
|
|
|
- */
|
|
|
- private function isOssEnabled(): bool
|
|
|
- {
|
|
|
- $config = $this->getOssConfig();
|
|
|
- return !empty($config['accessKeyId'])
|
|
|
- && !empty($config['accessKeySecret'])
|
|
|
- && !empty($config['endpoint'])
|
|
|
- && !empty($config['bucket']);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 归一化 OSS 对象键
|
|
|
- */
|
|
|
- private function normalizeOssObjectKey(string $objectKey): string
|
|
|
- {
|
|
|
- return ltrim(str_replace('\\', '/', trim($objectKey)), '/');
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 上传本地文件到 OSS
|
|
|
- */
|
|
|
- private function putFileToOss(string $localFullPath, string $objectKey): bool
|
|
|
- {
|
|
|
- if (!$this->isOssEnabled() || !is_file($localFullPath)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- $config = $this->getOssConfig();
|
|
|
- $objectKey = $this->normalizeOssObjectKey($objectKey);
|
|
|
- if ($objectKey === '') {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- $ossClient = new OssClient(
|
|
|
- $config['accessKeyId'],
|
|
|
- $config['accessKeySecret'],
|
|
|
- $config['endpoint']
|
|
|
- );
|
|
|
- $ossClient->uploadFile($config['bucket'], $objectKey, $localFullPath);
|
|
|
- return true;
|
|
|
- } catch (\Throwable $e) {
|
|
|
- $this->logError('[ImageGeneration/OSS] ' . $e->getMessage() . ' | objectKey=' . $objectKey);
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 相对路径转 OSS 公网 URL
|
|
|
- */
|
|
|
- private function buildOssPublicUrl(string $path): string
|
|
|
- {
|
|
|
- $path = trim($path);
|
|
|
- if ($path === '' || stripos($path, 'http://') === 0 || stripos($path, 'https://') === 0) {
|
|
|
- return $path;
|
|
|
- }
|
|
|
-
|
|
|
- $config = $this->getOssConfig();
|
|
|
- $host = trim((string)($config['host'] ?? ''));
|
|
|
- if ($host === '') {
|
|
|
- return $path;
|
|
|
- }
|
|
|
- if (stripos($host, 'http://') !== 0 && stripos($host, 'https://') !== 0) {
|
|
|
- $host = 'https://' . $host;
|
|
|
- }
|
|
|
-
|
|
|
- return rtrim($host, '/') . '/' . ltrim($path, '/');
|
|
|
- }
|
|
|
-
|
|
|
- private function logError(string $message): void
|
|
|
- {
|
|
|
- Log::write($message, 'error');
|
|
|
- }
|
|
|
-
|
|
|
- private function logWarning(string $message): void
|
|
|
- {
|
|
|
- Log::write($message, 'warning');
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 合并 JSON Body 与 form 参数
|
|
|
- */
|
|
|
- private function mergeRequestParams(): array
|
|
|
- {
|
|
|
- $params = $this->request->param();
|
|
|
- if (!empty($params['prompt']) || !empty($params['image']) || !empty($params['images'])) {
|
|
|
- return $params;
|
|
|
- }
|
|
|
-
|
|
|
- $content = $this->request->getInput();
|
|
|
- if (!is_string($content) || $content === '') {
|
|
|
- return $params;
|
|
|
- }
|
|
|
-
|
|
|
- $json = json_decode($content, true);
|
|
|
- if (!is_array($json)) {
|
|
|
- return $params;
|
|
|
- }
|
|
|
-
|
|
|
- return array_merge($params, $json);
|
|
|
- }
|
|
|
-}
|