This commit is contained in:
arys
2026-03-01 14:01:54 +05:00
parent fa4bfff326
commit 679fe31c69
3 changed files with 88 additions and 74 deletions
@@ -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());
}