[腾讯云] 使用EdgeOne边缘函数搭建无服务器AI绘图站

AI绘画需要强大的数据和算力支持,只有经过良好训练的算法和数据集才能创造出卓越作品。然而,这对于想探索AI绘画的人来说门槛较高。直到我发现了腾讯云的AI绘图产品,开通送500张,用完后购买1000张也不到30,使用一圈后觉得还挺不错的。以前自己用sd搭建费时费钱,折腾环境和锻炼的耗时不说,高峰期任务量大服务器性能不足、低谷期没任务服务器在那干费钱。现在好多了,直接可以不用GPU服务器一台轻量搞定,不管高峰低谷出图时间都很稳定,而且灵活性增加成本大大降低。

结合EdgeOne边缘函数,通过靠近用户的边缘节点运行AI绘图调用程序,不仅省去了服务器,还可提升访问速度。

开通AI绘画

进入AI绘画控制台,点击立即开通。

AI绘画控制台

开通后会赠送500次免费额度,新购的话目前有活动,

例如我下面这1000张就是在这个活动买的:

AI绘画新用户活动

29.9能买1000张,一张不到3分钱,还是特别划算的。

资源包管理

获取API密钥

进入API密钥管理,新建密钥

然后点击生成的密钥右侧的显示按钮,用管理员微信扫码。

记下现在获取到的SecretId和SecretKey

API密钥管理

了解腾讯云API调用过程与AI绘画相关文档

调用分4步:

  1. 组合请求参数
  2. 使用API密钥对请求进行签名
  3. 将签名结果附加到请求头中
  4. 发送请求

AI绘画API文档链接:https://cloud.tencent.com/document/api/1668/88064

以下是总结表格

参数名参数位置格式与说明示例
Action请求头String,固定值TextToImage
Version请求头String,固定值2022-12-29
Region请求头String,地域ap-guangdong
Prompt请求体JSONString,绘图描述蓝天白云,草地牛羊
NegativePrompt请求体JSONString,反向描述山川
Styles请求体JSON数组类型,绘画风格[“103”]
ResultConfig请求体JSON字典类型,结果配置(例如图片大小){“Resolution”:”768:1024”}

编写EdgeOne边缘函数

完整代码如下:

修改位置有3处:

  1. 上面获取到的API密钥SecretId
  2. 上面获取到的API密钥SecretKey
  3. 访问密钥acckey,为避免他人未授权调用,请勿为空
