diff --git a/Dockerfile b/Dockerfile
index 7b06cb3..ef27477 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,7 +14,7 @@
# docker run -i --rm -p 8080:8080 -e GOOGLE_AI_API_KEY=your-api-key quarkus/playing-with-google-java-genai
#
###
-FROM registry.access.redhat.com/ubi8/openjdk-17:1.18
+FROM registry.access.redhat.com/ubi8/openjdk-21:1.20
ENV LANGUAGE='en_US:en'
diff --git a/README.md b/README.md
index b80eba2..d4c1e3a 100644
--- a/README.md
+++ b/README.md
@@ -89,27 +89,30 @@ The application provides several pre-configured templates for different use case
```bash
# Show help
-mvn quarkus:dev -Dquarkus.args="--help"
+quarkus dev -Dquarkus.args="--help"
# Generate image for a blog post (single author)
-mvn quarkus:dev -Dquarkus.args="image --template-name generate-image-blog-post --title 'Introduction to DuckDB' --name 'John Doe' --photo images/people/john-doe.png -o output.png"
+quarkus dev -Dquarkus.args="image --template-name generate-image-blog-post --title=IntroductiontoDuckDB --name=John_Doe --photo=images/people/john-doe.png -o output.png"
# Generate image for a blog post (2 authors)
-mvn quarkus:dev -Dquarkus.args="image --template-name generate-image-2-blog-post --title 'Exploring Firebase Studio' --name 'Alice Smith' --name2 'Bob Johnson' --photo images/people/alice.png --photo2 images/people/bob.png -o output.png"
+quarkus dev -Dquarkus.args="image --template-name generate-image-2-blog-post --title=Exploring_Firebas_Studio --name=Alice-Smith --name2=Bob_Johnson --photo=images/people/alice.png --photo2 images/people/bob.png -o output.png"
# Generate image for a conference speaker
-mvn quarkus:dev -Dquarkus.args="image --template-name generate-image-speaker-event --title 'My Great Talk' --name 'Speaker Name' --photo images/people/speaker.png --conf-photo images/logos/conference.png -o output.png"
+quarkus dev -Dquarkus.args="image --template-name generate-image-speaker-event --title=My_Great_Talk --name=Speaker_Name --photo=images/people/speaker.png --conf-photo=images/logos/conference.png -o output.png"
# Generate image for a conference with 2 speakers
-mvn quarkus:dev -Dquarkus.args="image --template-name generate-image-2-speaker-event --title 'Firebase Studio' --name 'Benjamin Bourgeois' --name2 'Jean-Phi Baconnais' --photo images/people/benjamin-bourgeois.png --photo2 images/people/jeanphi-baconnais.png --conf-photo images/logos/conference.png -o output.png"
+quarkus dev -Dquarkus.args="image --template-name generate-image-2-speaker-event --title=Firebase_Studio --name=Speaker_1 --name2=Speaker_2 --photo images/people/peolple1.png --photo2=images/people/people2.png --conf-photo=images/logos/conference.png -o output.png"
+
+# Generate video
+quarkus dev -Dquarkus.args="video --prompt 'Conference intro' --vertex"
```
### Running the Application
-**Development Mode (with hot reload):**
+**With CLI arguments:**
```bash
-mvn quarkus:dev
+quarkus dev -Dquarkus.args="--type image --prompt 'Your prompt here' --output result.png"
```
**Production mode:**
@@ -189,7 +192,7 @@ The application automatically detects and supports the following image formats:
For support and questions:
-- Create an issue in the GitLab repository
+- Create an issue in the GitHub repository
- Check the [Google AI documentation](https://ai.google.dev/docs)
- Review the [Quarkus guides](https://quarkus.io/guides/)
diff --git a/pom.xml b/pom.xml
index 2b88662..3b9cca3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,8 +14,8 @@
io.quarkus.platform
true
3.11.0
- 21
- 3.15.1
+ 25
+ 3.27.0
3.2.5
@@ -86,13 +86,19 @@
+
+ --add-opens java.base/java.lang=ALL-UNNAMED
+
maven-compiler-plugin
${compiler-plugin.version}
+ 25
+ 25
-parameters
+ --enable-preview
@@ -125,15 +131,6 @@
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
- 21
- 21
- --enable-preview
-
-
diff --git a/src/main/java/zenika/marketing/cli/GenerateImageCommand.java b/src/main/java/zenika/marketing/cli/GenerateImageCommand.java
index 26b5acb..f25ce96 100644
--- a/src/main/java/zenika/marketing/cli/GenerateImageCommand.java
+++ b/src/main/java/zenika/marketing/cli/GenerateImageCommand.java
@@ -65,7 +65,8 @@ public class GenerateImageCommand implements Runnable {
"generate-image-blog-post", this::generateImageBlogPost,
"generate-image-2-blog-post", this::generateImage2BlogPost,
"generate-image-speaker-event", this::generateImageSpeakerEvent,
- "generate-image-2-speaker-event", this::generateImage2SpeakerEvent
+ "generate-image-2-speaker-event", this::generateImage2SpeakerEvent,
+ "generate-image-binome-speaker-event", this::generateImageBinomeSpeakerEvent
// generate-post-speaker-event
);
@@ -232,6 +233,42 @@ private void generateImage2SpeakerEvent(Template template) {
prepareCallGemini(template, content, completedPrompt);
}
+ private void generateImageBinomeSpeakerEvent(Template template) {
+ Content content = null;
+ config.setDefaultName(name != null ? name : config.getDefaultName());
+ config.setDefaultName2(name2 != null ? name2 : config.getDefaultName2());
+ config.setDefaultTitle(title != null ? title : config.getDefaultTitle());
+ config.setDefaultPhoto(photo);
+ config.setDefaultPhoto2(photo2);
+ config.setDefaultConfPhoto(confPhoto);
+
+ String completedPrompt = templateService.preparePrompt(template, config);
+
+ Path templateFile = Path.of(template.template());
+ Path zPhoto = Path.of(config.getDefaultPhoto());
+ Path zPhoto2 = Path.of(config.getDefaultPhoto2());
+ Path confPhoto = Path.of(config.getDefaultConfPhoto());
+
+ checkisFileExist(templateFile);
+ checkisFileExist(zPhoto);
+ checkisFileExist(zPhoto2);
+ checkisFileExist(confPhoto);
+
+ try {
+ content = Content.fromParts(
+ Part.fromBytes(Files.readAllBytes(templateFile), Utils.getMimeType(templateFile.toString())),
+ Part.fromBytes(Files.readAllBytes(zPhoto), Utils.getMimeType(zPhoto.toString())),
+ Part.fromBytes(Files.readAllBytes(zPhoto2), Utils.getMimeType(zPhoto2.toString())),
+ Part.fromBytes(Files.readAllBytes(confPhoto), Utils.getMimeType(confPhoto.toString())),
+ Part.fromText(completedPrompt));
+ } catch (IOException e) {
+ Log.error("❌ Error: " + e.getMessage(), e);
+ System.exit(1);
+ }
+
+ prepareCallGemini(template, content, completedPrompt);
+ }
+
private void prepareCallGemini(Template template, Content content, String prompt) {
config.setDefaultResultFilename(output != null ? output : config.getDefaultResultFilename());
config.setDefaultResultFilename(
diff --git a/src/main/java/zenika/marketing/cli/GenerateVideoCommand.java b/src/main/java/zenika/marketing/cli/GenerateVideoCommand.java
index cc3de3d..c797454 100644
--- a/src/main/java/zenika/marketing/cli/GenerateVideoCommand.java
+++ b/src/main/java/zenika/marketing/cli/GenerateVideoCommand.java
@@ -5,94 +5,61 @@
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import zenika.marketing.config.ConfigProperties;
-import zenika.marketing.config.MODE_FEATURE;
+import zenika.marketing.domain.Template;
import zenika.marketing.services.GeminiVideoServices;
import zenika.marketing.services.TemplateService;
-@Command(
- name = "video",
- mixinStandardHelpOptions = true,
- description = "Generate a video using Gemini AI."
-)
+@Command(name = "video", mixinStandardHelpOptions = true, description = "Generate a video using Gemini AI.")
public class GenerateVideoCommand implements Runnable {
- @Inject
- GeminiVideoServices geminiServices;
+ @Inject
+ GeminiVideoServices geminiServices;
- @Inject
- ConfigProperties config;
+ @Inject
+ ConfigProperties config;
- @Inject
- TemplateService templateService;
+ @Inject
+ TemplateService templateService;
- @Option(
- names = {"-o", "--output"},
- description = "Output filename (default: ${DEFAULT-VALUE})"
- )
- String output;
+ @Option(names = { "-o", "--output" }, description = "Output filename")
+ String output;
- @Option(
- names = {"--template"},
- description = "Path to template image"
- )
- String templatePath;
+ @Option(names = { "--photo" }, description = "Path to image to use")
+ String photo;
- @Option(
- names = {"-m", "--model"},
- description = "Gemini model to use"
- )
- String model;
+ @Option(names = { "--template-name" }, description = "Name of the template to use", required = true)
+ String templateName;
- @Option(
- names = {"--template-name"},
- description = "Name of the template to use",
- required = true
- )
- String templateName;
+ @Option(names = { "--ratio" }, description = "Video aspect ratio (default: configured default)")
+ String videoRatio;
- @Option(
- names = {"--ratio"},
- description = "Video aspect ratio (default: configured default)"
- )
- String videoRatio;
+ @Option(names = { "--resolution" }, description = "Video resolution (default: configured default)")
+ String videoResolution;
- @Option(
- names = {"--resolution"},
- description = "Video resolution (default: configured default)"
- )
- String videoResolution;
+ public void run() {
+ try {
+ Template template = templateService.waitAValidTemplateByUser(templateName);
+ config.setDefaultPrompt(template.prompt());
+ String finalOutput = output != null ? output : config.getDefaultResultFilenameVideo();
+ config.setDefaultPhoto(photo != null ? photo : config.getDefaultPhoto());
+ config.setDefaultGeminiVeoModel(config.getDefaultGeminiVeoModel());
+ config.setDefaultVideoResolution(videoResolution != null ? videoResolution
+ : config.getDefaultVideoResolution());
+ config.setDefaultVideoRatio(videoRatio != null ? videoRatio : config.getDefaultVideoRatio());
+ config.setDefaultPhoto(photo);
+ config.setDefaultResultFilename(output != null ? output : config.getDefaultResultFilename());
- @Override
- public void run() {
- try {
- var template = templateService.waitAValidTemplateByUser(templateName);
+ String completedPrompt = templateService.preparePrompt(template, config);
- // Check that the template is of type VIDEO
- if (!template.type().equals(MODE_FEATURE.VIDEO)) {
- Log.error("❌ Error: Template '" + templateName + "' is not a VIDEO template");
- System.exit(1);
- return;
- }
+ Log.infof("-> generate Video %s", template.name());
+ Log.info("\uD83D\uDCDD \uD83D\uDC49 Prompt: \n \t " + completedPrompt + "\n");
- String templatePrompt = template.prompt();
- String finalOutput = output != null ? output : config.getDefaultResultFilename();
- String finalTemplatePath = templatePath != null ? templatePath : config.getDefaultTemplatePath();
- String finalModel = model != null ? model : config.getDefaultGeminiVeoModel();
- String finalVideoRatio = videoRatio != null ? videoRatio : config.getDefaultVideoRatio();
- String finalVideoResolution = videoResolution != null ? videoResolution : config.getDefaultVideoResolution();
+ geminiServices.generateVideo(finalOutput, config);
+ System.exit(0);
- geminiServices.generateVideo(
- finalModel,
- templatePrompt,
- finalOutput,
- finalTemplatePath,
- finalVideoRatio,
- finalVideoResolution
- );
-
- } catch (Exception e) {
- Log.error("❌ Error: " + e.getMessage(), e);
- System.exit(1);
+ } catch (Exception e) {
+ Log.error("❌ Error: " + e.getMessage(), e);
+ System.exit(1);
+ }
}
- }
}
diff --git a/src/main/java/zenika/marketing/config/ConfigProperties.java b/src/main/java/zenika/marketing/config/ConfigProperties.java
index c552b42..35fe3c8 100644
--- a/src/main/java/zenika/marketing/config/ConfigProperties.java
+++ b/src/main/java/zenika/marketing/config/ConfigProperties.java
@@ -15,6 +15,10 @@ public class ConfigProperties {
@ConfigProperty(name = "app.result.filename")
String defaultResultFilename;
+ @ConfigProperty(name = "app.result.filename.video")
+ String defaultResultFilenameVideo;
+
+ @ConfigProperty(name = "app.prompt")
String defaultPrompt;
String defaultTemplatePath;
@@ -101,6 +105,10 @@ public String getDefaultConfPhoto() {
return defaultConfPhoto;
}
+ public String getDefaultResultFilenameVideo() {
+ return defaultResultFilenameVideo;
+ }
+
public void setDefaultPhoto2(String defaultPhoto2) {
this.defaultPhoto2 = defaultPhoto2;
}
@@ -165,6 +173,10 @@ public void setDefaultConfPhoto(String defaultConfPhoto) {
this.defaultConfPhoto = defaultConfPhoto;
}
+ public void setDefaultResultFilenameVideo(String defaultResultFilenameVideo) {
+ this.defaultResultFilenameVideo = defaultResultFilenameVideo;
+ }
+
public String getFieldByValue(String field, ConfigProperties config) {
return switch (FIELDS_PROMPT.valueOf(field)) {
case NAME, NAME1 -> config.getDefaultName();
diff --git a/src/main/java/zenika/marketing/services/GeminiVideoServices.java b/src/main/java/zenika/marketing/services/GeminiVideoServices.java
index 124629c..cf6229c 100644
--- a/src/main/java/zenika/marketing/services/GeminiVideoServices.java
+++ b/src/main/java/zenika/marketing/services/GeminiVideoServices.java
@@ -1,48 +1,48 @@
package zenika.marketing.services;
+import java.nio.file.Path;
+
import com.google.genai.Client;
import com.google.genai.errors.GenAiIOException;
-import com.google.genai.types.*;
+import com.google.genai.types.GenerateVideosConfig;
+import com.google.genai.types.GenerateVideosOperation;
+import com.google.genai.types.GenerateVideosSource;
+import com.google.genai.types.Image;
+import com.google.genai.types.Video;
+
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import zenika.marketing.config.ConfigProperties;
import zenika.marketing.utils.Utils;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Objects;
-
@ApplicationScoped
public class GeminiVideoServices {
@Inject
ConfigProperties config;
- public void generateVideo(String model, String prompt, String output, String templatePath,
- String ratio, String resolution) {
+ public void generateVideo(String prompt, ConfigProperties config) {
try (Client client = new Client.Builder()
.project(System.getenv("GOOGLE_CLOUD_PROJECT_ID"))
.location(System.getenv("GOOGLE_CLOUD_LOCATION"))
.vertexAI(true)
.build()) {
- Log.info("✨ Start using Google AI API with model " + model);
+ Log.info("✨ Start using Google AI API with model " + config.getDefaultGeminiVeoModel());
GenerateVideosOperation operation = client.models.generateVideos(
- model,
+ config.getDefaultGeminiVeoModel(),
GenerateVideosSource.builder()
.prompt(prompt)
- .image(Image.fromFile(templatePath, Utils.getMimeType(Path.of(templatePath).toString())))
+ .image(Image.fromFile(config.getDefaultPhoto(),
+ Utils.getMimeType(Path.of(config.getDefaultPhoto()).toString())))
.build(),
GenerateVideosConfig.builder()
- .aspectRatio(ratio)
- .resolution(resolution)
+ .aspectRatio(config.getDefaultVideoRatio())
+ .resolution(config.getDefaultVideoResolution())
.generateAudio(true)
- .build()
- );
+ .build());
while (!operation.done().filter(Boolean::booleanValue).isPresent()) {
try {
@@ -60,14 +60,13 @@ public void generateVideo(String model, String prompt, String output, String tem
Video generatedVideo = operation.response().get().generatedVideos().get().get(0).video().get();
try {
- client.files.download(generatedVideo, output, null);
- Log.info("✨ Video downloaded to " + output);
+ client.files.download(generatedVideo, "generated/" + config.getDefaultResultFilenameVideo(), null);
+ Log.info("✨ Video downloaded to " + config.getDefaultResultFilenameVideo());
} catch (GenAiIOException e) {
Log.error("An error occurred while downloading the video: " + e.getMessage());
}
- Log.info("✨ Video generated: " + output);
+ Log.info("✨ Video generated: " + config.getDefaultResultFilenameVideo());
}
}
-
}
diff --git a/src/main/java/zenika/marketing/services/TemplateService.java b/src/main/java/zenika/marketing/services/TemplateService.java
index 6c52b94..d9c0484 100644
--- a/src/main/java/zenika/marketing/services/TemplateService.java
+++ b/src/main/java/zenika/marketing/services/TemplateService.java
@@ -74,14 +74,18 @@ public Template waitAValidTemplateByUser(String templateName) {
public String preparePrompt(Template temp, ConfigProperties config) {
var finalPrompt = temp.prompt();
+ var templateRegex = "%".concat(FIELDS_PROMPT.TEMPLATE.getValue()).concat("%");
- finalPrompt = finalPrompt.replaceFirst("%".concat(FIELDS_PROMPT.TEMPLATE.getValue()).concat("%"),
- temp.template());
+ if (temp.template() != null && finalPrompt.indexOf(templateRegex) != -1) {
+ finalPrompt = finalPrompt.replaceFirst(templateRegex, temp.template());
+ }
if (!temp.fields().isEmpty()) {
for (String field : temp.fields()) {
- finalPrompt = finalPrompt.replaceFirst("%".concat(field).concat("%"),
- config.getFieldByValue(field, config));
+ if (finalPrompt.indexOf(field) != -1) {
+ finalPrompt = finalPrompt.replaceFirst("%".concat(field).concat("%"),
+ config.getFieldByValue(field, config));
+ }
}
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 69713ce..cb00184 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -2,38 +2,27 @@
app.gemini.model=gemini-3-pro-image-preview
app.gemini.model.veo=veo-3.0-fast-generate-preview
-app.prompt=I would like to create an image from the template image integrating these elements: \
- - replace "Speaker Name" by "Jean-Philippe Baconnais"\
- - replace "Talk title" by "OSINT : L art de trouver ce qui ne devrait pas tre trouv". Please center this title and use 2 lines at maximum. \
- - replace the first white square by the logo of the conference (in file1). \
- - replace the second white square by the speaker photo (file2). \
- Please don't modify the name of the author and the title of the talk.
-
-#app.prompt=app.prompt=I would like to create an image for a NightClazz event from the template image integrating elements. \
- The partner is Zenika Open Source and I haven't the logo associated.
-
-#app.prompt=I'd like to create a short animation from the template file to move the speaker photo, the title of the talk.
-
# Template Configuration
+app.prompt=prompt
app.result.filename=gemini-generation.png
+app.result.filename.video=gemini-generation.mp4
app.template.path=images/templates/blog.png
-app.file1.path=images/people/benjamin-bourgeois.png
-app.file2.path=images/people/jeanphi-baconnais.jpg
-app.photo=images/people/benjamin-bourgeois.png
-app.photo2=images/people/jeanphi-baconnais.jpg
-#
+app.file1.path=images/people/my-photo.png
+app.file2.path=images/people/my-photo.png
+app.photo=images/people/my-photo.png
+app.photo2=images/people/my-photo.png
app.name=Speaker name
app.title=Talk title
-app.z-photo=images/people/benjamin-bourgeois.png
+app.z-photo=images/people/my-photo.png
app.media.type=IMAGE
app.video.ratio=16:9
app.video.resolution=1080p
-
app.template.formats=png,jpg,jpeg
# Quarkus Configuration
quarkus.http.port=8080
+quarkus.native.additional-build-args=--add-opens=java.base/java.lang=ALL-UNNAMED
quarkus.log.level=INFO
quarkus.log.console.enable=true
quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n
diff --git a/src/main/resources/templates.json b/src/main/resources/templates.json
index c69df73..f668709 100644
--- a/src/main/resources/templates.json
+++ b/src/main/resources/templates.json
@@ -40,6 +40,21 @@
},
{
"name": "generate-image-2-speaker-event",
+ "description": "Generate an image to announce 2 speakers for a conference",
+ "type": "IMAGE",
+ "template": "images/templates/conf-2.png",
+ "fields": [
+ "NAME",
+ "NAME2",
+ "TITLE",
+ "PHOTO",
+ "PHOTO2",
+ "CONF_PHOTO"
+ ],
+ "prompt": "I would like to create an image from the template image available in this file %TEMPLATE% integrating these elements: - replace the first 'Speaker Name' by %NAME% - replace the second 'Speaker Name' by %NAME2% - replace 'Talk title' by %TITLE%. Please center this title and use 2 lines at maximum. - replace the first white square by the conference photo from this %CONF_PHOTO% file. - replace the first white square by the first speaker photo from this %PHOTO% file. - replace the second white square by the second speaker photo from this %PHOTO2% file. Please don't modify the names of the speakers and the title of the talk."
+ },
+ {
+ "name": "generate-image-binome-speaker-event",
"description": "Generate an image to announce a talk with 2 speakers for a conference",
"type": "IMAGE",
"template": "images/templates/conf-1-binome.png",
@@ -51,15 +66,17 @@
"PHOTO2",
"CONF_PHOTO"
],
- "prompt": "I would like to create an image from the template image available in this file %TEMPLATE% integrating these elements: - replace the first 'Speaker Name' by %NAME% - replace the second 'Speaker Name' by %NAME2% - replace 'Talk title' by %TITLE%. Please center this title and use 2 lines at maximum. - replace the first white square by the conference photo from this %CONF_PHOTO% file. - replace the first white square by the first speaker photo from this %PHOTO% file. - replace the second white square by the second speaker photo from this %PHOTO2% file. Please don't modify the names of the speakers and the title of the talk."
+ "prompt": "I would like to create an image from the template image available in this file %TEMPLATE% integrating these elements: - replace 'Speaker Name' by %NAME% - replace 'Talk title' by %TITLE%. Please center this title and use 2 lines at maximum. - replace the first white square by the logo of the conference (in file1). - replace the second white square by the speaker photo from this %PHOTO% file. Please don't modify the name of the author and the title of the talk."
},
{
- "name": "generate-video-speaker-event",
+ "name": "generate-video-speaker-event-blog",
"description": "Generate a video to annunce a speaker for a conference",
"type": "VIDEO",
"template": "",
- "fields": [],
- "prompt": "I'd like to create a short animation from the template file to move the speaker photo, the title of the talk."
+ "fields": [
+ "PHOTO"
+ ],
+ "prompt": "I'd like to create a short animation with the photo in this file %PHOTO%. - Start with a white page - progressively the speaker photo moves from the top of the page to the center and finish with a short rebound - the title of the talk from the left to the center"
},
{
"name": "generate-post-speaker-event",