request->param(); $service = new ImageService(); $service->handleImage($params); $this->success('成功存入队列中'); } /** * 开启队列任务 */ public function kaiStats() { // 判断是否已有监听进程在运行 $check = shell_exec("ps aux | grep 'queue:listen' | grep -v grep"); if ($check) { return json([ 'code' => 1, 'msg' => '监听进程已存在,请勿重复启动' ]); } // 启动监听 // $command = 'php think queue:listen --queue --timeout=300 --sleep=3 --memory=256 > /dev/null 2>&1 &'; $command = 'nohup php think queue:listen --queue --timeout=300 --sleep=3 --memory=256 > /var/log/job_queue.log 2>&1 &'; exec($command, $output, $status); if ($status === 0) { return json([ 'code' => 0, 'msg' => '队列监听已启动' ]); } else { return json([ 'code' => 1, 'msg' => '队列启动失败', 'output' => $output ]); } } /** * 查看队列任务 */ public function queueStats() { $statusCounts = Db::name('queue_log') ->field('status, COUNT(*) as total') ->whereTime('created_at', 'today') ->where('status','<>', 4) ->group('status') ->select(); $result = [ '待处理' => 0, '处理中' => 0, '成功' => 0, '失败' => 0 ]; $total = 0; foreach ($statusCounts as $row) { $count = $row['total']; $total += $count; switch ($row['status']) { case 0: $result['待处理'] = $count; break; case 1: $result['处理中'] = $count; break; case 2: $result['成功'] = $count; break; case 3: $result['失败'] = $count; break; } } return json([ 'code' => 0, 'msg' => '获取成功', 'data' => ['总任务数' => $total] + $result ]); } /** * 清空停止队列(同时删除近30分钟的队列日志,并停止 systemd 队列服务) */ public function stopQueueProcesses() { try { // 连接 Redis $redis = new \Redis(); $redis->connect('127.0.0.1', 6379); $redis->select(15); $key = 'queues:default'; $count = $redis->lLen($key); // 计算时间:当前时间前 30 分钟 $time = date('Y-m-d H:i:s', strtotime('-30 minutes')); // 删除数据库中状态为0且在近30分钟的数据 $deleteCount = Db::name('queue_log') ->where('status', 0) ->where('created_at', '>=', $time) ->delete(); // 如果 Redis 队列不为空,则清空 if ($count > 0) { $redis->del($key); } // 尝试停止 systemd 队列服务 exec('sudo /bin/systemctl stop think-queue.service', $output, $status); if ($status === 0) { return json([ 'code' => 0, 'msg' => "队列服务已成功停止" ]); } else { return json([ 'code' => 2, 'msg' => "但服务停止失败,请检查权限" ]); } } catch (\Exception $e) { return json([ 'code' => 500, 'msg' => '处理异常:' . $e->getMessage() ]); } } // public function stopQueueProcesses() // { // $redis = new \Redis(); // $redis->connect('127.0.0.1', 6379); // $redis->select(15); // // $key = 'queues:default'; // $count = $redis->lLen($key); // // 计算时间:当前时间前30分钟 // $time = date('Y-m-d H:i:s', strtotime('-30 minutes')); // // if ($count === 0) { // // 删除数据库中状态为0且 created_at 在最近30分钟的数据 // $deleteCount = Db::name('queue_log') // ->where('created_at', '>=', $time) // ->delete(); // return json([ // 'code' => 1, // 'msg' => '暂无队列需要停止' // ]); // } // // // 清空 Redis 队列 // $redis->del($key); // // // // // 删除数据库中状态为0且 created_at 在最近30分钟的数据 // $deleteCount = Db::name('queue_log') // ->where('created_at', '>=', $time) // ->delete(); // // return json([ // 'code' => 0, // 'msg' => '已成功停止队列任务' // ]); // } /** * 显示当前运行中的队列监听进程 */ public function viewQueueStatus() { $redis = new \Redis(); $redis->connect('127.0.0.1', 6379); $redis->select(15); $key = 'queues:default'; $count = $redis->lLen($key); $list = $redis->lRange($key, 0, 9); $parsed = array_map(function ($item) { return json_decode($item, true); }, $list); return json([ 'code' => 0, 'msg' => '查询成功', 'count' => $count, 'tasks_preview' => $parsed ]); } // public function imageToTexts() // { // $params = $this->request->param(); // // // 统一路径格式 // $sourceDirRaw = str_replace('\\', '/', trim($params['sourceDir'] ?? '', '/')); // $fileName = trim($params['file_name'] ?? ''); // // // 自动拆分文件名 // if (!$fileName && preg_match('/([^\/]+\.(jpg|jpeg|png))$/i', $sourceDirRaw, $matches)) { // $fileName = $matches[1]; // $sourceDirRaw = preg_replace('/\/' . preg_quote($fileName, '/') . '$/', '', $sourceDirRaw); // } // // // 参数校验 // if ($sourceDirRaw === '' || $fileName === '') { // return $this->error('参数错误:sourceDir 或 file_name 不能为空'); // } // // // 构建路径 // $rootPath = str_replace('\\', '/', ROOT_PATH); // $sourceDir = rtrim($rootPath . 'public/' . $sourceDirRaw, '/') . '/'; // $filePath = $sourceDir . $fileName; // $relativePath = $sourceDirRaw . '/' . $fileName; // // // 文件检查 // if (!is_dir($sourceDir)) { // return $this->error('源目录不存在:' . $sourceDir); // } // if (!is_file($filePath)) { // return $this->error('文件不存在:' . $filePath); // } // // // 避免重复处理 //// $exists = Db::name('text_to_image') //// ->where('old_image_url', $relativePath) //// ->where('chinese_description', '<>', '') //// ->find(); //// if ($exists) { //// return $this->success('该图片已生成描述,无需重复处理'); //// } // //// try { // // 获取图片信息 // $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}"; // // // 构建严格格式的提示词 // $userPrompt = preg_replace('/\s+/u', '', $params['prompt']); // 移除所有空白字符 // $strictPrompt = "严格遵守以下规则: //1. 只返回三段内容: // 第一段:纯中文图案描述 // 第二段:---json json--- // 第三段:纯英文图案描述 //2. 描述中必须体现图案的类型、颜色、风格等关键信息 //3. 不允许添加任何解释、引导、说明、示例等文字,必须只包含图案描述内容本身 //3. 示例: //这张图中的图案是代表达拉斯足球队的标志,包括一个头盔图形和围绕它的文字。头盔以灰色和白色为主,有蓝色和黑色的细节。 //---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; // // // 调用 GPT 接口 // $gptRes = $this->callGptApi($imageUrl, $strictPrompt); // $gptText = trim($gptRes['choices'][0]['message']['content'] ?? ''); // // // 验证 GPT 返回格式 // if (strpos($gptText, '---json json---') === false) { // throw new \Exception('GPT 返回格式不正确,缺少分隔符'); // } // // list($chineseDesc, $englishDesc) = array_map('trim', explode('---json json---', $gptText)); // // if ($chineseDesc === '' || $englishDesc === '') { // throw new \Exception('描述内容为空,请检查 GPT 返回'); // } // // // 插入数据库(成功时才插入) // $this->logToDatabase([ // 'old_image_url' => $relativePath, // 'chinese_description' => $chineseDesc, // 'english_description' => $englishDesc, // 'size' => "", // 'status' => 1 // ]); // // return $this->success('图生文成功', [ // 'chinese' => $chineseDesc, // 'english' => $englishDesc // ]); // //// } catch (\Exception $e) { //// // 只写日志,不重复插入数据库 ////// Log::error('图生文失败 [' . $relativePath . ']:' . $e->getMessage()); //// Db::name('text_to_image')->insert([ //// 'old_image_url' => $relativePath, //// 'error_msg' => $e->getMessage(), //// 'status' => 0, //// 'create_time' => date('Y-m-d H:i:s') // 可选:记录时间戳 //// ]); //// return json([ //// 'code' => 1, //// 'msg' => '图生文失败:' . $e->getMessage() //// ]); //// } // } // // /** // * 调用GPT API生成文字描述(改进版) // */ // 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); // } // // // /** // * 文字生成图片(第二步) - 等比缩放生成指定尺寸图 // */ // public function textToImage() // { // $params = $this->request->param(); // $fileName = trim($params['file_name'] ?? ''); // $outputDirRaw = trim($params['outputDir'] ?? '', '/'); // $width = intval($params['width'] ?? 512); // $height = intval($params['height'] ?? 512); // $prompt = trim($params['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); //// if (empty($prompt) || mb_strlen($prompt) < 10) { //// return json(['code' => 1, 'msg' => '提示词过短或为空,请填写有效的英文图像描述']); //// } //// if (preg_match('/[\x{4e00}-\x{9fa5}]/u', $prompt)) { //// return json(['code' => 1, 'msg' => '请使用英文提示词,当前提示内容包含中文']); //// } // // // 查询图像记录 // $record = Db::name('text_to_image') // ->where('old_image_url', 'like', "%{$fileName}") // ->order('id desc') // ->find(); //// echo "
";
//// print_r($record);
//// echo "";
//
// if (!$record) {
// return json(['code' => 1, 'msg' => '没有找到匹配的图像记录']);
// }
//
//// try {
// // 日志记录
// $logDir = ROOT_PATH . '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 API
// $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) {
// throw new \Exception("下载图像失败或内容异常");
// }
//
// // 保存原图
// $filename1024 = 'dalle_' . md5($record['old_image_url'] . microtime()) . '_1024.png';
// $savePath1024 = $fullBaseDir . '1024x1024/' . $filename1024;
// file_put_contents($savePath1024, $imgData1024);
//
// // 创建图像资源
// $im = @imagecreatefromstring($imgData1024);
// if (!$im) {
// throw new \Exception("图像格式不受支持或已损坏");
// }
//
//// 获取原图尺寸
// $srcWidth = imagesx($im);
// $srcHeight = imagesy($im);
//
//// 目标尺寸
// $targetWidth = $width;
// $targetHeight = $height;
//
//// 计算缩放比例(以覆盖为目标,可能会超出一边)
// $ratio = max($targetWidth / $srcWidth, $targetHeight / $srcHeight);
// $newWidth = intval($srcWidth * $ratio);
// $newHeight = intval($srcHeight * $ratio);
//
//// 创建目标图像(目标尺寸)
// $dstImg = imagecreatetruecolor($targetWidth, $targetHeight);
//
//// 缩放后居中裁剪偏移
// $offsetX = intval(($newWidth - $targetWidth) / 2);
// $offsetY = intval(($newHeight - $targetHeight) / 2);
//
//// 临时缩放图像
// $tempImg = imagecreatetruecolor($newWidth, $newHeight);
// imagecopyresampled($tempImg, $im, 0, 0, 0, 0, $newWidth, $newHeight, $srcWidth, $srcHeight);
//
//// 裁剪中间部分到最终尺寸
// imagecopy($dstImg, $tempImg, 0, 0, $offsetX, $offsetY, $targetWidth, $targetHeight);
//
//// 保存结果
// $filenameCustom = 'dalle_' . md5($record['old_image_url'] . microtime()) . "_custom.png";
// $savePathCustom = $fullBaseDir . "{$width}x{$height}/" . $filenameCustom;
// imagepng($dstImg, $savePathCustom);
//
//// 释放内存
// imagedestroy($im);
// imagedestroy($tempImg);
// 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 json([
// 'code' => 0,
// 'msg' => '文生图生成完成',
// 'data' => [
// 'new_image_url' => str_replace($rootPath . 'public/', '', $savePath1024)?? '',
// 'custom_image_url' => str_replace($rootPath . 'public/', '', $savePathCustom)?? '',
// 'size' => "{$width}x{$height}"
// ]
// ]);
//
//// } catch (\Exception $e) {
//// Db::name('text_to_image')->where('id', $record['id'])->update([
//// 'status' => 0,
//// 'error_msg' => $e->getMessage()
//// ]);
////
//// return json(['code' => 1, 'msg' => '文生图失败:' . $e->getMessage()]);
//// }
// }
protected $config = [
'gpt' => [
'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'
]
];
public function callDallesssss()
{
// 确保目录存在
$rootPath = str_replace('\\', '/', ROOT_PATH);
$imgDir = rtrim($rootPath . 'public/' . '/uploads/img/', '/') . '/';
if (!is_dir($imgDir)) mkdir($imgDir, 0755, true);
$filename = 'dalle_' . date('Ymd_His') . '_' . rand(1000, 9999) . '.png';
$savePath = $imgDir . $filename;
$prompt = "这幅图案呈现了一棵精致的树木,树干与枝条采用金色设计,树上盛开着纯白色的花朵。花瓣层层叠加,呈现出优雅的立体感,花心部分呈现细腻的黄色。树叶也以金色为主,整体色调偏向温暖的金白色,背景简洁纯净,整体给人一种高雅且现代的艺术感";
$response = $this->callDalleApi($prompt);
if (!isset($response['data'][0]['url'])) {
throw new \Exception("图像生成失败,未返回图片链接。返回内容:" . json_encode($response));
}
$imageUrl = $response['data'][0]['url'];
// 下载图片到本地目录
$imgContent = file_get_contents($imageUrl);
if ($imgContent === false) {
throw new \Exception("无法下载生成的图像:{$imageUrl}");
}
file_put_contents($savePath, $imgContent);
// 日志记录
$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}\nURL: {$imageUrl}\n", FILE_APPEND);
echo "图像生成成功:public/uploads/img/{$filename}\n";
}
//
// /**
// * 调用 DALL·E 接口
// * 文生图
// */
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);
$data = [
'prompt' => $prompt,
'model' => 'dall-e-2',
// 'model' => 'gpt-image-1',
'n' => 1,
'size' => '1024x1024',
'quality' => 'standard',
'style' => 'vivid',
'response_format' => 'url'
];
return $this->callApi($this->config['dalle']['api_url'], $this->config['dalle']['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}");
}
//
// /**
// * 记录到数据库
// */
// 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 getSourceImages()
// {
// $params = $this->request->param();
// $sourceDir = rtrim(ROOT_PATH . 'public/' . $params['sourceDir'], '/') . '/';
//
// if (!is_dir($sourceDir)) return $this->error('源目录不存在');
//
// $files = glob($sourceDir . '*.{jpg,jpeg,png}', GLOB_BRACE);
// $result = [];
//
// foreach ($files as $file) {
// $relativePath = trim($params['sourceDir'], '/') . '/' . basename($file);
//
// $exists = Db::name('text_to_image')
// ->where('old_image_url', $relativePath)
// ->where('status', 1)
// ->find();
//
// if (!$exists) {
// $result[] = basename($file);
// }
// }
//
// return $this->success('获取成功', $result);
// }
//
// /**
// * 获取处理成功的列表
// */
// public function getlist()
// {
// $today = date('Y-m-d');
// $tomorrow = date('Y-m-d', strtotime('+1 day'));
//
// $res = Db::name('text_to_image')
// ->where('status', 1)
// ->where('created_time', '>=', $today . ' 00:00:00')
// ->where('created_time', '<', $tomorrow . ' 00:00:00')
// ->order('id desc')
// ->select();
//
// foreach ($res as &$item) {
// $item['status_text'] = '成功';
// }
//
// return json(['code' => 0, 'msg' => '获取成功', 'data' => $res]);
// }
//
// /**
// * 获取错误日志
// */
// public function getErrorLogs()
// {
// $res = Db::name('text_to_image')
// ->where('status', 0)
// ->order('id desc')
// ->limit(20)
// ->select();
//
// return json(['code' => 0, 'msg' => '获取失败记录成功', 'data' => $res]);
// }
}