ホームページ >ウェブフロントエンド >jsチュートリアル >AWS JavaScript WordPress = 人工知能を使用した楽しいコンテンツ自動化戦略
数か月前、私はテクノロジー分野に焦点を当てたクライアント向けに、AI が生成したコンテンツに関するプロジェクトで共同作業を開始しました。私の役割は主に、Nuxt フロントエンドの ヘッドレス CMS として WordPress を使用して SSG をセットアップすることに重点を置いていました。
クライアントは、この分野に影響を与えるさまざまな傾向や状況について週に数回記事を書いていましたが、サイトへのトラフィックと記事の出力を増やすことを期待して、AI を使用して記事を生成することにしました。
しばらくすると、適切なプロンプトを使用すると、クライアントは人間が書いた記事と完全に一致する情報を入手できるようになり、それらが機械で作成されたものであることを見分けるのは非常に困難になります。
別の機能の開発に移ってからしばらくすると、特定のことを尋ねられるようになりました。
あの、この記事のアイキャッチ画像を更新してもらえますか?
投稿を毎日更新し続けて 2 週間後、ちょっとした発見がありました。
人工知能を使用して、これらの記事のアイキャッチ画像の生成を自動化してみませんか?
投稿の作成はすでに自動化されていますが、アイキャッチ画像を自動化しないのはなぜですか?
自由時間に、コンピューター上で生成 LLM を実験していたので、このサイドクエストに取り組む方法についてはある程度の確かなアイデアを持っていました。私はクライアントに、何が問題なのか、何をしたいのか、何がメリットになるのかを詳細に伝えるメッセージを送信しました。説得する必要もなく、この機能に取り組むことにゴーサインが得られ、すぐに実行に移しました。私の最初の一歩。
ローカルでモデルを実行する経験があったことを考えると、それらのモデルを自己ホストするのは不可能であることがすぐに分かりました。それを捨てて、テキストプロンプトに基づいて画像を生成する API を試し始めました。
注目の画像は、メインで構成されたグラフィックとキャッチーなキャッチフレーズの 2 つの部分で構成されています。
合成されたグラフィックは、記事に関連するいくつかの要素であり、ブランディングに続いていくつかの派手な効果を実現するためにいくつかのブレンド モードが適用されたいくつかの色とテクスチャがうまく配置されています。
キャッチフレーズは、その下に単純なドロップ シャドウが付いた 8 ~ 12 単語の短い文でした。
テストの結果、画像生成に AI の道を追求するのは現実的ではないことがわかりました。画質は期待を満たしておらず、プロセスに時間がかかりすぎて使用を正当化できませんでした。これが AWS Lambda 関数として実行されることを考慮すると、実行時間はコストに直接影響します。
それを捨てて、私はプラン B を採用しました。つまり、JavaScript の Canvas API を使用して画像とデザイン アセットを一緒にマッシュするというものです。
詳しく見てみると、主に 5 つのスタイルのシンプルな投稿があり、約 4 種類のテクスチャと、そのうちの 3 つは同じテキストの配置、スタイル、位置を使用していました。いくつかの計算をした後、私は次のように考えました:
うーん、これら 3 つの画像を取得し、8 つのテクスチャを取得し、ブレンド モードで再生すると、ポスト 24 のバリエーションを回避できます
これら 3 種類の投稿のテキスト スタイルが同じであることを考えると、実質的には 1 つのテンプレートでした。
これで解決したので、タグライン ジェネレーターに移りました。記事の内容とタイトルに基づいてキャッチフレーズを作成したいと思いました。会社がすでに料金を支払っていたことを考慮して、ChatGPT の API を使用することにしました。いくつかの実験とプロンプトの調整を経て、キャッチフレーズ ジェネレーターとして非常に優れた MVP が得られました。
タスクの最も難しい 2 つの部分を理解したので、Figma で時間をかけてサービスの最終アーキテクチャの図をまとめました。
計画では、投稿コンテンツの分析、キャッチフレーズの生成、注目の画像の組み立てが可能な Lambda 関数を作成し、これらすべてを WordPress とシームレスに統合することでした。
私はいくつかのコードを提供しますが、全体的なアイデアを ke に伝えるのに十分なだけです。
Lambda 関数は、受信イベント ペイロードから必要なパラメータを抽出することから始まります。
const { title: request_title、content、backend、app_password} = JSON.parse(event.body);
この関数の最初の主要なタスクは、analyzeContent 関数を使用してキャッチフレーズを生成することです。この関数は OpenAI の API を使用して、記事のタイトルとコンテンツに基づいてクリックに値するキャッチフレーズを作成します。
私たちの関数は投稿のタイトルと内容を受け取りますが、キャッチフレーズ、投稿が肯定的、否定的、または中立的な意見であるかどうかを知る投稿のセンチメント、および S&P 指数企業からのオプションの企業シンボルを返します。
const { タグライン、センチメント、会社 } = await AnalyticContent({ 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;
画像バッファーは、WordPress API との互換性を保つために Blob オブジェクトに変換されます。
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");
成功した場合は、同じメディア応答をラムダで返します。
これが私のラムダが最終的にどのように見えるかです。
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 中国語 Web サイトの他の関連記事を参照してください。