使用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

请求体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; /* 添加底边距 */
        }
    }
&lt;/style&gt;

</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>

&lt;script&gt;

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 === &#34;&#34;){
    alert(&#34;图片描述为空&#34;);
    return;
}
if(Ai_Image_AccKey === &#34;&#34;){
    alert(&#34;访问密钥为空&#34;);
    return;
}

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

var xhr = new XMLHttpRequest();
xhr.open(&#34;POST&#34;, &#34;https://api.9kr.cc/qcloud/text2img&#34;, true);
xhr.setRequestHeader(&#34;Content-Type&#34;, &#34;application/json&#34;);
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(&#34;image-container&#34;);
      imageContainer.style.backgroundImage = &#34;url(&#34; + response.image + &#34;)&#34;;
      imageContainer.style.backgroundSize = &#34;contain&#34;;

      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 + &#34;px&#34;;
      };
      img.src = response.image;
    } else {
      alert(response.msg);
    }
  }else{
      alert(&#34;请求错误:&#34; + xhr.status);
  }
}
};
xhr.onerror = function() {
    // 处理网络错误
    alert(&#34;网络错误&#34;);
};
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,然后放入下面的代码中,再复制到上图的函数代码框即可

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

免费试用活动