一元网络论坛

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 97|回复: 0

免费部署 CF Worker 无限绘画,支持多模型切换,API 可用。

[复制链接]

3万

主题

3万

帖子

9万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
96128
发表于 2024-10-4 06:48:56 | 显示全部楼层 |阅读模式
## Cloudflare Worker 无限免费绘画 API
**旧帖无法编辑,最新版本请访问以下链接:**
* [第一弹](https://linux.do/t/topic/185850)
* [第二弹](https://linux.do/t/topic/186692)
* [第三弹](https://linux.do/t/topic/222370)
---
**准备工作:**
1. 注册 Cloudflare 并开通 Worker AI Token
2. 注册 sm.ms 图床 (需自行申请: [https://sm.ms](https://sm.ms))
3. 复制以下代码部署到 Cloudflare Workers
**更新日志 (10 月 3 日):**
* 新增部分绘画模型
* 新增 `--ntl` (强制关闭翻译), `--tl` (强制翻译) 参数控制提示词优化翻译
* 新增测速模型
* 优化自定义图像大小,可使用参数: `--1:1`, `--1:2`, `--3:2`, `--4:3`, `--16:9`, `--9:16`

代码
//本项目授权api_key,防止被恶意调用(填入到one-api/new-api的渠道密钥中)
const API_KEY = "sk-1234567890";
//https://sm.ms 图床key,可自行申请,为空则返回base64编码后的图片
const SMMS_API_KEY = 'xxxxxxxxx';
//cloudflare账号列表,每次请求都会随机从列表里取一个账号
const CF_ACCOUNT_LIST = [{
    account_id: "xxxxxxxxx",
    token: "xxxxxxxxx"
}];
//在你输入的prompt中添加 ---ntl可强制禁止提示词翻译、优化功能
//在你输入的prompt中添加 ---tl可强制开启提示词翻译、优化功能
//是否开启提示词翻译、优化功能
const CF_IS_TRANSLATE = true;
//示词翻译、优化模型
const CF_TRANSLATE_MODEL = "@cf/qwen/qwen1.5-14b-chat-awq";
const RATIO_MAP = {
    "1:1": "1024x1024",
    "1:2": "1024x2048",
    "3:2": "1536x1024",
    "4:3": "1536x2048",
    "16:9": "2048x1152",
    "9:16": "1152x2048"
}
//模型映射,设置客户端可用的模型。one-api,new-api在添加渠道时可使用“获取模型列表”功能,一键添加模型
const CUSTOMER_MODEL_MAP = {
    "test": {
        body: {
            model: "test"
        }
    },
    "FLUX.1": {
        isImage2Image: false,
        body: {
            model: "@cf/black-forest-labs/flux-1-schnell",
            prompt: "",
            width: 1024,
            height: 1024,
            num_steps: 8
        },
        RATIO_MAP: RATIO_MAP
    },
    "dreamshaper-8": {
        isImage2Image: false,
        body: {
            model: "@cf/lykon/dreamshaper-8-lcm",
            prompt: "",
            width: 1024,
            height: 1024,
            num_steps: 20
        },
        RATIO_MAP: RATIO_MAP
    },
    "stable-diffusion-xl-base": {
        isImage2Image: false,
        body: {
            model: "@cf/stabilityai/stable-diffusion-xl-base-1.0",
            prompt: "",
            width: 1024,
            height: 1024,
            num_steps: 20
        },
        RATIO_MAP: RATIO_MAP
    },
    "stable-diffusion-xl-lightning": {
        isImage2Image: false,
        body: {
            model: "@cf/bytedance/stable-diffusion-xl-lightning",
            prompt: "",
            width: 1024,
            height: 1024,
            num_steps: 20
        },
        RATIO_MAP: RATIO_MAP
    },
    "stable-diffusion-v1-5": {
        isImage2Image: true,
        body: {
            model: "@cf/runwayml/stable-diffusion-v1-5-inpainting",
            prompt: "",
            width: 1024,
            height: 1024,
            num_steps: 20
        },
        RATIO_MAP: RATIO_MAP
    },
    "stable-diffusion-v1-5-img2img": {
        isImage2Image: true,
        body: {
            model: "@cf/runwayml/stable-diffusion-v1-5-img2img",
            prompt: "",
            width: 1024,
            height: 1024,
            num_steps: 20
        },
        RATIO_MAP: RATIO_MAP
    }   
};
async function handleRequest(request) {
    try {
        if (request.method === "OPTIONS") {
            return getResponse("", 204);
        }
        const authHeader = request.headers.get("Authorization");
        if (!authHeader || !authHeader.startsWith("Bearer ") || authHeader.split(" ")[1] !== API_KEY) {
            return getResponse("Unauthorized", 401);
        }
        if (request.url.endsWith("/v1/models")) {
            const arrs = [];
            Object.keys(CUSTOMER_MODEL_MAP).map(element => arrs.push({
                id: element,
                object: "model"
            }))
            const response = {
                data: arrs,
                success: true
            };
            return getResponse(JSON.stringify(response), 200);
        }
        if (request.method !== "POST") {
            return getResponse("Only POST requests are allowed", 405);
        }
        if (!request.url.endsWith("/v1/chat/completions")) {
            return getResponse("Not Found", 404);
        }
        const data = await request.json();
        const messages = data.messages || [];
        const modelInfo = CUSTOMER_MODEL_MAP[data.model] || CUSTOMER_MODEL_MAP["FLUX.1"];
        const stream = data.stream || false;
        const userMessage = messages.reverse().find((msg) => msg.role === "user")?.content;
        if (!userMessage) {
            return getResponse(JSON.stringify({
                error: "未找到用户消息"
            }), 400);
        }
        if (modelInfo.body.model == "test") {
            if (stream) {
                return handleStreamResponse(userMessage, "", "", data.model, "");
            } else {
                return handleNonStreamResponse(userMessage, "", "", data.model, "");
            }
        }
        const is_translate = extractTranslate(userMessage);
        const size = extractImageSize(userMessage, modelInfo.RATIO_MAP);
        const imageUrl = extractImageUrl(userMessage);
        const originalPrompt = cleanPromptString(userMessage);
        const translatedPrompt = is_translate ? await getPrompt(originalPrompt) : originalPrompt;
        let url;
        if (!imageUrl) {
            url = await generateImage(translatedPrompt, "", modelInfo, size);
        } else {
            const base64 = await convertImageToBase64(imageUrl);
            url = await generateImage(translatedPrompt, base64, modelInfo, size);
        }
        if (stream) {
            return handleStreamResponse(originalPrompt, translatedPrompt, size, data.model, url);
        } else {
            return handleNonStreamResponse(originalPrompt, translatedPrompt, size, data.model, url);
        }
    } catch (error) {
        return getResponse(JSON.stringify({
            error: `处理请求失败: ${error.message}`
        }), 500);
    }
}
async function generateImage(translatedPrompt, base64Image, modelInfo, imageSize) {
    try {
        const jsonBody = modelInfo.body;
        jsonBody.prompt = translatedPrompt;
        jsonBody.width = parseInt(imageSize.split('x')[0]);
        jsonBody.height = parseInt(imageSize.split('x')[1]);
        if (modelInfo.isImage2Image && base64Image) {
            jsonBody.image = [...new Uint8Array(base64ToArrayBuffer(base64Image))];
            jsonBody.mask = [...new Uint8Array(base64ToArrayBuffer(base64Image))];
        }
        let requestBody={};
        for (let key in jsonBody) {
            if(key!="model"){
                requestBody[key]=jsonBody[key];
            }
        }
        const response = await postRequest(modelInfo.body.model, requestBody);
        let image_blob;
        let image_b64
        try {
            const jsonResponse = await response.clone().json();
            if (jsonResponse && jsonResponse.success) {
                image_b64 = jsonResponse.result.image;
                image_blob = base64ToBlob(image_b64, "image/png");
            } else {
                return "生成图像失败," + jsonResponse.errors[0]?.message;
            }
        } catch (error) {
            const arrayBuffer = await response.clone().arrayBuffer();
            image_b64 = arrayBufferToBase64(arrayBuffer);
            image_blob = new Blob([arrayBuffer], {
                type: "image/png"
            });
        }
        if (SMMS_API_KEY) {
            const imageUrl = await uploadImage(image_blob);
            return imageUrl;
        } else {
            return `data:image/webp;base64,${image_b64}`;
        }
    } catch (error) {
        return "图像生成或转换失败,请检查!" + error.message;
    }
}
async function uploadImage(imageBlob) {
    try {
        //const imageBlob = await response.blob();
        const formData = new FormData();
        formData.append("smfile", imageBlob, "image.jpg");
        const uploadResponse = await fetch("https://sm.ms/api/v2/upload", {
            method: 'POST',
            headers: {
                'Authorization': `${SMMS_API_KEY}`,
            },
            body: formData,
        });
        if (!uploadResponse.ok) {
            throw new Error("Failed to upload image");
        }
        const uploadResult = await uploadResponse.json();
        const imageUrl = uploadResult.data?.url;
        return imageUrl;
    } catch (error) {
        return "图像上传失败,请检查!" + error.message;
    }
}
async function convertImageToArrayBuffer(imageUrl) {
    try {
        const response = await fetch(imageUrl);
        if (!response.ok) {
            //throw new Error('Failed to download image');
            return null;
        }
        return [...new Uint8Array(await response.arrayBuffer())]
    } catch (error) {
        return null;
    }
}
async function convertImageToBase64(imageUrl) {
    try {
        const response = await fetch(imageUrl);
        if (!response.ok) {
            //throw new Error('Failed to download image');
            return "";
        }
        const arrayBuffer = await response.arrayBuffer();
        const base64Image = arrayBufferToBase64(arrayBuffer);
        return base64Image;
        //return `data:image/webp;base64,${base64Image}`;
    } catch (error) {
        return "";
    }
}
function base64ToBlob(base64, mimeType = '') {
    let bstr = atob(base64),
        n = bstr.length,
        u8arr = new Uint8Array(n);
    while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8arr], {
        type: mimeType
    });
}
function base64ToArrayBuffer(base64) {
    // 解码 base64
    let binaryString = atob(base64);
    // 创建 ArrayBuffer
    let len = binaryString.length;
    let bytes = new Uint8Array(len);
    // 将每个字符的 UTF-8 值转换为字节
    for (let i = 0; i  {
    event.respondWith(handleRequest(event.request));
});
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|一元网络论坛

GMT+8, 2024-11-26 06:41 , Processed in 0.069322 second(s), 20 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表