[ '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 { if ($logId) { Db::name('queue_log')->where('id', $logId)->update([ 'status' => 1, // 正在处理 'updated_at' => date('Y-m-d H:i:s') ]); } // 执行业务逻辑 $this->processImage($data); if ($logId) { Db::name('queue_log')->where('id', $logId)->update([ 'status' => 2, 'log' => '执行成功', 'updated_at' => date('Y-m-d H:i:s') ]); } $job->delete(); } catch (\Exception $e) { 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() < 3) { $job->release(30); } else { $job->failed(); } } } // public function fire(Job $job, $data) // { // echo "队列任务开始执行\n"; // echo "接收的数据: " . json_encode($data) . "\n"; // // try { // // 执行实际的业务逻辑 // $this->processImage($data); // // // 任务执行成功后删除 // $job->delete(); // echo "任务执行成wwww功\n"; // // } catch (\Exception $e) { // echo "任务执行失败: " . $e->getMessage() . "\n"; // // // 重试机制 // if ($job->attempts() < 3) { // $job->release(30); // 30秒后重试 // echo "任务重新入队,重试次数: " . $job->attempts() . "\n"; // } else { // $job->failed(); // echo "任务最终失败\n"; // } // } // } /** * 任务失败时的处理 */ public function failed($data) { // 记录失败日志或发送通知 \think\Log::error("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}"; // 构建严格格式的提示词 //请严格按以下要求分析图案:只提取图案本身的视觉元素(图形、字母、文字、符号),忽略所有背景和载体信息(如壁画载体、衣服等), 描述必须包含: 主体图形特征(形状/颜色/材质感),文字内容(字母/单词/数字及其样式),空间排列关系,整体艺术风格---json json--- 格式:{纯图案的客观中文描述,不包含任何背景说明}---json json---{ "prompt": "English description focusing only on graphic elements with style details, on pure black background","size": "1024x1024","color_palette": ["主色1", "主色2"],"style": "图案风格"}---json json--- $userPrompt = preg_replace('/\s+/u', '', $prompt); // 移除所有空白字符 $strictPrompt = "严格遵守以下规则: 1. 只返回三段内容: 第一段:纯中文图案描述 第二段:---json json--- 第三段:纯英文图案描述 2. 描述中必须体现图案的类型、颜色、风格等关键信息 3. 不允许添加任何解释、引导、说明、示例等文字,必须只包含图案描述内容本身 4. 示例: 这张图中的图案是代表达拉斯足球队的标志,包括一个头盔图形和围绕它的文字。头盔以灰色和白色为主,有蓝色和黑色的细节。 ---json json--- The pattern in this picture is the logo representing the Dallas football team, including a helmet figure and the text around it. The helmet is mainly gray and white, with blue and black details. 请直接描述这个图案: " . $userPrompt; // 调用图生文 $gptRes = $this->callGptApi($imageUrl, $strictPrompt); $gptText = trim($gptRes['choices'][0]['message']['content'] ?? ''); // 验证 GPT 返回格式 if (strpos($gptText, '---json json---') === false) { return 'GPT 返回格式不正确,缺少分隔符'; } list($chineseDesc, $englishDesc) = array_map('trim', explode('---json json---', $gptText)); if ($chineseDesc === '' || $englishDesc === '') { return '描述内容为空,请检查 GPT 返回'; } // 插入数据库(成功时才插入) $this->logToDatabase([ 'old_image_url' => $relativePath, 'chinese_description' => $chineseDesc, 'english_description' => $englishDesc, 'size' => "", 'status' => 1 ]); //进行文字转图片 $res = $this->textToImage($fileName,$call_data["outputDir"],$call_data["width"],$call_data["height"],$chineseDesc.$englishDesc); return $res; } public function textToImage($fileName, $outputDirRaw, $width, $height, $prompt) { // 统一路径分隔符为 / $rootPath = str_replace('\\', '/', ROOT_PATH); $outputDir = rtrim($rootPath . 'public/' . $outputDirRaw, '/') . '/'; $dateDir = date('Y-m-d') . '/'; $fullBaseDir = $outputDir . $dateDir; // 创建所需目录 foreach ([$fullBaseDir, $fullBaseDir . '1024x1024/', $fullBaseDir . "{$width}x{$height}/"] as $dir) { if (!is_dir($dir)) { mkdir($dir, 0755, true); } } // 规范化提示词 $prompt = preg_replace('/[\r\n\t]+/', ' ', $prompt); // 查询数据库记录 $record = Db::name('text_to_image') ->where('old_image_url', 'like', "%{$fileName}") ->order('id desc') ->find(); if (!$record) { return '没有找到匹配的图像记录'; } // 记录提示词日志 $logDir = $rootPath . 'runtime/logs/'; if (!is_dir($logDir)) mkdir($logDir, 0755, true); file_put_contents($logDir . 'prompt_log.txt', date('Y-m-d H:i:s') . " prompt: {$prompt}\n", FILE_APPEND); // 调用 DALL·E 接口 $dalle1024 = $this->callDalleApi($prompt); file_put_contents($logDir . 'dalle_response.log', date('Y-m-d H:i:s') . "\n" . print_r($dalle1024, true) . "\n", FILE_APPEND); // 校验返回链接 if (!isset($dalle1024['data'][0]['url']) || empty($dalle1024['data'][0]['url'])) { $errorText = $dalle1024['error']['message'] ?? '未知错误'; throw new \Exception('DALL·E 生成失败:' . $errorText); } $imgUrl1024 = $dalle1024['data'][0]['url']; $imgData1024 = @file_get_contents($imgUrl1024); if (!$imgData1024 || strlen($imgData1024) < 1000) { return "下载图像失败或内容异常"; } // 保存原图 $filename1024 = 'dalle_' . md5($record['old_image_url'] . microtime()) . '_1024.png'; $savePath1024 = $fullBaseDir . '1024x1024/' . $filename1024; file_put_contents($savePath1024, $imgData1024); // 创建图像资源 $im = @imagecreatefromstring($imgData1024); if (!$im) { return "图像格式不受支持或已损坏"; } // 获取原图尺寸 $srcWidth = imagesx($im); $srcHeight = imagesy($im); // 创建目标图像(缩放到目标尺寸,无裁剪) $dstImg = imagecreatetruecolor($width, $height); imagecopyresampled($dstImg, $im, 0, 0, 0, 0, $width, $height, $srcWidth, $srcHeight); // 保存缩放图 $filenameCustom = 'dalle_' . md5($record['old_image_url'] . microtime()) . "_custom.png"; $savePathCustom = $fullBaseDir . "{$width}x{$height}/" . $filenameCustom; imagepng($dstImg, $savePathCustom); // 释放资源 imagedestroy($im); imagedestroy($dstImg); // 更新数据库 Db::name('text_to_image')->where('id', $record['id'])->update([ 'new_image_url' => str_replace($rootPath . 'public/', '', $savePath1024), 'custom_image_url' => str_replace($rootPath . 'public/', '', $savePathCustom), 'error_msg' => '', 'size' => "{$width}x{$height}", 'updated_time' => date('Y-m-d H:i:s') ]); return 0; } public function callDalleApi($prompt) { $data = [ 'prompt' => $prompt, 'model' => 'dall-e-2', 'n' => 1, 'size' => '1024x1024' ]; return $this->callApi($this->config['dalle']['api_url'], $this->config['dalle']['api_key'], $data); } 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'] ?? '', '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' => 'dall-e-2', '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}"); } }