|
@@ -23,938 +23,137 @@ class Index extends Api
|
|
|
$this->success('请求成功');
|
|
$this->success('请求成功');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 图生文接口
|
|
|
|
|
- */
|
|
|
|
|
- public function img_to_txt() {
|
|
|
|
|
- $params = $this->request->param();
|
|
|
|
|
-
|
|
|
|
|
- $prompt = trim($params['prompt']);
|
|
|
|
|
- $old_path = trim($params['path']);
|
|
|
|
|
- $model = trim($params['model']);
|
|
|
|
|
- $status_val = trim($params['status_val']);
|
|
|
|
|
-
|
|
|
|
|
- // 参数验证
|
|
|
|
|
- if (empty($prompt) || empty($old_path) || empty($model)) {
|
|
|
|
|
- return json([
|
|
|
|
|
- 'code' => 1,
|
|
|
|
|
- 'msg' => '缺少必要参数',
|
|
|
|
|
- 'data' => null
|
|
|
|
|
- ]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $aiGateway = new AIGatewayService();
|
|
|
|
|
-
|
|
|
|
|
- // 获取图片的base64数据和MIME类型
|
|
|
|
|
- $imageData = AIGatewayService::file_get_contents($old_path);
|
|
|
|
|
- $base64Data = $imageData['base64Data'];
|
|
|
|
|
- $mimeType = $imageData['mimeType'];
|
|
|
|
|
-
|
|
|
|
|
- $formattedPrompt = "1. 输出语言:所有内容必须为纯简体中文,禁止出现任何英文、拼音、数字、注释、解释性文字、引导语、示例、标点外的特殊符号;
|
|
|
|
|
- 2. 第一步(提取原图产品):
|
|
|
|
|
- - 用1句完整中文描述原图产品,字数控制在50字以内;
|
|
|
|
|
- - 必须包含「主体细节、产品名称、商标、类型、颜色、风格」核心;
|
|
|
|
|
- 3. 第二步(生成新图提示词):
|
|
|
|
|
- - 仅替换【模板提示词】中「产品主体」相关内容为第一步的产品描述;
|
|
|
|
|
- - 严格保留模板中「设计风格、光影、背景、比例、排版」等非产品相关信息;
|
|
|
|
|
- - 完全排除模板中的标题类信息,仅保留提示词核心内容;
|
|
|
|
|
- - 替换后必须完全保留原图产品的核心特征,禁止修改模板非产品核心信息;
|
|
|
|
|
- 4. 输出格式:不允许添加任何解释、引导、说明、示例、备注,仅返回「产品描述 + 替换后提示词」,直接输出纯文本;
|
|
|
|
|
- 【模板提示词】{$prompt}";
|
|
|
|
|
-
|
|
|
|
|
- $result = $aiGateway->callGptApi($model, $formattedPrompt, $mimeType, $base64Data);
|
|
|
|
|
-
|
|
|
|
|
- // Gemini模型响应格式处理
|
|
|
|
|
- $imgToTxtContent = $result['candidates'][0]['content']['parts'][0]['text'];
|
|
|
|
|
-
|
|
|
|
|
- // // 图生文成功后,调用文生文功能(gemini-2.0-flash)
|
|
|
|
|
- $txtToTxtPrompt = "转换成中文格式,去掉其他特殊符号,不允许添加任何解释、引导、说明、示例等文字:\n\n{$imgToTxtContent}";
|
|
|
|
|
- $txtToTxtResult = $aiGateway->txtGptApi($txtToTxtPrompt, 'gemini-2.0-flash');
|
|
|
|
|
- $finalContent = $txtToTxtResult['candidates'][0]['content']['parts'][0]['text'];
|
|
|
|
|
-
|
|
|
|
|
- // 处理调试输出内容
|
|
|
|
|
- $debugContent = json_encode($finalContent, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
|
|
|
- // 去掉特殊符号(只保留字母、数字、中文、常用标点和换行)
|
|
|
|
|
- $debugContent = preg_replace('/[^\p{Han}\w\s\n\r\.,,。!!??\-\::;;]/u', '', $debugContent);
|
|
|
|
|
- // 去掉第一个冒号前的文字
|
|
|
|
|
- $debugContent = preg_replace('/^[^:]+:/', '', $debugContent);
|
|
|
|
|
-
|
|
|
|
|
- return json([
|
|
|
|
|
- 'code' => 0,
|
|
|
|
|
- 'msg' => '处理成功',
|
|
|
|
|
- 'data' => [
|
|
|
|
|
- 'content' => $debugContent,
|
|
|
|
|
- 'english_content' => $imgToTxtContent
|
|
|
|
|
- ]
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 向量引擎配置
|
|
|
|
|
+ private $baseUrl = "https://api.vectorengine.ai/v1";
|
|
|
|
|
+ private $apiKey = "sk-P877pnXMk2erRS2an7qEa3Kdb3rIb7JVAWZ39lhA8HeN71gZ"; // 从控制台获取
|
|
|
|
|
+ private $timeout = 120; // 超时时间(秒),视频生成需要更长
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 文生图接口
|
|
* 文生图接口
|
|
|
|
|
+ * POST /api/index/textToImage
|
|
|
|
|
+ * 参数: prompt (string) 提示词
|
|
|
*/
|
|
*/
|
|
|
- public function txt_to_img() {
|
|
|
|
|
|
|
+ public function textToImage()
|
|
|
|
|
+ {
|
|
|
$params = $this->request->param();
|
|
$params = $this->request->param();
|
|
|
-
|
|
|
|
|
- $prompt = trim($params['prompt']);
|
|
|
|
|
- $model = trim($params['model']);
|
|
|
|
|
- $status_val = trim($params['status_val']);
|
|
|
|
|
- $size = trim($params['size']);
|
|
|
|
|
-
|
|
|
|
|
- // 获取产品信息
|
|
|
|
|
- $product = Db::name('product')->where('id', 1)->find();
|
|
|
|
|
- if (empty($product)) {
|
|
|
|
|
- return json([
|
|
|
|
|
- 'code' => 1,
|
|
|
|
|
- 'msg' => '产品不存在',
|
|
|
|
|
- 'data' => null
|
|
|
|
|
- ]);
|
|
|
|
|
|
|
+ if (empty($params)) {
|
|
|
|
|
+ $this->error('提示词不能为空');
|
|
|
}
|
|
}
|
|
|
- $product_code = $product['product_code'];
|
|
|
|
|
- $product_code_prefix = substr($product_code, 0, 9); // 前九位
|
|
|
|
|
-
|
|
|
|
|
- // 构建URL路径(使用正斜杠)
|
|
|
|
|
- $url_path = '/uploads/merchant/' . $product_code_prefix . '/' . $product_code . '/newimg/';
|
|
|
|
|
- // 构建物理路径(使用正斜杠确保统一格式)
|
|
|
|
|
- $save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'merchant' . '/' . $product_code_prefix . '/' . $product_code . '/' . 'newimg' . '/';
|
|
|
|
|
- // 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
|
|
|
|
|
- $save_path = str_replace('\\', '/', $save_path);
|
|
|
|
|
- // 自动创建文件夹(如果不存在)
|
|
|
|
|
- if (!is_dir($save_path)) {
|
|
|
|
|
- mkdir($save_path, 0755, true);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 调用AI生成图片
|
|
|
|
|
- $aiGateway = new AIGatewayService();
|
|
|
|
|
- $res = $aiGateway->callDalleApi($prompt, $model, $size);
|
|
|
|
|
-
|
|
|
|
|
- // 提取base64图片数据
|
|
|
|
|
- if (isset($res['candidates'][0]['content']['parts'][0]['text'])) {
|
|
|
|
|
- $text_content = $res['candidates'][0]['content']['parts'][0]['text'];
|
|
|
|
|
- // 匹配base64图片数据
|
|
|
|
|
- preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $text_content, $matches);
|
|
|
|
|
- if (empty($matches)) {
|
|
|
|
|
- return json([
|
|
|
|
|
- 'code' => 1,
|
|
|
|
|
- 'msg' => '未找到图片数据',
|
|
|
|
|
- 'data' => null
|
|
|
|
|
- ]);
|
|
|
|
|
- }
|
|
|
|
|
- $image_type = $matches[1];
|
|
|
|
|
- $base64_data = $matches[2];
|
|
|
|
|
-
|
|
|
|
|
- // 解码base64数据
|
|
|
|
|
- $image_data = base64_decode($base64_data);
|
|
|
|
|
- if ($image_data === false) {
|
|
|
|
|
- return json([
|
|
|
|
|
- 'code' => 1,
|
|
|
|
|
- 'msg' => '图片解码失败',
|
|
|
|
|
- 'data' => null
|
|
|
|
|
- ]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 生成唯一文件名(包含扩展名)
|
|
|
|
|
- $file_name = uniqid() . '.' . $image_type;
|
|
|
|
|
- $full_file_path = $save_path . $file_name;
|
|
|
|
|
-
|
|
|
|
|
- // 保存图片到文件系统
|
|
|
|
|
- if (!file_put_contents($full_file_path, $image_data)) {
|
|
|
|
|
- return json([
|
|
|
|
|
- 'code' => 1,
|
|
|
|
|
- 'msg' => '图片保存失败',
|
|
|
|
|
- 'data' => null
|
|
|
|
|
- ]);
|
|
|
|
|
- }
|
|
|
|
|
- // 生成数据库存储路径(使用正斜杠格式)
|
|
|
|
|
- $db_img_path = $url_path . $file_name;
|
|
|
|
|
|
|
|
|
|
- Db::name('product')->where('id', 1)->update(['product_new_img' => $db_img_path]);
|
|
|
|
|
|
|
+ $data = [
|
|
|
|
|
+ "model" => "gemini-3-pro-image-preview", // 可替换为 dall-e-3 等
|
|
|
|
|
+ "prompt" => $params['prompt'],
|
|
|
|
|
+ "n" => 1,
|
|
|
|
|
+ "size" => "1024x1024"
|
|
|
|
|
+ ];
|
|
|
|
|
|
|
|
- return json([
|
|
|
|
|
- 'code' => 0,
|
|
|
|
|
- 'msg' => '文生图请求成功并保存图片',
|
|
|
|
|
- 'data' => [
|
|
|
|
|
- 'img' => $db_img_path,
|
|
|
|
|
- 'product_id' => 1
|
|
|
|
|
- ]
|
|
|
|
|
- ]);
|
|
|
|
|
|
|
+ $result = $this->requestVectorEngine("/images/generations", $data);
|
|
|
|
|
+ if ($result['code'] === 0) {
|
|
|
|
|
+ $this->success('生成成功', $result['data']);
|
|
|
} else {
|
|
} else {
|
|
|
- return json([
|
|
|
|
|
- 'code' => 1,
|
|
|
|
|
- 'msg' => 'AI返回格式错误',
|
|
|
|
|
- 'data' => null
|
|
|
|
|
- ]);
|
|
|
|
|
|
|
+ $this->error($result['msg'], $result['data']);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 图生图:gemini-3-pro-image-preview
|
|
|
|
|
- * 产品图 + 参考模板图 + 提示词 → 生成新图
|
|
|
|
|
- * 接口访问路径:http://mes-ai-api:9091/index.php/api/Index/img_to_img(POST请求)
|
|
|
|
|
- * 参数:product_img, template_img(图片路径如 /uploads/merchant/xxx/xxx.png),prompt(可选),model(可选)
|
|
|
|
|
|
|
+ * 图生文接口
|
|
|
|
|
+ * POST /api/index/imageToText
|
|
|
|
|
+ * 参数: image_url (string) 公网图片URL, prompt (string) 提问指令
|
|
|
*/
|
|
*/
|
|
|
- public function img_to_img()
|
|
|
|
|
|
|
+ public function imageToText()
|
|
|
{
|
|
{
|
|
|
- try {
|
|
|
|
|
- // ========== 1. 基础配置 ==========
|
|
|
|
|
- $apiUrl = 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:generateContent';
|
|
|
|
|
- $apiKey = 'sk-IrIWvqkTs8DwvB9MFBRSWKQHdZRawNeKTnVPHjAJ0KryBWeF';
|
|
|
|
|
-
|
|
|
|
|
- // ========== 2. 获取请求参数(兼容 $_REQUEST,与本地测试脚本一致)==========
|
|
|
|
|
- $params = array_merge($_REQUEST, $this->request->param());
|
|
|
|
|
- $productImgRaw = trim($params['product_img'] ?? '/uploads/merchant/690377511/6903775111138/oldimg/伊利牛奶.png');
|
|
|
|
|
- $templateImgRaw = trim($params['template_img'] ?? '/uploads/template/2026-03-06/69aa45a34161f_20260306111027.png');
|
|
|
|
|
- $customPrompt = trim($params['prompt'] ?? '');
|
|
|
|
|
- if (empty($productImgRaw) || empty($templateImgRaw)) {
|
|
|
|
|
- $this->json_response(['code' => 1, 'msg' => '缺少必要参数:product_img、template_img', 'data' => null]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 将 /uploads/xxx 或 uploads/xxx 转为本地绝对路径
|
|
|
|
|
- $productImgPath = $this->resolveImagePath($productImgRaw);
|
|
|
|
|
- $templateImgPath = $this->resolveImagePath($templateImgRaw);
|
|
|
|
|
-
|
|
|
|
|
- // ========== 3. 提示词 ==========
|
|
|
|
|
- $prompt = $customPrompt ?: '请完成产品模板替换:
|
|
|
|
|
- 1. 从产品图提取产品主体、品牌名称、核心文案;
|
|
|
|
|
- 2. 从模板图继承版式布局、文字排版、色彩风格、背景元素;
|
|
|
|
|
- 3. 将模板图中的产品和文字替换为产品图的内容;
|
|
|
|
|
- 4. 最终生成的图片与模板图视觉风格100%统一,仅替换产品和文字。';
|
|
|
|
|
|
|
+ $imageUrl = $this->request->post('image_url');
|
|
|
|
|
+ $prompt = $this->request->post('prompt', '描述这张图片的内容');
|
|
|
|
|
|
|
|
- // ========== 4. 图片转Base64 ==========
|
|
|
|
|
- $productImg = $this->img_to_base64($productImgPath);
|
|
|
|
|
- if (isset($productImg['error'])) {
|
|
|
|
|
- $this->json_response(['code' => 1, 'msg' => '[步骤1]产品图加载失败:' . $productImg['error'], 'data' => ['path' => $productImgRaw]]);
|
|
|
|
|
|
|
+ if (empty($imageUrl)) {
|
|
|
|
|
+ $this->error('图片URL不能为空');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $templateImg = $this->img_to_base64($templateImgPath);
|
|
|
|
|
- if (isset($templateImg['error'])) {
|
|
|
|
|
- $this->json_response(['code' => 1, 'msg' => '[步骤2]模板图加载失败:' . $templateImg['error'], 'data' => ['path' => $templateImgRaw]]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ========== 5. 构造请求参数 ==========
|
|
|
|
|
- $requestData = [
|
|
|
|
|
- 'contents' => [
|
|
|
|
|
|
|
+ $data = [
|
|
|
|
|
+ "model" => "gpt-4-vision-preview",
|
|
|
|
|
+ "messages" => [
|
|
|
[
|
|
[
|
|
|
- 'role' => 'user',
|
|
|
|
|
- 'parts' => [
|
|
|
|
|
- ['text' => $prompt],
|
|
|
|
|
- ['inlineData' => ['mimeType' => $productImg['mime'], 'data' => $productImg['base64']]],
|
|
|
|
|
- ['inlineData' => ['mimeType' => $templateImg['mime'], 'data' => $templateImg['base64']]]
|
|
|
|
|
|
|
+ "role" => "user",
|
|
|
|
|
+ "content" => [
|
|
|
|
|
+ ["type" => "text", "text" => $prompt],
|
|
|
|
|
+ ["type" => "image_url", "image_url" => ["url" => $imageUrl]]
|
|
|
]
|
|
]
|
|
|
]
|
|
]
|
|
|
],
|
|
],
|
|
|
- 'generationConfig' => [
|
|
|
|
|
- 'responseModalities' => ['IMAGE'],
|
|
|
|
|
- 'imageConfig' => [
|
|
|
|
|
- 'aspectRatio' => '5:4',
|
|
|
|
|
- 'quality' => 'HIGH',
|
|
|
|
|
- 'width' => 1000,
|
|
|
|
|
- 'height' => 800
|
|
|
|
|
- ],
|
|
|
|
|
- 'temperature' => 0.3,
|
|
|
|
|
- 'topP' => 0.8,
|
|
|
|
|
- 'maxOutputTokens' => 2048
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ "max_tokens" => 1000
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
- // ========== 6. 发起CURL请求 ==========
|
|
|
|
|
- $ch = curl_init();
|
|
|
|
|
- curl_setopt_array($ch, [
|
|
|
|
|
- CURLOPT_URL => $apiUrl,
|
|
|
|
|
- CURLOPT_RETURNTRANSFER => true,
|
|
|
|
|
- CURLOPT_POST => true,
|
|
|
|
|
- CURLOPT_POSTFIELDS => json_encode($requestData, JSON_UNESCAPED_UNICODE),
|
|
|
|
|
- CURLOPT_HTTPHEADER => [
|
|
|
|
|
- 'Content-Type: application/json',
|
|
|
|
|
- 'Authorization: Bearer ' . $apiKey
|
|
|
|
|
- ],
|
|
|
|
|
- CURLOPT_TIMEOUT => 300,
|
|
|
|
|
- CURLOPT_SSL_VERIFYPEER => false,
|
|
|
|
|
- CURLOPT_SSL_VERIFYHOST => false
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- $response = curl_exec($ch);
|
|
|
|
|
- $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
|
- $curlErr = curl_error($ch);
|
|
|
|
|
- curl_close($ch);
|
|
|
|
|
-
|
|
|
|
|
- // ========== 7. 错误处理 ==========
|
|
|
|
|
- if ($curlErr) {
|
|
|
|
|
- $this->json_response(['code' => 1, 'msg' => '[步骤3]CURL请求失败:' . $curlErr, 'data' => null]);
|
|
|
|
|
- }
|
|
|
|
|
- if ($httpCode != 200) {
|
|
|
|
|
- $errDetail = is_string($response) ? substr($response, 0, 800) : json_encode($response);
|
|
|
|
|
- $this->json_response(['code' => 1, 'msg' => '[步骤4]API调用失败 HTTP' . $httpCode . ',可能原因:API Key过期/无效、配额不足、请求格式错误。响应:' . $errDetail, 'data' => null]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ========== 8. 解析响应 ==========
|
|
|
|
|
- $result = json_decode($response, true);
|
|
|
|
|
- if (json_last_error() !== JSON_ERROR_NONE) {
|
|
|
|
|
- $this->json_response(['code' => 1, 'msg' => '[步骤5]API响应非JSON格式:' . json_last_error_msg(), 'data' => ['raw' => substr($response, 0, 300)]]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ========== 9. 提取图片数据 ==========
|
|
|
|
|
- $base64Data = null;
|
|
|
|
|
- $imageType = 'png';
|
|
|
|
|
- if (isset($result['candidates'][0]['content']['parts'][0]['inlineData']['data'])) {
|
|
|
|
|
- $base64Data = $result['candidates'][0]['content']['parts'][0]['inlineData']['data'];
|
|
|
|
|
- } elseif (isset($result['candidates'][0]['content']['parts'][0]['text'])) {
|
|
|
|
|
- $text = $result['candidates'][0]['content']['parts'][0]['text'];
|
|
|
|
|
- if (preg_match('/data:image\/(png|jpg|jpeg|webp);base64,([^\s"\']+)/i', $text, $m)) {
|
|
|
|
|
- $imageType = $m[1];
|
|
|
|
|
- $base64Data = $m[2];
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!$base64Data) {
|
|
|
|
|
- $errMsg = isset($result['error']['message']) ? $result['error']['message'] : '响应结构异常';
|
|
|
|
|
- $this->json_response(['code' => 1, 'msg' => '[步骤6]未获取到图片数据。' . $errMsg . '。完整响应见data', 'data' => $result]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ========== 10. 保存图片到 uploads/ceshi/ ==========
|
|
|
|
|
- $imageData = base64_decode($base64Data);
|
|
|
|
|
- if ($imageData === false || strlen($imageData) < 100) {
|
|
|
|
|
- $this->json_response(['code' => 1, 'msg' => '[步骤7]图片Base64解码失败', 'data' => null]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $saveDir = str_replace('\\', '/', ROOT_PATH . 'public/uploads/ceshi/');
|
|
|
|
|
- if (!is_dir($saveDir)) {
|
|
|
|
|
- mkdir($saveDir, 0755, true);
|
|
|
|
|
- }
|
|
|
|
|
- $fileName = 'img2img-' . date('YmdHis') . '-' . uniqid() . '.' . $imageType;
|
|
|
|
|
- $fullPath = $saveDir . $fileName;
|
|
|
|
|
- if (!file_put_contents($fullPath, $imageData)) {
|
|
|
|
|
- $this->json_response(['code' => 1, 'msg' => '[步骤8]图片保存失败,请检查目录权限:' . $saveDir, 'data' => null]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ========== 11. 返回成功响应(返回可访问的Web路径) ==========
|
|
|
|
|
- $webPath = '/uploads/ceshi/' . $fileName;
|
|
|
|
|
- $this->json_response([
|
|
|
|
|
- 'code' => 0,
|
|
|
|
|
- 'msg' => '图生图成功',
|
|
|
|
|
- 'data' => ['image' => $webPath]
|
|
|
|
|
- ]);
|
|
|
|
|
- } catch (\Exception $e) {
|
|
|
|
|
- $this->json_response(['code' => 1, 'msg' => '[步骤9]图生图失败:' . $e->getMessage(), 'data' => null]);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 将接口传入的图片路径转为本地绝对路径(与本地测试脚本逻辑一致)
|
|
|
|
|
- * 支持:/uploads/xxx、uploads/xxx、public/uploads/xxx、或已是绝对路径
|
|
|
|
|
- * @param string $path 接口传入的路径
|
|
|
|
|
- * @return string 本地文件系统绝对路径
|
|
|
|
|
- */
|
|
|
|
|
- private function resolveImagePath($path)
|
|
|
|
|
- {
|
|
|
|
|
- $path = trim($path);
|
|
|
|
|
- if (empty($path)) {
|
|
|
|
|
- return '';
|
|
|
|
|
- }
|
|
|
|
|
- // 已是绝对路径且文件存在,直接返回(兼容本地测试传入的完整路径)
|
|
|
|
|
- $pathNorm = str_replace('\\', '/', $path);
|
|
|
|
|
- if (preg_match('#^[a-zA-Z]:/#', $pathNorm) || (strlen($pathNorm) > 1 && $pathNorm[0] === '/' && $pathNorm[1] !== '/')) {
|
|
|
|
|
- if (file_exists($path)) {
|
|
|
|
|
- return $pathNorm;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- // 统一为相对于 public 的路径:uploads/xxx
|
|
|
|
|
- $relPath = ltrim($pathNorm, '/');
|
|
|
|
|
- if (strpos($relPath, 'public/') === 0) {
|
|
|
|
|
- $relPath = substr($relPath, 7); // 去掉 public/
|
|
|
|
|
- }
|
|
|
|
|
- if (strpos($relPath, 'uploads/') !== 0) {
|
|
|
|
|
- $relPath = 'uploads/' . ltrim($relPath, '/');
|
|
|
|
|
- }
|
|
|
|
|
- $fullPath = rtrim(str_replace('\\', '/', ROOT_PATH), '/') . '/public/' . $relPath;
|
|
|
|
|
- return $fullPath;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 辅助函数:返回JSON响应
|
|
|
|
|
- * @param array $data 响应数据
|
|
|
|
|
- */
|
|
|
|
|
- private function json_response($data)
|
|
|
|
|
- {
|
|
|
|
|
- header('Content-Type: application/json; charset=utf-8');
|
|
|
|
|
- echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
|
|
|
|
- exit;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 辅助函数:图片URL/本地路径转Base64
|
|
|
|
|
- * @param string $imgPath 图片URL/本地路径
|
|
|
|
|
- * @return array 成功 ['mime','base64'] 失败 ['error'=>'错误信息']
|
|
|
|
|
- */
|
|
|
|
|
- private function img_to_base64($imgPath)
|
|
|
|
|
- {
|
|
|
|
|
- $imgContent = @file_get_contents($imgPath);
|
|
|
|
|
- if (!$imgContent) {
|
|
|
|
|
- return ['error' => '图片读取失败,请检查URL可访问性:' . $imgPath];
|
|
|
|
|
- }
|
|
|
|
|
- $finfo = new \finfo(FILEINFO_MIME_TYPE);
|
|
|
|
|
- $mime = $finfo->buffer($imgContent);
|
|
|
|
|
- if (!in_array($mime, ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'])) {
|
|
|
|
|
- return ['error' => '不支持的图片格式:' . $mime . ',仅支持png/jpeg/webp'];
|
|
|
|
|
- }
|
|
|
|
|
- // 规范 MIME:Gemini 要求 image/jpeg 而非 image/jpg
|
|
|
|
|
- if ($mime === 'image/jpg') {
|
|
|
|
|
- $mime = 'image/jpeg';
|
|
|
|
|
- }
|
|
|
|
|
- // 去除 base64 中的空白字符,避免「格式错误」
|
|
|
|
|
- $base64 = preg_replace('/\s+/', '', base64_encode($imgContent));
|
|
|
|
|
- return ['mime' => $mime, 'base64' => $base64];
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 图生图本地测试
|
|
|
|
|
- */
|
|
|
|
|
- public function imgtowimg()
|
|
|
|
|
- {
|
|
|
|
|
- $prompt = $this->request->param('prompt', '');
|
|
|
|
|
- $imgRelPath = 'uploads/operate/ai/Preview/arr/0835006071623.png';
|
|
|
|
|
-
|
|
|
|
|
- $imgPath = ROOT_PATH . 'public/' . $imgRelPath;
|
|
|
|
|
-
|
|
|
|
|
- if (!file_exists($imgPath)) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => '原图不存在:' . $imgRelPath]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $imgData = file_get_contents($imgPath);
|
|
|
|
|
- $base64Img = 'data:image/png;base64,' . base64_encode($imgData);
|
|
|
|
|
-
|
|
|
|
|
- $params = [
|
|
|
|
|
- 'prompt' => $prompt,
|
|
|
|
|
- 'negative_prompt' => '(deformed, distorted, disfigured:1.3), poorly drawn, bad anatomy',
|
|
|
|
|
- 'steps' => 20,
|
|
|
|
|
- 'sampler_name' => 'DPM++ 2M SDE',
|
|
|
|
|
- 'cfg_scale' => 7,
|
|
|
|
|
- 'seed' => -1,
|
|
|
|
|
- 'width' => 1024,
|
|
|
|
|
- 'height' => 1303,
|
|
|
|
|
-
|
|
|
|
|
- 'override_settings' => [
|
|
|
|
|
- 'sd_model_checkpoint' => 'realisticVisionV51_v51VAE-inpainting',
|
|
|
|
|
- 'sd_vae' => 'vae-ft-mse-840000-ema-pruned',
|
|
|
|
|
- 'CLIP_stop_at_last_layers' => 2
|
|
|
|
|
- ],
|
|
|
|
|
- 'clip_skip' => 2,
|
|
|
|
|
-
|
|
|
|
|
- 'alwayson_scripts' => [
|
|
|
|
|
- 'controlnet' => [
|
|
|
|
|
- 'args' => [[
|
|
|
|
|
- 'enabled' => true,
|
|
|
|
|
- 'input_image' => $base64Img,
|
|
|
|
|
- 'module' => 'inpaint_only+lama',
|
|
|
|
|
- 'model' => 'control_v11p_sd15_inpaint_fp16 [be8bc0ed]',
|
|
|
|
|
- 'weight' => 1,
|
|
|
|
|
- 'resize_mode' => 'Resize and Fill',
|
|
|
|
|
- 'pixel_perfect' => false,
|
|
|
|
|
- 'control_mode' => 'ControlNet is more important',
|
|
|
|
|
- 'starting_control_step' => 0,
|
|
|
|
|
- 'ending_control_step' => 1
|
|
|
|
|
- ]]
|
|
|
|
|
- ]
|
|
|
|
|
- ]
|
|
|
|
|
- ];
|
|
|
|
|
-
|
|
|
|
|
- $apiUrl = "http://20.0.17.188:45001/sdapi/v1/txt2img";
|
|
|
|
|
- $headers = ['Content-Type: application/json'];
|
|
|
|
|
-
|
|
|
|
|
- $ch = curl_init();
|
|
|
|
|
- curl_setopt($ch, CURLOPT_URL, $apiUrl);
|
|
|
|
|
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
|
|
|
- curl_setopt($ch, CURLOPT_POST, true);
|
|
|
|
|
- curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
|
|
|
|
- curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params, JSON_UNESCAPED_UNICODE));
|
|
|
|
|
- curl_setopt($ch, CURLOPT_TIMEOUT, 180);
|
|
|
|
|
-
|
|
|
|
|
- $response = curl_exec($ch);
|
|
|
|
|
- $error = curl_error($ch);
|
|
|
|
|
- curl_close($ch);
|
|
|
|
|
-
|
|
|
|
|
- if ($error) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => '请求失败:' . $error]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $data = json_decode($response, true);
|
|
|
|
|
- if (!isset($data['images'][0])) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => '接口未返回图像数据']);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $resultImg = base64_decode($data['images'][0]);
|
|
|
|
|
- $saveDir = ROOT_PATH . 'public/uploads/img2img/';
|
|
|
|
|
- if (!is_dir($saveDir)) {
|
|
|
|
|
- mkdir($saveDir, 0755, true);
|
|
|
|
|
|
|
+ $result = $this->requestVectorEngine("/chat/completions", $data);
|
|
|
|
|
+ if ($result['code'] === 0) {
|
|
|
|
|
+ $this->success('识别成功', $result['data']);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $this->error($result['msg'], $result['data']);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- $originalBaseName = pathinfo($imgRelPath, PATHINFO_FILENAME);
|
|
|
|
|
- $fileName = $originalBaseName . '-' . time() . '-1024x1248.png';
|
|
|
|
|
- $savePath = $saveDir . $fileName;
|
|
|
|
|
- file_put_contents($savePath, $resultImg);
|
|
|
|
|
-
|
|
|
|
|
- return json([
|
|
|
|
|
- 'code' => 0,
|
|
|
|
|
- 'msg' => '图像生成成功',
|
|
|
|
|
- 'data' => [
|
|
|
|
|
- 'origin_url' => '/uploads/img2img/' . $fileName
|
|
|
|
|
- ]
|
|
|
|
|
- ]);
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 后期图像处理-单张图片高清放大处理
|
|
|
|
|
|
|
+ * 文生视频接口
|
|
|
|
|
+ * POST /api/index/textToVideo
|
|
|
|
|
+ * 参数: prompt (string) 提示词, duration (int) 时长(秒), resolution (string) 分辨率
|
|
|
*/
|
|
*/
|
|
|
- public function extra_image()
|
|
|
|
|
|
|
+ public function textToVideo()
|
|
|
{
|
|
{
|
|
|
- // 配置参数
|
|
|
|
|
- $config = [
|
|
|
|
|
- 'input_dir' => 'uploads/img2img/',
|
|
|
|
|
- 'output_dir' => 'uploads/extra_image/',
|
|
|
|
|
- 'api_url' => 'http://20.0.17.188:45001/sdapi/v1/extra-single-image',
|
|
|
|
|
- 'timeout' => 120, // 增加超时时间,高清处理可能耗时较长
|
|
|
|
|
- 'upscale_params' => [
|
|
|
|
|
- 'resize_mode' => 0,
|
|
|
|
|
- 'show_extras_results' => true,
|
|
|
|
|
- 'gfpgan_visibility' => 0, // 人脸修复关闭
|
|
|
|
|
- 'codeformer_visibility' => 0, // 人脸修复关闭
|
|
|
|
|
- 'codeformer_weight' => 0,
|
|
|
|
|
- 'upscaling_resize' => 2.45, // 放大倍数
|
|
|
|
|
- 'upscaling_crop' => true,
|
|
|
|
|
- 'upscaler_1' => 'R-ESRGAN 4x+ Anime6B', // 主放大模型
|
|
|
|
|
- 'upscaler_2' => 'None', // 不使用第二放大器
|
|
|
|
|
- 'extras_upscaler_2_visibility' => 0,
|
|
|
|
|
- 'upscale_first' => false,
|
|
|
|
|
- ]
|
|
|
|
|
- ];
|
|
|
|
|
-
|
|
|
|
|
- // 输入文件处理
|
|
|
|
|
- $imgRelPath = '0835006071623-1757406184-1024x1248.png';
|
|
|
|
|
- $imgPath = ROOT_PATH . 'public/' . $config['input_dir'] . $imgRelPath;
|
|
|
|
|
-
|
|
|
|
|
- if (!file_exists($imgPath)) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => '原图不存在:' . $imgRelPath]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 读取并编码图片
|
|
|
|
|
- try {
|
|
|
|
|
- $imgData = file_get_contents($imgPath);
|
|
|
|
|
- if ($imgData === false) {
|
|
|
|
|
- throw new Exception('无法读取图片文件');
|
|
|
|
|
- }
|
|
|
|
|
- $base64Img = base64_encode($imgData);
|
|
|
|
|
- } catch (Exception $e) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => '图片处理失败:' . $e->getMessage()]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 准备API请求数据
|
|
|
|
|
- $postData = array_merge($config['upscale_params'], ['image' => $base64Img]);
|
|
|
|
|
- $jsonData = json_encode($postData);
|
|
|
|
|
- if ($jsonData === false) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => 'JSON编码失败']);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 调用API进行高清放大
|
|
|
|
|
- $ch = curl_init();
|
|
|
|
|
- curl_setopt_array($ch, [
|
|
|
|
|
- CURLOPT_URL => $config['api_url'],
|
|
|
|
|
- CURLOPT_RETURNTRANSFER => true,
|
|
|
|
|
- CURLOPT_POST => true,
|
|
|
|
|
- CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
|
|
|
|
- CURLOPT_POSTFIELDS => $jsonData,
|
|
|
|
|
- CURLOPT_TIMEOUT => $config['timeout'],
|
|
|
|
|
- CURLOPT_CONNECTTIMEOUT => 30,
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- $response = curl_exec($ch);
|
|
|
|
|
- $error = curl_error($ch);
|
|
|
|
|
- $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
|
- curl_close($ch);
|
|
|
|
|
-
|
|
|
|
|
- if ($error) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => 'API请求失败:' . $error]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if ($httpCode !== 200) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => 'API返回错误状态码:' . $httpCode]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $data = json_decode($response, true);
|
|
|
|
|
- if (json_last_error() !== JSON_ERROR_NONE) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => 'API返回数据解析失败']);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ $prompt = $this->request->post('prompt');
|
|
|
|
|
+ $duration = $this->request->post('duration', 5);
|
|
|
|
|
+ $resolution = $this->request->post('resolution', '720p');
|
|
|
|
|
|
|
|
- if (!isset($data['image']) || empty($data['image'])) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => '接口未返回有效的图像数据']);
|
|
|
|
|
|
|
+ if (empty($prompt)) {
|
|
|
|
|
+ $this->error('提示词不能为空');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 保存处理后的图片
|
|
|
|
|
- try {
|
|
|
|
|
- $resultImg = base64_decode($data['image']);
|
|
|
|
|
- if ($resultImg === false) {
|
|
|
|
|
- throw new Exception('Base64解码失败');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $saveDir = ROOT_PATH . 'public/' . $config['output_dir'];
|
|
|
|
|
- if (!is_dir($saveDir) && !mkdir($saveDir, 0755, true)) {
|
|
|
|
|
- throw new Exception('无法创建输出目录');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $originalBaseName = pathinfo($imgRelPath, PATHINFO_FILENAME);
|
|
|
|
|
- $fileName = $originalBaseName . '-hd.png'; // 使用-hd后缀更明确
|
|
|
|
|
- $savePath = $saveDir . $fileName;
|
|
|
|
|
-
|
|
|
|
|
- if (file_put_contents($savePath, $resultImg) === false) {
|
|
|
|
|
- throw new Exception('无法保存处理后的图片');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 返回成功响应
|
|
|
|
|
- return json([
|
|
|
|
|
- 'code' => 0,
|
|
|
|
|
- 'msg' => '图像高清放大处理成功',
|
|
|
|
|
- 'data' => [
|
|
|
|
|
- 'url' => '/' . $config['output_dir'] . $fileName,
|
|
|
|
|
- 'original_size' => filesize($imgPath),
|
|
|
|
|
- 'processed_size' => filesize($savePath),
|
|
|
|
|
- 'resolution' => getimagesize($savePath), // 返回新图片的分辨率
|
|
|
|
|
- ]
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- } catch (Exception $e) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => '保存结果失败:' . $e->getMessage()]);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 文本生成图片并保存第一张结果
|
|
|
|
|
- * @param array $params 请求参数
|
|
|
|
|
- * @return array 返回结果
|
|
|
|
|
- */
|
|
|
|
|
- public function txttowimg()
|
|
|
|
|
- {
|
|
|
|
|
- // API配置
|
|
|
|
|
- $config = [
|
|
|
|
|
- 'api_url' => 'https://chatapi.onechats.ai/mj/submit/imagine',
|
|
|
|
|
- 'fetch_url' => 'https://chatapi.onechats.ai/mj/task/',
|
|
|
|
|
- 'api_key' => 'sk-iURfrAgzAjhZ4PpPLwzmWIAhM7zKfrkwDvyxk4RVBQ4ouJNK',
|
|
|
|
|
- 'default_prompt' => '一个猫',
|
|
|
|
|
- 'wait_time' => 3 // 等待生成完成的秒数
|
|
|
|
|
|
|
+ $data = [
|
|
|
|
|
+ "model" => "kling-1.6", // 可替换为 seedance-2.0 等
|
|
|
|
|
+ "prompt" => $prompt,
|
|
|
|
|
+ "duration" => (int)$duration,
|
|
|
|
|
+ "resolution" => $resolution
|
|
|
];
|
|
];
|
|
|
- try {
|
|
|
|
|
- // 1. 准备请求数据
|
|
|
|
|
- $prompt = $config['default_prompt'];
|
|
|
|
|
- $postData = [
|
|
|
|
|
- 'botType' => 'MID_JOURNEY',
|
|
|
|
|
- 'prompt' => $prompt,
|
|
|
|
|
- 'base64Array' => [],
|
|
|
|
|
- 'accountFilter' => [
|
|
|
|
|
- 'channelId' => "",
|
|
|
|
|
- 'instanceId' => "",
|
|
|
|
|
- 'modes' => [],
|
|
|
|
|
- 'remark' => "",
|
|
|
|
|
- 'remix' => true,
|
|
|
|
|
- 'remixAutoConsidered' => true
|
|
|
|
|
- ],
|
|
|
|
|
- 'notifyHook' => "",
|
|
|
|
|
- 'state' => ""
|
|
|
|
|
- ];
|
|
|
|
|
|
|
|
|
|
- // 2. 提交生成请求
|
|
|
|
|
- $generateResponse = $this->sendApiRequest($config['api_url'], $postData, $config['api_key']);
|
|
|
|
|
- $generateData = json_decode($generateResponse, true);
|
|
|
|
|
- if (empty($generateData['result'])) {
|
|
|
|
|
- throw new Exception('生成失败: '.($generateData['message'] ?? '未知错误'));
|
|
|
|
|
- }
|
|
|
|
|
- $taskId = $generateData['result'];
|
|
|
|
|
- // 3. 等待图片生成完成
|
|
|
|
|
- sleep($config['wait_time']);
|
|
|
|
|
- // 4. 获取生成结果
|
|
|
|
|
- $fetchUrl = $config['fetch_url'].$taskId.'/fetch';
|
|
|
|
|
- $fetchResponse = $this->sendApiRequest($fetchUrl, [], $config['api_key'], 'GET');
|
|
|
|
|
- $fetchData = json_decode($fetchResponse, true);
|
|
|
|
|
- if (empty($fetchData['imageUrl'])) {
|
|
|
|
|
- throw new Exception('获取图片失败: '.($fetchData['message'] ?? '未知错误'));
|
|
|
|
|
- }
|
|
|
|
|
- // 5. 处理返回的图片数组(取第一张)
|
|
|
|
|
- $imageUrls = is_array($fetchData['imageUrl']) ? $fetchData['imageUrl'] : [$fetchData['imageUrl']];
|
|
|
|
|
- $firstImageUrl = $imageUrls[0];
|
|
|
|
|
- // 6. 保存图片到本地
|
|
|
|
|
- $savePath = $this->saveImage($firstImageUrl);
|
|
|
|
|
- // 7. 返回结果
|
|
|
|
|
- return [
|
|
|
|
|
- 'code' => 200,
|
|
|
|
|
- 'msg' => '图片生成并保存成功',
|
|
|
|
|
- 'data' => [
|
|
|
|
|
- 'local_path' => $savePath,
|
|
|
|
|
- 'web_url' => request()->domain().$savePath,
|
|
|
|
|
- 'task_id' => $taskId
|
|
|
|
|
- ]
|
|
|
|
|
- ];
|
|
|
|
|
- } catch (Exception $e) {
|
|
|
|
|
- // 错误处理
|
|
|
|
|
- return [
|
|
|
|
|
- 'code' => 500,
|
|
|
|
|
- 'msg' => '处理失败: '.$e->getMessage(),
|
|
|
|
|
- 'data' => null
|
|
|
|
|
- ];
|
|
|
|
|
|
|
+ $result = $this->requestVectorEngine("/videos/generations", $data);
|
|
|
|
|
+ if ($result['code'] === 0) {
|
|
|
|
|
+ $this->success('生成成功', $result['data']);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $this->error($result['msg'], $result['data']);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 发送API请求
|
|
|
|
|
- * @param string $url 请求地址
|
|
|
|
|
- * @param array $data 请求数据
|
|
|
|
|
- * @param string $apiKey API密钥
|
|
|
|
|
- * @param string $method 请求方法
|
|
|
|
|
- * @return string 响应内容
|
|
|
|
|
- * @throws Exception
|
|
|
|
|
|
|
+ * 封装向量引擎通用CURL请求
|
|
|
*/
|
|
*/
|
|
|
- private function sendApiRequest($url, $data, $apiKey, $method = 'POST')
|
|
|
|
|
|
|
+ private function requestVectorEngine($endpoint, $data)
|
|
|
{
|
|
{
|
|
|
- $ch = curl_init();
|
|
|
|
|
|
|
+ $url = $this->baseUrl . $endpoint;
|
|
|
|
|
+ $ch = curl_init();
|
|
|
|
|
|
|
|
curl_setopt_array($ch, [
|
|
curl_setopt_array($ch, [
|
|
|
- CURLOPT_URL => $url,
|
|
|
|
|
|
|
+ CURLOPT_URL => $url,
|
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
|
- CURLOPT_CUSTOMREQUEST => $method,
|
|
|
|
|
- CURLOPT_HTTPHEADER => [
|
|
|
|
|
- 'Authorization: Bearer '.$apiKey,
|
|
|
|
|
- 'Accept: application/json',
|
|
|
|
|
- 'Content-Type: application/json'
|
|
|
|
|
|
|
+ CURLOPT_POST => true,
|
|
|
|
|
+ CURLOPT_POSTFIELDS => json_encode($data),
|
|
|
|
|
+ CURLOPT_HTTPHEADER => [
|
|
|
|
|
+ "Content-Type: application/json",
|
|
|
|
|
+ "Authorization: Bearer " . $this->apiKey
|
|
|
],
|
|
],
|
|
|
- CURLOPT_POSTFIELDS => $method === 'POST' ? json_encode($data) : null,
|
|
|
|
|
- CURLOPT_SSL_VERIFYPEER => false,
|
|
|
|
|
- CURLOPT_SSL_VERIFYHOST => false,
|
|
|
|
|
- CURLOPT_TIMEOUT => 60,
|
|
|
|
|
- CURLOPT_FAILONERROR => true
|
|
|
|
|
|
|
+ CURLOPT_SSL_VERIFYPEER => false, // 生产环境建议开启
|
|
|
|
|
+ CURLOPT_TIMEOUT => $this->timeout
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
$response = curl_exec($ch);
|
|
$response = curl_exec($ch);
|
|
|
- $error = curl_error($ch);
|
|
|
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
|
+ $error = curl_error($ch);
|
|
|
curl_close($ch);
|
|
curl_close($ch);
|
|
|
|
|
|
|
|
if ($error) {
|
|
if ($error) {
|
|
|
- throw new Exception('API请求失败: '.$error);
|
|
|
|
|
|
|
+ return ['code' => -1, 'msg' => '请求失败: ' . $error, 'data' => null];
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- if ($httpCode < 200 || $httpCode >= 300) {
|
|
|
|
|
- throw new Exception('API返回错误状态码: '.$httpCode);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return $response;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 保存图片到本地
|
|
|
|
|
- * @param string $imageUrl 图片URL
|
|
|
|
|
- * @return string 本地保存路径
|
|
|
|
|
- * @throws Exception
|
|
|
|
|
- */
|
|
|
|
|
- private function saveImage($imageUrl)
|
|
|
|
|
- {
|
|
|
|
|
- // 1. 创建存储目录
|
|
|
|
|
- $saveDir = ROOT_PATH.'public'.DS.'uploads'.DS.'midjourney'.DS.date('Ymd');
|
|
|
|
|
- if (!is_dir($saveDir)) {
|
|
|
|
|
- mkdir($saveDir, 0755, true);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 2. 生成唯一文件名
|
|
|
|
|
- $filename = uniqid().'.png';
|
|
|
|
|
- $localPath = DS.'uploads'.DS.'midjourney'.DS.date('Ymd').DS.$filename;
|
|
|
|
|
- $fullPath = $saveDir.DS.$filename;
|
|
|
|
|
-
|
|
|
|
|
- // 3. 下载图片
|
|
|
|
|
- $ch = curl_init($imageUrl);
|
|
|
|
|
- curl_setopt_array($ch, [
|
|
|
|
|
- CURLOPT_RETURNTRANSFER => true,
|
|
|
|
|
- CURLOPT_FOLLOWLOCATION => true,
|
|
|
|
|
- CURLOPT_SSL_VERIFYPEER => false,
|
|
|
|
|
- CURLOPT_CONNECTTIMEOUT => 15
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- $imageData = curl_exec($ch);
|
|
|
|
|
- $error = curl_error($ch);
|
|
|
|
|
- curl_close($ch);
|
|
|
|
|
-
|
|
|
|
|
- if (!$imageData) {
|
|
|
|
|
- throw new Exception('图片下载失败: '.$error);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 4. 验证图片类型
|
|
|
|
|
- $imageInfo = getimagesizefromstring($imageData);
|
|
|
|
|
- if (!in_array($imageInfo['mime'] ?? '', ['image/png', 'image/jpeg'])) {
|
|
|
|
|
- throw new Exception('下载内容不是有效图片');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 5. 保存文件
|
|
|
|
|
- if (!file_put_contents($fullPath, $imageData)) {
|
|
|
|
|
- throw new Exception('图片保存失败');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return $localPath;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 将图片背景变为透明
|
|
|
|
|
- * @param string $imagePath 图片路径
|
|
|
|
|
- * @return string 处理后的图片路径
|
|
|
|
|
- */
|
|
|
|
|
- public function makeImageBackgroundTransparent($imagePath) {
|
|
|
|
|
- // 检查图片是否存在
|
|
|
|
|
- if (!file_exists($imagePath)) {
|
|
|
|
|
- return '图片不存在';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 获取图片信息
|
|
|
|
|
- $imageInfo = getimagesize($imagePath);
|
|
|
|
|
- if (!$imageInfo) {
|
|
|
|
|
- return '无法获取图片信息';
|
|
|
|
|
- }
|
|
|
|
|
- $width = $imageInfo[0];
|
|
|
|
|
- $height = $imageInfo[1];
|
|
|
|
|
-
|
|
|
|
|
- // 根据图片类型创建图像资源
|
|
|
|
|
- switch ($imageInfo[2]) {
|
|
|
|
|
- case IMAGETYPE_JPEG:
|
|
|
|
|
- $source = imagecreatefromjpeg($imagePath);
|
|
|
|
|
- break;
|
|
|
|
|
- case IMAGETYPE_PNG:
|
|
|
|
|
- $source = imagecreatefrompng($imagePath);
|
|
|
|
|
- break;
|
|
|
|
|
- case IMAGETYPE_GIF:
|
|
|
|
|
- $source = imagecreatefromgif($imagePath);
|
|
|
|
|
- break;
|
|
|
|
|
- default:
|
|
|
|
|
- return '不支持的图片类型';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!$source) {
|
|
|
|
|
- return '无法创建图像资源';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 创建透明画布
|
|
|
|
|
- $transparent = imagecreatetruecolor($width, $height);
|
|
|
|
|
- if (!$transparent) {
|
|
|
|
|
- imagedestroy($source);
|
|
|
|
|
- return '无法创建透明画布';
|
|
|
|
|
- }
|
|
|
|
|
- imagealphablending($transparent, false);
|
|
|
|
|
- imagesavealpha($transparent, true);
|
|
|
|
|
- $transparentColor = imagecolorallocatealpha($transparent, 255, 255, 255, 127);
|
|
|
|
|
- imagefilledrectangle($transparent, 0, 0, $width, $height, $transparentColor);
|
|
|
|
|
-
|
|
|
|
|
- // 背景色阈值(根据实际图片调整)
|
|
|
|
|
- $bgThreshold = 150; // 降低阈值,提高背景检测灵敏度
|
|
|
|
|
- $colorThreshold = 60; // 颜色差值阈值
|
|
|
|
|
- $saturationThreshold = 20; // 饱和度阈值
|
|
|
|
|
-
|
|
|
|
|
- // 逐像素处理
|
|
|
|
|
- for ($x = 0; $x < $width; $x++) {
|
|
|
|
|
- for ($y = 0; $y < $height; $y++) {
|
|
|
|
|
- $pixelColor = imagecolorat($source, $x, $y);
|
|
|
|
|
- $r = ($pixelColor >> 16) & 0xFF;
|
|
|
|
|
- $g = ($pixelColor >> 8) & 0xFF;
|
|
|
|
|
- $b = $pixelColor & 0xFF;
|
|
|
|
|
-
|
|
|
|
|
- // 计算亮度
|
|
|
|
|
- $brightness = ($r + $g + $b) / 3;
|
|
|
|
|
-
|
|
|
|
|
- // 计算颜色饱和度
|
|
|
|
|
- $max = max($r, $g, $b);
|
|
|
|
|
- $min = min($r, $g, $b);
|
|
|
|
|
- $saturation = ($max > 0) ? (($max - $min) / $max) * 100 : 0;
|
|
|
|
|
-
|
|
|
|
|
- // 计算与背景色的差值(黑色/白色背景)
|
|
|
|
|
- $bgDiff1 = sqrt(pow($r, 2) + pow($g, 2) + pow($b, 2)); // 与黑色的差值
|
|
|
|
|
- $bgDiff2 = sqrt(pow(255 - $r, 2) + pow(255 - $g, 2) + pow(255 - $b, 2)); // 与白色的差值
|
|
|
|
|
- $minBgDiff = min($bgDiff1, $bgDiff2);
|
|
|
|
|
-
|
|
|
|
|
- // 综合判断:亮度较高(纸巾)或颜色与背景差异大的像素保留
|
|
|
|
|
- if ($brightness > $bgThreshold || $minBgDiff > $colorThreshold || $saturation > $saturationThreshold) {
|
|
|
|
|
- // 保留前景像素
|
|
|
|
|
- imagesetpixel($transparent, $x, $y, $pixelColor);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 生成输出路径
|
|
|
|
|
- $saveDir = ROOT_PATH . 'public/uploads/ceshi/';
|
|
|
|
|
- if (!is_dir($saveDir)) {
|
|
|
|
|
- if (!mkdir($saveDir, 0755, true)) {
|
|
|
|
|
- imagedestroy($source);
|
|
|
|
|
- imagedestroy($transparent);
|
|
|
|
|
- return '无法创建保存目录';
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 生成唯一文件名
|
|
|
|
|
- $pathInfo = pathinfo($imagePath);
|
|
|
|
|
- $filename = $pathInfo['filename'] . '_transparent_' . date('YmdHis') . '.png';
|
|
|
|
|
- $outputPath = $saveDir . $filename;
|
|
|
|
|
-
|
|
|
|
|
- // 保存透明图片
|
|
|
|
|
- if (!imagepng($transparent, $outputPath)) {
|
|
|
|
|
- imagedestroy($source);
|
|
|
|
|
- imagedestroy($transparent);
|
|
|
|
|
- return '无法保存透明图片';
|
|
|
|
|
|
|
+ if ($httpCode !== 200) {
|
|
|
|
|
+ return ['code' => $httpCode, 'msg' => 'API返回异常', 'data' => json_decode($response, true)];
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 释放资源
|
|
|
|
|
- imagedestroy($source);
|
|
|
|
|
- imagedestroy($transparent);
|
|
|
|
|
-
|
|
|
|
|
- return $outputPath;
|
|
|
|
|
|
|
+ return ['code' => 0, 'msg' => '成功', 'data' => json_decode($response, true)];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 处理图片背景透明化请求
|
|
|
|
|
- */
|
|
|
|
|
- public function imgimg() {
|
|
|
|
|
- // 使用本地图片路径
|
|
|
|
|
- $localPath = ROOT_PATH . 'public/uploads/zhi.jpg';
|
|
|
|
|
-
|
|
|
|
|
- // 检查图片是否存在
|
|
|
|
|
- if (!file_exists($localPath)) {
|
|
|
|
|
- return json(['code' => 1, 'msg' => '本地图片不存在,请检查文件路径']);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 处理图片背景透明化
|
|
|
|
|
- $result = $this->makeImageBackgroundTransparent($localPath);
|
|
|
|
|
-
|
|
|
|
|
- // 返回结果
|
|
|
|
|
- if (file_exists($result)) {
|
|
|
|
|
- // 生成可访问的URL
|
|
|
|
|
- $relativePath = str_replace(ROOT_PATH . 'public', '', $result);
|
|
|
|
|
- // 确保路径以正斜杠开头
|
|
|
|
|
- if (substr($relativePath, 0, 1) !== '/') {
|
|
|
|
|
- $relativePath = '/' . $relativePath;
|
|
|
|
|
- }
|
|
|
|
|
- $imageUrl = 'http://20.0.16.128:9093' . $relativePath;
|
|
|
|
|
-
|
|
|
|
|
- $res = [
|
|
|
|
|
- 'code' => 0,
|
|
|
|
|
- 'msg' => '处理成功',
|
|
|
|
|
- 'data' => [
|
|
|
|
|
- 'transparent_image' => $imageUrl
|
|
|
|
|
- ]
|
|
|
|
|
- ];
|
|
|
|
|
- } else {
|
|
|
|
|
- $res = [
|
|
|
|
|
- 'code' => 1,
|
|
|
|
|
- 'msg' => $result
|
|
|
|
|
- ];
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return json($res);
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|