AEM Cloud Service - Text Component Firefly RTE Plugin for Generative AI Images

Goal

Adobe Experience Manager 2023.10.14029.20231020T111912Z-231000

RTE (Rich Text Editor) Plugin for generating images from authored text content using Firefly Text to Image API. Might be useful in setting up quick simple pages (not relying on creatives for images), here we create a Road Safety Tips page and generate images for the tip text using Firefly...

Demo | Package Install | Github


RTE Firefly Plugin


Enable Plugin in Template


Plugin Config Saved in CRX


Firefly Credentials in CS Env Configuration



Solution

1) Create the maven project ...

mvn -B org.apache.maven.plugins:maven-archetype-plugin:3.2.1:generate -D archetypeGroupId=com.adobe.aem -D archetypeArtifactId=aem-project-archetype 
-D archetypeVersion=36 -D aemVersion="cloud" -D appTitle="Experience AEM Firefly RTE Plugin" -D appId="eaem-rte-firefly-plugin"
-D groupId="apps.experienceaem.sites" -D frontendModule=none -D includeExamples=n -D includeDispatcherConfig=n


2) Create service apps.experienceaem.sites.core.services.impl.FireflyServiceImpl for communicating with IMS, generate access token, post text to firefly and request a base64 encoded image

package apps.experienceaem.sites.core.services.impl;

import apps.experienceaem.sites.core.services.FireflyService;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.SocketConfig;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;


@Component(service = FireflyService.class)
@Designate(ocd = FireflyServiceImpl.FireflyConfiguration.class)
public class FireflyServiceImpl implements FireflyService {
private final Logger logger = LoggerFactory.getLogger(FireflyServiceImpl.class);

private String clientId = "";
private String clientSecret = "";
private String IMS_URL = "https://ims-na1.adobelogin.com/ims/token/v3";
private String FF_URL = "https://firefly-beta.adobe.io/v1/images/generations";

@Activate
@Modified
protected void activate(final FireflyConfiguration config) {
clientId = config.clientId();
clientSecret = config.clientSecret();
}

private String getAccessToken() throws IOException {
CloseableHttpClient client = null;
String responseBody = "";

SocketConfig sc = SocketConfig.custom().setSoTimeout(180000).build();
client = HttpClients.custom().setDefaultSocketConfig(sc).build();

HttpPost post = new HttpPost(IMS_URL);
StringEntity entity = new StringEntity(getAccessTokenRequestBody(), "UTF-8");

post.addHeader("Content-Type", "application/x-www-form-urlencoded");
post.setEntity(entity);

HttpResponse response = client.execute(post);

HttpEntity responseEntity = response.getEntity();

responseBody = IOUtils.toString(responseEntity.getContent(), "UTF-8");

client.close();

Gson gson = new Gson();

JsonObject responseObject = gson.fromJson(responseBody, JsonObject.class);

return responseObject.get("access_token").getAsString();
}

private String getAccessTokenRequestBody(){
return "grant_type=client_credentials&client_id=" + clientId + "&client_secret=" + clientSecret + "&scope=openid,AdobeID,firefly_api";
}

private String getGenerateImageRequestBody(String text){
JsonObject body = new JsonObject();
JsonArray styles = new JsonArray();

styles.add("concept art");

body.addProperty("size", "1024x1024");
body.addProperty("batch", 2);
body.addProperty("prompt", text);
body.addProperty("contentClass", "photo");
body.add("styles", styles);

return body.toString();
}

public String generateImage(String text){
String base64Image = "";

try{
String accessToken = getAccessToken();

SocketConfig sc = SocketConfig.custom().setSoTimeout(180000).build();
CloseableHttpClient client = HttpClients.custom().setDefaultSocketConfig(sc).build();

HttpPost post = new HttpPost(FF_URL);
StringEntity entity = new StringEntity(getGenerateImageRequestBody(text), "UTF-8");

post.addHeader("X-Api-Key", clientId);
post.addHeader("Authorization", "Bearer " + accessToken);
post.addHeader("Accept", "application/json+base64");
post.addHeader("Content-Type", "application/json");
post.setEntity(entity);

HttpResponse response = client.execute(post);

HttpEntity responseEntity = response.getEntity();

String responseBody = IOUtils.toString(responseEntity.getContent(), "UTF-8");

client.close();

Gson gson = new Gson();

JsonObject responseObject = gson.fromJson(responseBody, JsonObject.class);

base64Image = ((JsonObject)((JsonArray)responseObject.get("images")).get(0)).get("base64").getAsString();
}catch(Exception e){
logger.error("Error generating Image for text : " + text,e);
throw new RuntimeException("Error generating image", e);
}

return base64Image;
}

@ObjectClassDefinition(name = "Firefly Configuration")
public @interface FireflyConfiguration {
@AttributeDefinition(
name = "Client Id",
description = "Firefly account clientId",
type = AttributeType.STRING
)
String clientId() default "";

@AttributeDefinition(
name = "Client Secret",
description = "Firefly account clientSecret",
type = AttributeType.STRING
)
String clientSecret() default "";
}
}


