diff --git a/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java b/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java index 1765ec6..214ac8e 100644 --- a/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java @@ -1,86 +1,128 @@ package kz.konturai.parser.service; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import com.google.auth.oauth2.GoogleCredentials; +import io.netty.resolver.DefaultAddressResolverGroup; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.netty.http.client.HttpClient; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.time.Duration; +import java.util.*; @Service -@RequiredArgsConstructor -@Slf4j public class GeminiVideoGenerationService { - private final RestTemplate restTemplate = new RestTemplate(); - private final ObjectMapper objectMapper = new ObjectMapper(); + private static final String VERTEX_API_TEMPLATE = "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:predict"; + private static final String CREDENTIALS_FILE_PATH = "keys/google-key.json"; - @Value("${gemini.veo.api.url:https://generativelanguage.googleapis.com/v1beta/models/veo:generateVideo}") - private String videoApiUrl; + private final WebClient webClient; - @Value("${gemini.veo.api.key:YOUR_API_KEY}") - private String apiKey; + @Value("${google.cloud.project-id}") + private String projectId; + + @Value("${google.cloud.location:us-central1}") + private String location; + + @Value("${google.gemini.video.model:veo-2.0-generate-001}") + private String model; + + public GeminiVideoGenerationService() { + HttpClient httpClient = HttpClient.create() + .resolver(DefaultAddressResolverGroup.INSTANCE) + .responseTimeout(Duration.ofMillis(300000)); + + ExchangeStrategies strategies = ExchangeStrategies.builder() + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(100 * 1024 * 1024)) + .build(); + + this.webClient = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .exchangeStrategies(strategies) + .build(); + } public byte[] generateVideo(String prompt) { + if (projectId == null || projectId.isEmpty()) { + return null; + } + try { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("x-goog-api-key", apiKey); - - Map requestBody = new HashMap<>(); - requestBody.put("prompt", prompt); - requestBody.put("aspectRatio", "9:16"); - requestBody.put("resolution", "1080p"); - - HttpEntity> entity = new HttpEntity<>(requestBody, headers); - - ResponseEntity response = restTemplate.exchange( - videoApiUrl, - HttpMethod.POST, - entity, - String.class - ); - - if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { - String jsonBody = response.getBody(); - JsonNode rootNode = objectMapper.readTree(jsonBody); - String base64Video = null; - - if (rootNode.has("predictions") && rootNode.get("predictions").isArray()) { - JsonNode prediction = rootNode.get("predictions").get(0); - if (prediction.has("bytesBase64Encoded")) { - base64Video = prediction.get("bytesBase64Encoded").asText(); - } - } else if (rootNode.has("candidates") && rootNode.get("candidates").isArray()) { - JsonNode parts = rootNode.at("/candidates/0/content/parts"); - if (parts.isArray() && parts.size() > 0 && parts.get(0).has("inlineData")) { - base64Video = parts.get(0).at("/inlineData/data").asText(); - } - } else if (rootNode.has("videoBase64")) { - base64Video = rootNode.get("videoBase64").asText(); - } else if (rootNode.has("base64")) { - base64Video = rootNode.get("base64").asText(); - } - - if (base64Video != null && !base64Video.isEmpty()) { - return Base64.getDecoder().decode(base64Video); - } else { - if (!jsonBody.trim().startsWith("{")) { - return jsonBody.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); - } - return null; - } - } else { + String accessToken = getAccessTokenFromResources(); + if (accessToken == null) { return null; } + + String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model); + + Map requestBody = new HashMap<>(); + List> instances = new ArrayList<>(); + Map instance = new HashMap<>(); + instance.put("prompt", prompt); + instances.add(instance); + requestBody.put("instances", instances); + + Map parameters = new HashMap<>(); + parameters.put("aspectRatio", "9:16"); + requestBody.put("parameters", parameters); + + Map response = webClient.post() + .uri(URI.create(endpointUrl)) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .retryWhen(reactor.util.retry.Retry.backoff(3, Duration.ofSeconds(15)) + .filter(t -> t instanceof WebClientResponseException && + ((WebClientResponseException) t).getStatusCode().value() == 429)) + .block(Duration.ofMillis(300000)); + + return extractVideoFromResponse(response); + } catch (Exception e) { return null; } } + + private String getAccessTokenFromResources() { + try { + ClassPathResource resource = new ClassPathResource(CREDENTIALS_FILE_PATH); + if (!resource.exists()) return null; + try (InputStream is = resource.getInputStream()) { + GoogleCredentials credentials = GoogleCredentials.fromStream(is) + .createScoped("https://www.googleapis.com/auth/cloud-platform"); + credentials.refreshIfExpired(); + return credentials.getAccessToken().getTokenValue(); + } + } catch (IOException e) { + return null; + } + } + + @SuppressWarnings("unchecked") + private byte[] extractVideoFromResponse(Map response) { + if (response == null) return null; + try { + List> predictions = (List>) response.get("predictions"); + if (predictions != null && !predictions.isEmpty()) { + Map firstPrediction = predictions.get(0); + String base64Video = (String) firstPrediction.get("bytesBase64Encoded"); + if (base64Video != null) { + return Base64.getDecoder().decode(base64Video); + } + } + } catch (Exception e) { + } + return null; + } } \ No newline at end of file diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java index df33879..28316b2 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java @@ -42,7 +42,7 @@ public class MarketingStrategyV3Service { @Value("${openai.model.name.text:gpt-4o}") private String highIntelligenceModel; - @Value("${image.generation.delayBetweenRequestsMs:5000}") // Увеличили задержку до 5 сек + @Value("${image.generation.delayBetweenRequestsMs:5000}") private long delayBetweenRequestsMs; public Optional getStrategyById(String id) { @@ -204,8 +204,6 @@ public class MarketingStrategyV3Service { } MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex); - - // Убеждаемся, что тип контента фото, если регенерируем фото item.setContentType("фото"); Optional analysisOpt = analysisRepository.findById(strategy.getAnalysisId()); @@ -237,7 +235,6 @@ public class MarketingStrategyV3Service { return item; } - // НОВЫЙ МЕТОД ДЛЯ РЕГЕНЕРАЦИИ ВИДЕО public MarketingStrategy.PostCalendarItem regeneratePostVideo(String strategyId, int postIndex) { Optional optStrategy = repository.findById(strategyId); if (optStrategy.isEmpty()) { @@ -252,8 +249,6 @@ public class MarketingStrategyV3Service { } MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex); - - // Меняем тип контента на видео item.setContentType("видео"); Optional analysisOpt = analysisRepository.findById(strategy.getAnalysisId()); @@ -266,7 +261,6 @@ public class MarketingStrategyV3Service { doGenerateVideo(item, businessContext); - // Если видео не сгенерировалось (фоллбек на фото), сохраняем изменения repository.save(strategy); return item; } @@ -403,7 +397,7 @@ public class MarketingStrategyV3Service { try { if (contentType.contains("видео") || contentType.contains("reels") || contentType.contains("tiktok")) { - doGenerateVideoWithFallback(item, businessContext, brandName, currentRefBytes); + doGenerateVideo(item, businessContext); } else { doGenerateImage(item, businessContext, brandName, currentRefBytes); } @@ -416,30 +410,6 @@ public class MarketingStrategyV3Service { } } - // Выделенный метод для генерации видео с фоллбеком - private void doGenerateVideoWithFallback(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName, byte[] currentRefBytes) { - String videoPrompt = "High quality cinematic commercial video. Business niche: " + businessContext + ". Scene: " + item.getTheme() + ". Photorealistic, dynamic motion, 4k."; - - byte[] videoBytes = null; - try { - videoBytes = geminiVideoService.generateVideo(videoPrompt); - } catch (Exception e) { - log.warn("Video service failed: {}", e.getMessage()); - } - - 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 { - log.warn("Fallback: Video generation failed. Switching to Image generation for theme: {}", item.getTheme()); - item.setContentType("фото"); - doGenerateImage(item, businessContext, brandName, currentRefBytes); - } - } - - // Выделенный метод генерации видео БЕЗ фоллбека (для ручной регенерации) private void doGenerateVideo(MarketingStrategy.PostCalendarItem item, String businessContext) { String videoPrompt = "High quality cinematic commercial video. Business niche: " + businessContext + ". Scene: " + item.getTheme() + ". Photorealistic, dynamic motion, 4k."; try { @@ -449,14 +419,17 @@ public class MarketingStrategyV3Service { minIOService.uploadFile(filename, videoBytes, "video/mp4"); item.setVideoUrl(filename); item.setVideoFilename(filename); - // Очищаем данные фото, так как теперь это видео item.setImageUrl(null); item.setImageFilename(null); } else { - log.error("Failed to regenerate video, returned null bytes"); + log.error("Video generation returned null for theme: {}", item.getTheme()); + item.setVideoFilename(null); + item.setVideoUrl(null); } } catch (Exception e) { - log.error("Error regenerating video: {}", e.getMessage()); + log.error("Error generating video: {}", e.getMessage()); + item.setVideoFilename(null); + item.setVideoUrl(null); } } @@ -469,12 +442,17 @@ public class MarketingStrategyV3Service { minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG_VALUE); item.setImageUrl(filename); item.setImageFilename(filename); - // Очищаем данные видео, так как теперь это фото item.setVideoUrl(null); item.setVideoFilename(null); + } else { + log.error("Image generation returned null for theme: {}", item.getTheme()); + item.setImageFilename(null); + item.setImageUrl(null); } } catch (Exception e) { - log.error("Image generation failed: {}", e.getMessage()); + log.error("Image generation crashed: {}", e.getMessage()); + item.setImageFilename(null); + item.setImageUrl(null); } } diff --git a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java index 2ac6dc7..bee5a6b 100644 --- a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java @@ -46,10 +46,10 @@ public class NanoBananaImageGenerationService implements ImageGenerationService @Value("${google.gemini.timeoutMs:120000}") private long timeoutMs; - @Value("${google.gemini.retry.maxAttempts:3}") + @Value("${google.gemini.retry.maxAttempts:5}") private int maxRetryAttempts; - @Value("${google.gemini.rateLimit.delayMs:12000}") + @Value("${google.gemini.rateLimit.delayMs:15000}") private long rateLimitDelayMs; public NanoBananaImageGenerationService() { @@ -79,14 +79,12 @@ public class NanoBananaImageGenerationService implements ImageGenerationService try { return generateImageWithReference(prompt, referenceLogo.getBytes()); } catch (IOException e) { - logger.error("Error reading MultipartFile logo: {}", e.getMessage()); return generateImage(prompt); } } public byte[] generateImageWithReference(String prompt, byte[] referenceImageBytes) { if (projectId == null || projectId.trim().isEmpty()) { - logger.error("Project ID is missing! Check application.properties"); return null; } @@ -104,7 +102,7 @@ public class NanoBananaImageGenerationService implements ImageGenerationService } String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model); - String finalPrompt = sanitizePromptForText(prompt); + String finalPrompt = prompt != null ? prompt : "High quality image"; try { Map requestBody = buildImagenRequestBody(finalPrompt, referenceImageBytes); @@ -113,18 +111,17 @@ public class NanoBananaImageGenerationService implements ImageGenerationService return extractImageFromImagenResponse(response); } catch (WebClientResponseException.BadRequest badRequestEx) { if (referenceImageBytes != null && referenceImageBytes.length > 0) { - logger.warn("Vertex AI rejected the request with reference image (400 Bad Request). Retrying WITHOUT reference image..."); Map fallbackBody = buildImagenRequestBody(finalPrompt, null); Map fallbackResponse = executeVertexRequest(endpointUrl, accessToken, fallbackBody); lastRequestTimestamp = System.currentTimeMillis(); return extractImageFromImagenResponse(fallbackResponse); - } else { - throw badRequestEx; } + return null; + } catch (Exception ex) { + return null; } } catch (Exception e) { - logger.error("Fatal error in NanoBanana (Vertex AI): {}", e.getMessage(), e); return null; } finally { semaphore.release(); @@ -132,15 +129,27 @@ public class NanoBananaImageGenerationService implements ImageGenerationService } private Map executeVertexRequest(String endpointUrl, String accessToken, Map requestBody) { - return webClient.post() - .uri(URI.create(endpointUrl)) - .header("Authorization", "Bearer " + accessToken) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(requestBody) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() {}) - .retryWhen(createRetrySpecFor429()) - .block(Duration.ofMillis(timeoutMs)); + try { + return webClient.post() + .uri(URI.create(endpointUrl)) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .retryWhen(reactor.util.retry.Retry.backoff(maxRetryAttempts, Duration.ofSeconds(10)) + .maxBackoff(Duration.ofSeconds(60)) + .filter(t -> t instanceof WebClientResponseException && + ((WebClientResponseException) t).getStatusCode().value() == 429)) + .block(Duration.ofMillis(timeoutMs)); + } catch (WebClientResponseException e) { + if (e.getStatusCode().value() == 400) { + throw e; + } + return null; + } catch (Exception e) { + return null; + } } private String getAccessTokenFromResources() { @@ -149,7 +158,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService if (!resource.exists()) { return null; } - try (InputStream is = resource.getInputStream()) { GoogleCredentials credentials = GoogleCredentials.fromStream(is) .createScoped("https://www.googleapis.com/auth/cloud-platform"); @@ -161,11 +169,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService } } - private String sanitizePromptForText(String originalPrompt) { - if (originalPrompt == null) return "High quality, photorealistic image"; - return originalPrompt; // Убрали лишнюю очистку, которая могла ломать логику ИИ - } - private Map buildImagenRequestBody(String prompt, byte[] referenceImageBytes) { Map body = new HashMap<>(); List> instances = new ArrayList<>(); @@ -175,22 +178,18 @@ public class NanoBananaImageGenerationService implements ImageGenerationService if (referenceImageBytes != null && referenceImageBytes.length > 0) { String base64Image = Base64.getEncoder().encodeToString(referenceImageBytes); - Map referenceImageParams = new HashMap<>(); - Map imageBytesMap = new HashMap<>(); - imageBytesMap.put("bytesBase64Encoded", base64Image); - // Убрали mimeType, чтобы избежать 400 Bad Request из-за несоответствия типов + imageBytesMap.put("bytesBase64Encoded", base64Image); referenceImageParams.put("referenceImage", imageBytesMap); - // Возвращаем STYLE, так как он меньше ломает геометрию - referenceImageParams.put("referenceType", "STYLE"); + referenceImageParams.put("referenceType", "SUBJECT"); List> refImagesList = new ArrayList<>(); refImagesList.add(referenceImageParams); instance.put("referenceImages", refImagesList); - instance.put("prompt", prompt + ". Use the provided reference image."); + instance.put("prompt", prompt + ". Naturally integrate the provided reference subject into the scene."); } instances.add(instance); @@ -201,8 +200,7 @@ public class NanoBananaImageGenerationService implements ImageGenerationService parameters.put("aspectRatio", "1:1"); parameters.put("safetyFilterLevel", "block_some"); parameters.put("personGeneration", "allow_adult"); - // Убрали text из негативного промпта - parameters.put("negativePrompt", "nsfw, nudity, violence, deformed, ugly, blurry, distorted"); + parameters.put("negativePrompt", "text, typography, watermark, signature, blurry, distorted, bad anatomy"); body.put("parameters", parameters); return body; @@ -224,15 +222,7 @@ public class NanoBananaImageGenerationService implements ImageGenerationService } } } catch (Exception e) { - logger.error("Parsing error: {}", e.getMessage()); } return null; } - - private reactor.util.retry.RetryBackoffSpec createRetrySpecFor429() { - return reactor.util.retry.Retry.backoff(maxRetryAttempts, Duration.ofSeconds(5)) - .maxBackoff(Duration.ofSeconds(30)) - .filter(t -> t instanceof WebClientResponseException && - ((WebClientResponseException) t).getStatusCode().value() == 429); - } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 416f501..69d8a28 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,17 +3,15 @@ ollama.model=gemma3:1b ollama.timeoutMs=18000000 ollama.connectTimeoutMs=10000000 ollama.responseTimeoutMs=18000000 -# MinIO configuration + minio.endpoint=http://92.38.48.166:9000 minio.access-key=z9HhZTjzhVWqw3D20u7q minio.secret-key=lhhHtJow1b10z6EZRMTQIU1tsKkoaxCVpSypCrcb minio.bucket-name=konturai -# Headless mode for chart generation in Docker java.awt.headless=true spring.application.name=parser -# MongoDB Configuration spring.data.mongodb.host=92.38.48.166 spring.data.mongodb.port=27017 spring.data.mongodb.database=parser_db @@ -21,24 +19,20 @@ spring.data.mongodb.username=konturai spring.data.mongodb.password=konturai2024 spring.data.mongodb.authentication-database=admin -# RSS Feed URLs rss.feed.url=https://kursiv.media/feed/ rss.kapital.url=https://kapital.kz/rss/ rss.lsm.url=https://lsm.kz/rss rss.rbc.url=https://static.feed.rbc.ru/rbc/logical/footer/news.rss rss.vedomosti.url=https://www.vedomosti.ru/rss/rubric/technology/internet -# Scheduler Configuration parser.scheduler.enabled=false spring.task.scheduling.pool.size=5 spring.task.scheduling.thread-name-prefix=scheduled-task- -# MongoDB Connection Settings spring.data.mongodb.connection-timeout=30000 spring.data.mongodb.socket-timeout=30000 spring.data.mongodb.server-selection-timeout=30000 -# Logging Configuration logging.level.kz.konturai.parser.service.KursivParserService=INFO logging.level.kz.konturai.parser.service.KapitalParserService=INFO logging.level.kz.konturai.parser.service.LsmParserService=INFO @@ -53,7 +47,6 @@ logging.level.kz.konturai.parser.service.MarketingAnalysisService=DEBUG logging.level.kz.konturai.parser.config.MongoConfig=DEBUG logging.level.org.springframework.data.mongodb.core.convert=DEBUG -# OpenAI Configuration openai.api.key=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 openai.api.url=https://api.openai.com/v1/chat/completions openai.model.name=gpt-4o-mini @@ -69,7 +62,6 @@ openai.retry.rateLimitInitialDelayMs=5000 openai.retry.rateLimitMaxDelayMs=300000 openai.rateLimit.maxConcurrentRequests=3 -# Serper.dev Web Search Configuration serper.api.key=5ae7ad0baf7dfacb8bcd5fc8dca01f61b3207e75 serper.api.url=https://google.serper.dev serper.timeoutMs=30000 @@ -78,27 +70,25 @@ serper.retry.initialDelayMs=1000 serper.retry.maxDelayMs=10000 serper.retry.multiplier=2.0 -# OpenAI DALL-E Image Generation Configuration openai.image.model=dall-e-3 openai.image.size=1024x1024 openai.image.quality=hd openai.image.style=natural -# Image Generation Provider Selection image.generation.provider=nano-banana image.generation.delayBetweenRequestsMs=3000 google.cloud.project-id=data-totality-483616-t5 google.cloud.location=us-central1 google.gemini.image.model=imagen-3.0-generate-001 +google.gemini.video.model=veo-2.0-generate-001 google.gemini.timeoutMs=120000 -google.gemini.retry.maxAttempts=3 +google.gemini.retry.maxAttempts=5 google.gemini.retry.initialDelayMs=10000 google.gemini.retry.maxDelayMs=120000 google.gemini.retry.multiplier=2.0 -google.gemini.rateLimit.delayMs=12000 +google.gemini.rateLimit.delayMs=20000 -# Email Configuration spring.mail.host=smtp.gmail.com spring.mail.port=587 spring.mail.username=your-email@gmail.com @@ -107,29 +97,17 @@ spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true -# Deep Research API Configuration deep-research.api.url=http://185.35.223.45:3051 deep-research.api.timeout=1800000 -# JWT Configuration security.jwt.secret-base64=ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl -# Encryption Configuration encryption.secret-key=ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl -# Posting Scheduler Configuration posting.scheduler.enabled=true -# Telegram API Configuration telegram.api.timeout=30000 -# ========================================== -# FACEBOOK MESSENGER CONFIGURATION -# ========================================== facebook.api.timeout=30000 - facebook.verify.token=konturAI - -# ???? ?????? ??????? ????? ???????? (Page Access Token) -# ?????? ??? ? Meta Developers -> Messenger -> Settings -> Access Tokens (Generate Token) facebook.page.access.token=EAAaocqgT3JoBQtNeU4uqBrywSRwGmFZBQDZBnd1ynWjX070wAa09QBKebjrd9vyjAZCiJuZAZCU8266VlJYTAfwue20QNvENgM3wpjWNmCrTO0gTMAkmPZBKZBXKvZA8eCA4aZCUhmU2jsBRluYqKUyqpYV3ZCK928OVqLqCNZCUhqU6nUU0xP0DOzk90x58tmxpS2y1i7hjQZDZD \ No newline at end of file