image content generation
This commit is contained in:
@@ -177,6 +177,8 @@ public class MarketingStrategyResponse {
|
||||
private String postText;
|
||||
private List<String> hashtags;
|
||||
private String publishTime;
|
||||
private String imageUrl;
|
||||
private String imageFilename;
|
||||
|
||||
public PostCalendarItem() {
|
||||
}
|
||||
@@ -246,6 +248,22 @@ public class MarketingStrategyResponse {
|
||||
public void setPublishTime(String publishTime) {
|
||||
this.publishTime = publishTime;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public String getImageFilename() {
|
||||
return imageFilename;
|
||||
}
|
||||
|
||||
public void setImageFilename(String imageFilename) {
|
||||
this.imageFilename = imageFilename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -234,6 +234,12 @@ public class MarketingStrategy {
|
||||
@Field("publish_time")
|
||||
private String publishTime; // время публикации в формате HH:mm
|
||||
|
||||
@Field("image_url")
|
||||
private String imageUrl;
|
||||
|
||||
@Field("image_filename")
|
||||
private String imageFilename;
|
||||
|
||||
public PostCalendarItem() {
|
||||
}
|
||||
|
||||
@@ -302,6 +308,22 @@ public class MarketingStrategy {
|
||||
public void setPublishTime(String publishTime) {
|
||||
this.publishTime = publishTime;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public String getImageFilename() {
|
||||
return imageFilename;
|
||||
}
|
||||
|
||||
public void setImageFilename(String imageFilename) {
|
||||
this.imageFilename = imageFilename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,12 @@ public class PostingTask {
|
||||
@Field("error_message")
|
||||
private String errorMessage;
|
||||
|
||||
@Field("image_url")
|
||||
private String imageUrl;
|
||||
|
||||
@Field("image_filename")
|
||||
private String imageFilename;
|
||||
|
||||
public PostingTask() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.status = "pending";
|
||||
@@ -148,5 +154,21 @@ public class PostingTask {
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public String getImageFilename() {
|
||||
return imageFilename;
|
||||
}
|
||||
|
||||
public void setImageFilename(String imageFilename) {
|
||||
this.imageFilename = imageFilename;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
@@ -21,7 +28,7 @@ public class FacebookPostingService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FacebookPostingService.class);
|
||||
private static final String FACEBOOK_GRAPH_API_BASE = "https://graph.facebook.com/v18.0";
|
||||
|
||||
|
||||
private final WebClient webClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@@ -31,12 +38,12 @@ public class FacebookPostingService {
|
||||
public FacebookPostingService() {
|
||||
HttpClient httpClient = HttpClient.create()
|
||||
.responseTimeout(Duration.ofMillis(30000));
|
||||
|
||||
|
||||
this.webClient = WebClient.builder()
|
||||
.baseUrl(FACEBOOK_GRAPH_API_BASE)
|
||||
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||
.build();
|
||||
|
||||
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@@ -44,8 +51,8 @@ public class FacebookPostingService {
|
||||
* Публикует пост в Facebook через Graph API
|
||||
*
|
||||
* @param accessToken Access Token пользователя
|
||||
* @param postText Текст поста
|
||||
* @param hashtags Список хештегов
|
||||
* @param postText Текст поста
|
||||
* @param hashtags Список хештегов
|
||||
* @return ID опубликованного поста
|
||||
* @throws RuntimeException если публикация не удалась
|
||||
*/
|
||||
@@ -53,16 +60,16 @@ public class FacebookPostingService {
|
||||
try {
|
||||
// Формируем полный текст поста с хештегами
|
||||
String fullPostText = buildPostText(postText, hashtags);
|
||||
|
||||
|
||||
// Получаем ID страницы пользователя (me)
|
||||
String pageId = getPageId(accessToken);
|
||||
|
||||
|
||||
// Публикуем пост на странице
|
||||
String postId = publishPost(accessToken, pageId, fullPostText);
|
||||
|
||||
|
||||
logger.info("Successfully posted to Facebook. Post ID: {}", postId);
|
||||
return postId;
|
||||
|
||||
|
||||
} catch (WebClientResponseException e) {
|
||||
logger.error("Facebook API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||
throw new RuntimeException("Failed to post to Facebook: " + e.getMessage(), e);
|
||||
@@ -91,11 +98,13 @@ public class FacebookPostingService {
|
||||
return jsonNode.get("id").asText();
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.error("Failed to parse Facebook page ID response", e);
|
||||
// Если не удалось получить ID страницы, используем "me" для публикации на стене пользователя
|
||||
// Если не удалось получить ID страницы, используем "me" для публикации на стене
|
||||
// пользователя
|
||||
return "me";
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to get Facebook page ID", e);
|
||||
// Если не удалось получить ID страницы, используем "me" для публикации на стене пользователя
|
||||
// Если не удалось получить ID страницы, используем "me" для публикации на стене
|
||||
// пользователя
|
||||
return "me";
|
||||
}
|
||||
}
|
||||
@@ -103,7 +112,7 @@ public class FacebookPostingService {
|
||||
/**
|
||||
* Публикует пост на странице Facebook
|
||||
*/
|
||||
private String publishPost(String accessToken, String pageId, String message)
|
||||
private String publishPost(String accessToken, String pageId, String message)
|
||||
throws JsonProcessingException {
|
||||
String response = webClient.post()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
@@ -119,26 +128,93 @@ public class FacebookPostingService {
|
||||
return jsonNode.get("id").asText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Публикует пост в Facebook с изображением через Graph API
|
||||
*
|
||||
* @param accessToken Access Token пользователя
|
||||
* @param postText Текст поста
|
||||
* @param hashtags Список хештегов
|
||||
* @param imageData Данные изображения
|
||||
* @param imageContentType MIME тип изображения (например, "image/png")
|
||||
* @return ID опубликованного поста
|
||||
* @throws RuntimeException если публикация не удалась
|
||||
*/
|
||||
public String postToFacebookWithImage(String accessToken, String postText, List<String> hashtags,
|
||||
byte[] imageData, String imageContentType) {
|
||||
try {
|
||||
// Формируем полный текст поста с хештегами
|
||||
String fullPostText = buildPostText(postText, hashtags);
|
||||
|
||||
// Получаем ID страницы пользователя (me)
|
||||
String pageId = getPageId(accessToken);
|
||||
|
||||
// Загружаем изображение и публикуем пост
|
||||
String postId = publishPostWithImage(accessToken, pageId, fullPostText, imageData, imageContentType);
|
||||
|
||||
logger.info("Successfully posted to Facebook with image. Post ID: {}", postId);
|
||||
return postId;
|
||||
|
||||
} catch (WebClientResponseException e) {
|
||||
logger.error("Facebook API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||
throw new RuntimeException("Failed to post to Facebook: " + e.getMessage(), e);
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error posting to Facebook", e);
|
||||
throw new RuntimeException("Failed to post to Facebook", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Публикует пост с изображением на странице Facebook
|
||||
* Использует endpoint /photos для загрузки изображения с текстом
|
||||
*/
|
||||
private String publishPostWithImage(String accessToken, String pageId, String message,
|
||||
byte[] imageData, String imageContentType)
|
||||
throws JsonProcessingException {
|
||||
// Создаем multipart form data для загрузки изображения
|
||||
DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
|
||||
DataBuffer imageBuffer = bufferFactory.wrap(imageData);
|
||||
|
||||
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
|
||||
formData.add("message", message);
|
||||
formData.add("source", imageBuffer);
|
||||
|
||||
String response = webClient.post()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path("/{pageId}/photos")
|
||||
.queryParam("access_token", accessToken)
|
||||
.build(pageId))
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromMultipartData(formData))
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block(Duration.ofMillis(timeoutMs * 2)); // Увеличиваем таймаут для загрузки изображения
|
||||
|
||||
JsonNode jsonNode = objectMapper.readTree(response);
|
||||
String photoId = jsonNode.get("id").asText();
|
||||
|
||||
// Возвращаем ID фото, который также является ID поста
|
||||
return photoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Формирует полный текст поста с хештегами
|
||||
*/
|
||||
private String buildPostText(String postText, List<String> hashtags) {
|
||||
StringBuilder fullText = new StringBuilder(postText != null ? postText : "");
|
||||
|
||||
|
||||
if (hashtags != null && !hashtags.isEmpty()) {
|
||||
if (fullText.length() > 0) {
|
||||
fullText.append("\n\n");
|
||||
}
|
||||
|
||||
|
||||
// Добавляем хештеги, убеждаясь что они начинаются с #
|
||||
String hashtagsText = hashtags.stream()
|
||||
.map(tag -> tag.startsWith("#") ? tag : "#" + tag)
|
||||
.collect(Collectors.joining(" "));
|
||||
|
||||
|
||||
fullText.append(hashtagsText);
|
||||
}
|
||||
|
||||
|
||||
return fullText.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,13 @@ import kz.konturai.parser.dto.MarketingAnalysisResult;
|
||||
import kz.konturai.parser.dto.MarketingStrategyRequest;
|
||||
import kz.konturai.parser.dto.MarketingStrategyResponse;
|
||||
import kz.konturai.parser.dto.StatusHistoryEntry;
|
||||
import kz.konturai.parser.model.MarketingAnalysis;
|
||||
import kz.konturai.parser.model.MarketingStrategy;
|
||||
import kz.konturai.parser.repository.MarketingAnalysisRepository;
|
||||
import kz.konturai.parser.repository.MarketingStrategyRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -25,16 +28,25 @@ public class MarketingStrategyService {
|
||||
|
||||
private final MarketingStrategyRepository repository;
|
||||
private final MarketingAnalysisService marketingAnalysisService;
|
||||
private final MarketingAnalysisRepository analysisRepository;
|
||||
private final OpenAIAnalyticsService openAIAnalyticsService;
|
||||
private final OpenAIImageGenerationService imageGenerationService;
|
||||
private final MinIOService minIOService;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public MarketingStrategyService(
|
||||
MarketingStrategyRepository repository,
|
||||
MarketingAnalysisService marketingAnalysisService,
|
||||
OpenAIAnalyticsService openAIAnalyticsService) {
|
||||
MarketingAnalysisRepository analysisRepository,
|
||||
OpenAIAnalyticsService openAIAnalyticsService,
|
||||
OpenAIImageGenerationService imageGenerationService,
|
||||
MinIOService minIOService) {
|
||||
this.repository = repository;
|
||||
this.marketingAnalysisService = marketingAnalysisService;
|
||||
this.analysisRepository = analysisRepository;
|
||||
this.openAIAnalyticsService = openAIAnalyticsService;
|
||||
this.imageGenerationService = imageGenerationService;
|
||||
this.minIOService = minIOService;
|
||||
}
|
||||
|
||||
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId) {
|
||||
@@ -98,13 +110,16 @@ public class MarketingStrategyService {
|
||||
// Build context from analysis
|
||||
String context = buildContextFromAnalysis(analysisResult);
|
||||
|
||||
// Get business context for image generation
|
||||
String businessContext = getBusinessContextFromAnalysis(analysisId);
|
||||
|
||||
// Generate weekly plans
|
||||
List<MarketingStrategy.WeeklyPlan> weeklyPlans = generateWeeklyPlans(
|
||||
context, strategy.getDurationWeeks(), strategy.getPriorityPlatforms());
|
||||
|
||||
// Generate post calendar
|
||||
List<MarketingStrategy.PostCalendarItem> postCalendar = generatePostCalendar(
|
||||
context, strategy.getDurationWeeks(), strategy.getPriorityPlatforms(), weeklyPlans);
|
||||
context, strategy.getDurationWeeks(), strategy.getPriorityPlatforms(), weeklyPlans, businessContext);
|
||||
|
||||
// Update strategy
|
||||
strategy.setStatus("completed");
|
||||
@@ -265,9 +280,29 @@ public class MarketingStrategyService {
|
||||
}
|
||||
}
|
||||
|
||||
private String getBusinessContextFromAnalysis(String analysisId) {
|
||||
try {
|
||||
Optional<MarketingAnalysis> optAnalysis = analysisRepository.findById(analysisId);
|
||||
if (optAnalysis.isPresent()) {
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
StringBuilder businessContext = new StringBuilder();
|
||||
if (analysis.getBusinessNiche() != null) {
|
||||
businessContext.append("Ниша бизнеса: ").append(analysis.getBusinessNiche()).append(". ");
|
||||
}
|
||||
if (analysis.getProduct() != null) {
|
||||
businessContext.append("Продукт: ").append(analysis.getProduct()).append(".");
|
||||
}
|
||||
return businessContext.toString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to get business context from analysis: {}", e.getMessage());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private List<MarketingStrategy.PostCalendarItem> generatePostCalendar(
|
||||
String context, Integer durationWeeks, List<String> priorityPlatforms,
|
||||
List<MarketingStrategy.WeeklyPlan> weeklyPlans) {
|
||||
List<MarketingStrategy.WeeklyPlan> weeklyPlans, String businessContext) {
|
||||
try {
|
||||
String platformsStr = priorityPlatforms != null && !priorityPlatforms.isEmpty()
|
||||
? String.join(", ", priorityPlatforms)
|
||||
@@ -371,6 +406,9 @@ public class MarketingStrategyService {
|
||||
|
||||
item.setPublishTime((String) postMap.get("publishTime"));
|
||||
|
||||
// Generate image for the post
|
||||
generateAndSaveImageForPost(item, businessContext);
|
||||
|
||||
calendar.add(item);
|
||||
}
|
||||
|
||||
@@ -425,6 +463,59 @@ public class MarketingStrategyService {
|
||||
return plans;
|
||||
}
|
||||
|
||||
private void generateAndSaveImageForPost(MarketingStrategy.PostCalendarItem item, String businessContext) {
|
||||
try {
|
||||
// Create prompt for image generation
|
||||
String imagePrompt = buildImagePrompt(item, businessContext);
|
||||
|
||||
// Generate image
|
||||
byte[] imageBytes = imageGenerationService.generateImage(imagePrompt);
|
||||
|
||||
if (imageBytes != null && imageBytes.length > 0) {
|
||||
// Save image to MinIO
|
||||
String filename = "post_image_" + System.currentTimeMillis() + "_" + item.hashCode() + ".png";
|
||||
minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG.toString());
|
||||
|
||||
// Save filename and URL in item
|
||||
item.setImageFilename(filename);
|
||||
item.setImageUrl(filename); // In MinIO, filename is also the URL/path
|
||||
|
||||
logger.info("Successfully generated and saved image for post: {}", filename);
|
||||
} else {
|
||||
logger.warn("Failed to generate image for post with theme: {}", item.getTheme());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error generating image for post: {}", e.getMessage(), e);
|
||||
// Continue without image - post will be published without image
|
||||
}
|
||||
}
|
||||
|
||||
private String buildImagePrompt(MarketingStrategy.PostCalendarItem item, String businessContext) {
|
||||
StringBuilder prompt = new StringBuilder();
|
||||
prompt.append("Создай привлекательное маркетинговое изображение для поста в социальных сетях. ");
|
||||
|
||||
if (businessContext != null && !businessContext.isEmpty()) {
|
||||
prompt.append(businessContext).append(" ");
|
||||
}
|
||||
|
||||
if (item.getTheme() != null && !item.getTheme().isEmpty()) {
|
||||
prompt.append("Тема поста: ").append(item.getTheme()).append(". ");
|
||||
}
|
||||
|
||||
if (item.getPostText() != null && !item.getPostText().isEmpty()) {
|
||||
// Use first 100 characters of post text for context
|
||||
String textPreview = item.getPostText().length() > 100
|
||||
? item.getPostText().substring(0, 100) + "..."
|
||||
: item.getPostText();
|
||||
prompt.append("Текст поста: ").append(textPreview).append(". ");
|
||||
}
|
||||
|
||||
prompt.append("Стиль: современный, профессиональный, привлекающий внимание, подходящий для социальных сетей. ");
|
||||
prompt.append("Изображение должно быть ярким, но не перегруженным, с акцентом на главную идею поста.");
|
||||
|
||||
return prompt.toString();
|
||||
}
|
||||
|
||||
private List<MarketingStrategy.PostCalendarItem> generateDefaultPostCalendar(
|
||||
Integer durationWeeks, List<String> priorityPlatforms, LocalDateTime startDate) {
|
||||
List<MarketingStrategy.PostCalendarItem> calendar = new ArrayList<>();
|
||||
@@ -493,6 +584,8 @@ public class MarketingStrategyService {
|
||||
dtoItem.setPostText(item.getPostText());
|
||||
dtoItem.setHashtags(item.getHashtags());
|
||||
dtoItem.setPublishTime(item.getPublishTime());
|
||||
dtoItem.setImageUrl(item.getImageUrl());
|
||||
dtoItem.setImageFilename(item.getImageFilename());
|
||||
postCalendar.add(dtoItem);
|
||||
}
|
||||
strategyContent.setPostCalendar(postCalendar);
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class OpenAIImageGenerationService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OpenAIImageGenerationService.class);
|
||||
private static final String DALL_E_API_URL = "https://api.openai.com/v1/images/generations";
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
@Value("${openai.api.key}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${openai.image.model:dall-e-3}")
|
||||
private String model;
|
||||
|
||||
@Value("${openai.image.size:1024x1024}")
|
||||
private String imageSize;
|
||||
|
||||
@Value("${openai.image.quality:standard}")
|
||||
private String imageQuality;
|
||||
|
||||
@Value("${openai.timeoutMs:90000}")
|
||||
private long timeoutMs;
|
||||
|
||||
@Value("${openai.retry.maxAttempts:3}")
|
||||
private int maxRetryAttempts;
|
||||
|
||||
@Value("${openai.retry.initialDelayMs:2000}")
|
||||
private long initialRetryDelayMs;
|
||||
|
||||
@Value("${openai.retry.maxDelayMs:60000}")
|
||||
private long maxRetryDelayMs;
|
||||
|
||||
@Value("${openai.retry.multiplier:2.0}")
|
||||
private double retryMultiplier;
|
||||
|
||||
public OpenAIImageGenerationService() {
|
||||
HttpClient httpClient = HttpClient.create()
|
||||
.responseTimeout(Duration.ofMillis(90000));
|
||||
|
||||
this.webClient = WebClient.builder()
|
||||
.baseUrl(DALL_E_API_URL)
|
||||
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует изображение через OpenAI DALL-E API
|
||||
*
|
||||
* @param prompt Промпт для генерации изображения
|
||||
* @return Массив байтов изображения в формате PNG
|
||||
*/
|
||||
public byte[] generateImage(String prompt) {
|
||||
return generateImage(prompt, imageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует изображение через OpenAI DALL-E API с указанным размером
|
||||
*
|
||||
* @param prompt Промпт для генерации изображения
|
||||
* @param size Размер изображения (1024x1024, 1792x1024, 1024x1792)
|
||||
* @return Массив байтов изображения в формате PNG
|
||||
*/
|
||||
public byte[] generateImage(String prompt, String size) {
|
||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||
logger.error("OpenAI API key is not configured. Cannot generate image.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (prompt == null || prompt.trim().isEmpty()) {
|
||||
logger.warn("Empty prompt provided for image generation");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model);
|
||||
requestBody.put("prompt", prompt);
|
||||
requestBody.put("n", 1);
|
||||
requestBody.put("size", size);
|
||||
requestBody.put("quality", imageQuality);
|
||||
requestBody.put("response_format", "b64_json"); // Получаем изображение в base64
|
||||
|
||||
logger.info("Requesting image generation from DALL-E with prompt: {}", prompt.substring(0, Math.min(100, prompt.length())));
|
||||
|
||||
Map<String, Object> response = webClient.post()
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||
.retryWhen(createRetrySpec("generateImage"))
|
||||
.onErrorResume(err -> {
|
||||
logger.error("DALL-E API request failed after retries: {} - {}", err.getMessage(),
|
||||
err.getClass().getSimpleName());
|
||||
if (err instanceof WebClientResponseException) {
|
||||
WebClientResponseException wcre = (WebClientResponseException) err;
|
||||
if (wcre.getStatusCode().value() == 401) {
|
||||
logger.error("OpenAI API key is invalid or expired. Please check your openai.api.key configuration.");
|
||||
} else if (wcre.getStatusCode().value() == 429) {
|
||||
logger.error("OpenAI API rate limit exceeded after all retry attempts.");
|
||||
} else if (wcre.getStatusCode().value() >= 500) {
|
||||
logger.error("OpenAI API server error after all retry attempts.");
|
||||
}
|
||||
}
|
||||
return Mono.empty();
|
||||
})
|
||||
.block(Duration.ofMillis(timeoutMs));
|
||||
|
||||
if (response == null) {
|
||||
logger.error("Failed to generate image: empty response from DALL-E API");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Извлекаем изображение из ответа
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> data = (List<Map<String, Object>>) response.get("data");
|
||||
if (data == null || data.isEmpty()) {
|
||||
logger.error("No image data in DALL-E API response");
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> imageData = data.get(0);
|
||||
String b64Json = (String) imageData.get("b64_json");
|
||||
if (b64Json == null) {
|
||||
logger.error("No b64_json in DALL-E API response");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Декодируем base64 в массив байтов
|
||||
byte[] imageBytes = java.util.Base64.getDecoder().decode(b64Json);
|
||||
logger.info("Successfully generated image. Size: {} bytes", imageBytes.length);
|
||||
return imageBytes;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error generating image with DALL-E: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private RetryBackoffSpec createRetrySpec(String operation) {
|
||||
return Retry.backoff(maxRetryAttempts, Duration.ofMillis(initialRetryDelayMs))
|
||||
.maxBackoff(Duration.ofMillis(maxRetryDelayMs))
|
||||
.multiplier(retryMultiplier)
|
||||
.filter(throwable -> {
|
||||
if (throwable instanceof WebClientResponseException) {
|
||||
WebClientResponseException wcre = (WebClientResponseException) throwable;
|
||||
int statusCode = wcre.getStatusCode().value();
|
||||
// Only retry on 429 (rate limit) and 5xx (server errors)
|
||||
return statusCode == 429 || statusCode >= 500;
|
||||
}
|
||||
// Retry on network errors
|
||||
return throwable instanceof java.util.concurrent.TimeoutException
|
||||
|| throwable instanceof java.net.ConnectException
|
||||
|| throwable instanceof java.io.IOException;
|
||||
})
|
||||
.doBeforeRetry(retrySignal -> {
|
||||
long attempt = retrySignal.totalRetries() + 1;
|
||||
Throwable failure = retrySignal.failure();
|
||||
|
||||
Duration retryAfter = null;
|
||||
if (failure instanceof WebClientResponseException) {
|
||||
WebClientResponseException wcre = (WebClientResponseException) failure;
|
||||
if (wcre.getStatusCode().value() == 429) {
|
||||
String retryAfterHeader = wcre.getHeaders().getFirst("Retry-After");
|
||||
if (retryAfterHeader != null) {
|
||||
try {
|
||||
int seconds = Integer.parseInt(retryAfterHeader);
|
||||
retryAfter = Duration.ofSeconds(seconds);
|
||||
logger.warn(
|
||||
"DALL-E API returned 429 for operation '{}'. Retry-After: {} seconds. Will retry in {}ms (attempt {}/{})",
|
||||
operation, seconds, retryAfter.toMillis(), attempt, maxRetryAttempts);
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore if header is not a number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (retryAfter == null) {
|
||||
logger.warn("DALL-E API returned error for operation '{}'. Will retry (attempt {}/{})",
|
||||
operation, attempt, maxRetryAttempts);
|
||||
}
|
||||
})
|
||||
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
|
||||
logger.error("DALL-E request for operation '{}' exhausted all {} retry attempts. Giving up.",
|
||||
operation, maxRetryAttempts);
|
||||
return retrySignal.failure();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,16 +23,19 @@ public class PostingTaskService {
|
||||
private final MarketingStrategyRepository strategyRepository;
|
||||
private final SocialMediaCredentialsService credentialsService;
|
||||
private final FacebookPostingService facebookPostingService;
|
||||
private final MinIOService minIOService;
|
||||
|
||||
public PostingTaskService(
|
||||
PostingTaskRepository taskRepository,
|
||||
MarketingStrategyRepository strategyRepository,
|
||||
SocialMediaCredentialsService credentialsService,
|
||||
FacebookPostingService facebookPostingService) {
|
||||
FacebookPostingService facebookPostingService,
|
||||
MinIOService minIOService) {
|
||||
this.taskRepository = taskRepository;
|
||||
this.strategyRepository = strategyRepository;
|
||||
this.credentialsService = credentialsService;
|
||||
this.facebookPostingService = facebookPostingService;
|
||||
this.minIOService = minIOService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +52,7 @@ public class PostingTaskService {
|
||||
}
|
||||
|
||||
MarketingStrategy strategy = optStrategy.get();
|
||||
|
||||
|
||||
if (!"completed".equals(strategy.getStatus())) {
|
||||
throw new IllegalStateException("Strategy is not completed yet. Status: " + strategy.getStatus());
|
||||
}
|
||||
@@ -90,8 +93,11 @@ public class PostingTaskService {
|
||||
calendarItem.getPlatform(),
|
||||
calendarItem.getPostText(),
|
||||
calendarItem.getHashtags(),
|
||||
calendarItem.getPublishDate()
|
||||
);
|
||||
calendarItem.getPublishDate());
|
||||
|
||||
// Копируем данные об изображении
|
||||
task.setImageUrl(calendarItem.getImageUrl());
|
||||
task.setImageFilename(calendarItem.getImageFilename());
|
||||
|
||||
// Если дата публикации в прошлом, выполняем сразу
|
||||
if (task.getPublishDate().isBefore(now) || task.getPublishDate().isEqual(now)) {
|
||||
@@ -159,14 +165,35 @@ public class PostingTaskService {
|
||||
throw new IllegalStateException("Credentials not found for platform: " + task.getPlatform());
|
||||
}
|
||||
|
||||
// Загружаем изображение, если оно есть
|
||||
byte[] imageData = null;
|
||||
if (task.getImageFilename() != null && !task.getImageFilename().isEmpty()) {
|
||||
try {
|
||||
java.io.InputStream imageStream = minIOService.downloadFile(task.getImageFilename());
|
||||
imageData = imageStream.readAllBytes();
|
||||
logger.info("Loaded image for task {}: {} bytes", taskId, imageData.length);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to load image for task {}: {}", taskId, e.getMessage());
|
||||
// Продолжаем без изображения
|
||||
}
|
||||
}
|
||||
|
||||
// Выполняем публикацию в зависимости от платформы
|
||||
String postId = null;
|
||||
if ("facebook".equalsIgnoreCase(task.getPlatform())) {
|
||||
postId = facebookPostingService.postToFacebook(
|
||||
credentials,
|
||||
task.getPostText(),
|
||||
task.getHashtags()
|
||||
);
|
||||
if (imageData != null && imageData.length > 0) {
|
||||
postId = facebookPostingService.postToFacebookWithImage(
|
||||
credentials,
|
||||
task.getPostText(),
|
||||
task.getHashtags(),
|
||||
imageData,
|
||||
"image/png");
|
||||
} else {
|
||||
postId = facebookPostingService.postToFacebook(
|
||||
credentials,
|
||||
task.getPostText(),
|
||||
task.getHashtags());
|
||||
}
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Platform not supported: " + task.getPlatform());
|
||||
}
|
||||
@@ -181,7 +208,7 @@ public class PostingTaskService {
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to execute task {}", taskId, e);
|
||||
|
||||
|
||||
// Обновляем статус на failed
|
||||
task.setStatus("failed");
|
||||
task.setExecutedAt(LocalDateTime.now());
|
||||
@@ -218,4 +245,3 @@ public class PostingTaskService {
|
||||
return taskRepository.findByStrategyId(strategyId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ openai.retry.maxDelayMs=60000
|
||||
openai.retry.multiplier=2.0
|
||||
openai.rateLimit.maxConcurrentRequests=2
|
||||
|
||||
# OpenAI DALL-E Image Generation Configuration
|
||||
openai.image.model=dall-e-3
|
||||
openai.image.size=1024x1024
|
||||
openai.image.quality=standard
|
||||
|
||||
# Email Configuration
|
||||
spring.mail.host=smtp.gmail.com
|
||||
spring.mail.port=587
|
||||
|
||||
Reference in New Issue
Block a user