几个月前,我开始为一个专注于科技领域的客户合作一个关于人工智能生成内容的项目。我的职责主要是使用 WordPress 设置 SSG 作为 Headless CMS 用于 Nuxt 前端。
客户过去每周写几次关于影响该行业的不同趋势或情况的文章,为了增加网站的流量和文章的输出,他决定使用人工智能为他生成文章。
一段时间后,在正确的提示下,客户得到的信息与人类撰写的文章几乎完全匹配,很难发现它们是机器制作的。
在我开始研究不同的功能后,我会不断被问到一件特定的事情。
哎,你能更新一下这篇文章的特色图片吗?
经过两周的每日更新帖子后,我突然灵光一现。
为什么我不使用人工智能自动为这些文章生成特色图像?
我们已经自动化撰写帖子,为什么不自动化精选图片?
在空闲时间,我在计算机上尝试生成法学硕士,因此我或多或少对如何解决这个支线任务有了一个扎实的想法。我向客户发送了一条消息,详细说明了问题是什么、我想要做什么以及优势是什么,无需令人信服,我就获得了使用此功能的绿灯,并立即同意了我的第一步。
鉴于我接触过在本地运行模型,我立即知道自行托管这些模型是不可行的。放弃这个之后,我开始尝试根据文本提示生成图像的 API。
特色图片由两部分组成:主要组成的图形和吸引人的标语。
组成的图形将是与文章相关的一些元素,以良好的方式排列,然后应用一些颜色和纹理,并应用一些混合模式来实现品牌之后的一些奇特效果。
标语是简短的 8-12 个单词的句子,下面有一个简单的阴影。
根据我的测试,我意识到追求图像生成的人工智能路线是不切实际的。图像质量没有达到预期,而且过程太耗时,无法证明其使用的合理性。考虑到这将作为 AWS Lambda 函数运行,其中执行时间直接影响成本。
放弃了这一点,我选择了 B 计划:使用 JavaScript 的 Canvas API 将图像和设计资源混合在一起。
深入观察,我们主要有 5 种风格的简单帖子,以及大约 4 种类型的纹理,其中 3 种使用相同的文本对齐方式、样式和位置。做了一些数学计算后,我想:
嗯,如果我拍摄这 3 个图像,抓取 8 个纹理并使用混合模式,我就可以解决 24 种变化
鉴于这 3 种类型的帖子具有相同的文本样式,它实际上是一个模板。
解决了这个问题后,我转向了标语生成器。我想根据文章的内容和标题创建一个口号。鉴于公司已经支付了费用,我决定使用 ChatGPT 的 API,经过一些实验和提示调整后,我的口号生成器有了一个非常好的 MVP。
弄清楚任务中最困难的 2 个部分后,我花了一些时间在 Figma 中整理出我服务的最终架构的图表。
计划创建一个 Lambda 函数,能够分析帖子内容、生成标语并组装特色图像 - 所有这些都与 WordPress 无缝集成。
我将提供一些代码,但足以向ke传达总体想法。
Lambda 函数首先从传入事件负载中提取必要的参数:
const { title: request_title, content, backend, app_password} = JSON.parse(event.body);
该函数的第一个主要任务是使用analyzeContent函数生成标语,该函数使用OpenAI的API根据文章的标题和内容制作值得点击的标语。
我们的函数获取帖子标题和内容,但返回标语、帖子情绪以了解帖子是正面、负面还是中性意见,以及来自标准普尔指数公司的可选公司符号。
const { 口号、情感、公司 } =等待analyzeContent({ title: request_title, content });
这一步很关键,因为口号直接影响图片的美感。
接下来,generateImage函数开始运行:
let buffer; buffer = await generateImage({ title: tagline, company_logo: company_logo, sentiment: sentiment, });
该函数处理:
以下是其工作原理的分步分析:
generateImage 函数首先设置一个空白画布,定义其尺寸,并准备好处理所有设计元素。
let buffer; buffer = await generateImage({ title: tagline, company_logo: company_logo, sentiment: sentiment, });
从那里,从预定义的资源集合中加载随机背景图像。这些图像经过精心设计,以适应以技术为导向的品牌,同时允许帖子之间有足够的多样性。背景图像是根据其情绪随机选择的。
为了确保每个背景图像看起来都很棒,我根据宽高比动态计算其尺寸。这样可以避免扭曲,同时保持视觉平衡完好。
标语很短,但根据一些规则,这个有影响力的句子被分成可管理的部分,并动态设计样式以确保它始终可读,无论长度或画布大小如何(基于行的字数、字长等) .
const COLOURS = { BLUE: "#33b8e1", BLACK: "#000000", } const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const images_path = path.join(__dirname, 'images/'); const files_length = fs.readdirSync(images_path).length; const images_folder = process.env.ENVIRONMENT === "local" ? "./images/" : "/var/task/images/"; registerFont("/var/task/fonts/open-sans.bold.ttf", { family: "OpenSansBold" }); registerFont("/var/task/fonts/open-sans.regular.ttf", { family: "OpenSans" }); console.log("1. Created canvas"); const canvas = createCanvas(1118, 806); let image = await loadImage(`${images_folder}/${Math.floor(Math.random() * (files_length - 1 + 1)) + 1}.jpg`); let textBlockHeight = 0; console.log("2. Image loaded"); const canvasWidth = canvas.width; const canvasHeight = canvas.height; const aspectRatio = image.width / image.height; console.log("3. Defined ASPECT RATIO",) let drawWidth, drawHeight; if (image.width > image.height) { // Landscape orientation: fit by width drawWidth = canvasWidth; drawHeight = canvasWidth / aspectRatio; } else { // Portrait orientation: fit by height drawHeight = canvasHeight; drawWidth = canvasHeight * aspectRatio; } // Center the image const x = (canvasWidth - drawWidth) / 2; const y = (canvasHeight - drawHeight) / 2; const ctx = canvas.getContext("2d"); console.log("4. Centered Image") ctx.drawImage(image, x, y, drawWidth, drawHeight);
最后,画布被转换为 PNG 缓冲区。
console.log("4.1 Text splitting"); if (splitText.length === 1) { const isItWiderThanHalf = ctx.measureText(splitText[0]).width > ((canvasWidth / 2) + 160); const wordCount = splitText[0].split(" ").length; if (isItWiderThanHalf && wordCount > 4) { const refactored_line = splitText[0].split(" ").reduce((acc, curr, i) => { if (i % 3 === 0) { acc.push([curr]); } else { acc[acc.length - 1].push(curr); } return acc; }, []).map((item) => item.join(" ")); refactored_line[1] = "[s]" + refactored_line[1] + "[s]"; splitText = refactored_line } } let tagline = splitText.filter(item => item !== '' && item !== '[br]' && item !== '[s]' && item !== '[/s]' && item !== '[s]'); let headlineSentences = []; let lineCounter = { total: 0, reduced_line_counter: 0, reduced_lines_indexes: [] } console.log("4.2 Tagline Preparation", tagline); for (let i = 0; i < tagline.length; i++) { let line = tagline[i]; if (line.includes("[s]") || line.includes("[/s]")) { const finalLine = line.split(/(\[s\]|\[\/s\])/).filter(item => item !== '' && item !== '[s]' && item !== '[/s]'); const lineWidth = ctx.measureText(finalLine[0]).width const halfOfWidth = canvasWidth / 2; if (lineWidth > halfOfWidth && finalLine[0]) { let splitted_text = finalLine[0].split(" ").reduce((acc, curr, i) => { const modulus = finalLine[0].split(" ").length >= 5 ? 3 : 2; if (i % modulus === 0) { acc.push([curr]); } else { acc[acc.length - 1].push(curr); } return acc; }, []); let splitted_text_arr = [] splitted_text.forEach((item, _) => { let lineText = item.join(" "); item = lineText splitted_text_arr.push(item) }) headlineSentences[i] = splitted_text_arr[0] + '/s/' if (splitted_text_arr[1]) { headlineSentences.splice(i + 1, 0, splitted_text_arr[1] + '/s/') } } else { headlineSentences.push("/s/" + finalLine[0] + "/s/") } } else { headlineSentences.push(line) } } console.log("5. Drawing text on canvas", headlineSentences); const headlineSentencesLength = headlineSentences.length; let textHeightAccumulator = 0; for (let i = 0; i < headlineSentencesLength; i++) { headlineSentences = headlineSentences.filter(item => item !== '/s/'); const nextLine = headlineSentences[i + 1]; if (nextLine && /^\s*$/.test(nextLine)) { headlineSentences.splice(i + 1, 1); } let line = headlineSentences[i]; if (!line) continue; let lineText = line.trim(); let textY; ctx.font = " 72px OpenSans"; const cleanedUpLine = lineText.includes('/s/') ? lineText.replace(/\s+/g, ' ') : lineText; const lineWidth = ctx.measureText(cleanedUpLine).width const halfOfWidth = canvasWidth / 2; lineCounter.total += 1 const isLineTooLong = lineWidth > (halfOfWidth + 50); if (isLineTooLong) { if (lineText.includes(':')) { const split_line_arr = lineText.split(":") if (split_line_arr.length > 1) { lineText = split_line_arr[0] + ":"; if (split_line_arr[1]) { headlineSentences.splice(i + 1, 0, split_line_arr[1]) } } } ctx.font = "52px OpenSans"; lineCounter.reduced_line_counter += 1 if (i === 0 && headlineSentencesLength === 2) { is2LinesAndPreviewsWasReduced = true } lineCounter.reduced_lines_indexes.push(i) } else { if (i === 0 && headlineSentencesLength === 2) { is2LinesAndPreviewsWasReduced = false } } if (lineText.includes("/s/")) { lineText = lineText.replace(/\/s\//g, ""); if (headlineSentencesLength > (i + 1) && i < headlineSentencesLength - 1 && nextLine) { if (nextLine.slice(0, 2).includes("?") && nextLine.length < 3) { lineText += '?'; headlineSentences.pop(); } if (nextLine.slice(0, 2).includes(":")) { lineText += ':'; headlineSentences[i + 1] = headlineSentences[i + 1].slice(2); } } let lineWidth = ctx.measureText(lineText).width let assignedSize; if (lineText.split(" ").length <= 2) { if (lineWidth > (canvasWidth / 2.35)) { ctx.font = "84px OpenSansBold"; assignedSize = 80 } else { ctx.font = "84px OpenSansBold"; assignedSize = 84 } } else { if (i === headlineSentencesLength - 1 && lineWidth < (canvasWidth / 2.5) && lineText.split(" ").length === 3) { ctx.font = "84px OpenSansBold"; assignedSize = 84 } else { lineCounter.reduced_line_counter += 1; ctx.font = "52px OpenSansBold"; assignedSize = 52 } lineCounter.reduced_lines_indexes.push(i) } lineWidth = ctx.measureText(lineText).width if (lineWidth > (canvasWidth / 2) + 120) { if (assignedSize === 84) { ctx.font = "72px OpenSansBold"; } else if (assignedSize === 80) { ctx.font = "64px OpenSansBold"; textHeightAccumulator += 8 } else { ctx.font = "52px OpenSansBold"; } } } else { const textWidth = ctx.measureText(lineText).width if (textWidth > (canvasWidth / 2)) { ctx.font = "44px OpenSans"; textHeightAccumulator += 12 } else if (i === headlineSentencesLength - 1) { textHeightAccumulator += 12 } } ctx.fillStyle = "white"; ctx.textAlign = "center"; const textHeight = ctx.measureText(lineText).emHeightAscent; textHeightAccumulator += textHeight; if (headlineSentencesLength == 3) { textY = (canvasHeight / 3) } else if (headlineSentencesLength == 4) { textY = (canvasHeight / 3.5) } else { textY = 300 } textY += textHeightAccumulator; const words = lineText.split(' '); console.log("words", words, lineText, headlineSentences) const capitalizedWords = words.map(word => { if (word.length > 0) return word[0].toUpperCase() + word.slice(1) return word }); const capitalizedLineText = capitalizedWords.join(' '); ctx.fillText(capitalizedLineText, canvasWidth / 2, textY); }
成功生成图像缓冲区后,调用 uploadImageToWordpress 函数。
此函数通过对 WordPress 图像进行编码,处理使用其 REST API 将图像发送到 WordPress 的繁重工作。
该函数首先通过清理空格和特殊字符来准备用作文件名的标语:
const buffer = canvas.toBuffer("image/png"); return buffer;
然后将图像缓冲区转换为 Blob 对象,以使其与 WordPress API 兼容:
const file = new Blob([buffer], { type: "image/png" });
准备 API 请求 使用编码的图像和标语,该函数构建一个 FormData 对象,并添加可选的元数据,例如用于可访问性的 alt_text 和用于上下文的标题。
const createSlug = (string) => { return string.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, ''); }; const image_name = createSlug(tagline);
为了进行身份验证,用户名和应用程序密码以 Base64 进行编码并包含在请求标头中:
formData.append("file", file, image_name + ".png"); formData.append("alt_text", `${tagline} image`); formData.append("caption", "Uploaded via API");
发送图像使用准备好的数据和标头向 WordPress 媒体端点发出 POST 请求,并在等待响应后验证成功或错误。
const credentials = `${username}:${app_password}`; const base64Encoded = Buffer.from(credentials).toString("base64");
如果成功,我会在 lambda 中返回相同的媒体响应。
这就是我的 lambda 最终的样子。
const response = await fetch(`${wordpress_url}wp-json/wp/v2/media`, { method: "POST", headers: { Authorization: "Basic " + base64Encoded, contentType: "multipart/form-data", }, body: formData, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Error uploading image: ${response.statusText}, Details: ${errorText}`); }
这是我的脚本生成的示例图像。它没有在生产中使用,只是使用本示例的通用资源创建。
一段时间过去了,每个人都很高兴我们不再有劣质或空洞的无图像文章,图像与设计师制作的图像非常匹配,设计师很高兴他只专注于为整个公司的其他营销活动进行设计。
但随后出现了一个新问题:有时客户不喜欢生成的图像,他会要求我启动脚本为特定帖子生成新图像。
这给我带来了下一个支线任务:Wordpress 插件,使用人工智能为特定帖子手动生成特色图像
以上是AWS JavaScript WordPress = 使用人工智能的有趣内容自动化策略的详细内容。更多信息请关注PHP中文网其他相关文章!