image content generation

This commit is contained in:
root
2025-12-05 00:52:46 +05:00
parent 4fa0a01c61
commit 886aaca98f
8 changed files with 506 additions and 31 deletions
@@ -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