Index.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  1. <?php
  2. namespace app\api\controller;
  3. use app\common\controller\Api;
  4. use app\service\AIGatewayService;
  5. use think\Db;
  6. use think\Exception;
  7. /**
  8. * 首页接口
  9. */
  10. class Index extends Api
  11. {
  12. protected $noNeedLogin = ['*'];
  13. protected $noNeedRight = ['*'];
  14. /**
  15. * 首页
  16. */
  17. public function index()
  18. {
  19. $this->success('请求成功');
  20. }
  21. /**
  22. * 图生文接口
  23. */
  24. public function img_to_txt() {
  25. $params = $this->request->param();
  26. $prompt = trim($params['prompt']);
  27. $old_path = trim($params['path']);
  28. $model = trim($params['model']);
  29. $status_val = trim($params['status_val']);
  30. // 参数验证
  31. if (empty($prompt) || empty($old_path) || empty($model)) {
  32. return json([
  33. 'code' => 1,
  34. 'msg' => '缺少必要参数',
  35. 'data' => null
  36. ]);
  37. }
  38. $aiGateway = new AIGatewayService();
  39. // 获取图片的base64数据和MIME类型
  40. $imageData = AIGatewayService::file_get_contents($old_path);
  41. $base64Data = $imageData['base64Data'];
  42. $mimeType = $imageData['mimeType'];
  43. $formattedPrompt = "1. 输出语言:所有内容必须为纯简体中文,禁止出现任何英文、拼音、数字、注释、解释性文字、引导语、示例、标点外的特殊符号;
  44. 2. 第一步(提取原图产品):
  45. - 用1句完整中文描述原图产品,字数控制在50字以内;
  46. - 必须包含「主体细节、产品名称、商标、类型、颜色、风格」核心;
  47. 3. 第二步(生成新图提示词):
  48. - 仅替换【模板提示词】中「产品主体」相关内容为第一步的产品描述;
  49. - 严格保留模板中「设计风格、光影、背景、比例、排版」等非产品相关信息;
  50. - 完全排除模板中的标题类信息,仅保留提示词核心内容;
  51. - 替换后必须完全保留原图产品的核心特征,禁止修改模板非产品核心信息;
  52. 4. 输出格式:不允许添加任何解释、引导、说明、示例、备注,仅返回「产品描述 + 替换后提示词」,直接输出纯文本;
  53. 【模板提示词】{$prompt}";
  54. $result = $aiGateway->callGptApi($model, $formattedPrompt, $mimeType, $base64Data);
  55. // Gemini模型响应格式处理
  56. $imgToTxtContent = $result['candidates'][0]['content']['parts'][0]['text'];
  57. // // 图生文成功后,调用文生文功能(gemini-2.0-flash)
  58. $txtToTxtPrompt = "转换成中文格式,去掉其他特殊符号,不允许添加任何解释、引导、说明、示例等文字:\n\n{$imgToTxtContent}";
  59. $txtToTxtResult = $aiGateway->txtGptApi($txtToTxtPrompt, 'gemini-2.0-flash');
  60. $finalContent = $txtToTxtResult['candidates'][0]['content']['parts'][0]['text'];
  61. // 处理调试输出内容
  62. $debugContent = json_encode($finalContent, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
  63. // 去掉特殊符号(只保留字母、数字、中文、常用标点和换行)
  64. $debugContent = preg_replace('/[^\p{Han}\w\s\n\r\.,,。!!??\-\::;;]/u', '', $debugContent);
  65. // 去掉第一个冒号前的文字
  66. $debugContent = preg_replace('/^[^:]+:/', '', $debugContent);
  67. return json([
  68. 'code' => 0,
  69. 'msg' => '处理成功',
  70. 'data' => [
  71. 'content' => $debugContent,
  72. 'english_content' => $imgToTxtContent
  73. ]
  74. ]);
  75. }
  76. /**
  77. * 文生图接口
  78. */
  79. public function txt_to_img() {
  80. $params = $this->request->param();
  81. $prompt = trim($params['prompt']);
  82. $model = trim($params['model']);
  83. $status_val = trim($params['status_val']);
  84. $size = trim($params['size']);
  85. // 获取产品信息
  86. $product = Db::name('product')->where('id', 1)->find();
  87. if (empty($product)) {
  88. return json([
  89. 'code' => 1,
  90. 'msg' => '产品不存在',
  91. 'data' => null
  92. ]);
  93. }
  94. $product_code = $product['product_code'];
  95. $product_code_prefix = substr($product_code, 0, 9); // 前九位
  96. // 构建URL路径(使用正斜杠)
  97. $url_path = '/uploads/merchant/' . $product_code_prefix . '/' . $product_code . '/newimg/';
  98. // 构建物理路径(使用正斜杠确保统一格式)
  99. $save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'merchant' . '/' . $product_code_prefix . '/' . $product_code . '/' . 'newimg' . '/';
  100. // 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
  101. $save_path = str_replace('\\', '/', $save_path);
  102. // 自动创建文件夹(如果不存在)
  103. if (!is_dir($save_path)) {
  104. mkdir($save_path, 0755, true);
  105. }
  106. // 调用AI生成图片
  107. $aiGateway = new AIGatewayService();
  108. $res = $aiGateway->callDalleApi($prompt, $model, $size);
  109. // 提取base64图片数据
  110. if (isset($res['candidates'][0]['content']['parts'][0]['text'])) {
  111. $text_content = $res['candidates'][0]['content']['parts'][0]['text'];
  112. // 匹配base64图片数据
  113. preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $text_content, $matches);
  114. if (empty($matches)) {
  115. return json([
  116. 'code' => 1,
  117. 'msg' => '未找到图片数据',
  118. 'data' => null
  119. ]);
  120. }
  121. $image_type = $matches[1];
  122. $base64_data = $matches[2];
  123. // 解码base64数据
  124. $image_data = base64_decode($base64_data);
  125. if ($image_data === false) {
  126. return json([
  127. 'code' => 1,
  128. 'msg' => '图片解码失败',
  129. 'data' => null
  130. ]);
  131. }
  132. // 生成唯一文件名(包含扩展名)
  133. $file_name = uniqid() . '.' . $image_type;
  134. $full_file_path = $save_path . $file_name;
  135. // 保存图片到文件系统
  136. if (!file_put_contents($full_file_path, $image_data)) {
  137. return json([
  138. 'code' => 1,
  139. 'msg' => '图片保存失败',
  140. 'data' => null
  141. ]);
  142. }
  143. // 生成数据库存储路径(使用正斜杠格式)
  144. $db_img_path = $url_path . $file_name;
  145. Db::name('product')->where('id', 1)->update(['product_new_img' => $db_img_path]);
  146. return json([
  147. 'code' => 0,
  148. 'msg' => '文生图请求成功并保存图片',
  149. 'data' => [
  150. 'img' => $db_img_path,
  151. 'product_id' => 1
  152. ]
  153. ]);
  154. } else {
  155. return json([
  156. 'code' => 1,
  157. 'msg' => 'AI返回格式错误',
  158. 'data' => null
  159. ]);
  160. }
  161. }
  162. /**
  163. * 图生图:gemini-3-pro-image-preview
  164. * 产品图 + 参考模板图 + 提示词 → 生成新图
  165. * 接口访问路径:http://mes-ai-api:9091/index.php/api/Index/img_to_img(POST请求)
  166. * 参数:product_img, template_img(图片路径如 /uploads/merchant/xxx/xxx.png),prompt(可选),model(可选)
  167. */
  168. public function img_to_img()
  169. {
  170. try {
  171. // ========== 1. 基础配置 ==========
  172. $apiUrl = 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:generateContent';
  173. $apiKey = 'sk-IrIWvqkTs8DwvB9MFBRSWKQHdZRawNeKTnVPHjAJ0KryBWeF';
  174. // ========== 2. 获取请求参数(兼容 $_REQUEST,与本地测试脚本一致)==========
  175. $params = array_merge($_REQUEST, $this->request->param());
  176. $productImgRaw = trim($params['product_img'] ?? '/uploads/merchant/690377511/6903775111138/oldimg/伊利牛奶.png');
  177. $templateImgRaw = trim($params['template_img'] ?? '/uploads/template/2026-03-06/69aa45a34161f_20260306111027.png');
  178. $customPrompt = trim($params['prompt'] ?? '');
  179. if (empty($productImgRaw) || empty($templateImgRaw)) {
  180. $this->json_response(['code' => 1, 'msg' => '缺少必要参数:product_img、template_img', 'data' => null]);
  181. }
  182. // 将 /uploads/xxx 或 uploads/xxx 转为本地绝对路径
  183. $productImgPath = $this->resolveImagePath($productImgRaw);
  184. $templateImgPath = $this->resolveImagePath($templateImgRaw);
  185. // ========== 3. 提示词 ==========
  186. $prompt = $customPrompt ?: '请完成产品模板替换:
  187. 1. 从产品图提取产品主体、品牌名称、核心文案;
  188. 2. 从模板图继承版式布局、文字排版、色彩风格、背景元素;
  189. 3. 将模板图中的产品和文字替换为产品图的内容;
  190. 4. 最终生成的图片与模板图视觉风格100%统一,仅替换产品和文字。';
  191. // ========== 4. 图片转Base64 ==========
  192. $productImg = $this->img_to_base64($productImgPath);
  193. if (isset($productImg['error'])) {
  194. $this->json_response(['code' => 1, 'msg' => '[步骤1]产品图加载失败:' . $productImg['error'], 'data' => ['path' => $productImgRaw]]);
  195. }
  196. $templateImg = $this->img_to_base64($templateImgPath);
  197. if (isset($templateImg['error'])) {
  198. $this->json_response(['code' => 1, 'msg' => '[步骤2]模板图加载失败:' . $templateImg['error'], 'data' => ['path' => $templateImgRaw]]);
  199. }
  200. // ========== 5. 构造请求参数 ==========
  201. $requestData = [
  202. 'contents' => [
  203. [
  204. 'role' => 'user',
  205. 'parts' => [
  206. ['text' => $prompt],
  207. ['inlineData' => ['mimeType' => $productImg['mime'], 'data' => $productImg['base64']]],
  208. ['inlineData' => ['mimeType' => $templateImg['mime'], 'data' => $templateImg['base64']]]
  209. ]
  210. ]
  211. ],
  212. 'generationConfig' => [
  213. 'responseModalities' => ['IMAGE'],
  214. 'imageConfig' => [
  215. 'aspectRatio' => '5:4',
  216. 'quality' => 'HIGH',
  217. 'width' => 1000,
  218. 'height' => 800
  219. ],
  220. 'temperature' => 0.3,
  221. 'topP' => 0.8,
  222. 'maxOutputTokens' => 2048
  223. ]
  224. ];
  225. // ========== 6. 发起CURL请求 ==========
  226. $ch = curl_init();
  227. curl_setopt_array($ch, [
  228. CURLOPT_URL => $apiUrl,
  229. CURLOPT_RETURNTRANSFER => true,
  230. CURLOPT_POST => true,
  231. CURLOPT_POSTFIELDS => json_encode($requestData, JSON_UNESCAPED_UNICODE),
  232. CURLOPT_HTTPHEADER => [
  233. 'Content-Type: application/json',
  234. 'Authorization: Bearer ' . $apiKey
  235. ],
  236. CURLOPT_TIMEOUT => 300,
  237. CURLOPT_SSL_VERIFYPEER => false,
  238. CURLOPT_SSL_VERIFYHOST => false
  239. ]);
  240. $response = curl_exec($ch);
  241. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  242. $curlErr = curl_error($ch);
  243. curl_close($ch);
  244. // ========== 7. 错误处理 ==========
  245. if ($curlErr) {
  246. $this->json_response(['code' => 1, 'msg' => '[步骤3]CURL请求失败:' . $curlErr, 'data' => null]);
  247. }
  248. if ($httpCode != 200) {
  249. $errDetail = is_string($response) ? substr($response, 0, 800) : json_encode($response);
  250. $this->json_response(['code' => 1, 'msg' => '[步骤4]API调用失败 HTTP' . $httpCode . ',可能原因:API Key过期/无效、配额不足、请求格式错误。响应:' . $errDetail, 'data' => null]);
  251. }
  252. // ========== 8. 解析响应 ==========
  253. $result = json_decode($response, true);
  254. if (json_last_error() !== JSON_ERROR_NONE) {
  255. $this->json_response(['code' => 1, 'msg' => '[步骤5]API响应非JSON格式:' . json_last_error_msg(), 'data' => ['raw' => substr($response, 0, 300)]]);
  256. }
  257. // ========== 9. 提取图片数据 ==========
  258. $base64Data = null;
  259. $imageType = 'png';
  260. if (isset($result['candidates'][0]['content']['parts'][0]['inlineData']['data'])) {
  261. $base64Data = $result['candidates'][0]['content']['parts'][0]['inlineData']['data'];
  262. } elseif (isset($result['candidates'][0]['content']['parts'][0]['text'])) {
  263. $text = $result['candidates'][0]['content']['parts'][0]['text'];
  264. if (preg_match('/data:image\/(png|jpg|jpeg|webp);base64,([^\s"\']+)/i', $text, $m)) {
  265. $imageType = $m[1];
  266. $base64Data = $m[2];
  267. }
  268. }
  269. if (!$base64Data) {
  270. $errMsg = isset($result['error']['message']) ? $result['error']['message'] : '响应结构异常';
  271. $this->json_response(['code' => 1, 'msg' => '[步骤6]未获取到图片数据。' . $errMsg . '。完整响应见data', 'data' => $result]);
  272. }
  273. // ========== 10. 保存图片到 uploads/ceshi/ ==========
  274. $imageData = base64_decode($base64Data);
  275. if ($imageData === false || strlen($imageData) < 100) {
  276. $this->json_response(['code' => 1, 'msg' => '[步骤7]图片Base64解码失败', 'data' => null]);
  277. }
  278. $saveDir = str_replace('\\', '/', ROOT_PATH . 'public/uploads/ceshi/');
  279. if (!is_dir($saveDir)) {
  280. mkdir($saveDir, 0755, true);
  281. }
  282. $fileName = 'img2img-' . date('YmdHis') . '-' . uniqid() . '.' . $imageType;
  283. $fullPath = $saveDir . $fileName;
  284. if (!file_put_contents($fullPath, $imageData)) {
  285. $this->json_response(['code' => 1, 'msg' => '[步骤8]图片保存失败,请检查目录权限:' . $saveDir, 'data' => null]);
  286. }
  287. // ========== 11. 返回成功响应(返回可访问的Web路径) ==========
  288. $webPath = '/uploads/ceshi/' . $fileName;
  289. $this->json_response([
  290. 'code' => 0,
  291. 'msg' => '图生图成功',
  292. 'data' => ['image' => $webPath]
  293. ]);
  294. } catch (\Exception $e) {
  295. $this->json_response(['code' => 1, 'msg' => '[步骤9]图生图失败:' . $e->getMessage(), 'data' => null]);
  296. }
  297. }
  298. /**
  299. * 将接口传入的图片路径转为本地绝对路径(与本地测试脚本逻辑一致)
  300. * 支持:/uploads/xxx、uploads/xxx、public/uploads/xxx、或已是绝对路径
  301. * @param string $path 接口传入的路径
  302. * @return string 本地文件系统绝对路径
  303. */
  304. private function resolveImagePath($path)
  305. {
  306. $path = trim($path);
  307. if (empty($path)) {
  308. return '';
  309. }
  310. // 已是绝对路径且文件存在,直接返回(兼容本地测试传入的完整路径)
  311. $pathNorm = str_replace('\\', '/', $path);
  312. if (preg_match('#^[a-zA-Z]:/#', $pathNorm) || (strlen($pathNorm) > 1 && $pathNorm[0] === '/' && $pathNorm[1] !== '/')) {
  313. if (file_exists($path)) {
  314. return $pathNorm;
  315. }
  316. }
  317. // 统一为相对于 public 的路径:uploads/xxx
  318. $relPath = ltrim($pathNorm, '/');
  319. if (strpos($relPath, 'public/') === 0) {
  320. $relPath = substr($relPath, 7); // 去掉 public/
  321. }
  322. if (strpos($relPath, 'uploads/') !== 0) {
  323. $relPath = 'uploads/' . ltrim($relPath, '/');
  324. }
  325. $fullPath = rtrim(str_replace('\\', '/', ROOT_PATH), '/') . '/public/' . $relPath;
  326. return $fullPath;
  327. }
  328. /**
  329. * 辅助函数:返回JSON响应
  330. * @param array $data 响应数据
  331. */
  332. private function json_response($data)
  333. {
  334. header('Content-Type: application/json; charset=utf-8');
  335. echo json_encode($data, JSON_UNESCAPED_UNICODE);
  336. exit;
  337. }
  338. /**
  339. * 辅助函数:图片URL/本地路径转Base64
  340. * @param string $imgPath 图片URL/本地路径
  341. * @return array 成功 ['mime','base64'] 失败 ['error'=>'错误信息']
  342. */
  343. private function img_to_base64($imgPath)
  344. {
  345. $imgContent = @file_get_contents($imgPath);
  346. if (!$imgContent) {
  347. return ['error' => '图片读取失败,请检查URL可访问性:' . $imgPath];
  348. }
  349. $finfo = new \finfo(FILEINFO_MIME_TYPE);
  350. $mime = $finfo->buffer($imgContent);
  351. if (!in_array($mime, ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'])) {
  352. return ['error' => '不支持的图片格式:' . $mime . ',仅支持png/jpeg/webp'];
  353. }
  354. // 规范 MIME:Gemini 要求 image/jpeg 而非 image/jpg
  355. if ($mime === 'image/jpg') {
  356. $mime = 'image/jpeg';
  357. }
  358. // 去除 base64 中的空白字符,避免「格式错误」
  359. $base64 = preg_replace('/\s+/', '', base64_encode($imgContent));
  360. return ['mime' => $mime, 'base64' => $base64];
  361. }
  362. /**
  363. * 图生图本地测试
  364. */
  365. public function imgtowimg()
  366. {
  367. $prompt = $this->request->param('prompt', '');
  368. $imgRelPath = 'uploads/operate/ai/Preview/arr/0835006071623.png';
  369. $imgPath = ROOT_PATH . 'public/' . $imgRelPath;
  370. if (!file_exists($imgPath)) {
  371. return json(['code' => 1, 'msg' => '原图不存在:' . $imgRelPath]);
  372. }
  373. $imgData = file_get_contents($imgPath);
  374. $base64Img = 'data:image/png;base64,' . base64_encode($imgData);
  375. $params = [
  376. 'prompt' => $prompt,
  377. 'negative_prompt' => '(deformed, distorted, disfigured:1.3), poorly drawn, bad anatomy',
  378. 'steps' => 20,
  379. 'sampler_name' => 'DPM++ 2M SDE',
  380. 'cfg_scale' => 7,
  381. 'seed' => -1,
  382. 'width' => 1024,
  383. 'height' => 1303,
  384. 'override_settings' => [
  385. 'sd_model_checkpoint' => 'realisticVisionV51_v51VAE-inpainting',
  386. 'sd_vae' => 'vae-ft-mse-840000-ema-pruned',
  387. 'CLIP_stop_at_last_layers' => 2
  388. ],
  389. 'clip_skip' => 2,
  390. 'alwayson_scripts' => [
  391. 'controlnet' => [
  392. 'args' => [[
  393. 'enabled' => true,
  394. 'input_image' => $base64Img,
  395. 'module' => 'inpaint_only+lama',
  396. 'model' => 'control_v11p_sd15_inpaint_fp16 [be8bc0ed]',
  397. 'weight' => 1,
  398. 'resize_mode' => 'Resize and Fill',
  399. 'pixel_perfect' => false,
  400. 'control_mode' => 'ControlNet is more important',
  401. 'starting_control_step' => 0,
  402. 'ending_control_step' => 1
  403. ]]
  404. ]
  405. ]
  406. ];
  407. $apiUrl = "http://20.0.17.188:45001/sdapi/v1/txt2img";
  408. $headers = ['Content-Type: application/json'];
  409. $ch = curl_init();
  410. curl_setopt($ch, CURLOPT_URL, $apiUrl);
  411. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  412. curl_setopt($ch, CURLOPT_POST, true);
  413. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  414. curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params, JSON_UNESCAPED_UNICODE));
  415. curl_setopt($ch, CURLOPT_TIMEOUT, 180);
  416. $response = curl_exec($ch);
  417. $error = curl_error($ch);
  418. curl_close($ch);
  419. if ($error) {
  420. return json(['code' => 1, 'msg' => '请求失败:' . $error]);
  421. }
  422. $data = json_decode($response, true);
  423. if (!isset($data['images'][0])) {
  424. return json(['code' => 1, 'msg' => '接口未返回图像数据']);
  425. }
  426. $resultImg = base64_decode($data['images'][0]);
  427. $saveDir = ROOT_PATH . 'public/uploads/img2img/';
  428. if (!is_dir($saveDir)) {
  429. mkdir($saveDir, 0755, true);
  430. }
  431. $originalBaseName = pathinfo($imgRelPath, PATHINFO_FILENAME);
  432. $fileName = $originalBaseName . '-' . time() . '-1024x1248.png';
  433. $savePath = $saveDir . $fileName;
  434. file_put_contents($savePath, $resultImg);
  435. return json([
  436. 'code' => 0,
  437. 'msg' => '图像生成成功',
  438. 'data' => [
  439. 'origin_url' => '/uploads/img2img/' . $fileName
  440. ]
  441. ]);
  442. }
  443. /**
  444. * 后期图像处理-单张图片高清放大处理
  445. */
  446. public function extra_image()
  447. {
  448. // 配置参数
  449. $config = [
  450. 'input_dir' => 'uploads/img2img/',
  451. 'output_dir' => 'uploads/extra_image/',
  452. 'api_url' => 'http://20.0.17.188:45001/sdapi/v1/extra-single-image',
  453. 'timeout' => 120, // 增加超时时间,高清处理可能耗时较长
  454. 'upscale_params' => [
  455. 'resize_mode' => 0,
  456. 'show_extras_results' => true,
  457. 'gfpgan_visibility' => 0, // 人脸修复关闭
  458. 'codeformer_visibility' => 0, // 人脸修复关闭
  459. 'codeformer_weight' => 0,
  460. 'upscaling_resize' => 2.45, // 放大倍数
  461. 'upscaling_crop' => true,
  462. 'upscaler_1' => 'R-ESRGAN 4x+ Anime6B', // 主放大模型
  463. 'upscaler_2' => 'None', // 不使用第二放大器
  464. 'extras_upscaler_2_visibility' => 0,
  465. 'upscale_first' => false,
  466. ]
  467. ];
  468. // 输入文件处理
  469. $imgRelPath = '0835006071623-1757406184-1024x1248.png';
  470. $imgPath = ROOT_PATH . 'public/' . $config['input_dir'] . $imgRelPath;
  471. if (!file_exists($imgPath)) {
  472. return json(['code' => 1, 'msg' => '原图不存在:' . $imgRelPath]);
  473. }
  474. // 读取并编码图片
  475. try {
  476. $imgData = file_get_contents($imgPath);
  477. if ($imgData === false) {
  478. throw new Exception('无法读取图片文件');
  479. }
  480. $base64Img = base64_encode($imgData);
  481. } catch (Exception $e) {
  482. return json(['code' => 1, 'msg' => '图片处理失败:' . $e->getMessage()]);
  483. }
  484. // 准备API请求数据
  485. $postData = array_merge($config['upscale_params'], ['image' => $base64Img]);
  486. $jsonData = json_encode($postData);
  487. if ($jsonData === false) {
  488. return json(['code' => 1, 'msg' => 'JSON编码失败']);
  489. }
  490. // 调用API进行高清放大
  491. $ch = curl_init();
  492. curl_setopt_array($ch, [
  493. CURLOPT_URL => $config['api_url'],
  494. CURLOPT_RETURNTRANSFER => true,
  495. CURLOPT_POST => true,
  496. CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
  497. CURLOPT_POSTFIELDS => $jsonData,
  498. CURLOPT_TIMEOUT => $config['timeout'],
  499. CURLOPT_CONNECTTIMEOUT => 30,
  500. ]);
  501. $response = curl_exec($ch);
  502. $error = curl_error($ch);
  503. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  504. curl_close($ch);
  505. if ($error) {
  506. return json(['code' => 1, 'msg' => 'API请求失败:' . $error]);
  507. }
  508. if ($httpCode !== 200) {
  509. return json(['code' => 1, 'msg' => 'API返回错误状态码:' . $httpCode]);
  510. }
  511. $data = json_decode($response, true);
  512. if (json_last_error() !== JSON_ERROR_NONE) {
  513. return json(['code' => 1, 'msg' => 'API返回数据解析失败']);
  514. }
  515. if (!isset($data['image']) || empty($data['image'])) {
  516. return json(['code' => 1, 'msg' => '接口未返回有效的图像数据']);
  517. }
  518. // 保存处理后的图片
  519. try {
  520. $resultImg = base64_decode($data['image']);
  521. if ($resultImg === false) {
  522. throw new Exception('Base64解码失败');
  523. }
  524. $saveDir = ROOT_PATH . 'public/' . $config['output_dir'];
  525. if (!is_dir($saveDir) && !mkdir($saveDir, 0755, true)) {
  526. throw new Exception('无法创建输出目录');
  527. }
  528. $originalBaseName = pathinfo($imgRelPath, PATHINFO_FILENAME);
  529. $fileName = $originalBaseName . '-hd.png'; // 使用-hd后缀更明确
  530. $savePath = $saveDir . $fileName;
  531. if (file_put_contents($savePath, $resultImg) === false) {
  532. throw new Exception('无法保存处理后的图片');
  533. }
  534. // 返回成功响应
  535. return json([
  536. 'code' => 0,
  537. 'msg' => '图像高清放大处理成功',
  538. 'data' => [
  539. 'url' => '/' . $config['output_dir'] . $fileName,
  540. 'original_size' => filesize($imgPath),
  541. 'processed_size' => filesize($savePath),
  542. 'resolution' => getimagesize($savePath), // 返回新图片的分辨率
  543. ]
  544. ]);
  545. } catch (Exception $e) {
  546. return json(['code' => 1, 'msg' => '保存结果失败:' . $e->getMessage()]);
  547. }
  548. }
  549. /**
  550. * 文本生成图片并保存第一张结果
  551. * @param array $params 请求参数
  552. * @return array 返回结果
  553. */
  554. public function txttowimg()
  555. {
  556. // API配置
  557. $config = [
  558. 'api_url' => 'https://chatapi.onechats.ai/mj/submit/imagine',
  559. 'fetch_url' => 'https://chatapi.onechats.ai/mj/task/',
  560. 'api_key' => 'sk-iURfrAgzAjhZ4PpPLwzmWIAhM7zKfrkwDvyxk4RVBQ4ouJNK',
  561. 'default_prompt' => '一个猫',
  562. 'wait_time' => 3 // 等待生成完成的秒数
  563. ];
  564. try {
  565. // 1. 准备请求数据
  566. $prompt = $config['default_prompt'];
  567. $postData = [
  568. 'botType' => 'MID_JOURNEY',
  569. 'prompt' => $prompt,
  570. 'base64Array' => [],
  571. 'accountFilter' => [
  572. 'channelId' => "",
  573. 'instanceId' => "",
  574. 'modes' => [],
  575. 'remark' => "",
  576. 'remix' => true,
  577. 'remixAutoConsidered' => true
  578. ],
  579. 'notifyHook' => "",
  580. 'state' => ""
  581. ];
  582. // 2. 提交生成请求
  583. $generateResponse = $this->sendApiRequest($config['api_url'], $postData, $config['api_key']);
  584. $generateData = json_decode($generateResponse, true);
  585. if (empty($generateData['result'])) {
  586. throw new Exception('生成失败: '.($generateData['message'] ?? '未知错误'));
  587. }
  588. $taskId = $generateData['result'];
  589. // 3. 等待图片生成完成
  590. sleep($config['wait_time']);
  591. // 4. 获取生成结果
  592. $fetchUrl = $config['fetch_url'].$taskId.'/fetch';
  593. $fetchResponse = $this->sendApiRequest($fetchUrl, [], $config['api_key'], 'GET');
  594. $fetchData = json_decode($fetchResponse, true);
  595. if (empty($fetchData['imageUrl'])) {
  596. throw new Exception('获取图片失败: '.($fetchData['message'] ?? '未知错误'));
  597. }
  598. // 5. 处理返回的图片数组(取第一张)
  599. $imageUrls = is_array($fetchData['imageUrl']) ? $fetchData['imageUrl'] : [$fetchData['imageUrl']];
  600. $firstImageUrl = $imageUrls[0];
  601. // 6. 保存图片到本地
  602. $savePath = $this->saveImage($firstImageUrl);
  603. // 7. 返回结果
  604. return [
  605. 'code' => 200,
  606. 'msg' => '图片生成并保存成功',
  607. 'data' => [
  608. 'local_path' => $savePath,
  609. 'web_url' => request()->domain().$savePath,
  610. 'task_id' => $taskId
  611. ]
  612. ];
  613. } catch (Exception $e) {
  614. // 错误处理
  615. return [
  616. 'code' => 500,
  617. 'msg' => '处理失败: '.$e->getMessage(),
  618. 'data' => null
  619. ];
  620. }
  621. }
  622. /**
  623. * 发送API请求
  624. * @param string $url 请求地址
  625. * @param array $data 请求数据
  626. * @param string $apiKey API密钥
  627. * @param string $method 请求方法
  628. * @return string 响应内容
  629. * @throws Exception
  630. */
  631. private function sendApiRequest($url, $data, $apiKey, $method = 'POST')
  632. {
  633. $ch = curl_init();
  634. curl_setopt_array($ch, [
  635. CURLOPT_URL => $url,
  636. CURLOPT_RETURNTRANSFER => true,
  637. CURLOPT_CUSTOMREQUEST => $method,
  638. CURLOPT_HTTPHEADER => [
  639. 'Authorization: Bearer '.$apiKey,
  640. 'Accept: application/json',
  641. 'Content-Type: application/json'
  642. ],
  643. CURLOPT_POSTFIELDS => $method === 'POST' ? json_encode($data) : null,
  644. CURLOPT_SSL_VERIFYPEER => false,
  645. CURLOPT_SSL_VERIFYHOST => false,
  646. CURLOPT_TIMEOUT => 60,
  647. CURLOPT_FAILONERROR => true
  648. ]);
  649. $response = curl_exec($ch);
  650. $error = curl_error($ch);
  651. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  652. curl_close($ch);
  653. if ($error) {
  654. throw new Exception('API请求失败: '.$error);
  655. }
  656. if ($httpCode < 200 || $httpCode >= 300) {
  657. throw new Exception('API返回错误状态码: '.$httpCode);
  658. }
  659. return $response;
  660. }
  661. /**
  662. * 保存图片到本地
  663. * @param string $imageUrl 图片URL
  664. * @return string 本地保存路径
  665. * @throws Exception
  666. */
  667. private function saveImage($imageUrl)
  668. {
  669. // 1. 创建存储目录
  670. $saveDir = ROOT_PATH.'public'.DS.'uploads'.DS.'midjourney'.DS.date('Ymd');
  671. if (!is_dir($saveDir)) {
  672. mkdir($saveDir, 0755, true);
  673. }
  674. // 2. 生成唯一文件名
  675. $filename = uniqid().'.png';
  676. $localPath = DS.'uploads'.DS.'midjourney'.DS.date('Ymd').DS.$filename;
  677. $fullPath = $saveDir.DS.$filename;
  678. // 3. 下载图片
  679. $ch = curl_init($imageUrl);
  680. curl_setopt_array($ch, [
  681. CURLOPT_RETURNTRANSFER => true,
  682. CURLOPT_FOLLOWLOCATION => true,
  683. CURLOPT_SSL_VERIFYPEER => false,
  684. CURLOPT_CONNECTTIMEOUT => 15
  685. ]);
  686. $imageData = curl_exec($ch);
  687. $error = curl_error($ch);
  688. curl_close($ch);
  689. if (!$imageData) {
  690. throw new Exception('图片下载失败: '.$error);
  691. }
  692. // 4. 验证图片类型
  693. $imageInfo = getimagesizefromstring($imageData);
  694. if (!in_array($imageInfo['mime'] ?? '', ['image/png', 'image/jpeg'])) {
  695. throw new Exception('下载内容不是有效图片');
  696. }
  697. // 5. 保存文件
  698. if (!file_put_contents($fullPath, $imageData)) {
  699. throw new Exception('图片保存失败');
  700. }
  701. return $localPath;
  702. }
  703. /**
  704. * 将图片背景变为透明
  705. * @param string $imagePath 图片路径
  706. * @return string 处理后的图片路径
  707. */
  708. public function makeImageBackgroundTransparent($imagePath) {
  709. // 检查图片是否存在
  710. if (!file_exists($imagePath)) {
  711. return '图片不存在';
  712. }
  713. // 获取图片信息
  714. $imageInfo = getimagesize($imagePath);
  715. if (!$imageInfo) {
  716. return '无法获取图片信息';
  717. }
  718. $width = $imageInfo[0];
  719. $height = $imageInfo[1];
  720. // 根据图片类型创建图像资源
  721. switch ($imageInfo[2]) {
  722. case IMAGETYPE_JPEG:
  723. $source = imagecreatefromjpeg($imagePath);
  724. break;
  725. case IMAGETYPE_PNG:
  726. $source = imagecreatefrompng($imagePath);
  727. break;
  728. case IMAGETYPE_GIF:
  729. $source = imagecreatefromgif($imagePath);
  730. break;
  731. default:
  732. return '不支持的图片类型';
  733. }
  734. if (!$source) {
  735. return '无法创建图像资源';
  736. }
  737. // 创建透明画布
  738. $transparent = imagecreatetruecolor($width, $height);
  739. if (!$transparent) {
  740. imagedestroy($source);
  741. return '无法创建透明画布';
  742. }
  743. imagealphablending($transparent, false);
  744. imagesavealpha($transparent, true);
  745. $transparentColor = imagecolorallocatealpha($transparent, 255, 255, 255, 127);
  746. imagefilledrectangle($transparent, 0, 0, $width, $height, $transparentColor);
  747. // 背景色阈值(根据实际图片调整)
  748. $bgThreshold = 150; // 降低阈值,提高背景检测灵敏度
  749. $colorThreshold = 60; // 颜色差值阈值
  750. $saturationThreshold = 20; // 饱和度阈值
  751. // 逐像素处理
  752. for ($x = 0; $x < $width; $x++) {
  753. for ($y = 0; $y < $height; $y++) {
  754. $pixelColor = imagecolorat($source, $x, $y);
  755. $r = ($pixelColor >> 16) & 0xFF;
  756. $g = ($pixelColor >> 8) & 0xFF;
  757. $b = $pixelColor & 0xFF;
  758. // 计算亮度
  759. $brightness = ($r + $g + $b) / 3;
  760. // 计算颜色饱和度
  761. $max = max($r, $g, $b);
  762. $min = min($r, $g, $b);
  763. $saturation = ($max > 0) ? (($max - $min) / $max) * 100 : 0;
  764. // 计算与背景色的差值(黑色/白色背景)
  765. $bgDiff1 = sqrt(pow($r, 2) + pow($g, 2) + pow($b, 2)); // 与黑色的差值
  766. $bgDiff2 = sqrt(pow(255 - $r, 2) + pow(255 - $g, 2) + pow(255 - $b, 2)); // 与白色的差值
  767. $minBgDiff = min($bgDiff1, $bgDiff2);
  768. // 综合判断:亮度较高(纸巾)或颜色与背景差异大的像素保留
  769. if ($brightness > $bgThreshold || $minBgDiff > $colorThreshold || $saturation > $saturationThreshold) {
  770. // 保留前景像素
  771. imagesetpixel($transparent, $x, $y, $pixelColor);
  772. }
  773. }
  774. }
  775. // 生成输出路径
  776. $saveDir = ROOT_PATH . 'public/uploads/ceshi/';
  777. if (!is_dir($saveDir)) {
  778. if (!mkdir($saveDir, 0755, true)) {
  779. imagedestroy($source);
  780. imagedestroy($transparent);
  781. return '无法创建保存目录';
  782. }
  783. }
  784. // 生成唯一文件名
  785. $pathInfo = pathinfo($imagePath);
  786. $filename = $pathInfo['filename'] . '_transparent_' . date('YmdHis') . '.png';
  787. $outputPath = $saveDir . $filename;
  788. // 保存透明图片
  789. if (!imagepng($transparent, $outputPath)) {
  790. imagedestroy($source);
  791. imagedestroy($transparent);
  792. return '无法保存透明图片';
  793. }
  794. // 释放资源
  795. imagedestroy($source);
  796. imagedestroy($transparent);
  797. return $outputPath;
  798. }
  799. /**
  800. * 处理图片背景透明化请求
  801. */
  802. public function imgimg() {
  803. // 使用本地图片路径
  804. $localPath = ROOT_PATH . 'public/uploads/zhi.jpg';
  805. // 检查图片是否存在
  806. if (!file_exists($localPath)) {
  807. return json(['code' => 1, 'msg' => '本地图片不存在,请检查文件路径']);
  808. }
  809. // 处理图片背景透明化
  810. $result = $this->makeImageBackgroundTransparent($localPath);
  811. // 返回结果
  812. if (file_exists($result)) {
  813. // 生成可访问的URL
  814. $relativePath = str_replace(ROOT_PATH . 'public', '', $result);
  815. // 确保路径以正斜杠开头
  816. if (substr($relativePath, 0, 1) !== '/') {
  817. $relativePath = '/' . $relativePath;
  818. }
  819. $imageUrl = 'http://20.0.16.128:9093' . $relativePath;
  820. $res = [
  821. 'code' => 0,
  822. 'msg' => '处理成功',
  823. 'data' => [
  824. 'transparent_image' => $imageUrl
  825. ]
  826. ];
  827. } else {
  828. $res = [
  829. 'code' => 1,
  830. 'msg' => $result
  831. ];
  832. }
  833. return json($res);
  834. }
  835. }