3) Create a servlet apps.experienceaem.sites.core.servlets.GenerateFireFlyImageServlet for the RTE plugin...

package apps.experienceaem.sites.core.servlets;

import apps.experienceaem.sites.core.services.FireflyService;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(
name = "Experience AEM Generate Firefly Image Servlet",
immediate = true,
service = Servlet.class,
property = {
"sling.servlet.methods=GET",
"sling.servlet.paths=/bin/eaem/firefly/generate"
}
)
public class GenerateFireFlyImageServlet extends SlingAllMethodsServlet {
private static final Logger LOGGER = LoggerFactory.getLogger(GenerateFireFlyImageServlet.class);

@Reference
transient FireflyService ffService;

@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {

try{
String text = request.getParameter("text");

if(StringUtils.isEmpty(text) ){
response.getWriter().println("'text' parameter missing in request");
return;
}

String base64Image = ffService.generateImage(text);

response.getWriter().print(base64Image);
}catch(Exception e){
throw new ServletException("Error", e);
}
}
}


4) Create a clientlib /apps/eaem-rte-firefly-plugin/clientlibs/rte-firefly-plugin with categories=[cq.authoring.dialog.all] and add the following code in rte-firefly-plugin.js

(function($, CUI, $document){
const GROUP = "experience-aem",
FF_IMAGE_FEATURE = "fireFlyImage",
FF_SERVLET = "/bin/eaem/firefly/generate";

addPlugin();

addPluginToDefaultUISettings();

function addPluginToDefaultUISettings(){
const groupFeature = GROUP + "#" + FF_IMAGE_FEATURE,
toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.dialogFullScreen.toolbar;

if(toolbar.includes(groupFeature)){
return;
}

toolbar.splice(3, 0, groupFeature);
}

function addPlugin(){
const EAEMFireflyPlugin = new Class({
toString: "EAEMFireflyPlugin",

extend: CUI.rte.plugins.Plugin,

pickerUI: null,

getFeatures: function() {
return [ FF_IMAGE_FEATURE ];
},

initializeUI: function(tbGenerator) {
const plg = CUI.rte.plugins;

addPluginToDefaultUISettings();

if (!this.isFeatureEnabled(FF_IMAGE_FEATURE)) {
return;
}

this.pickerUI = tbGenerator.createElement(FF_IMAGE_FEATURE, this, false, { title: "Generate Firefly Image..." });
tbGenerator.addElement(GROUP, plg.Plugin.SORT_FORMAT, this.pickerUI, 10);

const groupFeature = GROUP + "#" + FF_IMAGE_FEATURE;
tbGenerator.registerIcon(groupFeature, "emotionJoyColor");
},

notifyPluginConfig: function (pluginConfig) {
pluginConfig = pluginConfig || {};

CUI.rte.Utils.applyDefaults(pluginConfig, {
'tooltips': {
fireFlyImage: {
'title': 'Firefly Image',
'text': 'Generate Firefly Image...'
}
}
});

this.config = pluginConfig;
},

execute: function (pluginCommand, value, envOptions) {
const context = envOptions.editContext,
ek = this.editorKernel;

if (pluginCommand != FF_IMAGE_FEATURE) {
return;
}

if(!isValidSelection()){
return;
}

const selection = CUI.rte.Selection.createProcessingSelection(context);
let startNode = selection.startNode;

if ( (selection.startOffset === startNode.length) && (startNode != selection.endNode)) {
startNode = startNode.nextSibling;
}

ek.relayCmd(pluginCommand, startNode.nodeValue);

function isValidSelection(){
const winSel = window.getSelection();
return winSel && winSel.rangeCount == 1 && winSel.getRangeAt(0).toString().length > 0;
}
}
});

const EAEMFireflyCmd = new Class({
toString: "EAEMFireflyCmd",

extend: CUI.rte.commands.Command,

isCommand: function (cmdStr) {
return (cmdStr.toLowerCase() == FF_IMAGE_FEATURE);
},

getProcessingOptions: function () {
const cmd = CUI.rte.commands.Command;
return cmd.PO_SELECTION | cmd.PO_BOOKMARK | cmd.PO_NODELIST;
},

execute: function (execDef) {
const text = execDef.value, context = execDef.editContext;;

if (!text) {
return;
}

const ui = $(window).adaptTo("foundation-ui");

ui.wait();

$.ajax(FF_SERVLET + "?text=" + text).done((base64Image) => {
let html = "<div style='display: grid; grid-template-columns: 320px 1fr; column-gap: 5px;'>" +
"<div style='max-width: 100%; max-height:100%;'>" +
"<img _rte_src='data:image/jpeg;base64," + base64Image + "' src='data:image/jpeg;base64," + base64Image + "' width='312' height='312'/></div>" +
"<div>" + text + "</div>" +
"</div>";
context.doc.execCommand('inserthtml', false, html);
ui.clearWait();
})
},

queryState: function(selectionDef, cmd) {
return false;
}
});

CUI.rte.commands.CommandRegistry.register(FF_IMAGE_FEATURE, EAEMFireflyCmd);

CUI.rte.plugins.PluginRegistry.register(GROUP,EAEMFireflyPlugin);
}
}(jQuery, window.CUI,jQuery(document)));


