ImageJob.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. <?php
  2. // 1. 正确的队列任务类 application/job/ImageJob.php
  3. namespace app\job;
  4. use think\Db;
  5. use think\queue\Job;
  6. use think\Queue;
  7. class ImageJob
  8. {
  9. protected $config = [
  10. 'gpt' => [
  11. 'api_key' => 'sk-Bhos1lXTRpZiAAmN06624a219a874eCd91Dc068b902a3e73',
  12. 'api_url' => 'https://one.opengptgod.com/v1/chat/completions'
  13. ],
  14. 'dalle' => [
  15. 'api_key' => 'sk-e0JuPjMntkbgi1BoMjrqyyzMKzAxILkQzyGMSy3xiMupuoWY',
  16. 'api_url' => 'https://niubi.zeabur.app/v1/images/generations'
  17. ]
  18. ];
  19. /**
  20. * fire方法是队列默认调用的方法
  21. * @param Job $job 当前的任务对象
  22. * @param array|mixed $data 发布任务时自定义的数据
  23. */
  24. public function fire(Job $job, $data)
  25. {
  26. echo "图生文开始\n";
  27. // echo "接收的数据: " . json_encode($data) . "\n";
  28. $logId = $data['log_id'] ?? null;
  29. try {
  30. // 如果有 log_id,更新任务状态为“正在处理”
  31. if ($logId) {
  32. Db::name('queue_log')->where('id', $logId)->update([
  33. 'status' => 1, // 正在处理
  34. 'updated_at' => date('Y-m-d H:i:s')
  35. ]);
  36. }
  37. // 执行业务逻辑
  38. $str = $this->processImage($data);
  39. echo $str;
  40. echo "图生文结束\n";
  41. $job->delete();
  42. } catch (\Exception $e) {
  43. // 如果有 log_id,更新任务状态为“执行失败”并记录错误信息
  44. if ($logId) {
  45. Db::name('queue_log')->where('id', $logId)->update([
  46. 'status' => 3, // 执行失败
  47. 'log' => $e->getMessage(),
  48. 'updated_at' => date('Y-m-d H:i:s')
  49. ]);
  50. }
  51. // 最多重试一次(总执行两次)
  52. if ($job->attempts() < 2) {
  53. $job->release(30); // 延迟30秒再次执行
  54. } else {
  55. $job->failed(); // 达到最大尝试次数,标记失败
  56. }
  57. }
  58. }
  59. /**
  60. * 任务失败时的处理
  61. */
  62. public function failed($data)
  63. {
  64. // 记录失败日志或发送通知
  65. echo "ImageJob failed: " . json_encode($data);
  66. }
  67. /**
  68. * 处理图片的具体逻辑
  69. */
  70. public function processImage($data)
  71. {
  72. // 根据传入的数据处理图片
  73. $res = $this->imageToText($data["sourceDir"],$data["file_name"],$data["prompt"],$data);
  74. echo $res;
  75. }
  76. /**
  77. * 图生文接口
  78. */
  79. public function imageToText($sourceDirRaw, $fileName, $prompt, $call_data)
  80. {
  81. // 自动拆分文件名
  82. if (!$fileName && preg_match('/([^\/]+\.(jpg|jpeg|png))$/i', $sourceDirRaw, $matches)) {
  83. $fileName = $matches[1];
  84. $sourceDirRaw = preg_replace('/\/' . preg_quote($fileName, '/') . '$/', '', $sourceDirRaw);
  85. }
  86. // 参数校验
  87. if ($sourceDirRaw === '' || $fileName === '') {
  88. return '参数错误:原图路径 或 图片名称 不能为空';
  89. }
  90. // 构建路径
  91. $rootPath = str_replace('\\', '/', ROOT_PATH);
  92. $sourceDir = rtrim($rootPath . 'public/' . $sourceDirRaw, '/') . '/';
  93. $filePath = $sourceDir . $fileName;
  94. $relativePath = $sourceDirRaw . '/' . $fileName;
  95. // 文件检查
  96. if (!is_dir($sourceDir)) {
  97. return '源目录不存在:' . $sourceDir;
  98. }
  99. if (!is_file($filePath)) {
  100. return '文件不存在:' . $filePath;
  101. }
  102. // 获取图片信息
  103. $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
  104. $mime = ($ext === 'jpg' || $ext === 'jpeg') ? 'jpeg' : $ext;
  105. list($width, $height) = getimagesize($filePath);
  106. $imageData = base64_encode(file_get_contents($filePath));
  107. if (!$imageData || strlen($imageData) < 1000) {
  108. throw new \Exception('图片内容读取失败');
  109. }
  110. $imageUrl = "data:image/{$mime};base64,{$imageData}";
  111. // 记录提示词日志
  112. $logDir = $rootPath . 'runtime/logs/';
  113. if (!is_dir($logDir)) mkdir($logDir, 0755, true);
  114. // file_put_contents(
  115. // $logDir . 'text.txt',
  116. // "\n====原始 " . date('Y-m-d H:i:s') . " ====\n" . $prompt . "\n\n",
  117. // FILE_APPEND
  118. // );
  119. // 调用图生文
  120. $gptRes = $this->callGptApi($imageUrl, $prompt);
  121. $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
  122. // 保存 GPT 返回内容
  123. // file_put_contents($logDir . 'text.txt',
  124. // "\n==== " . date('Y-m-d H:i:s') . " ====\n" . $gptText . "\n\n",
  125. // FILE_APPEND
  126. // );
  127. // 提取英文描述
  128. $patternEnglish = '/^([\s\S]+?)---json json---/';
  129. preg_match($patternEnglish, $gptText, $matchEn);
  130. $englishDesc = isset($matchEn[1]) ? trim($matchEn[1]) : '';
  131. // 提取中文描述
  132. $patternChinese = '/---json json---\s*([\x{4e00}-\x{9fa5}][\s\S]+?)---json json---/u';
  133. preg_match($patternChinese, $gptText, $matchZh);
  134. $chineseDesc = isset($matchZh[1]) ? trim($matchZh[1]) : '';
  135. // 提取图片名(可能是中文短句,也可能是关键词)
  136. $patternName = '/---json json---\s*(.+)$/s';
  137. preg_match($patternName, $gptText, $matchName);
  138. $rawName = isset($matchName[1]) ? trim($matchName[1]) : '';
  139. $img_name = preg_replace('/[^\x{4e00}-\x{9fa5}A-Za-z0-9_\- ]/u', '', $rawName);
  140. // file_put_contents(
  141. // $logDir . 'text.txt',
  142. // "\n==== " . date('Y-m-d H:i:s') . " ====\n" . $gptText . "\n\n",
  143. // FILE_APPEND
  144. // );
  145. // 验证 GPT 返回格式
  146. if (strpos($gptText, '---json json---') === false) {
  147. return 'GPT 返回格式不正确,缺少分隔符';
  148. }
  149. // 以 ---json json--- 分割
  150. $parts = array_map('trim', explode('---json json---', $gptText));
  151. // 清理“第一段”、“第二段”等标签前缀
  152. $cleanPrefix = function ($text) {
  153. return preg_replace('/^第[一二三四五六七八九十]+段[::]?\s*/u', '', $text);
  154. };
  155. // 防止越界,逐个安全提取
  156. $englishDesc = isset($parts[0]) ? $cleanPrefix(trim($parts[0])) : '';
  157. $chineseDesc = isset($parts[1]) ? $cleanPrefix(trim($parts[1])) : '';
  158. $part2 = isset($parts[2]) ? $cleanPrefix(trim($parts[2])) : '';
  159. // 提取图片名
  160. // 只保留中英文、数字、下划线、短横线、空格
  161. $img_name = preg_replace('/[^\x{4e00}-\x{9fa5}A-Za-z0-9_\- ]/u', '', $part2);
  162. // 成功后的日志
  163. // file_put_contents(
  164. // $logDir . 'img_name_success.txt',
  165. // "\n======== " . date('Y-m-d H:i:s') . " ========\n" .
  166. // $englishDesc . "\n---json json---\n" .
  167. // $chineseDesc . "\n---json json---\n" .
  168. // $img_name . "\n\n",
  169. // FILE_APPEND
  170. // );
  171. // 成功写入数据库
  172. $this->logToDatabase([
  173. 'img_name' => $img_name,
  174. 'old_image_url' => $relativePath,
  175. 'chinese_description' => $chineseDesc,
  176. 'english_description' => $englishDesc,
  177. 'size' => "",
  178. 'status' => 0 // 正常待图生图状态
  179. ]);
  180. //分解任务
  181. $arr = [
  182. "fileName" =>$fileName,
  183. "outputDir"=>$call_data["outputDir"],
  184. "width"=>$call_data["width"],
  185. "height"=>$call_data["height"],
  186. "englishDesc"=>$englishDesc,
  187. "img_name"=>$img_name
  188. ];
  189. echo "现在推送";
  190. Queue::push('app\job\TextToImageJob', $arr,'txttoimg');
  191. return ;
  192. // 执行文生图
  193. }
  194. public function logToDatabase($data)
  195. {
  196. $record = [
  197. 'old_image_url' => $data['old_image_url'] ?? '',
  198. 'new_image_url' => $data['new_image_url'] ?? '',
  199. 'custom_image_url' => $data['custom_image_url'] ?? '',
  200. 'img_name' => $data['img_name'],
  201. 'size' => isset($data['image_width'], $data['image_height']) ? $data['image_width'] . 'x' . $data['image_height'] : '',
  202. 'chinese_description' => $data['chinese_description'] ?? '',
  203. 'english_description' => $data['english_description'] ?? '',
  204. 'model' => 'gpt-image-1',
  205. 'quality' => 'standard',
  206. 'style' => 'vivid',
  207. 'status' => $data['status'] ?? 0,
  208. 'error_msg' => $data['error_msg'] ?? '',
  209. 'created_time' => date('Y-m-d H:i:s'),
  210. 'updated_time' => date('Y-m-d H:i:s')
  211. ];
  212. if (isset($data['id'])) {
  213. Db::name('text_to_image')->where('id', $data['id'])->update($record);
  214. } else {
  215. Db::name('text_to_image')->insert($record);
  216. }
  217. }
  218. public function callGptApi($imageUrl, $prompt)
  219. {
  220. $data = [
  221. "model" => "gpt-4-vision-preview",
  222. "messages" => [[
  223. "role" => "user",
  224. "content" => [
  225. ["type" => "text", "text" => $prompt],
  226. ["type" => "image_url", "image_url" => [
  227. "url" => $imageUrl,
  228. "detail" => "auto" // ✅ 显式添加 detail 字段,兼容 vision API
  229. ]]
  230. ]
  231. ]],
  232. "max_tokens" => 1000
  233. ];
  234. return $this->callApi($this->config['gpt']['api_url'], $this->config['gpt']['api_key'], $data);
  235. }
  236. /**
  237. * 通用API调用方法
  238. */
  239. public function callApi($url, $apiKey, $data)
  240. {
  241. $maxRetries = 2;
  242. $attempt = 0;
  243. $lastError = '';
  244. while ($attempt <= $maxRetries) {
  245. $ch = curl_init();
  246. curl_setopt_array($ch, [
  247. CURLOPT_URL => $url,
  248. CURLOPT_RETURNTRANSFER => true,
  249. CURLOPT_POST => true,
  250. CURLOPT_POSTFIELDS => json_encode($data),
  251. CURLOPT_HTTPHEADER => [
  252. 'Content-Type: application/json',
  253. 'Authorization: Bearer ' . $apiKey
  254. ],
  255. CURLOPT_TIMEOUT => 120,
  256. CURLOPT_SSL_VERIFYPEER => false,
  257. CURLOPT_SSL_VERIFYHOST => 0,
  258. CURLOPT_TCP_KEEPALIVE => 1,
  259. CURLOPT_FORBID_REUSE => false
  260. ]);
  261. $response = curl_exec($ch);
  262. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  263. $curlError = curl_error($ch);
  264. curl_close($ch);
  265. if ($response !== false && $httpCode === 200) {
  266. $result = json_decode($response, true);
  267. return $result;
  268. }
  269. $lastError = $curlError ?: "HTTP错误:{$httpCode}";
  270. $attempt++;
  271. sleep(1);
  272. }
  273. throw new \Exception("请求失败(重试{$maxRetries}次):{$lastError}");
  274. }
  275. }