diff --git a/src/main/java/ch/akop/homesystem/config/properties/OpenAIProperties.java b/src/main/java/ch/akop/homesystem/config/properties/OpenAIProperties.java new file mode 100644 index 0000000..0ac6638 --- /dev/null +++ b/src/main/java/ch/akop/homesystem/config/properties/OpenAIProperties.java @@ -0,0 +1,14 @@ +package ch.akop.homesystem.config.properties; + +import ch.akop.homesystem.openai.ImageRequest; +import lombok.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; + +@Value +@ConstructorBinding +@ConfigurationProperties(prefix = "home-automation.openai") +public class OpenAIProperties { + String apiKey; + ImageRequest.Size size; +} diff --git a/src/main/java/ch/akop/homesystem/openai/ImageRequest.java b/src/main/java/ch/akop/homesystem/openai/ImageRequest.java new file mode 100644 index 0000000..6d0726e --- /dev/null +++ b/src/main/java/ch/akop/homesystem/openai/ImageRequest.java @@ -0,0 +1,39 @@ +package ch.akop.homesystem.openai; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Data +public class ImageRequest { + private String prompt; + private int n; + private Size size; + + @JsonProperty("response_format") + private ResponseFormat responseFormat; + + @RequiredArgsConstructor + public enum Size { + SMALL("256x256"), + MEDIUM("512x512"), + BIG("1024x1024"); + + @Getter + @JsonValue + private final String text; + } + + @RequiredArgsConstructor + public enum ResponseFormat { + URL("url"), + B64_JSON("b64_json"); + + @Getter + @JsonValue + private final String text; + + } +} diff --git a/src/main/java/ch/akop/homesystem/openai/OpenAIService.java b/src/main/java/ch/akop/homesystem/openai/OpenAIService.java new file mode 100644 index 0000000..b9d8be1 --- /dev/null +++ b/src/main/java/ch/akop/homesystem/openai/OpenAIService.java @@ -0,0 +1,56 @@ +package ch.akop.homesystem.openai; + +import ch.akop.homesystem.config.properties.OpenAIProperties; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import javax.annotation.PostConstruct; +import java.util.Base64; + +import static ch.akop.homesystem.openai.ImageRequest.ResponseFormat.B64_JSON; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OpenAIService { + + private final OpenAIProperties openAIProperties; + private WebClient apiWebClient; + + + @PostConstruct + protected void initializeWebClients() { + apiWebClient = WebClient.builder() + .baseUrl("https://api.openai.com/v1/") + .defaultHeaders(header -> header.setBearerAuth(openAIProperties.getApiKey())) + .codecs(codecs -> codecs.defaultCodecs().maxInMemorySize(1024 * 1024 * 10)) + .build(); + } + + @SneakyThrows + public Mono requestImage(String text) { + var requestBody = new ImageRequest() + .setResponseFormat(B64_JSON) + .setN(1) + .setSize(openAIProperties.getSize()) + .setPrompt(text); + + log.info("Request a {} open-ai image for: {}", requestBody.getSize(), text); + + return apiWebClient.post() + .uri("images/generations") + .body(BodyInserters.fromValue(requestBody)) + .headers(header -> header.setContentType(MediaType.APPLICATION_JSON)) + .retrieve() + .bodyToMono(Response.class) + .map(response -> response.getData().get(0).getB64_json()) + .map(b64 -> Base64.getDecoder().decode(b64)); + } + +} diff --git a/src/main/java/ch/akop/homesystem/openai/Response.java b/src/main/java/ch/akop/homesystem/openai/Response.java new file mode 100644 index 0000000..1e1c2ee --- /dev/null +++ b/src/main/java/ch/akop/homesystem/openai/Response.java @@ -0,0 +1,18 @@ +package ch.akop.homesystem.openai; + +import lombok.Data; + +import java.util.List; + +@Data +public class Response { + + private List data; + + @Data + public static class ResponseData { + private String url; + private String b64_json; + } + +} diff --git a/src/main/java/ch/akop/homesystem/persistence/model/ImageOfOpenAI.java b/src/main/java/ch/akop/homesystem/persistence/model/ImageOfOpenAI.java new file mode 100644 index 0000000..598c640 --- /dev/null +++ b/src/main/java/ch/akop/homesystem/persistence/model/ImageOfOpenAI.java @@ -0,0 +1,26 @@ +package ch.akop.homesystem.persistence.model; + +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "openai_images") +@Getter +@Setter +public class ImageOfOpenAI { + + @Id + private LocalDateTime created = LocalDateTime.now(); + + @Column(nullable = false) + @NonNull + private String prompt; + + @Lob + private byte[] image; + +} diff --git a/src/main/java/ch/akop/homesystem/persistence/repository/OpenAIImageRepository.java b/src/main/java/ch/akop/homesystem/persistence/repository/OpenAIImageRepository.java new file mode 100644 index 0000000..3326e7e --- /dev/null +++ b/src/main/java/ch/akop/homesystem/persistence/repository/OpenAIImageRepository.java @@ -0,0 +1,12 @@ +package ch.akop.homesystem.persistence.repository; + +import ch.akop.homesystem.persistence.model.ImageOfOpenAI; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; + +@Repository +public interface OpenAIImageRepository extends JpaRepository { + +} diff --git a/src/main/java/ch/akop/homesystem/services/ImageCreatorService.java b/src/main/java/ch/akop/homesystem/services/ImageCreatorService.java new file mode 100644 index 0000000..b57f9a8 --- /dev/null +++ b/src/main/java/ch/akop/homesystem/services/ImageCreatorService.java @@ -0,0 +1,7 @@ +package ch.akop.homesystem.services; + +public interface ImageCreatorService { + + void generateAndSendDailyImage(); + +} diff --git a/src/main/java/ch/akop/homesystem/services/MessageService.java b/src/main/java/ch/akop/homesystem/services/MessageService.java index 6b57c92..1c7c23f 100644 --- a/src/main/java/ch/akop/homesystem/services/MessageService.java +++ b/src/main/java/ch/akop/homesystem/services/MessageService.java @@ -15,5 +15,7 @@ public interface MessageService { MessageService sendMessageToMainChannel(@Nullable String message); + MessageService sendImageToMainChannel(@NonNull byte [] image, @NonNull String caption); + MessageService sendMessageToUser(@Nullable String message, @NonNull String chatId); } diff --git a/src/main/java/ch/akop/homesystem/services/impl/ImageCreatorServiceImpl.java b/src/main/java/ch/akop/homesystem/services/impl/ImageCreatorServiceImpl.java new file mode 100644 index 0000000..9d1bb11 --- /dev/null +++ b/src/main/java/ch/akop/homesystem/services/impl/ImageCreatorServiceImpl.java @@ -0,0 +1,87 @@ +package ch.akop.homesystem.services.impl; + +import ch.akop.homesystem.openai.OpenAIService; +import ch.akop.homesystem.persistence.model.ImageOfOpenAI; +import ch.akop.homesystem.persistence.repository.OpenAIImageRepository; +import ch.akop.homesystem.services.ImageCreatorService; +import ch.akop.homesystem.services.MessageService; +import ch.akop.homesystem.services.WeatherService; +import ch.akop.weathercloud.Weather; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static ch.akop.weathercloud.rain.RainUnit.MILLIMETER_PER_HOUR; +import static ch.akop.weathercloud.temperature.TemperatureUnit.DEGREE; + +@Service +@RequiredArgsConstructor +public class ImageCreatorServiceImpl implements ImageCreatorService { + + private final OpenAIService imageService; + private final OpenAIImageRepository imageRepository; + private final MessageService messageService; + private final WeatherService weatherService; + + + @Override + public void generateAndSendDailyImage() { + var prompt = generatePrompt(); + imageService.requestImage(prompt) + .subscribe(image -> { + messageService.sendImageToMainChannel(image, prompt); + imageRepository.save(new ImageOfOpenAI().setPrompt(prompt).setImage(image)); + }); + } + + private String generatePrompt() { + var atTheBeginning = List.of("A swiss house in the mountains with a lake", + "A train passing wonderful mountains", + "A blue Ford Kuga MK 2 on the highway"); + + var inTheMiddle = weatherService.getWeather() + .take(1) + .map(this::extractTextFromWeather) + .blockingFirst(); + + var atTheEnd = List.of("as an oil painting", + "as a stained glass window", + "as an abstract pencil and watercolor drawing", + "in digital art", + "as a realistic photograph", + "as a 3D render", + "in Van Gogh style"); + + return "%s %s %s".formatted(atTheBeginning, inTheMiddle, atTheEnd); + } + + private String extractTextFromWeather(Weather weather) { + + var isRaining = weather.getRain().isBiggerThan(0, MILLIMETER_PER_HOUR); + var isCold = weather.getOuterTemperatur().isSmallerThan(5, DEGREE); + var isWarm = weather.getOuterTemperatur().isBiggerThan(15, DEGREE); + + if (isRaining && isCold) { + return "on cold and rainy day"; + } + + if (isRaining && isWarm) { + return "on a summer rainy day"; + } + + if (isRaining) { + return "on a rainy day"; + } + + if (isCold) { + return "in the winter"; + } + + if (isWarm) { + return "in the summer"; + } + + return ""; + } +} diff --git a/src/main/java/ch/akop/homesystem/services/impl/TelegramMessageService.java b/src/main/java/ch/akop/homesystem/services/impl/TelegramMessageService.java index 45b7f3e..e7197c0 100644 --- a/src/main/java/ch/akop/homesystem/services/impl/TelegramMessageService.java +++ b/src/main/java/ch/akop/homesystem/services/impl/TelegramMessageService.java @@ -4,6 +4,7 @@ import com.pengrad.telegrambot.TelegramBot; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.request.SendMessage; +import com.pengrad.telegrambot.request.SendPhoto; import com.pengrad.telegrambot.request.SetWebhook; import io.reactivex.rxjava3.subjects.PublishSubject; import io.reactivex.rxjava3.subjects.Subject; @@ -60,6 +61,15 @@ public MessageService sendMessageToMainChannel(@Nullable String message) { return sendMessageToUser(message, mainChannel); } + @Override + public MessageService sendImageToMainChannel(byte @NonNull [] image, @NonNull String caption) { + if (mainChannel == null) { + return this; + } + + return sendImageToUser(image, mainChannel, caption); + } + @Override public MessageService sendMessageToUser(@Nullable String message, @NonNull String chatId) { return sendMessageToUser(message, List.of(chatId)); @@ -74,6 +84,15 @@ public MessageService sendMessageToUser(@Nullable String message, @NonNull List< return this; } + public MessageService sendImageToUser(byte @NonNull [] image, @NonNull String chatId, @NonNull String text) { + if (bot != null) { + bot.execute(new SendPhoto(chatId, image) + .caption(text)); + } + + return this; + } + public void process(Update update) { log.info("Message from {}@{}: {}", update.message().from().firstName(), update.message().chat().id(), diff --git a/src/main/java/ch/akop/homesystem/states/SleepState.java b/src/main/java/ch/akop/homesystem/states/SleepState.java index a54c843..6a86ef2 100644 --- a/src/main/java/ch/akop/homesystem/states/SleepState.java +++ b/src/main/java/ch/akop/homesystem/states/SleepState.java @@ -3,10 +3,7 @@ import ch.akop.homesystem.config.properties.HomeSystemProperties; import ch.akop.homesystem.models.devices.other.Group; import ch.akop.homesystem.models.devices.other.Scene; -import ch.akop.homesystem.services.DeviceService; -import ch.akop.homesystem.services.MessageService; -import ch.akop.homesystem.services.UserService; -import ch.akop.homesystem.services.WeatherService; +import ch.akop.homesystem.services.*; import ch.akop.homesystem.services.impl.StateServiceImpl; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.disposables.Disposable; @@ -50,6 +47,7 @@ public class SleepState implements State { private final HomeSystemProperties homeSystemProperties; private final WeatherService weatherService; private final UserService userService; + private final ImageCreatorService imageCreatorService; private Disposable timerDoorOpen; @@ -104,6 +102,7 @@ public void turnLightsOff() { @Override public void leave() { messageService.sendMessageToMainChannel(POSSIBLE_MORNING_TEXTS.get(RANDOM.nextInt(POSSIBLE_MORNING_TEXTS.size()))); + imageCreatorService.generateAndSendDailyImage(); if (weatherService.isActive()) { var weather = weatherService.getWeather().blockingFirst();