// 将字符串编码为ArrayBuffer
function stringToArrayBuffer(str) {
    const encoder = new TextEncoder();
    return encoder.encode(str);
  }
  
  // 将ArrayBuffer转换为十六进制字符串
  function arrayBufferToHexString(arrayBuffer) {
    const byteArray = new Uint8Array(arrayBuffer);
    const hexCodes = [...byteArray].map(value => value.toString(16).padStart(2, '0'));
    return hexCodes.join('');
  }
  
  async function hmacSHA256(key, data) {
    const importedKey = await crypto.subtle.importKey(
      'raw',
      key,
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['sign']
    );
  
    const msgBuffer = stringToArrayBuffer(data);
    const signatureBuffer = await crypto.subtle.sign('HMAC', importedKey, msgBuffer);
  
    return signatureBuffer;
  }
  
  function uint8ArrayToHex(array) {
    return Array.from(array).map(byte => byte.toString(16).padStart(2, '0')).join('');
  }
  
  // 签名算法
  async function qcloud_v3_post(SecretId,SecretKey,Service,bodyString,headersOper) {
    const HTTPRequestMethod = "POST"
    const CanonicalURI = "/"
    const CanonicalQueryString = ""
  
    // 将 JSON 对象中的键按 ASCII 升序进行排序
    let sortedheadersOper = Object.keys(headersOper).filter(key => (key.toLowerCase() !== "x-tc-version")).sort();
    // 遍历排序后的键并拼接
    let SignedHeaders = sortedheadersOper.map(key => key.toLowerCase()).join(";");
    let CanonicalHeaders = sortedheadersOper.map(key => key.toLowerCase() + ":" + headersOper[key].toLowerCase()).join("\n");
    CanonicalHeaders = CanonicalHeaders + "\n"
  
    let HashedRequestPayload = await sha256(bodyString)
  
    const CanonicalRequest =
      HTTPRequestMethod + '\n' +
      CanonicalURI + '\n' +
      CanonicalQueryString + '\n' +
      CanonicalHeaders + '\n' +
      SignedHeaders + '\n' +
      HashedRequestPayload
  
    const currentDate = new Date();
    const year = currentDate.getUTCFullYear();
    const month = (currentDate.getUTCMonth() + 1).toString().padStart(2, '0');
    const day = currentDate.getUTCDate().toString().padStart(2, '0');
    const formattedDate = `${year}-${month}-${day}`;
  
  
    const Algorithm = "TC3-HMAC-SHA256"
    // 获取当前秒级时间戳
    const RequestTimestamp = Math.floor(Date.now() / 1000).toString();
    // const RequestTimestamp = "1688025007"
    const CredentialScope = formattedDate + "/" + Service + "/tc3_request"
    const HashedCanonicalRequest = await sha256(CanonicalRequest)
  
    const StringToSign =
      Algorithm + '\n' +
      RequestTimestamp + '\n' +
      CredentialScope + '\n' +
      HashedCanonicalRequest
    
    const SecretDate = await hmacSHA256(new Uint8Array([...stringToArrayBuffer("TC3"), ...new Uint8Array(SecretKey)]), formattedDate);
    const SecretService = await hmacSHA256(SecretDate, Service);
    const SecretSigning = await hmacSHA256(SecretService, "tc3_request");
  
    const Signature = arrayBufferToHexString(await hmacSHA256(SecretSigning, StringToSign));
  
    const Authorization =
      Algorithm + ' ' +
      'Credential=' + SecretId + '/' + CredentialScope + ', ' +
      'SignedHeaders=' + SignedHeaders + ', ' +
      'Signature=' + Signature

      headersOper["X-TC-Timestamp"] = RequestTimestamp;
      headersOper["Authorization"] = Authorization;
    
      return headersOper
  }

  // sha256 签名摘要
  async function sha256(message) {
    const msgBuffer = new TextEncoder().encode(message);
    const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
  
  
    return uint8ArrayToHex(new Uint8Array(hashBuffer));
  }
  
  // 密钥填写位置
  const SecretId = "";
  const SecretKey = stringToArrayBuffer("");
  const Service = "aiart";
  // 访问密钥设置处
  const acckey = ","
  
  async function handleRequest(request) {

    const json = await request.json()
    const text2imgjson = {}
    if(json.Acckey !== acckey){
        return new Response(JSON.stringify({"code":1,"msg":"密钥错误"}, null, 2), { 
        headers: { 'Content-Type': 'application/json',
        'Access-Control-Allow-Headers': 'Content-Type',
        'Access-Control-Allow-Methods': 'POST, OPTIONS',
        'Access-Control-Max-Age': '86400',
        'Access-Control-Allow-Origin': '*' },
        status: 200 
        })
    }
    text2imgjson["Prompt"] = json.Prompt
    if(json.NegativePrompt !== ""){
        text2imgjson["NegativePrompt"] = json.NegativePrompt
    }
    if(json.Styles !== ""){
        text2imgjson["Styles"] = [json.Styles]
    }
    if(json.Size !== ""){
        text2imgjson["ResultConfig"] = {"Resolution":json.Size}
    }
  
    const headersPending = {
      'Host': 'aiart.tencentcloudapi.com',
      'Content-Type': 'application/json',
      'X-TC-Action': 'TextToImage',
      'X-TC-Version': '2022-12-29',
      'X-TC-Region': 'ap-guangzhou',
    };
  
    const bodyString = JSON.stringify(text2imgjson)
  
    const headers = await qcloud_v3_post(SecretId,SecretKey,Service,bodyString,headersPending)
  
    const url1 = 'https://aiart.tencentcloudapi.com/';
  
  let qcloud_api_data;
  await fetch(url1, {
    method: 'POST',
    headers: headers,
    body: bodyString
  })
  .then(response => response.json())
  .then(data => qcloud_api_data = data)
  .catch(error => qcloud_api_data = error);

  let ResponseData = {};

  if(qcloud_api_data["Response"]["Error"] === undefined){
    ResponseData["code"] = 0;
    ResponseData["image"] = "data:image/jpg;base64," + qcloud_api_data["Response"]["ResultImage"]
  }else{
    ResponseData["code"] = 1;
    ResponseData["msg"] = qcloud_api_data["Response"]["Error"]["Message"]
    ResponseData["RequestId"] = qcloud_api_data["Response"]["RequestId"]
  }
  
  
    return new Response(JSON.stringify(ResponseData, null, 2), { 
        headers: { 'Content-Type': 'application/json',
        'Access-Control-Allow-Headers': 'Content-Type',
        'Access-Control-Allow-Methods': 'POST, OPTIONS',
        'Access-Control-Max-Age': '86400',
        'Access-Control-Allow-Origin': '*' },
        status: 200 
      })
  }

  // 处理预检
