前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用EdgeOne边缘函数搭建无服务器AI绘图站

使用EdgeOne边缘函数搭建无服务器AI绘图站

原创
作者头像
kr
修改2023-10-24 14:46:07
55.5K1
修改2023-10-24 14:46:07
举报
文章被收录于专栏:个人教程

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

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

开通AI绘画

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

AI绘画控制台
AI绘画控制台

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

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

AI绘画新用户活动
AI绘画新用户活动

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

资源包管理
资源包管理

获取API密钥

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

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

记下现在获取到的SecretId和SecretKey

API密钥管理
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

请求体JSON

String,绘图描述

蓝天白云,草地牛羊

NegativePrompt

请求体JSON

String,反向描述

山川

Styles

请求体JSON

数组类型,绘画风格

[“103”]

ResultConfig

请求体JSON

字典类型,结果配置(例如图片大小)

{“Resolution”:”768:1024”}

编写EdgeOne边缘函数

完整代码如下:

修改位置有3处:

1. 上面获取到的API密钥SecretId

2. 上面获取到的API密钥SecretKey

3. 访问密钥acckey,为避免他人未授权调用,请勿为空

代码语言:javascript
复制
// 将字符串编码为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网址

代码语言:javascript
复制
<!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购买页
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,然后放入下面的代码中,再复制到上图的函数代码框即可

代码语言:javascript
复制
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

免费试用活动
免费试用活动

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开通AI绘画
  • 获取API密钥
  • 了解腾讯云API调用过程与AI绘画相关文档
  • 编写EdgeOne边缘函数
  • 前端代码
  • EdgoOne边缘函数部署项目前后端
  • 效果展示
  • 后记
相关产品与服务
大模型图像创作引擎
大模型图像创作引擎是一款提供 AI 图像生成与处理能力的 API 技术服务,可以结合输入的文本或图像智能创作图像内容,具有更精美的绘图品质、更强大的中文理解能力、更多样的风格选择与更广泛的应用场景,为高质量的图像内容创作、内容运营提供技术支持。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档