diff --git a/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java b/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java index f9b1e48..9ab5532 100644 --- a/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java @@ -27,13 +27,11 @@ import java.util.*; /** * Генерация видео через Google Veo 3 (Vertex AI). * - * БЕЗ fallback — если Veo 3 недоступен, явная ошибка в лог. - * Логи: без base64 мусора, только полезная информация. + * КЛЮЧЕВОЙ ФАКТ: Veo 3 требует enhancePrompt=true ОБЯЗАТЕЛЬНО. + * Поэтому промпт пишем на английском — enhance улучшит его, + * а не переведёт с русского криво. * - * Если видите в логах: - * "VEO3: HTTP 404" → модель не включена в проекте, запросите доступ - * "VEO3: HTTP 403" → нет IAM прав на модель - * "VEO3: HTTP 400" → неверные параметры запроса + * Veo 3 поддерживает звук нативно — описываем audio в промпте. */ @Service public class GeminiVideoGenerationService { @@ -51,7 +49,6 @@ public class GeminiVideoGenerationService { @Value("${google.cloud.location:us-central1}") private String location; - // Только Veo 3 — без fallback @Value("${google.gemini.video.model:veo-3.0-generate-001}") private String model; @@ -110,45 +107,32 @@ public class GeminiVideoGenerationService { } } - // ===================================================================== - // PUBLIC API - // ===================================================================== - /** - * Генерирует видео через Veo 3. + * Генерирует видео. Принимает промпт на АНГЛИЙСКОМ языке. + * Промпт строится в MarketingStrategyV3Service.buildVideoPromptEnglish() * - * Veo 3 поддерживает: - * - Звук: описывай в промпте ("background music", "ambient sounds") - * - Русский язык в промпте (enhancePrompt=false чтобы не переводил) - * - Соотношение 9:16 для вертикального видео - * - * @param prompt Промпт на русском языке + * @param englishPrompt Промпт на английском языке * @return Байты MP4 или null при ошибке */ - public byte[] generateVideo(String prompt) { + public byte[] generateVideo(String englishPrompt) { if (projectId == null || projectId.trim().isEmpty()) { - logger.error("VEO3: ❌ Project ID not configured in application.properties"); + logger.error("VEO3: ❌ Project ID not configured"); return null; } - String accessToken = getValidAccessToken(); if (accessToken == null) return null; try { - return doGenerate(prompt, accessToken); + return doGenerate(englishPrompt, accessToken); } catch (Exception e) { logger.error("VEO3: ❌ Unexpected error: {}", e.getMessage(), e); return null; } } - // ===================================================================== - // GENERATION - // ===================================================================== - private byte[] doGenerate(String prompt, String accessToken) throws Exception { - // ═══ ШАГ 1: Запуск операции ═══════════════════════════════════════ + // ═══ ШАГ 1: Запуск ═══════════════════════════════════════════════ String generateEndpoint = String.format( "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:predictLongRunning", location, projectId, location, model); @@ -159,25 +143,21 @@ public class GeminiVideoGenerationService { Map parameters = new HashMap<>(); parameters.put("aspectRatio", "9:16"); parameters.put("sampleCount", 1); - // false = не переводить промпт на английский - parameters.put("enhancePrompt", false); - // Негативный промпт на английском — Veo понимает его лучше + // Veo 3 ТРЕБУЕТ enhancePrompt=true — нельзя отключить (code=3 если false) + parameters.put("enhancePrompt", true); parameters.put("negativePrompt", "text overlay, subtitles, captions, watermark, on-screen text, " + "blurry, low quality, bad anatomy, extra limbs, deformed face, " + - "cartoon, CGI look, 3D render, artificial, jerky motion, " + - "extra fingers, distorted proportions"); + "cartoon, 3D render, CGI, artificial look, jerky motion, " + + "extra fingers, distorted proportions, nsfw"); Map requestBody = new HashMap<>(); requestBody.put("instances", Collections.singletonList(instance)); requestBody.put("parameters", parameters); - logger.info("VEO3: ▶ Starting generation"); - logger.info("VEO3: Model: {}", model); - logger.info("VEO3: Endpoint: {}", generateEndpoint); - logger.info("VEO3: Prompt: {}", + logger.info("VEO3: ▶ Starting generation | Model={}", model); + logger.info("VEO3: Prompt: {}", prompt.length() > 300 ? prompt.substring(0, 300) + "..." : prompt); - logger.info("VEO3: Params: aspectRatio=9:16, sampleCount=1, enhancePrompt=false"); Map initResponse; try { @@ -191,36 +171,28 @@ public class GeminiVideoGenerationService { .block(Duration.ofSeconds(60)); } catch (WebClientResponseException e) { - // Подробный лог ошибки — без лишнего мусора - logger.error("VEO3: ❌ HTTP {} on START request", e.getStatusCode().value()); - logger.error("VEO3: Body: {}", e.getResponseBodyAsString()); - - if (e.getStatusCode().value() == 404) { - logger.error("VEO3: ПРИЧИНА: Модель '{}' недоступна в проекте '{}'", model, projectId); - logger.error("VEO3: РЕШЕНИЕ: Запросите доступ к Veo 3 на https://cloud.google.com/vertex-ai/generative-ai/docs/video/generate-videos"); - } else if (e.getStatusCode().value() == 403) { - logger.error("VEO3: ПРИЧИНА: Нет IAM прав. Сервис-аккаунт должен иметь роль 'Vertex AI User'"); - } else if (e.getStatusCode().value() == 400) { - logger.error("VEO3: ПРИЧИНА: Неверные параметры запроса. Проверьте структуру тела запроса"); - } + logger.error("VEO3: ❌ HTTP {} on START | Body: {}", + e.getStatusCode().value(), e.getResponseBodyAsString()); + if (e.getStatusCode().value() == 404) + logger.error("VEO3: → Модель недоступна. Запросите доступ к Veo 3 в Google Cloud Console"); + if (e.getStatusCode().value() == 403) + logger.error("VEO3: → Нет IAM прав. Добавьте роль 'Vertex AI User' сервис-аккаунту"); return null; } if (initResponse == null) { - logger.error("VEO3: ❌ Null response on START request"); + logger.error("VEO3: ❌ Null response on START"); return null; } String operationName = (String) initResponse.get("name"); if (operationName == null || operationName.isEmpty()) { - logger.error("VEO3: ❌ No 'name' field in response. Full response keys: {}", - initResponse.keySet()); + logger.error("VEO3: ❌ No 'name' in response. Keys: {}", initResponse.keySet()); return null; } + logger.info("VEO3: ✅ Operation: {}", operationName); - logger.info("VEO3: ✅ Operation started: {}", operationName); - - // ═══ ШАГ 2: Polling ════════════════════════════════════════════════ + // ═══ ШАГ 2: Polling ═══════════════════════════════════════════════ String fetchEndpoint = String.format( "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:fetchPredictOperation", location, projectId, location, model); @@ -228,8 +200,7 @@ public class GeminiVideoGenerationService { Map fetchBody = new HashMap<>(); fetchBody.put("operationName", operationName); - int attempts = 0; - int maxAttempts = 120; // 30 минут (120 × 15 сек) + int attempts = 0, maxAttempts = 120; while (attempts < maxAttempts) { Thread.sleep(15_000); @@ -237,7 +208,7 @@ public class GeminiVideoGenerationService { String currentToken = getValidAccessToken(); if (currentToken == null) { - logger.warn("VEO3: ⚠ No token on attempt {}, skipping", attempts); + logger.warn("VEO3: ⚠ No token on attempt {}", attempts); continue; } @@ -252,135 +223,94 @@ public class GeminiVideoGenerationService { .block(Duration.ofSeconds(60)); if (statusResponse == null) { - logger.warn("VEO3: ⚠ Null status on attempt {}/{}", attempts, maxAttempts); + logger.warn("VEO3: ⚠ Null status on attempt {}", attempts); continue; } - // Проверяем ошибку if (statusResponse.containsKey("error")) { @SuppressWarnings("unchecked") Map error = (Map) statusResponse.get("error"); - logger.error("VEO3: ❌ Operation failed with error:"); - logger.error("VEO3: code={}, message={}", + logger.error("VEO3: ❌ Operation error: code={}, message={}", error.get("code"), error.get("message")); return null; } - // Проверяем done Object doneObj = statusResponse.get("done"); boolean isDone = Boolean.TRUE.equals(doneObj) || "true".equalsIgnoreCase(String.valueOf(doneObj)); if (!isDone) { - // Показываем прогресс каждые 5 попыток - if (attempts % 5 == 0 || attempts <= 3) { - logger.info("VEO3: ⏳ Still processing... attempt {}/{} (~{}min elapsed)", - attempts, maxAttempts, (attempts * 15 / 60)); - } + if (attempts % 4 == 0 || attempts <= 2) + logger.info("VEO3: ⏳ Processing... {}/{} (~{}min)", + attempts, maxAttempts, attempts * 15 / 60); continue; } - logger.info("VEO3: ✅ DONE on attempt {} (~{}min)", attempts, (attempts * 15 / 60)); - - // ═══ ШАГ 3: Извлечение видео ═════════════════════════════ + logger.info("VEO3: ✅ DONE on attempt {} (~{}min)", attempts, attempts * 15 / 60); return extractVideo(statusResponse); } catch (WebClientResponseException e) { logger.error("VEO3: ❌ HTTP {} on attempt {}: {}", e.getStatusCode().value(), attempts, e.getResponseBodyAsString()); - if (e.getStatusCode().is4xxClientError()) { - logger.error("VEO3: 4xx error — stopping poll"); - return null; - } - // 5xx — продолжаем - logger.warn("VEO3: 5xx error — will retry"); + if (e.getStatusCode().is4xxClientError()) return null; } } - logger.error("VEO3: ❌ TIMEOUT after {} minutes", (maxAttempts * 15 / 60)); + logger.error("VEO3: ❌ TIMEOUT after {} min", maxAttempts * 15 / 60); return null; } - // ===================================================================== - // VIDEO EXTRACTION — без логирования base64 - // ===================================================================== - private byte[] extractVideo(Map statusResponse) { @SuppressWarnings("unchecked") - Map responseObj = (Map) statusResponse.get("response"); + Map responseObj = + (Map) statusResponse.get("response"); if (responseObj == null) { - logger.warn("VEO3: No 'response' field, trying root object"); + logger.warn("VEO3: No 'response' field, trying root"); responseObj = statusResponse; } - // Вариант 1 — base64 (НЕ логируем значение — это мусор в логах) String b64 = findFirstValue(responseObj, "bytesBase64Encoded"); if (b64 != null && !b64.isEmpty()) { - logger.info("VEO3: Found base64 encoded video (length={}chars)", b64.length()); + logger.info("VEO3: Found base64 ({}chars)", b64.length()); byte[] decoded = decodeBase64(b64); - if (decoded != null) { - logger.info("VEO3: ✅ Decoded video: {} bytes ({} MB)", + if (decoded != null) + logger.info("VEO3: ✅ Video decoded: {} bytes ({} MB)", decoded.length, String.format("%.1f", decoded.length / 1024.0 / 1024.0)); - } return decoded; } - // Вариант 2 — GCS URI String videoUri = findFirstValue(responseObj, "videoUri"); if (videoUri == null) videoUri = findFirstValue(responseObj, "uri"); if (videoUri != null && !videoUri.isEmpty()) { - logger.info("VEO3: Found video URI: {}", videoUri); + logger.info("VEO3: Found URI: {}", videoUri); return downloadFromGcs(videoUri, getValidAccessToken()); } - // Вариант 3 — predictions @SuppressWarnings("unchecked") List> predictions = (List>) responseObj.get("predictions"); if (predictions != null && !predictions.isEmpty()) { Map first = predictions.get(0); - logger.info("VEO3: Checking predictions[0]. Keys: {}", first.keySet()); - + logger.info("VEO3: predictions[0] keys: {}", first.keySet()); b64 = (String) first.get("bytesBase64Encoded"); if (b64 != null && !b64.isEmpty()) { - logger.info("VEO3: Found base64 in predictions (length={}chars)", b64.length()); byte[] decoded = decodeBase64(b64); if (decoded != null) - logger.info("VEO3: ✅ Decoded: {} bytes ({} MB)", - decoded.length, String.format("%.1f", decoded.length / 1024.0 / 1024.0)); + logger.info("VEO3: ✅ Video from predictions: {} bytes", decoded.length); return decoded; } - - String uri = (String) first.get("videoUri"); - if (uri == null) uri = (String) first.get("uri"); - if (uri != null && !uri.isEmpty()) { - logger.info("VEO3: Found URI in predictions: {}", uri); - return downloadFromGcs(uri, getValidAccessToken()); - } + String uri = (String) first.getOrDefault("videoUri", first.get("uri")); + if (uri != null && !uri.isEmpty()) return downloadFromGcs(uri, getValidAccessToken()); } - // Ничего не нашли — логируем только ключи (не значения!) - logger.error("VEO3: ❌ Video not found in response!"); - logger.error("VEO3: statusResponse keys: {}", statusResponse.keySet()); - if (responseObj != statusResponse) { - logger.error("VEO3: responseObj keys: {}", responseObj.keySet()); - } - if (predictions != null && !predictions.isEmpty()) { - logger.error("VEO3: predictions[0] keys: {}", predictions.get(0).keySet()); - } + logger.error("VEO3: ❌ Video not found. Response keys: {}", responseObj.keySet()); return null; } - // ===================================================================== - // HELPERS - // ===================================================================== - private byte[] decodeBase64(String b64) { try { String clean = b64.replaceAll("\\s", ""); - if (clean.contains("base64,")) { - clean = clean.substring(clean.indexOf("base64,") + 7); - } + if (clean.contains("base64,")) clean = clean.substring(clean.indexOf("base64,") + 7); return Base64.getDecoder().decode(clean); } catch (Exception e) { logger.error("VEO3: ❌ base64 decode failed: {}", e.getMessage()); @@ -389,54 +319,43 @@ public class GeminiVideoGenerationService { } private byte[] downloadFromGcs(String uri, String accessToken) { - if (accessToken == null) { - logger.error("VEO3: ❌ Cannot download GCS — no access token"); - return null; - } + if (accessToken == null) return null; try { - String downloadUrl = uri.startsWith("gs://") - ? "https://storage.googleapis.com/" + uri.substring(5) - : uri; - - logger.info("VEO3: Downloading from GCS: {}", downloadUrl); - + String url = uri.startsWith("gs://") + ? "https://storage.googleapis.com/" + uri.substring(5) : uri; + logger.info("VEO3: Downloading from GCS: {}", url); byte[] bytes = webClient.get() - .uri(URI.create(downloadUrl)) + .uri(URI.create(url)) .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) - .retrieve() - .bodyToMono(byte[].class) - .block(Duration.ofMinutes(5)); - + .retrieve().bodyToMono(byte[].class).block(Duration.ofMinutes(5)); if (bytes != null && bytes.length > 0) { - logger.info("VEO3: ✅ Downloaded {} bytes ({} MB) from GCS", - bytes.length, String.format("%.1f", bytes.length / 1024.0 / 1024.0)); + logger.info("VEO3: ✅ GCS download: {} bytes", bytes.length); return bytes; } - logger.error("VEO3: ❌ Empty download from GCS: {}", uri); + logger.error("VEO3: ❌ Empty GCS download"); return null; - } catch (Exception e) { - logger.error("VEO3: ❌ GCS download error: {}", e.getMessage()); + logger.error("VEO3: ❌ GCS error: {}", e.getMessage()); return null; } } - private String findFirstValue(Object obj, String targetKey) { + private String findFirstValue(Object obj, String key) { if (obj instanceof Map) { @SuppressWarnings("unchecked") Map map = (Map) obj; - if (map.containsKey(targetKey)) { - Object val = map.get(targetKey); + if (map.containsKey(key)) { + Object val = map.get(key); if (val instanceof String s && !s.isEmpty()) return s; } - for (Object value : map.values()) { - String found = findFirstValue(value, targetKey); - if (found != null) return found; + for (Object v : map.values()) { + String f = findFirstValue(v, key); + if (f != null) return f; } } else if (obj instanceof List) { for (Object item : (List) obj) { - String found = findFirstValue(item, targetKey); - if (found != null) return found; + String f = findFirstValue(item, key); + if (f != null) return f; } } return null; diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java index e46556a..0f25a1a 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java @@ -687,72 +687,27 @@ public class MarketingStrategyV3Service { private boolean doGenerateVideo(MarketingStrategy.PostCalendarItem item, String niche, String brand, String city) { - String theme = item.getTheme() != null ? item.getTheme() : "Презентация продукта"; + String theme = item.getTheme() != null ? item.getTheme() : "Product presentation"; String postText = item.getPostText() != null ? item.getPostText() : ""; String platform = item.getPlatform() != null ? item.getPlatform().toLowerCase() : "instagram"; - // Берём суть из текста поста — первые 120 символов - String postContext = postText.length() > 120 - ? postText.substring(0, 120).trim() : postText.trim(); + // Строим промпт на английском + String englishPrompt = buildVideoPromptEnglish(theme, postText, niche, brand, city, platform); - // ── Стиль по платформе (на английском — Veo 3 лучше понимает стиль на EN) ── - String styleEn; - String audioEn; - switch (platform) { - case "tiktok" -> { - styleEn = "dynamic TikTok Reels style, fast cuts, trendy transitions, " + - "energetic handheld camera, vertical 9:16"; - audioEn = "upbeat modern Kazakh music, energetic rhythm, no vocals"; - } - case "telegram" -> { - styleEn = "clean informative style, steady camera, professional look, vertical 9:16"; - audioEn = "soft background music, calm ambient, subtle"; - } - default -> { // instagram - styleEn = "cinematic Instagram Reels style, smooth camera movement, " + - "aesthetic lifestyle feel, warm color grading, vertical 9:16"; - audioEn = "modern ambient music, soft background, atmospheric, no lyrics"; - } - } - - // ── Финальный промпт: русский контекст + английские технические команды ── - // Структура: [ЧТО ПОКАЗАТЬ на русском] + [КАК СНЯТЬ на английском] - // Это даёт Veo 3 лучший результат — понимает контент И технические требования - String videoPrompt = String.format( - // Часть 1: ЧТО показывать (контекст ниши — на русском) - "Видео для бизнеса в нише «%s», бренд «%s», город %s. " + - "Тема: «%s». " + - "Показать: %s. " + - // Часть 2: КАК снимать (технические требования — на английском для точности) - "Cinematic photorealistic 8K vertical video. " + - "Style: %s. " + - // ЗВУК — явное указание для Veo 3 - "Audio: %s. " + - // КРИТИЧНО: запрет текста в кадре - "IMPORTANT: NO text on screen, NO subtitles, NO captions, NO watermarks, NO logos. " + - // Требования к людям и качеству - "People: natural Kazakh appearance, authentic emotions, realistic proportions. " + - "Quality: sharp focus, professional lighting, no blur, no CGI look.", - niche, brand, city, - theme, - postContext.isEmpty() ? theme : postContext, - styleEn, - audioEn - ); - - // Сохраняем промпт для отладки - item.setVideoGenerationPrompt(videoPrompt); + // Сохраняем для отладки + item.setVideoGenerationPrompt(englishPrompt); log.info("[VEO3] Generating video:"); - log.info("[VEO3] Platform = {}", platform); - log.info("[VEO3] Theme = {}", theme); - log.info("[VEO3] Niche = {} | Brand = {} | City = {}", niche, brand, city); - log.info("[VEO3] Prompt = {}", - videoPrompt.length() > 400 ? videoPrompt.substring(0, 400) + "..." : videoPrompt); + log.info("[VEO3] Platform : {}", platform); + log.info("[VEO3] Theme : {}", theme); + log.info("[VEO3] Niche : {} | Brand : {} | City : {}", niche, brand, city); + log.info("[VEO3] Prompt : {}", + englishPrompt.length() > 400 + ? englishPrompt.substring(0, 400) + "..." : englishPrompt); try { - byte[] videoBytes = geminiVideoService.generateVideo(videoPrompt); + byte[] videoBytes = geminiVideoService.generateVideo(englishPrompt); if (videoBytes != null && videoBytes.length > 0) { String filename = "video_" + System.currentTimeMillis() @@ -766,8 +721,7 @@ public class MarketingStrategyV3Service { filename, String.format("%.1f", videoBytes.length / 1024.0 / 1024.0)); return true; } else { - log.error("[VEO3] ❌ Empty response for theme '{}'", theme); - log.error("[VEO3] Проверьте логи GeminiVideoGenerationService для причины ошибки"); + log.error("[VEO3] ❌ Empty response for theme: '{}'", theme); item.setVideoFilename(null); item.setVideoUrl(null); return false; @@ -780,6 +734,129 @@ public class MarketingStrategyV3Service { } } + /** + * Строит промпт для Veo 3 на английском языке. + * + * Структура промпта: + * [SCENE] → что происходит в кадре + * [STYLE] → как снято, монтаж, операторская работа + * [AUDIO] → звук (Veo 3 поддерживает нативно) + * [RULES] → что запрещено (текст в кадре и т.д.) + */ + private String buildVideoPromptEnglish(String theme, String postText, + String niche, String brand, + String city, String platform) { + + // Первые 100 символов текста поста — для контекста сцены + String sceneContext = postText.length() > 100 + ? postText.substring(0, 100).trim() : postText.trim(); + + // Убираем кириллицу из контекста для промпта — оставляем только смысл + // (Переводить не нужно — Veo 3 enhance сам улучшит EN-промпт) + // Описываем тему своими словами на EN + + // Определяем тип сцены по теме поста + String sceneType = resolveSceneType(theme, niche); + + // Стиль и звук по платформе + String style, audio; + switch (platform) { + case "tiktok" -> { + style = "dynamic TikTok Reels style, fast energetic cuts, trendy transitions, " + + "handheld camera movement, authentic street style"; + audio = "upbeat modern music, energetic rhythm, suitable for young Kazakh audience, " + + "no lyrics, ambient background"; + } + case "telegram" -> { + style = "clean professional look, steady camera, informative documentary style"; + audio = "calm soft background music, subtle ambient sounds, professional feel"; + } + default -> { // instagram + style = "cinematic Instagram Reels, smooth gimbal movement, warm color grading, " + + "aesthetic lifestyle composition, shallow depth of field"; + audio = "modern soft background music, atmospheric ambient, " + + "natural environment sounds, warm and inviting"; + } + } + + return String.format( + // СЦЕНА + "Vertical 9:16 cinematic video for %s business in %s, Kazakhstan. " + + "Business niche: %s. Brand: %s. " + + "Scene: %s. " + + "Visual concept: %s. " + + // ЛЮДИ + "People: Kazakh appearance, natural authentic look, " + + "real emotions, perfect anatomy, photorealistic. " + + // СТИЛЬ + "Style: %s. " + + "Quality: 8K resolution, professional lighting, sharp focus, " + + "no blur, no CGI, photorealistic, commercial grade. " + + // ЗВУК — явно для Veo 3 + "Sound design: %s. " + + // ЗАПРЕТЫ — критично важны + "STRICT RULES: " + + "NO text on screen. " + + "NO subtitles. " + + "NO captions. " + + "NO watermarks. " + + "NO on-screen graphics. " + + "NO logos overlaid. " + + "NO artificial CGI look.", + platform, city, + niche, brand, + sceneType, + theme, + style, + audio + ); + } + + /** + * Определяет тип визуальной сцены по теме поста. + * Переводит суть темы в описание конкретной сцены на английском. + */ + private String resolveSceneType(String theme, String niche) { + if (theme == null) return "business activity scene in " + niche; + + String t = theme.toLowerCase(); + + // Кейсы и результаты + if (t.contains("кейс") || t.contains("результат") || t.contains("успех") || t.contains("история")) + return "happy satisfied client sharing their success story, before/after transformation"; + + // Обучение и советы + if (t.contains("совет") || t.contains("как") || t.contains("обучен") || t.contains("урок") || t.contains("разбор")) + return "expert explaining key tips and techniques in " + niche + ", educational demonstration"; + + // Процесс работы + if (t.contains("процесс") || t.contains("работа") || t.contains("как мы") || t.contains("за кадром")) + return "behind the scenes work process, professional team in action, " + niche + " workflow"; + + // Отзывы + if (t.contains("отзыв") || t.contains("клиент") || t.contains("мнение")) + return "real client testimonial, satisfied customer talking about their experience"; + + // Продажи / акции + if (t.contains("акци") || t.contains("скидк") || t.contains("предложен") || t.contains("оффер")) + return "attractive product/service showcase with special offer highlight, " + niche; + + // Демонстрация продукта + if (t.contains("продукт") || t.contains("услуг") || t.contains("обзор") || t.contains("демонстрац")) + return "detailed product/service demonstration, close-up shots, " + niche; + + // Команда + if (t.contains("команда") || t.contains("специалист") || t.contains("эксперт")) + return "professional team introduction, confident experts in " + niche + " environment"; + + // Вопрос-ответ / вовлечение + if (t.contains("вопрос") || t.contains("голосован") || t.contains("опрос")) + return "engaging interactive moment, person asking audience a relatable question about " + niche; + + // По умолчанию — общая сцена из ниши + return "authentic lifestyle scene showing " + niche + " activity, " + + "real people benefiting from the service/product"; + } /** * Генерация изображений. * КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: промпт строится из темы И текста поста — картинка строго в тему.