AI绘画需要强大的数据和算力支持,只有经过良好训练的算法和数据集才能创造出卓越作品。然而,这对于想探索AI绘画的人来说门槛较高。直到我发现了腾讯云的AI绘图产品,开通送500张,用完后购买1000张也不到30,使用一圈后觉得还挺不错的。以前自己用sd搭建费时费钱,折腾环境和锻炼的耗时不说,高峰期任务量大服务器性能不足、低谷期没任务服务器在那干费钱。现在好多了,直接可以不用GPU服务器一台轻量搞定,不管高峰低谷出图时间都很稳定,而且灵活性增加成本大大降低。
结合EdgeOne边缘函数,通过靠近用户的边缘节点运行AI绘图调用程序,不仅省去了服务器,还可提升访问速度。
开通AI绘画
进入AI绘画控制台,点击立即开通。
开通后会赠送500次免费额度,新购的话目前有活动,
例如我下面这1000张就是在这个活动买的:
29.9能买1000张,一张不到3分钱,还是特别划算的。
获取API密钥
进入API密钥管理,新建密钥
然后点击生成的密钥右侧的显示按钮,用管理员微信扫码。
记下现在获取到的SecretId和SecretKey
了解腾讯云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,为避免他人未授权调用,请勿为空
// 将字符串编码为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
进入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