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]);
//    }



}