ImageJob.php 12 KB

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