[ 'api_key' => 'sk-Bhos1lXTRpZiAAmN06624a219a874eCd91Dc068b902a3e73', 'api_url' => 'https://one.opengptgod.com/v1/chat/completions' ], 'dalle' => [ 'api_key' => 'sk-e0JuPjMntkbgi1BoMjrqyyzMKzAxILkQzyGMSy3xiMupuoWY', 'api_url' => 'https://niubi.zeabur.app/v1/images/generations' ] ]; /** * fire方法是队列默认调用的方法 * @param Job $job 当前的任务对象 * @param array|mixed $data 发布任务时自定义的数据 */ public function fire(Job $job, $data) { echo "图生文开始\n"; // echo "接收的数据: " . json_encode($data) . "\n"; $logId = $data['log_id'] ?? null; try { // 如果有 log_id,更新任务状态为“正在处理” if ($logId) { Db::name('queue_log')->where('id', $logId)->update([ 'status' => 1, // 正在处理 'updated_at' => date('Y-m-d H:i:s') ]); } // 执行业务逻辑 $str = $this->processImage($data); echo $str; echo "图生文结束\n"; $job->delete(); } catch (\Exception $e) { // 如果有 log_id,更新任务状态为“执行失败”并记录错误信息 if ($logId) { Db::name('queue_log')->where('id', $logId)->update([ 'status' => 3, // 执行失败 'log' => $e->getMessage(), 'updated_at' => date('Y-m-d H:i:s') ]); } // 最多重试一次(总执行两次) if ($job->attempts() < 2) { $job->release(30); // 延迟30秒再次执行 } else { $job->failed(); // 达到最大尝试次数,标记失败 } } } /** * 任务失败时的处理 */ public function failed($data) { // 记录失败日志或发送通知 echo "ImageJob failed: " . json_encode($data); } /** * 处理图片的具体逻辑 */ public function processImage($data) { // 根据传入的数据处理图片 $res = $this->imageToText($data["sourceDir"],$data["file_name"],$data["prompt"],$data); echo $res; } /** * 图生文接口 */ public function imageToText($sourceDirRaw, $fileName, $prompt, $call_data) { // 自动拆分文件名 if (!$fileName && preg_match('/([^\/]+\.(jpg|jpeg|png))$/i', $sourceDirRaw, $matches)) { $fileName = $matches[1]; $sourceDirRaw = preg_replace('/\/' . preg_quote($fileName, '/') . '$/', '', $sourceDirRaw); } // 参数校验 if ($sourceDirRaw === '' || $fileName === '') { return '参数错误:原图路径 或 图片名称 不能为空'; } // 构建路径 $rootPath = str_replace('\\', '/', ROOT_PATH); $sourceDir = rtrim($rootPath . 'public/' . $sourceDirRaw, '/') . '/'; $filePath = $sourceDir . $fileName; $relativePath = $sourceDirRaw . '/' . $fileName; // 文件检查 if (!is_dir($sourceDir)) { return '源目录不存在:' . $sourceDir; } if (!is_file($filePath)) { return '文件不存在:' . $filePath; } // 获取图片信息 $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); $mime = ($ext === 'jpg' || $ext === 'jpeg') ? 'jpeg' : $ext; list($width, $height) = getimagesize($filePath); $imageData = base64_encode(file_get_contents($filePath)); if (!$imageData || strlen($imageData) < 1000) { throw new \Exception('图片内容读取失败'); } $imageUrl = "data:image/{$mime};base64,{$imageData}"; // 记录提示词日志 $logDir = $rootPath . 'runtime/logs/'; if (!is_dir($logDir)) mkdir($logDir, 0755, true); // file_put_contents( // $logDir . 'text.txt', // "\n====原始 " . date('Y-m-d H:i:s') . " ====\n" . $prompt . "\n\n", // FILE_APPEND // ); // 调用图生文 $gptRes = $this->callGptApi($imageUrl, $prompt); $gptText = trim($gptRes['choices'][0]['message']['content'] ?? ''); // 保存 GPT 返回内容 // file_put_contents($logDir . 'text.txt', // "\n==== " . date('Y-m-d H:i:s') . " ====\n" . $gptText . "\n\n", // FILE_APPEND // ); // 提取英文描述 $patternEnglish = '/^([\s\S]+?)---json json---/'; preg_match($patternEnglish, $gptText, $matchEn); $englishDesc = isset($matchEn[1]) ? trim($matchEn[1]) : ''; // 提取中文描述 $patternChinese = '/---json json---\s*([\x{4e00}-\x{9fa5}][\s\S]+?)---json json---/u'; preg_match($patternChinese, $gptText, $matchZh); $chineseDesc = isset($matchZh[1]) ? trim($matchZh[1]) : ''; // 提取图片名(可能是中文短句,也可能是关键词) $patternName = '/---json json---\s*(.+)$/s'; preg_match($patternName, $gptText, $matchName); $rawName = isset($matchName[1]) ? trim($matchName[1]) : ''; $img_name = preg_replace('/[^\x{4e00}-\x{9fa5}A-Za-z0-9_\- ]/u', '', $rawName); // file_put_contents( // $logDir . 'text.txt', // "\n==== " . date('Y-m-d H:i:s') . " ====\n" . $gptText . "\n\n", // FILE_APPEND // ); // 验证 GPT 返回格式 if (strpos($gptText, '---json json---') === false) { return 'GPT 返回格式不正确,缺少分隔符'; } // 以 ---json json--- 分割 $parts = array_map('trim', explode('---json json---', $gptText)); // 清理“第一段”、“第二段”等标签前缀 $cleanPrefix = function ($text) { return preg_replace('/^第[一二三四五六七八九十]+段[::]?\s*/u', '', $text); }; // 防止越界,逐个安全提取 $englishDesc = isset($parts[0]) ? $cleanPrefix(trim($parts[0])) : ''; $chineseDesc = isset($parts[1]) ? $cleanPrefix(trim($parts[1])) : ''; $part2 = isset($parts[2]) ? $cleanPrefix(trim($parts[2])) : ''; // 提取图片名 // 只保留中英文、数字、下划线、短横线、空格 $img_name = preg_replace('/[^\x{4e00}-\x{9fa5}A-Za-z0-9_\- ]/u', '', $part2); // 成功后的日志 // file_put_contents( // $logDir . 'img_name_success.txt', // "\n======== " . date('Y-m-d H:i:s') . " ========\n" . // $englishDesc . "\n---json json---\n" . // $chineseDesc . "\n---json json---\n" . // $img_name . "\n\n", // FILE_APPEND // ); // 成功写入数据库 $this->logToDatabase([ 'img_name' => $img_name, 'old_image_url' => $relativePath, 'chinese_description' => $chineseDesc, 'english_description' => $englishDesc, 'size' => "", 'status' => 0 // 正常待图生图状态 ]); //分解任务 $arr = [ "fileName" =>$fileName, "outputDir"=>$call_data["outputDir"], "width"=>$call_data["width"], "height"=>$call_data["height"], "englishDesc"=>$englishDesc, "img_name"=>$img_name ]; echo "现在推送"; Queue::push('app\job\TextToImageJob', $arr,'txttoimg'); return ; // 执行文生图 } public function logToDatabase($data) { $record = [ 'old_image_url' => $data['old_image_url'] ?? '', 'new_image_url' => $data['new_image_url'] ?? '', 'custom_image_url' => $data['custom_image_url'] ?? '', 'img_name' => $data['img_name'], 'size' => isset($data['image_width'], $data['image_height']) ? $data['image_width'] . 'x' . $data['image_height'] : '', 'chinese_description' => $data['chinese_description'] ?? '', 'english_description' => $data['english_description'] ?? '', 'model' => 'gpt-image-1', 'quality' => 'standard', 'style' => 'vivid', 'status' => $data['status'] ?? 0, 'error_msg' => $data['error_msg'] ?? '', 'created_time' => date('Y-m-d H:i:s'), 'updated_time' => date('Y-m-d H:i:s') ]; if (isset($data['id'])) { Db::name('text_to_image')->where('id', $data['id'])->update($record); } else { Db::name('text_to_image')->insert($record); } } public function callGptApi($imageUrl, $prompt) { $data = [ "model" => "gpt-4-vision-preview", "messages" => [[ "role" => "user", "content" => [ ["type" => "text", "text" => $prompt], ["type" => "image_url", "image_url" => [ "url" => $imageUrl, "detail" => "auto" // ✅ 显式添加 detail 字段,兼容 vision API ]] ] ]], "max_tokens" => 1000 ]; return $this->callApi($this->config['gpt']['api_url'], $this->config['gpt']['api_key'], $data); } /** * 通用API调用方法 */ public function callApi($url, $apiKey, $data) { $maxRetries = 2; $attempt = 0; $lastError = ''; while ($attempt <= $maxRetries) { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $apiKey ], CURLOPT_TIMEOUT => 120, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0, CURLOPT_TCP_KEEPALIVE => 1, CURLOPT_FORBID_REUSE => false ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($response !== false && $httpCode === 200) { $result = json_decode($response, true); return $result; } $lastError = $curlError ?: "HTTP错误:{$httpCode}"; $attempt++; sleep(1); } throw new \Exception("请求失败(重试{$maxRetries}次):{$lastError}"); } }