Il y a quelques mois, j'ai commencé à collaborer sur un projet consacré au contenu généré par l'IA pour un client axé sur le secteur technologique. Mon rôle était principalement axé sur la mise en place de SSG en utilisant WordPress comme Headless CMS pour un front-end Nuxt.
Le client avait l'habitude d'écrire des articles plusieurs fois par semaine sur différentes tendances ou situations affectant le secteur, dans l'espoir d'augmenter le trafic vers le site et sa production d'articles, il a décidé d'utiliser l'IA pour générer des articles pour lui.
Après un certain temps, avec les bonnes invites, le client a obtenu des informations qui correspondaient presque exactement à un article écrit par un humain, il est très difficile de repérer qu'elles ont été créées par une machine.
Quelque temps après avoir commencé à travailler sur différentes fonctionnalités, on me demandait continuellement une chose spécifique.
Ey, pouvez-vous mettre à jour l'image sélectionnée pour cet article ?
Après 2 semaines de mise à jour quotidienne des messages, j'ai eu un petit moment d'eurêka.
Pourquoi ne pas automatiser la génération d'images sélectionnées pour ces articles à l'aide de l'Intelligence artificielle ?
Nous avons déjà automatisé la rédaction des articles, pourquoi ne pas automatiser les images présentées ?
Pendant mon temps libre, j'expérimentais des LLM génératifs sur mon ordinateur, j'avais donc une bonne idée de plus ou moins comment aborder cette quête secondaire. J'ai envoyé un message au client détaillant quel est le problème, ce que je veux faire et quels seraient les avantages et sans avoir à convaincre, j'ai eu le feu vert pour travailler sur cette fonctionnalité et j'ai tout de suite opté pour mon premier pas.
Étant donné que j'ai eu une certaine exposition à l'exécution de modèles localement, j'ai tout de suite su qu'il n'était pas possible d'héberger soi-même ces modèles. Cela mis de côté, j'ai commencé à jouer avec des API qui généraient des images basées sur des invites textuelles.
Les images présentées se composaient de 2 parties : le graphique principal composé et un slogan accrocheur.
Le graphique composé serait constitué de quelques éléments liés à l'article, disposés de manière agréable avec ensuite des couleurs et des textures avec des modes de fusion appliqués pour obtenir des effets fantaisistes suivant le branding.
Les slogans étaient des phrases courtes de 8 à 12 mots avec une simple ombre portée en dessous.
Sur la base de mes tests, j'ai réalisé que poursuivre la voie de l'IA pour la génération d'images n'était pas pratique. La qualité de l’image n’était pas à la hauteur des attentes et le processus prenait trop de temps pour justifier son utilisation. Considérant que cela fonctionnerait comme une fonction AWS Lambda, où le temps d'exécution aurait un impact direct sur les coûts.
Cela étant écarté, j'ai opté pour le plan B : mélanger des images et des éléments de conception à l'aide de l'API Canvas de JavaScript.
En y regardant de plus près, nous avions principalement 5 styles de messages simples, et environ 4 types de textures, dont 3 utilisant le même alignement de texte, le même style et la même position. Après avoir fait quelques calculs, j'ai pensé :
Hmm, si je prends ces 3 images, saisis 8 textures et joue avec les modes de fusion, je peux me débrouiller avec 24 variations
Étant donné que ces 3 types de messages avaient le même style de texte, il s'agissait pratiquement d'un seul modèle.
Une fois cela réglé, je suis passé au générateur de slogan. Je voulais créer un slogan basé sur le contenu et le titre de l'article. J'ai décidé d'utiliser l'API de ChatGPT étant donné que l'entreprise payait déjà pour cela, et après quelques expérimentations et ajustements des invites, j'ai eu un très bon MVP pour mon générateur de slogan.
Une fois les 2 parties les plus difficiles de la tâche comprises, j'ai passé du temps dans Figma à assembler le schéma de l'architecture finale de mon service.
Le plan était de créer une fonction Lambda capable d'analyser le contenu de la publication, de générer un slogan et d'assembler une image sélectionnée, le tout parfaitement intégré à WordPress.
Je fournirai du code mais juste assez pour communiquer l'idée générale à ke.
La fonction Lambda commence par extraire les paramètres nécessaires de la charge utile de l'événement entrant :
const { title : request_title, content, backend, app_password} = JSON.parse(event.body);
La première tâche principale de la fonction consiste à générer un slogan à l'aide de la fonction analyseContent, qui utilise l'API d'OpenAI pour créer un slogan digne d'un clic basé sur le titre et le contenu de l'article.
Notre fonction prend le titre et le contenu de la publication mais renvoie un slogan, un sentiment de publication pour savoir si la publication est une opinion positive, négative ou neutre et un symbole d'entreprise facultatif des sociétés de l'indice S&P.
const { slogan, sentiment, entreprise } = wait analyseContent({ titre : request_title, content });
Cette étape est cruciale, car le slogan influence directement l’esthétique de l’image.
Ensuite, la fonction generateImage entre en jeu :
let buffer; buffer = await generateImage({ title: tagline, company_logo: company_logo, sentiment: sentiment, });
Cette fonction gère :
Voici un aperçu étape par étape de son fonctionnement :
La fonction generateImage commence par créer un canevas vierge, définir ses dimensions et le préparer à gérer tous les éléments de conception.
let buffer; buffer = await generateImage({ title: tagline, company_logo: company_logo, sentiment: sentiment, });
À partir de là, une image d'arrière-plan aléatoire est chargée à partir d'une collection prédéfinie d'actifs. Ces images ont été organisées pour correspondre à la marque axée sur la technologie tout en permettant suffisamment de variété entre les publications. L'image d'arrière-plan est sélectionnée au hasard en fonction de son sentiment.
Pour garantir que chaque image d'arrière-plan soit superbe, j'ai calculé ses dimensions de manière dynamique en fonction du rapport hauteur/largeur. Cela évite les distorsions tout en gardant l'équilibre visuel intact.
Le slogan est court mais basé sur certaines règles, cette phrase percutante est divisée en morceaux gérables et est stylisée de manière dynamique pour garantir qu'elle est toujours lisible, quelle que soit la longueur ou la taille du canevas en fonction du nombre de mots pour la ligne, de la longueur des mots, etc. .
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);
Enfin, le canevas est converti en tampon 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); }
Après avoir généré avec succès le tampon d'image, la fonction uploadImageToWordpress est appelée.
Cette fonction gère le gros du travail consistant à envoyer l'image à WordPress à l'aide de son API REST en codant l'image pour WordPress.
La fonction prépare d'abord le slogan à utiliser comme nom de fichier en nettoyant les espaces et les caractères spéciaux :
const buffer = canvas.toBuffer("image/png"); return buffer;
Le buffer d'image est ensuite converti en objet Blob pour le rendre compatible avec l'API WordPress :
const file = new Blob([buffer], { type : "image/png" });
Préparation de la requête API À l'aide de l'image et du slogan encodés, la fonction crée un objet FormData et j'ajoute des métadonnées facultatives, telles que alt_text pour l'accessibilité et une légende pour le contexte.
const createSlug = (string) => { return string.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, ''); }; const image_name = createSlug(tagline);
Pour l'authentification, le nom d'utilisateur et le mot de passe de l'application sont encodés en Base64 et inclus dans les en-têtes de requête :
formData.append("file", file, image_name + ".png"); formData.append("alt_text", `${tagline} image`); formData.append("caption", "Uploaded via API");
Envoi de l'image Une requête POST est faite au point de terminaison média WordPress avec les données et en-têtes préparés et après avoir attendu la réponse, je valide le succès ou les erreurs.
const credentials = `${username}:${app_password}`; const base64Encoded = Buffer.from(credentials).toString("base64");
En cas de succès, je renvoie la même réponse médiatique dans le lambda.
Voici à quoi ressemble ma lambda au final.
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}`); }
Ceci est un exemple d'image produit par mon script. Il n'est pas utilisé en production, juste créé avec des actifs génériques pour cet exemple.
Un certain temps a passé et tout le monde est heureux que nous n'ayons plus d'articles sans images de mauvaise qualité ou vides, que les images correspondent étroitement à celles que le designer crée, le designer est heureux de pouvoir se concentrer uniquement sur concevoir pour d'autres efforts de marketing à travers l'entreprise.
Mais ensuite un nouveau problème est apparu : parfois, le client n'aimait pas l'image générée et il me demandait de lancer mon script pour en générer une nouvelle pour un article spécifique.
Cela m'a amené à ma prochaine quête secondaire : un Plugin Wordpress pour générer manuellement une image en vedette à l'aide de l'intelligence artificielle pour une publication spécifique
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!