async function handleOptions(request) {
    const headers = {
      'Access-Control-Allow-Headers': 'Content-Type',
      'Access-Control-Allow-Methods': 'POST, OPTIONS',
      'Access-Control-Max-Age': '86400',
      'Access-Control-Allow-Origin': '*'
    }
  
    return new Response(null, {
      headers: headers
    })
  }
  
  addEventListener('fetch', (event) => {
    if (event.request.method === 'OPTIONS') {
      event.respondWith(handleOptions(event.request))
    } else {
      event.respondWith(handleRequest(event.request))
    }
  });

前端代码

效果图展示:

前端效果图

源码:

记得替换api网址

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI绘图</title>
    <style>
        body {
            font-family: Arial, sans-serif;
        }
        
        .container {
            display: flex;
            justify-content: center;
        }

        .box {
            margin: 20px;
            border: none; /* 移除框的边框 */
            padding: 10px;
            background-color: #f5f5f5; /* 设置框的背景颜色 */
            border-radius: 10px; /* 添加圆角 */
            display: flex; /* 使用flex布局 */
            flex-direction: column; /* 垂直布局 */
            align-items: center; /* 居中对齐 */
            text-align: center; /* 文字居中对齐 */
        }
        .box:nth-child(2) {
          align-self: start; /* 右边的box顶部对齐 */
        }

        .box-title {
            margin-bottom: 10px;
            border-bottom: 1px solid grey; /* 添加标题底部的分隔线 */
            padding-bottom: 5px; /* 添加一些底部间距以增强视觉效果 */
        }

        #image-container {
            width: 300px;
            height: 380px;
            background-color: #eee;
            margin: auto; /* 在父元素中水平居中 */
            background-image: none;
        }

        #text-description {
            width: 300px;
            height: 100px;
            margin-bottom: 10px;
            border: 1px solid grey; /* 添加外边框 */
            padding: 5px;
            resize: none; /* 禁止调整大小 */
            font-size: 14px;
            outline: none; /* 移除输入框默认的外边框 */
            border-radius: 0; /* 移除圆角 */
        }
        
        #text-description2 {
            width: 300px;
            height: 100px;
            margin-bottom: 10px;
            border: 1px solid grey; /* 添加外边框 */
            padding: 5px;
            resize: none; /* 禁止调整大小 */
            font-size: 14px;
            outline: none; /* 移除输入框默认的外边框 */
            border-radius: 0; /* 移除圆角 */
        }
        
        #dropdown {
            padding: 10px 20px; /* 增大按钮的内边距 */
            cursor: pointer;
            margin-bottom: 10px;
            border: 1px solid grey; /* 添加外边框 */
            width: 100%; /* 将宽度设置为100%以与容器对齐 */
            align-self: flex-start; /* 按钮左对齐 */
        }
        
        #dropdown2 {
            padding: 10px 20px; /* 增大按钮的内边距 */
            cursor: pointer;
            margin-bottom: 10px;
            border: 1px solid grey; /* 添加外边框 */
            width: 100%; /* 将宽度设置为100%以与容器对齐 */
            align-self: flex-start; /* 按钮左对齐 */
        }
        
        #acckey {
            padding: 10px 20px; /* 增大按钮的内边距 */
            cursor: pointer;
            margin-bottom: 10px;
            border: 1px solid grey; /* 添加外边框 */
            width: 100%; /* 将宽度设置为100%以与容器对齐 */
            align-self: stretch; /* 按钮左对齐 */
        }

        #input-container {
            flex-grow: 1;
            display: flex;
            flex-direction: column; /* 垂直布局 */
            align-items: center; /* 居中对齐 */
        }

        #text-input {
            width: 300px;
            height: 30px;
            margin-bottom: 10px;
        }

        #generate-button {
            padding: 10px 20px; /* 增大按钮的内边距 */
            background-color: #4caf50;
            color: white;
            border: none;
            cursor: pointer;
            width: 100%; /* 将宽度设置为100%以与容器对齐 */
            align-self: flex-start; /* 按钮左对齐 */
        }
        
        @media (max-width: 600px) {
            .container {
                flex-wrap: wrap; /* 在屏幕宽度不足时换行显示 */
            }

            .box {
                width: 100%; /* 让框占满一行 */
                margin-bottom: 20px; /* 添加底边距 */
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="box">
            <h2 class="box-title">绘图结果</h2>
            <div id="image-container"></div>
        </div>
        <div class="box">
            <h2 class="box-title">文本描述</h2>
            <div id="input-container">
                <textarea id="text-description" title="文本描述" placeholder="文本描述。
算法将根据输入的文本智能生成与之相关的图像。建议详细描述画面主体、细节、场景等,文本描述越丰富,生成效果越精美。
不能为空,推荐使用中文。最多传512个字符。"></textarea>
                <textarea id="text-description2" title="反向文本描述" placeholder="反向文本描述。
用于一定程度上从反面引导模型生成的走向,减少生成结果中出现描述内容的可能,但不能完全杜绝。
推荐使用中文。最多传512个字符。"></textarea>
                <select id="dropdown" title="分辨率">
                  <option value="768:1024">分辨率(默认768:1024)</option>
                  <option value="768:768">768:768</option>
                  <option value="1024:768">1024:768</option>
                </select>
                <select id="dropdown2" title="绘画风格">
                  <option value="201">绘画风格(默认日系动漫风格)</option>
                  <option value="202">怪兽风格</option>
                  <option value="301">游戏卡通手绘</option>
                  <option value="101">水墨画</option>
                  <option value="102">概念艺术</option>
                  <option value="103">油画</option>
                  <option value="104">水彩画</option>
                  <option value="106">厚涂风格</option>
                  <option value="107">插图</option>
                  <option value="108">剪纸风格</option>
                  <option value="109">印象派</option>
                  <option value="110">2.5D人像</option>
                  <option value="111">肖像画</option>
                  <option value="112">黑白素描画</option>
                  <option value="113">赛博朋克</option>
                  <option value="114">科幻风格</option>
                  <option value="000">不限定风格</option>
                </select>
                <div style="display: flex; justify-content: flex-start; width: 100%;">
                  <input id="acckey" type="password" placeholder="访问密钥" style="width: 100%;">
               </div>
                <button id="generate-button">生成</button>
            </div>
        </div>
    </div>

    <script>
  document.getElementById("generate-button").addEventListener("click", function() {
    var Ai_Image_Prompt = document.getElementById("text-description").value;
    var Ai_Image_NegativePrompt = document.getElementById("text-description2").value;
    var Ai_Image_Size = document.getElementById("dropdown").value;
    var Ai_Image_Styles = document.getElementById("dropdown2").value;
    var Ai_Image_AccKey = document.getElementById("acckey").value;
    
    if(Ai_Image_Prompt === ""){
        alert("图片描述为空");
        return;
    }
    if(Ai_Image_AccKey === ""){
        alert("访问密钥为空");
        return;
    }

    var data = {
      "Prompt": Ai_Image_Prompt,
      "NegativePrompt": Ai_Image_NegativePrompt,
      "Styles": Ai_Image_Styles,
      "Size": Ai_Image_Size,
      "Acckey": Ai_Image_AccKey
    };

    var xhr = new XMLHttpRequest();
    xhr.open("POST", "https://api.9kr.cc/qcloud/text2img", true);
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.onreadystatechange = function() {
        if(xhr.readyState === 4){
      if (xhr.status === 200) {
        var response = JSON.parse(xhr.responseText);
        if (response.code === 0) {
          var imageContainer = document.getElementById("image-container");
          imageContainer.style.backgroundImage = "url(" + response.image + ")";
          imageContainer.style.backgroundSize = "contain";

          var img = new Image();
          img.onload = function() {
            var imageWidth = this.width;
            var imageHeight = this.height;

            var containerWidth = imageContainer.offsetWidth;
            var adjustedHeight = (containerWidth / imageWidth) * imageHeight;

            imageContainer.style.height = adjustedHeight + "px";
          };
          img.src = response.image;
        } else {
          alert(response.msg);
        }
      }else{
          alert("请求错误:" + xhr.status);
      }
    }
    };
    xhr.onerror = function() {
        // 处理网络错误
        alert("网络错误");
    };
    xhr.send(JSON.stringify(data));
  });
</script>

</body>
</html>

EdgoOne边缘函数部署项目前后端

购买EdgeOne套餐,购买链接:https://buy.cloud.tencent.com/edgeone

EdgeOne购买页

进入EdgoOne控制台添加:https://console.cloud.tencent.com/edgeone/zones/

注意点只有一个:绑定套餐时选择绑定已购套餐,即可看到刚才购买的套餐

添加站点

添加站点后选择域名服务à域名管理à添加域名,添加两个域名

  1. ai.xxxx.com --- 用来放前端
  2. api.xxx.com --- 用来放后端

ps.其实添加一个域名,然后根据path区分前后端分别处理信息也可以,但是不方便管理。

添加域名

点击边缘函数à函数管理à添加函数,分别添加两个函数

新建函数

函数一,用于展示前端页面,也就是前面的ai.xxx.com

进入 函数添加页面

新建函数

函数代码如下:

将前面的前端代码中的api网址由” https://api.9kr.cc/qcloud/text2img”替换成https://api.xxx.com,然后放入下面的代码中,再复制到上图的函数代码框即可

const html = `
这里填入刚才上面展示的前端代码
`;
async function handleRequest(request) {
return new Response(html, {
headers: {
'content-type': 'text/html; charset=UTF-8',
'x-edgefunctions-test': 'Welcome to use Edge Functions.',
},
});
}
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});

点击 创建并部署 后,点击新增触发规则

部署成功

在弹出的窗口中,选择HOSTà等于àai.xxx.com,再点击确定

新增触发规则

函数二,用于处理前端绘图请求并返回绘图结果,也就是前面的api.xxx.com

像函数一那样再新建一个函数,然后将第四步修改API密钥和访问密钥后的代码复制上去,最后将触发规则的HOST设置成”api.xxx.com”即可。

效果展示

完成上述操作后打开ai.9kr.cc,可以看到如下界面:

网页效果

输入文本描述,以及上面设置的访问密钥,点击生成:

绘画结果

后记

至此AI绘图站搭建完成。

除了AI绘图,腾讯云还有不少AI产品提供免费试用,后面应该会把这一系列产品做完。

如下为免费试用产品列表,

感兴趣的可以点击这个链接了解:https://cloud.tencent.com/act/free?from=20893
免费试用活动

腾讯云活动
最后修改:2023 年 07 月 21 日
如果觉得我的文章对你有用,请随意赞赏

发表评论
使用cookie技术保留您的个人信息以便您下次快速评论,继续评论表示您已同意该条款

🎲