5) Add the design dialog config /apps/eaem-rte-firefly-plugin/components/text/cq:design_dialog for Show Generate Firefly Image Plugin feature...

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Text"
sling:resourceType="cq/gui/components/authoring/dialog"
helpPath="https://www.adobe.com/go/aem_cmp_text_v2">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<tabs
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/tabs"
maximized="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<plugins
jcr:primaryType="nt:unstructured"
jcr:title="Plugins"
sling:resourceType="granite/ui/components/coral/foundation/accordion"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<experience-aem
jcr:primaryType="nt:unstructured"
jcr:title="Experience AEM"
sling:resourceType="granite/ui/components/coral/foundation/container"
maximized="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<fixedcol
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
maximized="{Boolean}false">
<items jcr:primaryType="nt:unstructured">
<col1
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<ffPlugin
granite:class="js-cq-IPEPlugin-group"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<ffPlugin
jcr:primaryType="nt:unstructured"
jcr:title="Show Generate Firefly Image Plugin"
sling:resourceType="cq/gui/components/authoring/dialog/inplaceediting/configuration/pluginfeature"
name="fireFlyImage"/>
</items>
<granite:data
jcr:primaryType="nt:unstructured"
name="experience-aem"
plugin-root-name="rtePlugins"/>
</ffPlugin>
</items>
</col1>
</items>
</fixedcol>
</items>
</experience-aem>
</items>
</plugins>
</items>
</tabs>
</items>
</content>
</jcr:root>


6) Add the HTL script /apps/eaem-rte-firefly-plugin/components/text/text.html for rendering base64 encoded images 

<div data-sly-use.textModel="com.adobe.cq.wcm.core.components.models.Text"
data-sly-use.component="com.adobe.cq.wcm.core.components.models.Component"
data-sly-use.templates="core/wcm/components/commons/v1/templates.html"
data-sly-test.text="${textModel.text}"
data-cmp-data-layer="${textModel.data.json}"
id="${component.id}"
class="cmp-text">
<p class="cmp-text__paragraph"
data-sly-unwrap="${textModel.isRichText}">${text @ context = textModel.isRichText ? 'unsafe' : 'text'}</p>
</div>
<sly data-sly-call="${templates.placeholder @ isEmpty = !text, classAppend='cmp-text'}"></sly>


 

No comments:

Post a Comment