fix
This commit is contained in:
@@ -38,7 +38,6 @@ public class MarketingAnalysisV3Controller {
|
||||
private final MinIOService minIOService;
|
||||
private final JwtService jwtService;
|
||||
|
||||
// ЕДИНЫЙ МЕТОД ДЛЯ ИЗВЛЕЧЕНИЯ USER ID ИЗ JWT (КАК В СТАРОМ КОНТРОЛЛЕРЕ)
|
||||
private String extractUserIdFromHeader(String authHeader) {
|
||||
if (authHeader == null || authHeader.isEmpty()) {
|
||||
return null;
|
||||
@@ -118,10 +117,6 @@ public class MarketingAnalysisV3Controller {
|
||||
// БЛОК 2: СТРАТЕГИЯ (СКОРИНГ, ЛОГОТИПЫ, МЕДИА)
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* ПРЕВЬЮ СТРАТЕГИИ: Вызывается, когда юзер открывает раздел Стратегии.
|
||||
* Моментально возвращает идеальную модель на основе ответов опросника.
|
||||
*/
|
||||
@GetMapping("/{analysisId}/strategy-preview")
|
||||
public ResponseEntity<?> previewStrategy(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@@ -133,16 +128,14 @@ public class MarketingAnalysisV3Controller {
|
||||
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", "На основе ваших ответов ИИ рекомендует эту стратегию для максимального результата."
|
||||
"description", "ИИ автоматически подберет идеальные платформы и длительность контент-плана для этой стратегии."
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success("Рекомендация сформирована", responseData));
|
||||
@@ -152,30 +145,23 @@ public class MarketingAnalysisV3Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ГЕНЕРАЦИЯ СТРАТЕГИИ: Принимает настройки и файл логотипа (опционально)
|
||||
* ИСПОЛЬЗУЕТ JWT ДЛЯ АВТОРИЗАЦИИ (БЕЗ РУЧНОГО USER-ID)
|
||||
*/
|
||||
|
||||
@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 = "request", required = false) MarketingStrategyRequest request,
|
||||
@RequestPart(value = "logo", required = false) MultipartFile logoFile
|
||||
) {
|
||||
// Достаем ID пользователя из JWT токена
|
||||
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 (logoFile != null && !logoFile.isEmpty()) {
|
||||
String originalExt = logoFile.getOriginalFilename() != null && logoFile.getOriginalFilename().contains(".") ?
|
||||
logoFile.getOriginalFilename().substring(logoFile.getOriginalFilename().lastIndexOf(".")) : ".png";
|
||||
@@ -184,15 +170,19 @@ public class MarketingAnalysisV3Controller {
|
||||
log.info("Логотип клиента успешно загружен в MinIO: {}", logoFilename);
|
||||
}
|
||||
|
||||
// Запускаем полную генерацию, передавая userId из токена
|
||||
// Защита от NullPointerException, если фронтенд вообще не прислал объект request
|
||||
if (request == null) {
|
||||
request = new MarketingStrategyRequest();
|
||||
}
|
||||
|
||||
MarketingStrategy strategy = strategyService.generateStrategy(analysisId, request, userId, logoFilename);
|
||||
|
||||
Map<String, String> responseData = Map.of(
|
||||
"strategyId", strategy.getId(),
|
||||
"status", strategy.getStatus(),
|
||||
"message", "Генерация стратегии и медиафайлов успешно запущена"
|
||||
"message", "Генерация запущена. ИИ самостоятельно анализирует нишу для выбора лучших платформ и длительности кампании."
|
||||
);
|
||||
return ResponseEntity.accepted().body(ApiResponse.success("Запущено", responseData));
|
||||
return ResponseEntity.accepted().body(ApiResponse.success("Автономная генерация запущена", responseData));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to generate strategy for analysis: {}", analysisId, e);
|
||||
@@ -200,9 +190,6 @@ public class MarketingAnalysisV3Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ПОЛУЧЕНИЕ СТРАТЕГИИ: Для поллинга (проверки статуса) и отображения готового плана
|
||||
*/
|
||||
@GetMapping("/strategy/{strategyId}")
|
||||
public ResponseEntity<?> getStrategyById(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
|
||||
@@ -4,7 +4,6 @@ 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;
|
||||
@@ -48,7 +47,6 @@ public class MarketingStrategyV3Service {
|
||||
@Value("${image.generation.delayBetweenRequestsMs:3000}")
|
||||
private long delayBetweenRequestsMs;
|
||||
|
||||
// ДОБАВЛЕННЫЙ МЕТОД, КОТОРЫЙ ИСКАЛ КОНТРОЛЛЕР
|
||||
public Optional<MarketingStrategy> getStrategyById(String id) {
|
||||
return repository.findById(id);
|
||||
}
|
||||
@@ -59,18 +57,17 @@ public class MarketingStrategyV3Service {
|
||||
|
||||
MarketingStrategy strategy = new MarketingStrategy(analysisId);
|
||||
strategy.setUserId(userId);
|
||||
strategy.setDurationWeeks(request.getDurationWeeks() != null ? request.getDurationWeeks() : 4);
|
||||
strategy.setPriorityPlatforms(request.getPriorityPlatforms());
|
||||
strategy.setDurationWeeks(4); // Дефолт, ИИ его перезапишет
|
||||
strategy.setPriorityPlatforms(new ArrayList<>());
|
||||
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", "Запуск генерации стратегии");
|
||||
addStatusHistoryEntry(strategy, "queued", "Запуск генерации. ИИ подбирает оптимальные платформы...");
|
||||
|
||||
strategy = repository.save(strategy);
|
||||
processStrategyGenerationAsync(strategy.getId(), analysisDoc, strategy);
|
||||
@@ -82,43 +79,50 @@ public class MarketingStrategyV3Service {
|
||||
public void processStrategyGenerationAsync(String strategyId, MarketingAnalysisV3Document analysis, MarketingStrategy strategy) {
|
||||
try {
|
||||
strategy.setStatus("processing");
|
||||
addStatusHistoryEntry(strategy, "processing", "Расчет баллов и генерация глубокого контент-плана...");
|
||||
addStatusHistoryEntry(strategy, "processing", "Расчет баллов, выбор платформ и генерация контент-плана...");
|
||||
repository.save(strategy);
|
||||
|
||||
// 1. Считаем лучшую модель
|
||||
StrategyModel bestModel = calculateBestScoringModel(analysis.getRequestData());
|
||||
log.info("[Strategy ID: {}] Выбрана модель: {}", strategyId, bestModel.name());
|
||||
|
||||
// 2. Генерируем JSON
|
||||
String systemPrompt = buildSystemPrompt(bestModel);
|
||||
String userPrompt = buildUserPrompt(analysis, strategy, bestModel);
|
||||
String userPrompt = buildUserPrompt(analysis, 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"));
|
||||
if (jsonResponse == null) {
|
||||
throw new IllegalStateException("AI не вернул валидный JSON");
|
||||
}
|
||||
|
||||
// 3. Парсим ответ
|
||||
Map<String, Object> strategyData = objectMapper.readValue(jsonResponse, new TypeReference<>() {});
|
||||
|
||||
if (strategy.getStrategyData() != null) {
|
||||
strategy.getStrategyData().forEach(strategyData::putIfAbsent);
|
||||
}
|
||||
|
||||
// 4. Заполняем сущность
|
||||
populateStrategyEntity(strategy, strategyData);
|
||||
repository.save(strategy);
|
||||
|
||||
// 5. Генерируем медиа НАДЕЖНЫМ способом из V1
|
||||
generateMediaAssets(strategy, analysis);
|
||||
|
||||
strategy.setStatus("completed");
|
||||
strategy.setCompletedAt(LocalDateTime.now());
|
||||
addStatusHistoryEntry(strategy, "completed", "Стратегия и все медиа успешно созданы");
|
||||
addStatusHistoryEntry(strategy, "completed", "Стратегия и все медиа успешно созданы!");
|
||||
repository.save(strategy);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Ошибка при генерации стратегии {}: {}", strategyId, e.getMessage(), e);
|
||||
markAsFailed(strategyId, e.getMessage());
|
||||
markAsFailed(strategyId, "Ошибка при генерации стратегии: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ТЕПЕРЬ PUBLIC, ЧТОБЫ КОНТРОЛЛЕР МОГ ЕГО ВЫЗВАТЬ ДЛЯ ПРЕВЬЮ
|
||||
public StrategyModel calculateBestScoringModel(MarketingAnalysisV3Request req) {
|
||||
public StrategyModel calculateBestScoringModel(kz.konturai.parser.dto.MarketingAnalysisV3Request req) {
|
||||
int entry = 0, authority = 0, trust = 0, conversion = 0;
|
||||
|
||||
if (req.getCustomerBehaviors() != null) {
|
||||
@@ -168,6 +172,13 @@ public class MarketingStrategyV3Service {
|
||||
private void populateStrategyEntity(MarketingStrategy strategy, Map<String, Object> strategyData) {
|
||||
strategy.setStrategyData(strategyData);
|
||||
|
||||
if (strategyData.containsKey("recommendedDurationWeeks")) {
|
||||
strategy.setDurationWeeks((Integer) strategyData.get("recommendedDurationWeeks"));
|
||||
}
|
||||
if (strategyData.containsKey("recommendedPlatforms")) {
|
||||
strategy.setPriorityPlatforms((List<String>) strategyData.get("recommendedPlatforms"));
|
||||
}
|
||||
|
||||
List<Map<String, Object>> weeksMap = (List<Map<String, Object>>) strategyData.get("weeklyPlans");
|
||||
List<MarketingStrategy.WeeklyPlan> weeklyPlans = new ArrayList<>();
|
||||
if (weeksMap != null) {
|
||||
@@ -184,14 +195,24 @@ public class MarketingStrategyV3Service {
|
||||
|
||||
List<Map<String, Object>> calendarMap = (List<Map<String, Object>>) strategyData.get("postCalendar");
|
||||
List<MarketingStrategy.PostCalendarItem> postCalendar = new ArrayList<>();
|
||||
LocalDateTime startDate = LocalDateTime.now().plusDays(1);
|
||||
|
||||
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));
|
||||
|
||||
// Безопасный парсинг даты как в V1
|
||||
String dateStr = (String) c.get("publishDate");
|
||||
if (dateStr != null) {
|
||||
try {
|
||||
item.setPublishDate(LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||
} catch (Exception e) {
|
||||
item.setPublishDate(startDate.plusDays(postCalendar.size()));
|
||||
}
|
||||
} else {
|
||||
item.setPublishDate(startDate.plusDays(postCalendar.size()));
|
||||
}
|
||||
|
||||
item.setPlatform((String) c.get("platform"));
|
||||
item.setContentType((String) c.get("contentType"));
|
||||
item.setTheme((String) c.get("theme"));
|
||||
@@ -199,33 +220,30 @@ public class MarketingStrategyV3Service {
|
||||
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);
|
||||
// ==========================================
|
||||
// НАДЕЖНАЯ ГЕНЕРАЦИЯ МЕДИА (ПОРТИРОВАНО ИЗ V1)
|
||||
// ==========================================
|
||||
|
||||
// 1. Загружаем логотип клиента, если он был передан
|
||||
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();
|
||||
log.info("Успешно загружен референсный логотип клиента для интеграции: {}", logoFilename);
|
||||
log.info("Загружен логотип: {}", logoFilename);
|
||||
} catch (Exception e) {
|
||||
log.error("Не удалось скачать логотип клиента из MinIO {}: {}", logoFilename, e.getMessage());
|
||||
log.error("Не удалось скачать логотип {}: {}", logoFilename, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,81 +252,123 @@ public class MarketingStrategyV3Service {
|
||||
|
||||
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);
|
||||
}
|
||||
String videoPrompt = "Кинематографичное видео. Бизнес: " + businessContext + ". Сюжет: " + item.getTheme();
|
||||
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()) {
|
||||
// ИСПОЛЬЗУЕМ СТАРЫЙ ДОБРЫЙ BUILDER ПРОМПТОВ!
|
||||
String imagePrompt = buildImagePrompt(item, businessContext, brandName);
|
||||
log.info("Generating image with prompt: {}", imagePrompt);
|
||||
|
||||
// 2. ВЫЗЫВАЕМ НОВЫЙ МЕТОД С РЕФЕРЕНСОМ ЛОГОТИПА
|
||||
byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientLogoBytes);
|
||||
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 (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 getBusinessContext(MarketingAnalysisV3Document analysis) {
|
||||
if (analysis == null || analysis.getRequestData() == null) return "";
|
||||
return "Ниша: " + analysis.getRequestData().getBusinessNiche() + ", Продукт: " + analysis.getRequestData().getProductName();
|
||||
}
|
||||
|
||||
private String buildImagePrompt(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName) {
|
||||
StringBuilder prompt = new StringBuilder();
|
||||
|
||||
String style = "высококачественная фотография, реалистичное освещение, 8k, высокая детализация";
|
||||
if (item.getPlatform() != null) {
|
||||
String platform = item.getPlatform().toLowerCase();
|
||||
if (platform.contains("linkedin")) {
|
||||
style += ", корпоративный стиль, профессионально, минимализм, чисто";
|
||||
} else if (platform.contains("instagram") || platform.contains("tiktok")) {
|
||||
style += ", лайфстайл, эстетика, яркие цвета, привлекательно, инстаграм-стиль";
|
||||
} else if (platform.contains("facebook")) {
|
||||
style += ", уютно, дружелюбно, для сообщества";
|
||||
}
|
||||
}
|
||||
|
||||
prompt.append("Фотореалистичное изображение. ");
|
||||
|
||||
if (businessContext != null && !businessContext.isEmpty()) {
|
||||
prompt.append("Контекст: ").append(businessContext).append(". ");
|
||||
}
|
||||
|
||||
if (brandName != null && !brandName.isEmpty()) {
|
||||
prompt.append("Органично и нативно интегрируй название бренда '").append(brandName).append("' в окружение (например, на вывеске, упаковке, кружке или блокноте). ");
|
||||
}
|
||||
|
||||
if (item.getTheme() != null && !item.getTheme().isEmpty()) {
|
||||
prompt.append("Сюжет изображения: ").append(item.getTheme()).append(". ");
|
||||
}
|
||||
|
||||
prompt.append("Композиция: продукт или услуга в центре внимания. ");
|
||||
prompt.append("Если на фото есть люди, они должны взаимодействовать с продуктом естественно. ");
|
||||
prompt.append("Избегать: абстрактных концепций, искусственных поз, размытия, искажений. ");
|
||||
prompt.append("Визуальный стиль: ").append(style).append(".");
|
||||
|
||||
return prompt.toString();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ПРОМПТЫ ДЛЯ ИИ (ТЕПЕРЬ БЕЗ ПРОМПТОВ ДЛЯ КАРТИНОК В JSON)
|
||||
// ==========================================
|
||||
|
||||
private String buildSystemPrompt(StrategyModel bestModel) {
|
||||
return """
|
||||
РОЛЬ: Ты — топовый Голливудский Креативный Директор и SMM Стратег.
|
||||
РОЛЬ: Ты — топовый Голливудский Креативный Директор и Chief Media Planner.
|
||||
Бэкенд-система произвела расчет и выбрала доминирующую стратегию: %s.
|
||||
|
||||
Твоя задача — сгенерировать глубокий, продуманный контент-план, строго соблюдая правила этой модели:
|
||||
Твоя задача — проанализировать нишу и САМОСТОЯТЕЛЬНО:
|
||||
1. Выбрать наиболее эффективные платформы (от 1 до 3 платформ из: Instagram, Facebook, LinkedIn, Telegram, TikTok, YouTube).
|
||||
2. Определить оптимальную длительность первой итерации стратегии (от 2 до 8 недель).
|
||||
3. Сгенерировать контент-план на основе твоих решений.
|
||||
|
||||
ПРАВИЛА МОДЕЛИ:
|
||||
%s
|
||||
|
||||
КРИТИЧЕСКИЕ ПРАВИЛА ГЕНЕРАЦИИ МЕДИА-ПРОМПТОВ:
|
||||
1. Если contentType = 'фото' или 'баннер', ОБЯЗАТЕЛЬНО добавь поле 'imageGenerationPrompt' (на английском языке).
|
||||
Опиши сцену (высшее качество, 8k, фотореализм).
|
||||
ИНТЕГРАЦИЯ БРЕНДА: Нативно впиши название бренда или продукта в физический мир картинки!
|
||||
ИИ должен сам решить, куда органично вписать логотип (на кружку, футболку, ноутбук, билборд).
|
||||
|
||||
2. Если contentType = 'видео' или 'reels', ОБЯЗАТЕЛЬНО добавь поле 'videoGenerationPrompt' (на английском языке).
|
||||
Опиши режиссерский сценарий для нейросети генерации видео (движение камеры, свет, объекты).
|
||||
|
||||
ВЫВОДИ ТОЛЬКО ЧИСТЫЙ JSON ПОСЛЕ БЛОКА <scratchpad>!
|
||||
ВЫВОДИ ТОЛЬКО ЧИСТЫЙ JSON! Никакого лишнего текста.
|
||||
""".formatted(bestModel.getTitle(), bestModel.getContentRules());
|
||||
}
|
||||
|
||||
private String buildUserPrompt(MarketingAnalysisV3Document analysis, MarketingStrategy strategy, StrategyModel bestModel) throws Exception {
|
||||
private String buildUserPrompt(MarketingAnalysisV3Document analysis, 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();
|
||||
|
||||
// УБРАЛИ imageGenerationPrompt ИЗ ШАБЛОНА, ЧТОБЫ ИИ НЕ ЛОМАЛ JSON!
|
||||
return """
|
||||
ДАННЫЕ БИЗНЕСА:
|
||||
%s
|
||||
|
||||
Настройки: %d недель. Платформы: %s. Старт: %s
|
||||
Бренд клиента: "%s" (Обязательно используй это имя для интеграции в 'imageGenerationPrompt'!).
|
||||
Старт публикаций: %s
|
||||
ВЫБРАННАЯ МОДЕЛЬ: %s
|
||||
|
||||
ВЫВЕДИ ТОЛЬКО JSON:
|
||||
ВЫВЕДИ ТОЛЬКО JSON СТРОГО ПО ЭТОЙ СТРУКТУРЕ:
|
||||
{
|
||||
"selectedModel": "%s",
|
||||
"rationale": "Детальное бизнес-обоснование",
|
||||
"rationale": "Обоснование выбора платформ",
|
||||
"recommendedDurationWeeks": 4,
|
||||
"recommendedPlatforms": ["Instagram"],
|
||||
"weeklyPlans": [
|
||||
{
|
||||
"weekNumber": 1,
|
||||
"mainThemes": ["Тема 1", "Тема 2"],
|
||||
"mainThemes": ["Тема 1"],
|
||||
"contentRecommendations": "Что снимать и писать",
|
||||
"priorityPlatforms": ["Instagram"]
|
||||
}
|
||||
@@ -319,31 +379,36 @@ public class MarketingStrategyV3Service {
|
||||
"platform": "Instagram",
|
||||
"contentType": "фото",
|
||||
"theme": "Тема поста",
|
||||
"postText": "Глубокий текст поста, готовый к публикации...",
|
||||
"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
|
||||
"publishTime": "10:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
""".formatted(clientDtoJson, strategy.getDurationWeeks(), platforms, startDate.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), brandName, bestModel.name(), bestModel.name(), brandName);
|
||||
""".formatted(
|
||||
clientDtoJson,
|
||||
startDate.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
||||
bestModel.name(),
|
||||
bestModel.name()
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// ==========================================
|
||||
// ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ (ИЗ V1)
|
||||
// ==========================================
|
||||
|
||||
private String extractBusinessContext(MarketingAnalysisV3Document analysis) {
|
||||
if (analysis == null || analysis.getRequestData() == null) return "";
|
||||
return "Ниша: " + analysis.getRequestData().getBusinessNiche() + ", Продукт: " + analysis.getRequestData().getProductName();
|
||||
private String extractCleanJson(String response) {
|
||||
if (response == null || response.trim().isEmpty()) return null;
|
||||
String cleaned = response.trim();
|
||||
|
||||
// Надежное извлечение как в старом коде
|
||||
int startIdx = cleaned.indexOf("{");
|
||||
int endIdx = cleaned.lastIndexOf("}");
|
||||
|
||||
if (startIdx >= 0 && endIdx > startIdx) {
|
||||
return cleaned.substring(startIdx, endIdx + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addStatusHistoryEntry(MarketingStrategy strategy, String status, String message) {
|
||||
|
||||
Reference in New Issue
Block a user