NEW THINGS
This commit is contained in:
@@ -4,20 +4,28 @@ import jakarta.validation.Valid;
|
|||||||
import kz.konturai.parser.dto.ApiResponse;
|
import kz.konturai.parser.dto.ApiResponse;
|
||||||
import kz.konturai.parser.dto.ErrorResponse;
|
import kz.konturai.parser.dto.ErrorResponse;
|
||||||
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
|
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
|
||||||
|
import kz.konturai.parser.dto.MarketingStrategyRequest;
|
||||||
|
import kz.konturai.parser.enums.StrategyModel;
|
||||||
import kz.konturai.parser.model.MarketingAnalysisV3Document;
|
import kz.konturai.parser.model.MarketingAnalysisV3Document;
|
||||||
|
import kz.konturai.parser.model.MarketingStrategy;
|
||||||
import kz.konturai.parser.service.JwtService;
|
import kz.konturai.parser.service.JwtService;
|
||||||
import kz.konturai.parser.service.MarketingAnalysisV3Service;
|
import kz.konturai.parser.service.MarketingAnalysisV3Service;
|
||||||
|
import kz.konturai.parser.service.MarketingStrategyV3Service;
|
||||||
|
import kz.konturai.parser.service.MinIOService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/marketing/v3")
|
@RequestMapping("/api/marketing/v3")
|
||||||
@@ -25,7 +33,9 @@ import java.util.Optional;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class MarketingAnalysisV3Controller {
|
public class MarketingAnalysisV3Controller {
|
||||||
|
|
||||||
private final MarketingAnalysisV3Service service;
|
private final MarketingAnalysisV3Service analysisService;
|
||||||
|
private final MarketingStrategyV3Service strategyService;
|
||||||
|
private final MinIOService minIOService;
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
|
|
||||||
private String extractUserIdFromHeader(String authHeader) {
|
private String extractUserIdFromHeader(String authHeader) {
|
||||||
@@ -40,6 +50,10 @@ public class MarketingAnalysisV3Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// БЛОК 1: АНАЛИЗ (ОПРОСНИК И ПАРСИНГ)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
@PostMapping("/start")
|
@PostMapping("/start")
|
||||||
public ResponseEntity<?> startAnalysis(
|
public ResponseEntity<?> startAnalysis(
|
||||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
@@ -49,7 +63,7 @@ public class MarketingAnalysisV3Controller {
|
|||||||
if (userId == null) return unauthorizedResponse();
|
if (userId == null) return unauthorizedResponse();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String analysisId = service.createAndStartAnalysis(request, userId);
|
String analysisId = analysisService.createAndStartAnalysis(request, userId);
|
||||||
Map<String, String> responseData = Map.of(
|
Map<String, String> responseData = Map.of(
|
||||||
"analysisId", analysisId,
|
"analysisId", analysisId,
|
||||||
"message", "Analysis V3 started"
|
"message", "Analysis V3 started"
|
||||||
@@ -70,7 +84,7 @@ public class MarketingAnalysisV3Controller {
|
|||||||
if (userId == null) return unauthorizedResponse();
|
if (userId == null) return unauthorizedResponse();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Optional<MarketingAnalysisV3Document> analysisOpt = service.getAnalysisById(id);
|
Optional<MarketingAnalysisV3Document> analysisOpt = analysisService.getAnalysisById(id);
|
||||||
if (analysisOpt.isEmpty()) return notFoundResponse("Анализ не найден");
|
if (analysisOpt.isEmpty()) return notFoundResponse("Анализ не найден");
|
||||||
|
|
||||||
MarketingAnalysisV3Document analysis = analysisOpt.get();
|
MarketingAnalysisV3Document analysis = analysisOpt.get();
|
||||||
@@ -91,7 +105,7 @@ public class MarketingAnalysisV3Controller {
|
|||||||
if (userId == null) return unauthorizedResponse();
|
if (userId == null) return unauthorizedResponse();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<MarketingAnalysisV3Document> analyses = service.getAllByUser(userId);
|
List<MarketingAnalysisV3Document> analyses = analysisService.getAllByUser(userId);
|
||||||
return ResponseEntity.ok(ApiResponse.success(analyses));
|
return ResponseEntity.ok(ApiResponse.success(analyses));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error fetching user analyses V3", e);
|
log.error("Error fetching user analyses V3", e);
|
||||||
@@ -99,6 +113,119 @@ public class MarketingAnalysisV3Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// БЛОК 2: СТРАТЕГИЯ (СКОРИНГ, ЛОГОТИПЫ, МЕДИА)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ПРЕВЬЮ СТРАТЕГИИ: Вызывается, когда юзер открывает раздел Стратегии.
|
||||||
|
* Моментально возвращает идеальную модель на основе ответов опросника.
|
||||||
|
*/
|
||||||
|
@GetMapping("/{analysisId}/strategy-preview")
|
||||||
|
public ResponseEntity<?> previewStrategy(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String analysisId
|
||||||
|
) {
|
||||||
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) return unauthorizedResponse();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Optional<MarketingAnalysisV3Document> analysisOpt = analysisService.getAnalysisById(analysisId);
|
||||||
|
if (analysisOpt.isEmpty()) return notFoundResponse("Анализ не найден");
|
||||||
|
|
||||||
|
if (!analysisOpt.get().getUserId().equals(userId)) return forbiddenResponse();
|
||||||
|
|
||||||
|
// Вызываем математику из сервиса напрямую
|
||||||
|
StrategyModel recommendedModel = strategyService.calculateBestScoringModel(analysisOpt.get().getRequestData());
|
||||||
|
|
||||||
|
Map<String, Object> responseData = Map.of(
|
||||||
|
"recommendedModel", recommendedModel.name(),
|
||||||
|
"modelTitle", recommendedModel.getTitle(),
|
||||||
|
"description", "На основе ваших ответов ИИ рекомендует эту стратегию для максимального результата."
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success("Рекомендация сформирована", responseData));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error previewing strategy for analysis: {}", analysisId, e);
|
||||||
|
return internalErrorResponse(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ГЕНЕРАЦИЯ СТРАТЕГИИ: Принимает настройки и файл логотипа (опционально)
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/{analysisId}/strategy/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
public ResponseEntity<?> generateStrategy(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String analysisId,
|
||||||
|
@RequestPart("request") @Valid MarketingStrategyRequest request,
|
||||||
|
@RequestPart(value = "logo", required = false) MultipartFile logo
|
||||||
|
) {
|
||||||
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) return unauthorizedResponse();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем доступ к анализу
|
||||||
|
Optional<MarketingAnalysisV3Document> analysisOpt = analysisService.getAnalysisById(analysisId);
|
||||||
|
if (analysisOpt.isEmpty()) return notFoundResponse("Анализ не найден");
|
||||||
|
if (!analysisOpt.get().getUserId().equals(userId)) return forbiddenResponse();
|
||||||
|
|
||||||
|
String logoFilename = null;
|
||||||
|
// Если юзер прикрепил логотип, сохраняем его в MinIO
|
||||||
|
if (logo != null && !logo.isEmpty()) {
|
||||||
|
String originalExt = logo.getOriginalFilename() != null ?
|
||||||
|
logo.getOriginalFilename().substring(logo.getOriginalFilename().lastIndexOf(".")) : ".png";
|
||||||
|
logoFilename = "logo_" + UUID.randomUUID() + originalExt;
|
||||||
|
minIOService.uploadFile(logoFilename, logo.getBytes(), logo.getContentType());
|
||||||
|
log.info("Логотип клиента успешно загружен в MinIO: {}", logoFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем полную генерацию
|
||||||
|
MarketingStrategy strategy = strategyService.generateStrategy(analysisId, request, userId, logoFilename);
|
||||||
|
|
||||||
|
Map<String, String> responseData = Map.of(
|
||||||
|
"strategyId", strategy.getId(),
|
||||||
|
"status", strategy.getStatus(),
|
||||||
|
"message", "Генерация стратегии и медиафайлов успешно запущена"
|
||||||
|
);
|
||||||
|
return ResponseEntity.accepted().body(ApiResponse.success("Запущено", responseData));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to generate strategy for analysis: {}", analysisId, e);
|
||||||
|
return internalErrorResponse(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ПОЛУЧЕНИЕ СТРАТЕГИИ: Для поллинга (проверки статуса) и отображения готового плана
|
||||||
|
*/
|
||||||
|
@GetMapping("/strategy/{strategyId}")
|
||||||
|
public ResponseEntity<?> getStrategyById(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String strategyId
|
||||||
|
) {
|
||||||
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) return unauthorizedResponse();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Предполагается, что в MarketingStrategyV3Service есть метод getStrategyById
|
||||||
|
Optional<MarketingStrategy> strategyOpt = strategyService.getStrategyById(strategyId);
|
||||||
|
if (strategyOpt.isEmpty()) return notFoundResponse("Стратегия не найдена");
|
||||||
|
|
||||||
|
MarketingStrategy strategy = strategyOpt.get();
|
||||||
|
if (!strategy.getUserId().equals(userId)) return forbiddenResponse();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(strategy));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error fetching strategy: {}", strategyId, e);
|
||||||
|
return internalErrorResponse(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// БЛОК 3: ОБРАБОТКА ОШИБОК
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public ResponseEntity<ApiResponse<ErrorResponse>> handleValidationException(MethodArgumentNotValidException ex) {
|
public ResponseEntity<ApiResponse<ErrorResponse>> handleValidationException(MethodArgumentNotValidException ex) {
|
||||||
Map<String, String> details = new HashMap<>();
|
Map<String, String> details = new HashMap<>();
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package kz.konturai.parser.enums;
|
||||||
|
|
||||||
|
public enum StrategyModel {
|
||||||
|
ENTRY("Модель Входа (Entry)", "Много объясняющего (30%) и демо (25%). Темы: 'Как мы работаем', 'Что это за продукт'."),
|
||||||
|
AUTHORITY("Экспертная модель (Authority)", "Экспертный контент (40%) и кейсы (25%). Темы: 'Разбор кейса', 'Аналитика'."),
|
||||||
|
TRUST("Модель Доверия (Trust)", "Кейсы (30%) и отзывы (25%). Темы: 'Отзывы клиентов', 'Процесс изнутри'."),
|
||||||
|
CONVERSION("Конверсионная модель (Conversion)", "Продающий (35%) и демо (30%). Темы: 'Акция', 'Ограниченное предложение'.");
|
||||||
|
|
||||||
|
private final String title;
|
||||||
|
private final String contentRules;
|
||||||
|
|
||||||
|
StrategyModel(String title, String contentRules) {
|
||||||
|
this.title = title;
|
||||||
|
this.contentRules = contentRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public String getContentRules() { return contentRules; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package kz.konturai.parser.model;
|
package kz.konturai.parser.model;
|
||||||
|
|
||||||
import kz.konturai.parser.dto.StatusHistoryEntry;
|
import kz.konturai.parser.dto.StatusHistoryEntry;
|
||||||
|
import lombok.*;
|
||||||
import org.springframework.data.annotation.Id;
|
import org.springframework.data.annotation.Id;
|
||||||
import org.springframework.data.mongodb.core.mapping.Document;
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
import org.springframework.data.mongodb.core.mapping.Field;
|
import org.springframework.data.mongodb.core.mapping.Field;
|
||||||
@@ -10,6 +11,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
@Document(collection = "marketing_strategy")
|
@Document(collection = "marketing_strategy")
|
||||||
public class MarketingStrategy {
|
public class MarketingStrategy {
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ public class MarketingStrategy {
|
|||||||
private LocalDateTime completedAt;
|
private LocalDateTime completedAt;
|
||||||
|
|
||||||
@Field("strategy_data")
|
@Field("strategy_data")
|
||||||
private Map<String, Object> strategyData; // JSON data with full strategy content
|
private Map<String, Object> strategyData;
|
||||||
|
|
||||||
@Field("user_id")
|
@Field("user_id")
|
||||||
private String userId;
|
private String userId;
|
||||||
@@ -60,102 +62,9 @@ public class MarketingStrategy {
|
|||||||
this.analysisId = analysisId;
|
this.analysisId = analysisId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
@Data
|
||||||
return id;
|
@AllArgsConstructor
|
||||||
}
|
@NoArgsConstructor
|
||||||
|
|
||||||
public void setId(String id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAnalysisId() {
|
|
||||||
return analysisId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAnalysisId(String analysisId) {
|
|
||||||
this.analysisId = analysisId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(String status) {
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getDurationWeeks() {
|
|
||||||
return durationWeeks;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDurationWeeks(Integer durationWeeks) {
|
|
||||||
this.durationWeeks = durationWeeks;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getPriorityPlatforms() {
|
|
||||||
return priorityPlatforms;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPriorityPlatforms(List<String> priorityPlatforms) {
|
|
||||||
this.priorityPlatforms = priorityPlatforms;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<WeeklyPlan> getWeeklyPlans() {
|
|
||||||
return weeklyPlans;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWeeklyPlans(List<WeeklyPlan> weeklyPlans) {
|
|
||||||
this.weeklyPlans = weeklyPlans;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<PostCalendarItem> getPostCalendar() {
|
|
||||||
return postCalendar;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPostCalendar(List<PostCalendarItem> postCalendar) {
|
|
||||||
this.postCalendar = postCalendar;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
|
||||||
return createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCreatedAt(LocalDateTime createdAt) {
|
|
||||||
this.createdAt = createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDateTime getCompletedAt() {
|
|
||||||
return completedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCompletedAt(LocalDateTime completedAt) {
|
|
||||||
this.completedAt = completedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Object> getStrategyData() {
|
|
||||||
return strategyData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStrategyData(Map<String, Object> strategyData) {
|
|
||||||
this.strategyData = strategyData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUserId() {
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUserId(String userId) {
|
|
||||||
this.userId = userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<StatusHistoryEntry> getStatusHistory() {
|
|
||||||
return statusHistory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatusHistory(List<StatusHistoryEntry> statusHistory) {
|
|
||||||
this.statusHistory = statusHistory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class WeeklyPlan {
|
public static class WeeklyPlan {
|
||||||
@Field("week_number")
|
@Field("week_number")
|
||||||
private Integer weekNumber;
|
private Integer weekNumber;
|
||||||
@@ -168,50 +77,11 @@ public class MarketingStrategy {
|
|||||||
|
|
||||||
@Field("priority_platforms")
|
@Field("priority_platforms")
|
||||||
private List<String> priorityPlatforms;
|
private List<String> priorityPlatforms;
|
||||||
|
|
||||||
public WeeklyPlan() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public WeeklyPlan(Integer weekNumber, List<String> mainThemes, String contentRecommendations, List<String> priorityPlatforms) {
|
|
||||||
this.weekNumber = weekNumber;
|
|
||||||
this.mainThemes = mainThemes;
|
|
||||||
this.contentRecommendations = contentRecommendations;
|
|
||||||
this.priorityPlatforms = priorityPlatforms;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getWeekNumber() {
|
|
||||||
return weekNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWeekNumber(Integer weekNumber) {
|
|
||||||
this.weekNumber = weekNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getMainThemes() {
|
|
||||||
return mainThemes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMainThemes(List<String> mainThemes) {
|
|
||||||
this.mainThemes = mainThemes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getContentRecommendations() {
|
|
||||||
return contentRecommendations;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContentRecommendations(String contentRecommendations) {
|
|
||||||
this.contentRecommendations = contentRecommendations;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getPriorityPlatforms() {
|
|
||||||
return priorityPlatforms;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPriorityPlatforms(List<String> priorityPlatforms) {
|
|
||||||
this.priorityPlatforms = priorityPlatforms;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
public static class PostCalendarItem {
|
public static class PostCalendarItem {
|
||||||
@Field("publish_date")
|
@Field("publish_date")
|
||||||
private LocalDateTime publishDate;
|
private LocalDateTime publishDate;
|
||||||
@@ -220,7 +90,7 @@ public class MarketingStrategy {
|
|||||||
private String platform;
|
private String platform;
|
||||||
|
|
||||||
@Field("content_type")
|
@Field("content_type")
|
||||||
private String contentType; // пост, сторис, видео, баннер
|
private String contentType;
|
||||||
|
|
||||||
@Field("theme")
|
@Field("theme")
|
||||||
private String theme;
|
private String theme;
|
||||||
@@ -232,7 +102,7 @@ public class MarketingStrategy {
|
|||||||
private List<String> hashtags;
|
private List<String> hashtags;
|
||||||
|
|
||||||
@Field("publish_time")
|
@Field("publish_time")
|
||||||
private String publishTime; // время публикации в формате HH:mm
|
private String publishTime;
|
||||||
|
|
||||||
@Field("image_url")
|
@Field("image_url")
|
||||||
private String imageUrl;
|
private String imageUrl;
|
||||||
@@ -240,90 +110,13 @@ public class MarketingStrategy {
|
|||||||
@Field("image_filename")
|
@Field("image_filename")
|
||||||
private String imageFilename;
|
private String imageFilename;
|
||||||
|
|
||||||
public PostCalendarItem() {
|
@Field("video_url")
|
||||||
}
|
private String videoUrl;
|
||||||
|
|
||||||
public PostCalendarItem(LocalDateTime publishDate, String platform, String contentType, String theme, String postText, List<String> hashtags, String publishTime) {
|
@Field("video_filename")
|
||||||
this.publishDate = publishDate;
|
private String videoFilename;
|
||||||
this.platform = platform;
|
|
||||||
this.contentType = contentType;
|
|
||||||
this.theme = theme;
|
|
||||||
this.postText = postText;
|
|
||||||
this.hashtags = hashtags;
|
|
||||||
this.publishTime = publishTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDateTime getPublishDate() {
|
@Field("video_generation_prompt")
|
||||||
return publishDate;
|
private String videoGenerationPrompt;
|
||||||
}
|
|
||||||
|
|
||||||
public void setPublishDate(LocalDateTime publishDate) {
|
|
||||||
this.publishDate = publishDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPlatform() {
|
|
||||||
return platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPlatform(String platform) {
|
|
||||||
this.platform = platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getContentType() {
|
|
||||||
return contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContentType(String contentType) {
|
|
||||||
this.contentType = contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTheme() {
|
|
||||||
return theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTheme(String theme) {
|
|
||||||
this.theme = theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPostText() {
|
|
||||||
return postText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPostText(String postText) {
|
|
||||||
this.postText = postText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getHashtags() {
|
|
||||||
return hashtags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHashtags(List<String> hashtags) {
|
|
||||||
this.hashtags = hashtags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPublishTime() {
|
|
||||||
return publishTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package kz.konturai.parser.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.*;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class GeminiVideoGenerationService {
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate(); // Можно заинжектить через @Bean, если есть
|
||||||
|
|
||||||
|
@Value("${gemini.veo.api.url:https://generativelanguage.googleapis.com/v1beta/models/veo:generateVideo}")
|
||||||
|
private String videoApiUrl;
|
||||||
|
|
||||||
|
@Value("${gemini.veo.api.key:YOUR_API_KEY}")
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
public byte[] generateVideo(String prompt) {
|
||||||
|
log.info("Запуск генерации видео через Gemini Veo. Промпт: {}", prompt);
|
||||||
|
try {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("x-goog-api-key", apiKey);
|
||||||
|
|
||||||
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
|
requestBody.put("prompt", prompt);
|
||||||
|
requestBody.put("aspectRatio", "9:16");
|
||||||
|
requestBody.put("resolution", "1080p");
|
||||||
|
|
||||||
|
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> response = restTemplate.exchange(
|
||||||
|
videoApiUrl,
|
||||||
|
HttpMethod.POST,
|
||||||
|
entity,
|
||||||
|
byte[].class
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
|
||||||
|
log.info("Видео успешно сгенерировано. Размер: {} байт", response.getBody().length);
|
||||||
|
return response.getBody();
|
||||||
|
} else {
|
||||||
|
log.error("Ошибка API генерации видео. Код: {}", response.getStatusCode());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Критическая ошибка при генерации видео: {}", e.getMessage(), e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,5 +13,7 @@ public interface ImageGenerationService {
|
|||||||
* @return Массив байтов изображения в формате PNG, или null в случае ошибки
|
* @return Массив байтов изображения в формате PNG, или null в случае ошибки
|
||||||
*/
|
*/
|
||||||
byte[] generateImage(String prompt);
|
byte[] generateImage(String prompt);
|
||||||
|
|
||||||
|
byte[] generateImageWithReference(String prompt, byte[] referenceImageBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
package kz.konturai.parser.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import kz.konturai.parser.dto.MarketingStrategyRequest;
|
||||||
|
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
|
||||||
|
import kz.konturai.parser.dto.StatusHistoryEntry;
|
||||||
|
import kz.konturai.parser.enums.StrategyModel;
|
||||||
|
import kz.konturai.parser.model.MarketingAnalysisV3Document;
|
||||||
|
import kz.konturai.parser.model.MarketingStrategy;
|
||||||
|
import kz.konturai.parser.repository.MarketingAnalysisV3Repository;
|
||||||
|
import kz.konturai.parser.repository.MarketingStrategyRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class MarketingStrategyV3Service {
|
||||||
|
|
||||||
|
private final MarketingStrategyRepository repository;
|
||||||
|
private final MarketingAnalysisV3Repository analysisRepository;
|
||||||
|
private final OpenAIAnalyticsService aiService;
|
||||||
|
private final ImageGenerationService imageGenerationService;
|
||||||
|
private final GeminiVideoGenerationService geminiVideoService;
|
||||||
|
private final MinIOService minIOService;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper()
|
||||||
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
|
||||||
|
@Value("${openai.model.name.text:gpt-4o}")
|
||||||
|
private String highIntelligenceModel;
|
||||||
|
|
||||||
|
@Value("${image.generation.delayBetweenRequestsMs:3000}")
|
||||||
|
private long delayBetweenRequestsMs;
|
||||||
|
|
||||||
|
// ДОБАВЛЕННЫЙ МЕТОД, КОТОРЫЙ ИСКАЛ КОНТРОЛЛЕР
|
||||||
|
public Optional<MarketingStrategy> getStrategyById(String id) {
|
||||||
|
return repository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId, String uploadedLogoFilename) {
|
||||||
|
MarketingAnalysisV3Document analysisDoc = analysisRepository.findById(analysisId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Анализ V3 не найден"));
|
||||||
|
|
||||||
|
MarketingStrategy strategy = new MarketingStrategy(analysisId);
|
||||||
|
strategy.setUserId(userId);
|
||||||
|
strategy.setDurationWeeks(request.getDurationWeeks() != null ? request.getDurationWeeks() : 4);
|
||||||
|
strategy.setPriorityPlatforms(request.getPriorityPlatforms());
|
||||||
|
strategy.setStatus("queued");
|
||||||
|
|
||||||
|
// Сохраняем имя файла логотипа в strategyData, чтобы асинхронный процесс мог его скачать
|
||||||
|
Map<String, Object> initialData = new HashMap<>();
|
||||||
|
if (uploadedLogoFilename != null && !uploadedLogoFilename.isEmpty()) {
|
||||||
|
initialData.put("clientLogoFilename", uploadedLogoFilename);
|
||||||
|
}
|
||||||
|
strategy.setStrategyData(initialData);
|
||||||
|
|
||||||
|
addStatusHistoryEntry(strategy, "queued", "Запуск генерации стратегии");
|
||||||
|
|
||||||
|
strategy = repository.save(strategy);
|
||||||
|
processStrategyGenerationAsync(strategy.getId(), analysisDoc, strategy);
|
||||||
|
|
||||||
|
return strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async("reportGenerationExecutor")
|
||||||
|
public void processStrategyGenerationAsync(String strategyId, MarketingAnalysisV3Document analysis, MarketingStrategy strategy) {
|
||||||
|
try {
|
||||||
|
strategy.setStatus("processing");
|
||||||
|
addStatusHistoryEntry(strategy, "processing", "Расчет баллов и генерация глубокого контент-плана...");
|
||||||
|
repository.save(strategy);
|
||||||
|
|
||||||
|
StrategyModel bestModel = calculateBestScoringModel(analysis.getRequestData());
|
||||||
|
log.info("[Strategy ID: {}] Выбрана модель: {}", strategyId, bestModel.name());
|
||||||
|
|
||||||
|
String systemPrompt = buildSystemPrompt(bestModel);
|
||||||
|
String userPrompt = buildUserPrompt(analysis, strategy, bestModel);
|
||||||
|
|
||||||
|
String rawResponse = aiService.generateWithInstructionWithModel("{}", userPrompt, "ru", highIntelligenceModel, systemPrompt, 16000, 240000L);
|
||||||
|
String jsonResponse = extractCleanJson(rawResponse);
|
||||||
|
|
||||||
|
Map<String, Object> strategyData = objectMapper.readValue(jsonResponse, new TypeReference<>() {});
|
||||||
|
|
||||||
|
// Сохраняем старые данные (например, clientLogoFilename) перед перезаписью
|
||||||
|
if (strategy.getStrategyData() != null && strategy.getStrategyData().containsKey("clientLogoFilename")) {
|
||||||
|
strategyData.put("clientLogoFilename", strategy.getStrategyData().get("clientLogoFilename"));
|
||||||
|
}
|
||||||
|
|
||||||
|
populateStrategyEntity(strategy, strategyData);
|
||||||
|
repository.save(strategy);
|
||||||
|
|
||||||
|
generateMediaAssets(strategy, analysis);
|
||||||
|
|
||||||
|
strategy.setStatus("completed");
|
||||||
|
strategy.setCompletedAt(LocalDateTime.now());
|
||||||
|
addStatusHistoryEntry(strategy, "completed", "Стратегия и все медиа успешно созданы");
|
||||||
|
repository.save(strategy);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Ошибка при генерации стратегии {}: {}", strategyId, e.getMessage(), e);
|
||||||
|
markAsFailed(strategyId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ТЕПЕРЬ PUBLIC, ЧТОБЫ КОНТРОЛЛЕР МОГ ЕГО ВЫЗВАТЬ ДЛЯ ПРЕВЬЮ
|
||||||
|
public StrategyModel calculateBestScoringModel(MarketingAnalysisV3Request req) {
|
||||||
|
int entry = 0, authority = 0, trust = 0, conversion = 0;
|
||||||
|
|
||||||
|
if (req.getCustomerBehaviors() != null) {
|
||||||
|
for (var cb : req.getCustomerBehaviors()) {
|
||||||
|
String name = cb.name();
|
||||||
|
if (name.contains("FAST") || name.contains("IMPULSE")) { conversion += 3; }
|
||||||
|
else if (name.contains("COMPARE")) { authority += 2; trust += 1; }
|
||||||
|
else if (name.contains("CONSULT")) { authority += 3; }
|
||||||
|
else if (name.contains("CASE") || name.contains("REVIEW")) { trust += 3; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getAverageCheck() != null) {
|
||||||
|
String check = req.getAverageCheck().name();
|
||||||
|
if (check.contains("LOW")) { conversion += 2; }
|
||||||
|
else if (check.contains("MID")) { authority += 1; trust += 1; conversion += 1; }
|
||||||
|
else if (check.contains("HIGH")) { authority += 2; trust += 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getBusinessStage() != null) {
|
||||||
|
String stage = req.getBusinessStage().name();
|
||||||
|
if (stage.contains("LAUNCH")) { entry += 3; }
|
||||||
|
else if (stage.contains("LESS")) { entry += 2; authority += 1; }
|
||||||
|
else { authority += 1; trust += 1; conversion += 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getClientTarget() != null) {
|
||||||
|
String target = req.getClientTarget().name();
|
||||||
|
if (target.contains("B2B")) { authority += 2; trust += 1; }
|
||||||
|
else if (target.contains("B2C")) { trust += 1; conversion += 2; }
|
||||||
|
else { authority += 1; trust += 1; conversion += 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
int max = Math.max(Math.max(entry, authority), Math.max(trust, conversion));
|
||||||
|
|
||||||
|
if (max == authority && max == trust && req.getAverageCheck() != null && req.getAverageCheck().name().contains("HIGH")) return StrategyModel.AUTHORITY;
|
||||||
|
if (max == trust && max == conversion) return StrategyModel.TRUST;
|
||||||
|
if (max == entry && req.getBusinessStage() != null && req.getBusinessStage().name().contains("LAUNCH")) return StrategyModel.ENTRY;
|
||||||
|
|
||||||
|
if (max == conversion) return StrategyModel.CONVERSION;
|
||||||
|
if (max == trust) return StrategyModel.TRUST;
|
||||||
|
if (max == authority) return StrategyModel.AUTHORITY;
|
||||||
|
return StrategyModel.ENTRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void populateStrategyEntity(MarketingStrategy strategy, Map<String, Object> strategyData) {
|
||||||
|
strategy.setStrategyData(strategyData);
|
||||||
|
|
||||||
|
List<Map<String, Object>> weeksMap = (List<Map<String, Object>>) strategyData.get("weeklyPlans");
|
||||||
|
List<MarketingStrategy.WeeklyPlan> weeklyPlans = new ArrayList<>();
|
||||||
|
if (weeksMap != null) {
|
||||||
|
for (Map<String, Object> w : weeksMap) {
|
||||||
|
MarketingStrategy.WeeklyPlan plan = new MarketingStrategy.WeeklyPlan();
|
||||||
|
plan.setWeekNumber((Integer) w.get("weekNumber"));
|
||||||
|
plan.setMainThemes((List<String>) w.get("mainThemes"));
|
||||||
|
plan.setContentRecommendations((String) w.get("contentRecommendations"));
|
||||||
|
plan.setPriorityPlatforms((List<String>) w.get("priorityPlatforms"));
|
||||||
|
weeklyPlans.add(plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strategy.setWeeklyPlans(weeklyPlans);
|
||||||
|
|
||||||
|
List<Map<String, Object>> calendarMap = (List<Map<String, Object>>) strategyData.get("postCalendar");
|
||||||
|
List<MarketingStrategy.PostCalendarItem> postCalendar = new ArrayList<>();
|
||||||
|
if (calendarMap != null) {
|
||||||
|
for (Map<String, Object> c : calendarMap) {
|
||||||
|
MarketingStrategy.PostCalendarItem item = new MarketingStrategy.PostCalendarItem();
|
||||||
|
try {
|
||||||
|
item.setPublishDate(LocalDateTime.parse((String) c.get("publishDate"), DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||||
|
} catch (Exception e) {
|
||||||
|
item.setPublishDate(LocalDateTime.now().plusDays(postCalendar.size() + 1));
|
||||||
|
}
|
||||||
|
item.setPlatform((String) c.get("platform"));
|
||||||
|
item.setContentType((String) c.get("contentType"));
|
||||||
|
item.setTheme((String) c.get("theme"));
|
||||||
|
item.setPostText((String) c.get("postText"));
|
||||||
|
item.setHashtags((List<String>) c.get("hashtags"));
|
||||||
|
item.setPublishTime((String) c.get("publishTime"));
|
||||||
|
|
||||||
|
if (c.containsKey("videoGenerationPrompt")) {
|
||||||
|
item.setVideoGenerationPrompt((String) c.get("videoGenerationPrompt"));
|
||||||
|
}
|
||||||
|
if (c.containsKey("imageGenerationPrompt")) {
|
||||||
|
item.setImageFilename((String) c.get("imageGenerationPrompt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
postCalendar.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strategy.setPostCalendar(postCalendar);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateMediaAssets(MarketingStrategy strategy, MarketingAnalysisV3Document analysis) {
|
||||||
|
String businessContext = extractBusinessContext(analysis);
|
||||||
|
|
||||||
|
// 1. Загружаем логотип клиента, если он был передан
|
||||||
|
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();
|
||||||
|
log.info("Успешно загружен референсный логотип клиента для интеграции: {}", logoFilename);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Не удалось скачать логотип клиента из MinIO {}: {}", logoFilename, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) {
|
||||||
|
String contentType = item.getContentType() != null ? item.getContentType().toLowerCase() : "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (contentType.contains("видео") || contentType.contains("reels") || contentType.contains("tiktok")) {
|
||||||
|
String videoPrompt = item.getVideoGenerationPrompt();
|
||||||
|
if (videoPrompt != null && !videoPrompt.isEmpty()) {
|
||||||
|
byte[] videoBytes = geminiVideoService.generateVideo(videoPrompt);
|
||||||
|
if (videoBytes != null && videoBytes.length > 0) {
|
||||||
|
String filename = "video_" + System.currentTimeMillis() + "_" + item.hashCode() + ".mp4";
|
||||||
|
minIOService.uploadFile(filename, videoBytes, "video/mp4");
|
||||||
|
item.setVideoUrl(filename);
|
||||||
|
item.setVideoFilename(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String imagePrompt = item.getImageFilename();
|
||||||
|
if (imagePrompt != null && !imagePrompt.isEmpty()) {
|
||||||
|
|
||||||
|
// 2. ВЫЗЫВАЕМ НОВЫЙ МЕТОД С РЕФЕРЕНСОМ ЛОГОТИПА
|
||||||
|
byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientLogoBytes);
|
||||||
|
|
||||||
|
if (imageBytes != null && imageBytes.length > 0) {
|
||||||
|
String filename = "image_" + System.currentTimeMillis() + "_" + item.hashCode() + ".png";
|
||||||
|
minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG.toString());
|
||||||
|
item.setImageUrl(filename);
|
||||||
|
item.setImageFilename(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (delayBetweenRequestsMs > 0) Thread.sleep(delayBetweenRequestsMs);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Ошибка создания медиа для поста '{}': {}", item.getTheme(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildSystemPrompt(StrategyModel bestModel) {
|
||||||
|
return """
|
||||||
|
РОЛЬ: Ты — топовый Голливудский Креативный Директор и SMM Стратег.
|
||||||
|
Бэкенд-система произвела расчет и выбрала доминирующую стратегию: %s.
|
||||||
|
|
||||||
|
Твоя задача — сгенерировать глубокий, продуманный контент-план, строго соблюдая правила этой модели:
|
||||||
|
%s
|
||||||
|
|
||||||
|
КРИТИЧЕСКИЕ ПРАВИЛА ГЕНЕРАЦИИ МЕДИА-ПРОМПТОВ:
|
||||||
|
1. Если contentType = 'фото' или 'баннер', ОБЯЗАТЕЛЬНО добавь поле 'imageGenerationPrompt' (на английском языке).
|
||||||
|
Опиши сцену (высшее качество, 8k, фотореализм).
|
||||||
|
ИНТЕГРАЦИЯ БРЕНДА: Нативно впиши название бренда или продукта в физический мир картинки!
|
||||||
|
ИИ должен сам решить, куда органично вписать логотип (на кружку, футболку, ноутбук, билборд).
|
||||||
|
|
||||||
|
2. Если contentType = 'видео' или 'reels', ОБЯЗАТЕЛЬНО добавь поле 'videoGenerationPrompt' (на английском языке).
|
||||||
|
Опиши режиссерский сценарий для нейросети генерации видео (движение камеры, свет, объекты).
|
||||||
|
|
||||||
|
ВЫВОДИ ТОЛЬКО ЧИСТЫЙ JSON ПОСЛЕ БЛОКА <scratchpad>!
|
||||||
|
""".formatted(bestModel.getTitle(), bestModel.getContentRules());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildUserPrompt(MarketingAnalysisV3Document analysis, MarketingStrategy strategy, StrategyModel bestModel) throws Exception {
|
||||||
|
String clientDtoJson = objectMapper.writeValueAsString(analysis.getRequestData());
|
||||||
|
String platforms = String.join(", ", strategy.getPriorityPlatforms());
|
||||||
|
LocalDateTime startDate = LocalDateTime.now().plusDays(1);
|
||||||
|
String brandName = analysis.getRequestData().getProductName();
|
||||||
|
|
||||||
|
return """
|
||||||
|
ДАННЫЕ БИЗНЕСА:
|
||||||
|
%s
|
||||||
|
|
||||||
|
Настройки: %d недель. Платформы: %s. Старт: %s
|
||||||
|
Бренд клиента: "%s" (Обязательно используй это имя для интеграции в 'imageGenerationPrompt'!).
|
||||||
|
ВЫБРАННАЯ МОДЕЛЬ: %s
|
||||||
|
|
||||||
|
ВЫВЕДИ ТОЛЬКО JSON:
|
||||||
|
{
|
||||||
|
"selectedModel": "%s",
|
||||||
|
"rationale": "Детальное бизнес-обоснование",
|
||||||
|
"weeklyPlans": [
|
||||||
|
{
|
||||||
|
"weekNumber": 1,
|
||||||
|
"mainThemes": ["Тема 1", "Тема 2"],
|
||||||
|
"contentRecommendations": "Что снимать и писать",
|
||||||
|
"priorityPlatforms": ["Instagram"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postCalendar": [
|
||||||
|
{
|
||||||
|
"publishDate": "2024-01-15T10:00:00",
|
||||||
|
"platform": "Instagram",
|
||||||
|
"contentType": "фото",
|
||||||
|
"theme": "Тема поста",
|
||||||
|
"postText": "Глубокий текст поста, готовый к публикации...",
|
||||||
|
"hashtags": ["#тег"],
|
||||||
|
"publishTime": "10:00",
|
||||||
|
"imageGenerationPrompt": "Подробный промпт на английском. Пример: A high-end lifestyle shot of a workspace. The brand name '%s' is subtly engraved on the premium leather notebook lying on the table. Photorealistic, 8k.",
|
||||||
|
"videoGenerationPrompt": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".formatted(clientDtoJson, strategy.getDurationWeeks(), platforms, startDate.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), brandName, bestModel.name(), bestModel.name(), brandName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractCleanJson(String rawResponse) {
|
||||||
|
if (rawResponse == null || rawResponse.trim().isEmpty()) return null;
|
||||||
|
String cleaned = rawResponse.trim();
|
||||||
|
int scratchpadEnd = cleaned.indexOf("</scratchpad>");
|
||||||
|
if (scratchpadEnd != -1) cleaned = cleaned.substring(scratchpadEnd + "</scratchpad>".length()).trim();
|
||||||
|
int firstBrace = cleaned.indexOf("{");
|
||||||
|
int lastBrace = cleaned.lastIndexOf("}");
|
||||||
|
if (firstBrace != -1 && lastBrace != -1 && firstBrace <= lastBrace) return cleaned.substring(firstBrace, lastBrace + 1);
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractBusinessContext(MarketingAnalysisV3Document analysis) {
|
||||||
|
if (analysis == null || analysis.getRequestData() == null) return "";
|
||||||
|
return "Ниша: " + analysis.getRequestData().getBusinessNiche() + ", Продукт: " + analysis.getRequestData().getProductName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addStatusHistoryEntry(MarketingStrategy strategy, String status, String message) {
|
||||||
|
if (strategy.getStatusHistory() == null) strategy.setStatusHistory(new ArrayList<>());
|
||||||
|
strategy.getStatusHistory().add(new StatusHistoryEntry(status, LocalDateTime.now(), message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markAsFailed(String strategyId, String errorMsg) {
|
||||||
|
repository.findById(strategyId).ifPresent(s -> {
|
||||||
|
s.setStatus("failed");
|
||||||
|
addStatusHistoryEntry(s, "failed", errorMsg);
|
||||||
|
repository.save(s);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import org.springframework.core.ParameterizedTypeReference;
|
|||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||||
@@ -66,8 +67,27 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Стандартный метод (если логотипа нет)
|
||||||
@Override
|
@Override
|
||||||
public byte[] generateImage(String prompt) {
|
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);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return generateImageWithReference(prompt, referenceLogo.getBytes());
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Ошибка при чтении MultipartFile логотипа: {}", e.getMessage());
|
||||||
|
return generateImage(prompt); // Fallback на обычную генерацию, если файл битый
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Основная логика генерации с поддержкой референсного изображения (логотипа)
|
||||||
|
public byte[] generateImageWithReference(String prompt, byte[] referenceImageBytes) {
|
||||||
if (projectId == null || projectId.trim().isEmpty()) {
|
if (projectId == null || projectId.trim().isEmpty()) {
|
||||||
logger.error("Project ID is missing! Check application.properties");
|
logger.error("Project ID is missing! Check application.properties");
|
||||||
return null;
|
return null;
|
||||||
@@ -88,15 +108,16 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
|||||||
|
|
||||||
String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model);
|
String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model);
|
||||||
|
|
||||||
|
|
||||||
String finalPrompt = prompt;
|
String finalPrompt = prompt;
|
||||||
if (prompt != null && prompt.length() < 50) {
|
if (prompt != null && prompt.length() < 50) {
|
||||||
finalPrompt = "High quality, photorealistic image of: " + prompt;
|
finalPrompt = "High quality, photorealistic image of: " + prompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> requestBody = buildImagenRequestBody(finalPrompt);
|
// Передаем байты картинки в сборщик JSON
|
||||||
|
Map<String, Object> requestBody = buildImagenRequestBody(finalPrompt, referenceImageBytes);
|
||||||
|
|
||||||
logger.info("Sending request to Vertex AI. Project: {}, Model: {}", projectId, model);
|
logger.info("Sending request to Vertex AI. Project: {}, Model: {}. With Logo: {}",
|
||||||
|
projectId, model, (referenceImageBytes != null));
|
||||||
|
|
||||||
Map<String, Object> response = webClient.post()
|
Map<String, Object> response = webClient.post()
|
||||||
.uri(URI.create(endpointUrl))
|
.uri(URI.create(endpointUrl))
|
||||||
@@ -145,22 +166,41 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> buildImagenRequestBody(String prompt) {
|
// ОБНОВЛЕННЫЙ БИЛДЕР: Вшивает логотип в JSON запрос
|
||||||
|
private Map<String, Object> buildImagenRequestBody(String prompt, byte[] referenceImageBytes) {
|
||||||
Map<String, Object> body = new HashMap<>();
|
Map<String, Object> body = new HashMap<>();
|
||||||
List<Map<String, Object>> instances = new ArrayList<>();
|
List<Map<String, Object>> instances = new ArrayList<>();
|
||||||
Map<String, Object> instance = new HashMap<>();
|
Map<String, Object> instance = new HashMap<>();
|
||||||
|
|
||||||
instance.put("prompt", prompt);
|
instance.put("prompt", prompt);
|
||||||
|
|
||||||
|
// Если передали логотип — добавляем его как SUBJECT REFERENCE
|
||||||
|
if (referenceImageBytes != null && referenceImageBytes.length > 0) {
|
||||||
|
String base64Image = Base64.getEncoder().encodeToString(referenceImageBytes);
|
||||||
|
|
||||||
|
Map<String, Object> referenceImageParams = new HashMap<>();
|
||||||
|
Map<String, Object> imageBytesMap = new HashMap<>();
|
||||||
|
|
||||||
|
imageBytesMap.put("bytesBase64Encoded", base64Image);
|
||||||
|
referenceImageParams.put("referenceImage", imageBytesMap);
|
||||||
|
// SUBJECT - заставляет ИИ интегрировать этот объект в сцену
|
||||||
|
referenceImageParams.put("referenceType", "SUBJECT");
|
||||||
|
|
||||||
|
List<Map<String, Object>> refImagesList = new ArrayList<>();
|
||||||
|
refImagesList.add(referenceImageParams);
|
||||||
|
|
||||||
|
instance.put("referenceImages", refImagesList);
|
||||||
|
}
|
||||||
|
|
||||||
instances.add(instance);
|
instances.add(instance);
|
||||||
body.put("instances", instances);
|
body.put("instances", instances);
|
||||||
|
|
||||||
Map<String, Object> parameters = new HashMap<>();
|
Map<String, Object> parameters = new HashMap<>();
|
||||||
parameters.put("sampleCount", 1);
|
parameters.put("sampleCount", 1);
|
||||||
parameters.put("aspectRatio", "1:1");
|
parameters.put("aspectRatio", "1:1"); // Можно вынести в настройки, если нужны сторисы 9:16
|
||||||
|
|
||||||
parameters.put("safetyFilterLevel", "block_some");
|
parameters.put("safetyFilterLevel", "block_some");
|
||||||
parameters.put("personGeneration", "allow_adult");
|
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", "nsfw, nudity, sexual content, lgbt symbols, rainbow flags, provocative clothing, violence, gore, blood, deformed, ugly, watermark, text, signature, low quality, blurry, distorted, unrealistic");
|
|
||||||
|
|
||||||
body.put("parameters", parameters);
|
body.put("parameters", parameters);
|
||||||
return body;
|
return body;
|
||||||
|
|||||||
@@ -85,6 +85,11 @@ public class OpenAIImageGenerationService implements ImageGenerationService {
|
|||||||
return generateImage(prompt, imageSize);
|
return generateImage(prompt, imageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] generateImageWithReference(String prompt, byte[] referenceImageBytes) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Генерирует изображение через OpenAI DALL-E API с указанным размером
|
* Генерирует изображение через OpenAI DALL-E API с указанным размером
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user