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",