fix
This commit is contained in:
@@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -258,7 +259,7 @@ public class MarketingAnalysisV3Controller {
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String analysisId,
|
||||
@RequestPart(value = "request", required = false) MarketingStrategyRequest request,
|
||||
@RequestPart(value = "logo", required = false) MultipartFile logoFile
|
||||
@RequestPart(value = "references", required = false) List<MultipartFile> referenceFiles
|
||||
) {
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) return unauthorizedResponse();
|
||||
@@ -268,19 +269,24 @@ public class MarketingAnalysisV3Controller {
|
||||
if (analysisOpt.isEmpty()) return notFoundResponse("Анализ не найден");
|
||||
if (!analysisOpt.get().getUserId().equals(userId)) return forbiddenResponse();
|
||||
|
||||
String logoFilename = null;
|
||||
if (logoFile != null && !logoFile.isEmpty()) {
|
||||
String originalExt = logoFile.getOriginalFilename() != null && logoFile.getOriginalFilename().contains(".") ?
|
||||
logoFile.getOriginalFilename().substring(logoFile.getOriginalFilename().lastIndexOf(".")) : ".png";
|
||||
logoFilename = "logo_" + UUID.randomUUID() + originalExt;
|
||||
minIOService.uploadFile(logoFilename, logoFile.getBytes(), logoFile.getContentType());
|
||||
List<String> savedReferenceFilenames = new ArrayList<>();
|
||||
if (referenceFiles != null && !referenceFiles.isEmpty()) {
|
||||
for (MultipartFile file : referenceFiles) {
|
||||
if (!file.isEmpty()) {
|
||||
String originalExt = file.getOriginalFilename() != null && file.getOriginalFilename().contains(".") ?
|
||||
file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")) : ".png";
|
||||
String filename = "ref_" + UUID.randomUUID() + originalExt;
|
||||
minIOService.uploadFile(filename, file.getBytes(), file.getContentType());
|
||||
savedReferenceFilenames.add(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request == null) {
|
||||
request = new MarketingStrategyRequest();
|
||||
}
|
||||
|
||||
MarketingStrategy strategy = strategyService.generateStrategy(analysisId, request, userId, logoFilename);
|
||||
MarketingStrategy strategy = strategyService.generateStrategy(analysisId, request, userId, savedReferenceFilenames);
|
||||
|
||||
Map<String, String> responseData = Map.of(
|
||||
"strategyId", strategy.getId(),
|
||||
@@ -478,17 +484,16 @@ public class MarketingAnalysisV3Controller {
|
||||
MarketingStrategy.PostCalendarItem updatedItem = strategyService.regeneratePostImage(strategyId, postIndex);
|
||||
if (updatedItem == null) return notFoundResponse("Пост не найден");
|
||||
|
||||
Map<String, Object> responseData = Map.of(
|
||||
"strategyId", strategyId,
|
||||
"postIndex", postIndex,
|
||||
"imageUrl", updatedItem.getImageUrl() != null ? updatedItem.getImageUrl() : "",
|
||||
"imageFilename", updatedItem.getImageFilename() != null ? updatedItem.getImageFilename() : "",
|
||||
"videoUrl", updatedItem.getVideoUrl() != null ? updatedItem.getVideoUrl() : "",
|
||||
"videoFilename", updatedItem.getVideoFilename() != null ? updatedItem.getVideoFilename() : "",
|
||||
"theme", updatedItem.getTheme() != null ? updatedItem.getTheme() : "",
|
||||
"platform", updatedItem.getPlatform() != null ? updatedItem.getPlatform() : "",
|
||||
"publishDate", updatedItem.getPublishDate()
|
||||
);
|
||||
Map<String, Object> responseData = new HashMap<>();
|
||||
responseData.put("strategyId", strategyId);
|
||||
responseData.put("postIndex", postIndex);
|
||||
responseData.put("imageUrl", updatedItem.getImageUrl() != null ? updatedItem.getImageUrl() : "");
|
||||
responseData.put("imageFilename", updatedItem.getImageFilename() != null ? updatedItem.getImageFilename() : "");
|
||||
responseData.put("videoUrl", updatedItem.getVideoUrl() != null ? updatedItem.getVideoUrl() : "");
|
||||
responseData.put("videoFilename", updatedItem.getVideoFilename() != null ? updatedItem.getVideoFilename() : "");
|
||||
responseData.put("theme", updatedItem.getTheme() != null ? updatedItem.getTheme() : "");
|
||||
responseData.put("platform", updatedItem.getPlatform() != null ? updatedItem.getPlatform() : "");
|
||||
responseData.put("publishDate", updatedItem.getPublishDate());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success("Изображение для поста успешно регенерировано", responseData));
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -57,7 +57,7 @@ public class MarketingStrategyV3Service {
|
||||
return repository.findByUserIdOrderByCreatedAtDesc(userId);
|
||||
}
|
||||
|
||||
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId, String uploadedLogoFilename) {
|
||||
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId, List<String> referenceFilenames) {
|
||||
MarketingAnalysisV3Document analysisDoc = analysisRepository.findById(analysisId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Analysis V3 not found"));
|
||||
|
||||
@@ -68,8 +68,8 @@ public class MarketingStrategyV3Service {
|
||||
strategy.setStatus("queued");
|
||||
|
||||
Map<String, Object> initialData = new HashMap<>();
|
||||
if (uploadedLogoFilename != null && !uploadedLogoFilename.isEmpty()) {
|
||||
initialData.put("clientLogoFilename", uploadedLogoFilename);
|
||||
if (referenceFilenames != null && !referenceFilenames.isEmpty()) {
|
||||
initialData.put("referenceFilenames", referenceFilenames);
|
||||
}
|
||||
strategy.setStrategyData(initialData);
|
||||
|
||||
@@ -125,7 +125,6 @@ public class MarketingStrategyV3Service {
|
||||
}
|
||||
}
|
||||
|
||||
// ИДЕАЛЬНЫЙ СБОРЩИК ОТВЕТА (Интеграция с Автопостингом)
|
||||
public Map<String, Object> getStrategyResult(String strategyId) {
|
||||
Optional<MarketingStrategy> optStrategy = repository.findById(strategyId);
|
||||
if (optStrategy.isEmpty()) {
|
||||
@@ -190,6 +189,7 @@ public class MarketingStrategyV3Service {
|
||||
return response;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public MarketingStrategy.PostCalendarItem regeneratePostImage(String strategyId, int postIndex) {
|
||||
Optional<MarketingStrategy> optStrategy = repository.findById(strategyId);
|
||||
if (optStrategy.isEmpty()) {
|
||||
@@ -214,19 +214,22 @@ public class MarketingStrategyV3Service {
|
||||
String businessContext = getBusinessContext(analysis);
|
||||
String brandName = analysis.getRequestData().getProductName();
|
||||
|
||||
byte[] clientLogoBytes = null;
|
||||
if (strategy.getStrategyData() != null && strategy.getStrategyData().containsKey("clientLogoFilename")) {
|
||||
String logoFilename = (String) strategy.getStrategyData().get("clientLogoFilename");
|
||||
try {
|
||||
InputStream logoStream = minIOService.downloadFile(logoFilename);
|
||||
clientLogoBytes = logoStream.readAllBytes();
|
||||
logoStream.close();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to download logo for regeneration: {}", e.getMessage());
|
||||
byte[] clientRefBytes = null;
|
||||
if (strategy.getStrategyData() != null && strategy.getStrategyData().containsKey("referenceFilenames")) {
|
||||
List<String> refFiles = (List<String>) strategy.getStrategyData().get("referenceFilenames");
|
||||
if (refFiles != null && !refFiles.isEmpty()) {
|
||||
String randomRef = refFiles.get(new Random().nextInt(refFiles.size()));
|
||||
try {
|
||||
InputStream logoStream = minIOService.downloadFile(randomRef);
|
||||
clientRefBytes = logoStream.readAllBytes();
|
||||
logoStream.close();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to download ref file for regeneration: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doGenerateImage(item, businessContext, brandName, clientLogoBytes);
|
||||
doGenerateImage(item, businessContext, brandName, clientRefBytes);
|
||||
repository.save(strategy);
|
||||
return item;
|
||||
}
|
||||
@@ -334,25 +337,33 @@ public class MarketingStrategyV3Service {
|
||||
strategy.setPostCalendar(postCalendar);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void generateMediaAssets(MarketingStrategy strategy, MarketingAnalysisV3Document analysis) {
|
||||
String businessContext = getBusinessContext(analysis);
|
||||
String brandName = analysis.getRequestData().getProductName();
|
||||
byte[] clientLogoBytes = null;
|
||||
|
||||
if (strategy.getStrategyData() != null && strategy.getStrategyData().containsKey("clientLogoFilename")) {
|
||||
String logoFilename = (String) strategy.getStrategyData().get("clientLogoFilename");
|
||||
try {
|
||||
InputStream logoStream = minIOService.downloadFile(logoFilename);
|
||||
clientLogoBytes = logoStream.readAllBytes();
|
||||
logoStream.close();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to download logo: {}", e.getMessage());
|
||||
}
|
||||
List<String> refFiles = new ArrayList<>();
|
||||
if (strategy.getStrategyData() != null && strategy.getStrategyData().containsKey("referenceFilenames")) {
|
||||
refFiles = (List<String>) strategy.getStrategyData().get("referenceFilenames");
|
||||
}
|
||||
|
||||
Random random = new Random();
|
||||
|
||||
for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) {
|
||||
String contentType = item.getContentType() != null ? item.getContentType().toLowerCase() : "фото";
|
||||
|
||||
byte[] currentRefBytes = null;
|
||||
if (!refFiles.isEmpty()) {
|
||||
String selectedRef = refFiles.get(random.nextInt(refFiles.size()));
|
||||
try {
|
||||
InputStream logoStream = minIOService.downloadFile(selectedRef);
|
||||
currentRefBytes = logoStream.readAllBytes();
|
||||
logoStream.close();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to download ref file {}: {}", selectedRef, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (contentType.contains("видео") || contentType.contains("reels") || contentType.contains("tiktok")) {
|
||||
String videoPrompt = "High quality cinematic commercial video. Business niche: " + businessContext + ". Scene: " + item.getTheme() + ". Photorealistic, dynamic motion, 4k.";
|
||||
@@ -371,10 +382,10 @@ public class MarketingStrategyV3Service {
|
||||
item.setVideoFilename(filename);
|
||||
} else {
|
||||
item.setContentType("фото");
|
||||
doGenerateImage(item, businessContext, brandName, clientLogoBytes);
|
||||
doGenerateImage(item, businessContext, brandName, currentRefBytes);
|
||||
}
|
||||
} else {
|
||||
doGenerateImage(item, businessContext, brandName, clientLogoBytes);
|
||||
doGenerateImage(item, businessContext, brandName, currentRefBytes);
|
||||
}
|
||||
|
||||
if (delayBetweenRequestsMs > 0) Thread.sleep(delayBetweenRequestsMs);
|
||||
@@ -385,10 +396,10 @@ public class MarketingStrategyV3Service {
|
||||
}
|
||||
}
|
||||
|
||||
private void doGenerateImage(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName, byte[] clientLogoBytes) {
|
||||
private void doGenerateImage(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName, byte[] clientRefBytes) {
|
||||
String imagePrompt = buildImagePrompt(item, businessContext, brandName);
|
||||
try {
|
||||
byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientLogoBytes);
|
||||
byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientRefBytes);
|
||||
if (imageBytes != null && imageBytes.length > 0) {
|
||||
String filename = "image_" + System.currentTimeMillis() + "_" + item.hashCode() + ".png";
|
||||
minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG_VALUE);
|
||||
|
||||
@@ -67,13 +67,11 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
.build();
|
||||
}
|
||||
|
||||
// Стандартный метод (если логотипа нет)
|
||||
@Override
|
||||
public byte[] generateImage(String prompt) {
|
||||
return generateImageWithReference(prompt, (byte[]) null);
|
||||
}
|
||||
|
||||
// НОВЫЙ МЕТОД: Принимает MultipartFile от контроллера
|
||||
public byte[] generateImage(String prompt, MultipartFile referenceLogo) {
|
||||
if (referenceLogo == null || referenceLogo.isEmpty()) {
|
||||
return generateImage(prompt);
|
||||
@@ -82,11 +80,10 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
return generateImageWithReference(prompt, referenceLogo.getBytes());
|
||||
} catch (IOException e) {
|
||||
logger.error("Ошибка при чтении MultipartFile логотипа: {}", e.getMessage());
|
||||
return generateImage(prompt); // Fallback на обычную генерацию, если файл битый
|
||||
return generateImage(prompt);
|
||||
}
|
||||
}
|
||||
|
||||
// Основная логика генерации с поддержкой референсного изображения (логотипа)
|
||||
public byte[] generateImageWithReference(String prompt, byte[] referenceImageBytes) {
|
||||
if (projectId == null || projectId.trim().isEmpty()) {
|
||||
logger.error("Project ID is missing! Check application.properties");
|
||||
@@ -108,17 +105,11 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
|
||||
String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model);
|
||||
|
||||
String finalPrompt = prompt;
|
||||
if (prompt != null && prompt.length() < 50) {
|
||||
finalPrompt = "High quality, photorealistic image of: " + prompt;
|
||||
}
|
||||
// Очищаем промпт от прямых указаний "написать текст"
|
||||
String finalPrompt = sanitizePromptForText(prompt);
|
||||
|
||||
// Передаем байты картинки в сборщик JSON
|
||||
Map<String, Object> requestBody = buildImagenRequestBody(finalPrompt, referenceImageBytes);
|
||||
|
||||
logger.info("Sending request to Vertex AI. Project: {}, Model: {}. With Logo: {}",
|
||||
projectId, model, (referenceImageBytes != null));
|
||||
|
||||
Map<String, Object> response = webClient.post()
|
||||
.uri(URI.create(endpointUrl))
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
@@ -127,12 +118,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||
.retryWhen(createRetrySpecFor429())
|
||||
.doOnError(e -> {
|
||||
logger.error("Vertex AI API Error: {}", e.getMessage());
|
||||
if (e instanceof WebClientResponseException) {
|
||||
logger.error("Response Body: {}", ((WebClientResponseException) e).getResponseBodyAsString());
|
||||
}
|
||||
})
|
||||
.block(Duration.ofMillis(timeoutMs));
|
||||
|
||||
lastRequestTimestamp = System.currentTimeMillis();
|
||||
@@ -150,7 +135,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
try {
|
||||
ClassPathResource resource = new ClassPathResource(CREDENTIALS_FILE_PATH);
|
||||
if (!resource.exists()) {
|
||||
logger.error("Google Credentials file not found at classpath: {}", CREDENTIALS_FILE_PATH);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -161,12 +145,21 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
return credentials.getAccessToken().getTokenValue();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Error loading Google Credentials from resources", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ОБНОВЛЕННЫЙ БИЛДЕР: Вшивает логотип в JSON запрос
|
||||
// Умная очистка промпта: не позволяем ИИ пытаться писать текст буквами
|
||||
private String sanitizePromptForText(String originalPrompt) {
|
||||
if (originalPrompt == null) return "High quality, photorealistic image";
|
||||
|
||||
// Убираем фразы типа "integrate the brand name 'XXX'"
|
||||
String sanitized = originalPrompt.replaceAll("(?i)integrate the brand name '[^']+'", "integrate the provided logo image");
|
||||
sanitized = sanitized.replaceAll("(?i)with the text '[^']+'", "with the provided logo");
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildImagenRequestBody(String prompt, byte[] referenceImageBytes) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
List<Map<String, Object>> instances = new ArrayList<>();
|
||||
@@ -174,7 +167,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
|
||||
instance.put("prompt", prompt);
|
||||
|
||||
// Если передали логотип — добавляем его как SUBJECT REFERENCE
|
||||
if (referenceImageBytes != null && referenceImageBytes.length > 0) {
|
||||
String base64Image = Base64.getEncoder().encodeToString(referenceImageBytes);
|
||||
|
||||
@@ -183,13 +175,18 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
|
||||
imageBytesMap.put("bytesBase64Encoded", base64Image);
|
||||
referenceImageParams.put("referenceImage", imageBytesMap);
|
||||
// SUBJECT - заставляет ИИ интегрировать этот объект в сцену
|
||||
referenceImageParams.put("referenceType", "SUBJECT");
|
||||
|
||||
// STYLE заставляет ИИ использовать загруженное фото как логотип/паттерн,
|
||||
// а не пытаться впихнуть 3D-модель объекта. Для логотипов это работает лучше, чем SUBJECT.
|
||||
referenceImageParams.put("referenceType", "STYLE");
|
||||
|
||||
List<Map<String, Object>> refImagesList = new ArrayList<>();
|
||||
refImagesList.add(referenceImageParams);
|
||||
|
||||
instance.put("referenceImages", refImagesList);
|
||||
|
||||
// Если есть референс, усиливаем промпт, чтобы он наклеил его, а не писал текст
|
||||
instance.put("prompt", prompt + ". Use the provided reference image as a logo or decal on the main object. Do NOT generate any custom text or typography.");
|
||||
}
|
||||
|
||||
instances.add(instance);
|
||||
@@ -197,10 +194,12 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("sampleCount", 1);
|
||||
parameters.put("aspectRatio", "1:1"); // Можно вынести в настройки, если нужны сторисы 9:16
|
||||
parameters.put("aspectRatio", "1:1");
|
||||
parameters.put("safetyFilterLevel", "block_some");
|
||||
parameters.put("personGeneration", "allow_adult");
|
||||
parameters.put("negativePrompt", "nsfw, nudity, sexual content, lgbt symbols, rainbow flags, provocative clothing, violence, gore, blood, deformed, ugly, watermark, signature, low quality, blurry, distorted, unrealistic");
|
||||
|
||||
// Жесткий негативный промпт против генерации кривого текста
|
||||
parameters.put("negativePrompt", "text, typography, letters, words, writing, signature, watermark, nsfw, nudity, violence, deformed, ugly, blurry, distorted");
|
||||
|
||||
body.put("parameters", parameters);
|
||||
return body;
|
||||
@@ -221,7 +220,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
return java.util.Base64.getDecoder().decode(base64Image);
|
||||
}
|
||||
}
|
||||
logger.warn("No image data found in Vertex AI response.");
|
||||
} catch (Exception e) {
|
||||
logger.error("Parsing error: {}", e.getMessage());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user