exchange
This commit is contained in:
@@ -0,0 +1,524 @@
|
|||||||
|
# Документация для фронтенда: Генерация изображений для постов
|
||||||
|
|
||||||
|
## Обзор изменений
|
||||||
|
|
||||||
|
В систему добавлена автоматическая генерация изображений для постов в социальных сетях. Теперь при создании маркетинговой стратегии для каждого поста автоматически генерируется уникальное изображение с помощью OpenAI DALL-E API. Изображения сохраняются в MinIO и прикрепляются к постам при публикации в Facebook.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Новые поля в API
|
||||||
|
|
||||||
|
### 1. PostCalendarItem (Календарь постов)
|
||||||
|
|
||||||
|
В объекте `PostCalendarItem` добавлены два новых поля для работы с изображениями:
|
||||||
|
|
||||||
|
| Поле | Тип | Описание | Обязательное |
|
||||||
|
| --------------- | ------ | ------------------------------------------------------------------- | ------------ |
|
||||||
|
| `imageUrl` | string | Путь к изображению в MinIO (используется для получения изображения) | Нет |
|
||||||
|
| `imageFilename` | string | Имя файла изображения в MinIO | Нет |
|
||||||
|
|
||||||
|
**Важно:**
|
||||||
|
|
||||||
|
- Поля могут быть `null`, если генерация изображения не удалась
|
||||||
|
- В этом случае пост публикуется без изображения
|
||||||
|
- Оба поля содержат одинаковое значение (имя файла в MinIO)
|
||||||
|
|
||||||
|
### 2. PostingTask (Задачи публикации)
|
||||||
|
|
||||||
|
В объекте `PostingTask` также добавлены поля для изображений:
|
||||||
|
|
||||||
|
| Поле | Тип | Описание | Обязательное |
|
||||||
|
| --------------- | ------ | ----------------------------- | ------------ |
|
||||||
|
| `imageUrl` | string | Путь к изображению в MinIO | Нет |
|
||||||
|
| `imageFilename` | string | Имя файла изображения в MinIO | Нет |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Изменения в API эндпоинтах
|
||||||
|
|
||||||
|
### GET `/api/marketing/strategy/{strategyId}`
|
||||||
|
|
||||||
|
Ответ теперь включает поля изображений в каждом элементе календаря постов.
|
||||||
|
|
||||||
|
#### Пример ответа
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"strategyId": "507f1f77bcf86cd799439011",
|
||||||
|
"analysisId": "507f1f77bcf86cd799439012",
|
||||||
|
"status": "completed",
|
||||||
|
"strategy": {
|
||||||
|
"postCalendar": [
|
||||||
|
{
|
||||||
|
"publishDate": "2024-01-15T10:00:00",
|
||||||
|
"platform": "Facebook",
|
||||||
|
"contentType": "пост",
|
||||||
|
"theme": "Презентация нового продукта",
|
||||||
|
"postText": "Мы рады представить наш новый продукт...",
|
||||||
|
"hashtags": ["#новинка", "#продукт", "#маркетинг"],
|
||||||
|
"publishTime": "10:00",
|
||||||
|
"imageUrl": "post_image_1705312800000_1234567890.png",
|
||||||
|
"imageFilename": "post_image_1705312800000_1234567890.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET `/api/marketing/analysis/{analysisId}/strategy`
|
||||||
|
|
||||||
|
Аналогично, ответ включает поля изображений в календаре постов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Получение изображений
|
||||||
|
|
||||||
|
### Способ 1: Через MinIO API (рекомендуется)
|
||||||
|
|
||||||
|
Если у вас есть доступ к MinIO API, используйте `imageUrl` или `imageFilename` для получения изображения:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {minio-endpoint}/{bucket-name}/{imageFilename}
|
||||||
|
```
|
||||||
|
|
||||||
|
Где:
|
||||||
|
|
||||||
|
- `minio-endpoint` - адрес вашего MinIO сервера (например, `http://92.38.48.166:9000`)
|
||||||
|
- `bucket-name` - имя bucket (обычно `konturai`)
|
||||||
|
- `imageFilename` - значение из поля `imageFilename` или `imageUrl`
|
||||||
|
|
||||||
|
### Способ 2: Через бэкенд API (если реализован)
|
||||||
|
|
||||||
|
Если бэкенд предоставляет эндпоинт для получения изображений, используйте его:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/marketing/images/{imageFilename}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Примечание:** На данный момент такой эндпоинт не реализован. При необходимости его можно добавить.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Рекомендации по отображению
|
||||||
|
|
||||||
|
### 1. Проверка наличия изображения
|
||||||
|
|
||||||
|
Всегда проверяйте наличие изображения перед отображением:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Пример на JavaScript/TypeScript
|
||||||
|
const PostCard = ({ post }) => {
|
||||||
|
const hasImage = post.imageUrl && post.imageFilename;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='post-card'>
|
||||||
|
<h3>{post.theme}</h3>
|
||||||
|
<p>{post.postText}</p>
|
||||||
|
|
||||||
|
{hasImage ? (
|
||||||
|
<img
|
||||||
|
src={`${MINIO_ENDPOINT}/${BUCKET_NAME}/${post.imageFilename}`}
|
||||||
|
alt={post.theme}
|
||||||
|
onError={(e) => {
|
||||||
|
// Fallback если изображение не загрузилось
|
||||||
|
e.target.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className='no-image-placeholder'>Изображение не сгенерировано</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='hashtags'>
|
||||||
|
{post.hashtags.map((tag) => (
|
||||||
|
<span key={tag}>#{tag}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Обработка ошибок загрузки
|
||||||
|
|
||||||
|
Всегда предусматривайте fallback для случаев, когда:
|
||||||
|
|
||||||
|
- Изображение не сгенерировано (`imageUrl` = `null`)
|
||||||
|
- Изображение не найдено в MinIO
|
||||||
|
- Ошибка при загрузке изображения
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
|
const handleImageError = () => {
|
||||||
|
setImageError(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{post.imageUrl && !imageError ? (
|
||||||
|
<img
|
||||||
|
src={getImageUrl(post.imageFilename)}
|
||||||
|
alt={post.theme}
|
||||||
|
onError={handleImageError}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className='image-placeholder'>
|
||||||
|
<Icon name='image' />
|
||||||
|
<span>Изображение недоступно</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Оптимизация загрузки
|
||||||
|
|
||||||
|
Рекомендуется использовать lazy loading для изображений:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
post.imageUrl
|
||||||
|
? `${MINIO_ENDPOINT}/${BUCKET_NAME}/${post.imageFilename}`
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
alt={post.theme}
|
||||||
|
loading='lazy'
|
||||||
|
className='post-image'
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Размеры изображений
|
||||||
|
|
||||||
|
Изображения генерируются в размере **1024x1024 пикселей** (квадратные). При отображении учитывайте это при настройке CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.post-image {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 512px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### React компонент для отображения поста
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
interface PostCalendarItem {
|
||||||
|
publishDate: string;
|
||||||
|
platform: string;
|
||||||
|
contentType: string;
|
||||||
|
theme: string;
|
||||||
|
postText: string;
|
||||||
|
hashtags: string[];
|
||||||
|
publishTime: string;
|
||||||
|
imageUrl?: string | null;
|
||||||
|
imageFilename?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PostCardProps {
|
||||||
|
post: PostCalendarItem;
|
||||||
|
minioEndpoint: string;
|
||||||
|
bucketName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PostCard: React.FC<PostCardProps> = ({
|
||||||
|
post,
|
||||||
|
minioEndpoint,
|
||||||
|
bucketName,
|
||||||
|
}) => {
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
|
const getImageUrl = (): string | null => {
|
||||||
|
if (!post.imageFilename) return null;
|
||||||
|
return `${minioEndpoint}/${bucketName}/${post.imageFilename}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageUrl = getImageUrl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='post-card'>
|
||||||
|
<div className='post-header'>
|
||||||
|
<span className='platform-badge'>{post.platform}</span>
|
||||||
|
<span className='content-type'>{post.contentType}</span>
|
||||||
|
<span className='publish-time'>{post.publishTime}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className='post-theme'>{post.theme}</h3>
|
||||||
|
|
||||||
|
{imageUrl && !imageError ? (
|
||||||
|
<div className='post-image-container'>
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt={post.theme}
|
||||||
|
className='post-image'
|
||||||
|
loading='lazy'
|
||||||
|
onError={() => setImageError(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='no-image-placeholder'>
|
||||||
|
<svg width='64' height='64' viewBox='0 0 24 24' fill='none'>
|
||||||
|
<path
|
||||||
|
d='M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z'
|
||||||
|
fill='currentColor'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p>Изображение не сгенерировано</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className='post-text'>{post.postText}</p>
|
||||||
|
|
||||||
|
<div className='post-hashtags'>
|
||||||
|
{post.hashtags.map((tag, index) => (
|
||||||
|
<span key={index} className='hashtag'>
|
||||||
|
{tag.startsWith('#') ? tag : `#${tag}`}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='post-footer'>
|
||||||
|
<span className='publish-date'>
|
||||||
|
{new Date(post.publishDate).toLocaleDateString('ru-RU')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostCard;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vue компонент
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="post-card">
|
||||||
|
<div class="post-header">
|
||||||
|
<span class="platform-badge">{{ post.platform }}</span>
|
||||||
|
<span class="content-type">{{ post.contentType }}</span>
|
||||||
|
<span class="publish-time">{{ post.publishTime }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="post-theme">{{ post.theme }}</h3>
|
||||||
|
|
||||||
|
<div v-if="imageUrl && !imageError" class="post-image-container">
|
||||||
|
<img
|
||||||
|
:src="imageUrl"
|
||||||
|
:alt="post.theme"
|
||||||
|
class="post-image"
|
||||||
|
loading="lazy"
|
||||||
|
@error="imageError = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-image-placeholder">
|
||||||
|
<Icon name="image" />
|
||||||
|
<p>Изображение не сгенерировано</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="post-text">{{ post.postText }}</p>
|
||||||
|
|
||||||
|
<div class="post-hashtags">
|
||||||
|
<span v-for="(tag, index) in post.hashtags" :key="index" class="hashtag">
|
||||||
|
{{ tag.startsWith('#') ? tag : `#${tag}` }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post-footer">
|
||||||
|
<span class="publish-date">
|
||||||
|
{{ formatDate(post.publishDate) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
interface PostCalendarItem {
|
||||||
|
publishDate: string;
|
||||||
|
platform: string;
|
||||||
|
contentType: string;
|
||||||
|
theme: string;
|
||||||
|
postText: string;
|
||||||
|
hashtags: string[];
|
||||||
|
publishTime: string;
|
||||||
|
imageUrl?: string | null;
|
||||||
|
imageFilename?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
post: PostCalendarItem;
|
||||||
|
minioEndpoint: string;
|
||||||
|
bucketName: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const imageError = ref(false);
|
||||||
|
|
||||||
|
const imageUrl = computed(() => {
|
||||||
|
if (!props.post.imageFilename) return null;
|
||||||
|
return `${props.minioEndpoint}/${props.bucketName}/${props.post.imageFilename}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('ru-RU');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Важные замечания
|
||||||
|
|
||||||
|
### 1. Генерация изображений
|
||||||
|
|
||||||
|
- Изображения генерируются **автоматически** при создании стратегии
|
||||||
|
- Процесс генерации может занять время (обычно 10-30 секунд на изображение)
|
||||||
|
- Если генерация не удалась, пост все равно будет создан, но без изображения
|
||||||
|
|
||||||
|
### 2. Хранение изображений
|
||||||
|
|
||||||
|
- Все изображения хранятся в MinIO
|
||||||
|
- Формат изображений: **PNG**
|
||||||
|
- Размер изображений: **1024x1024 пикселей**
|
||||||
|
- Имя файла уникально для каждого поста
|
||||||
|
|
||||||
|
### 3. Публикация постов
|
||||||
|
|
||||||
|
- При публикации поста в Facebook изображение автоматически прикрепляется
|
||||||
|
- Если изображение отсутствует, пост публикуется только с текстом
|
||||||
|
- Это не влияет на успешность публикации
|
||||||
|
|
||||||
|
### 4. Обратная совместимость
|
||||||
|
|
||||||
|
- Старые стратегии, созданные до добавления этой функции, не будут иметь изображений
|
||||||
|
- Поля `imageUrl` и `imageFilename` будут `null` для таких постов
|
||||||
|
- Фронтенд должен корректно обрабатывать `null` значения
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
Для работы с изображениями вам понадобятся следующие настройки:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config.ts
|
||||||
|
export const MINIO_CONFIG = {
|
||||||
|
endpoint: 'http://92.38.48.166:9000', // или ваш MinIO endpoint
|
||||||
|
bucketName: 'konturai',
|
||||||
|
// Если требуется авторизация:
|
||||||
|
// accessKey: 'your-access-key',
|
||||||
|
// secretKey: 'your-secret-key',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Миграция существующего кода
|
||||||
|
|
||||||
|
Если у вас уже есть компоненты для отображения постов, обновите их следующим образом:
|
||||||
|
|
||||||
|
1. **Добавьте проверку наличия изображения:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const hasImage = post.imageUrl && post.imageFilename;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Добавьте отображение изображения:**
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
{
|
||||||
|
hasImage && <img src={getImageUrl(post)} alt={post.theme} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Обновите типы/интерфейсы:**
|
||||||
|
```typescript
|
||||||
|
interface PostCalendarItem {
|
||||||
|
// ... существующие поля
|
||||||
|
imageUrl?: string | null;
|
||||||
|
imageFilename?: string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддержка
|
||||||
|
|
||||||
|
При возникновении проблем:
|
||||||
|
|
||||||
|
1. Проверьте, что `imageUrl` и `imageFilename` не `null`
|
||||||
|
2. Убедитесь, что MinIO доступен и изображение существует
|
||||||
|
3. Проверьте консоль браузера на наличие ошибок CORS (если обращаетесь к MinIO напрямую)
|
||||||
|
4. Убедитесь, что используете правильный endpoint и bucket name
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Пример полного ответа API
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"strategyId": "507f1f77bcf86cd799439011",
|
||||||
|
"analysisId": "507f1f77bcf86cd799439012",
|
||||||
|
"status": "completed",
|
||||||
|
"createdAt": "2024-01-15T10:00:00",
|
||||||
|
"completedAt": "2024-01-15T10:05:00",
|
||||||
|
"durationWeeks": 4,
|
||||||
|
"priorityPlatforms": ["Facebook", "Instagram"],
|
||||||
|
"strategy": {
|
||||||
|
"weeklyPlans": [
|
||||||
|
{
|
||||||
|
"weekNumber": 1,
|
||||||
|
"mainThemes": ["Презентация продукта", "Преимущества"],
|
||||||
|
"contentRecommendations": "Создавайте контент...",
|
||||||
|
"priorityPlatforms": ["Facebook"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postCalendar": [
|
||||||
|
{
|
||||||
|
"publishDate": "2024-01-16T10:00:00",
|
||||||
|
"platform": "Facebook",
|
||||||
|
"contentType": "пост",
|
||||||
|
"theme": "Презентация нового продукта",
|
||||||
|
"postText": "Мы рады представить наш новый продукт, который поможет вам...",
|
||||||
|
"hashtags": ["#новинка", "#продукт", "#маркетинг"],
|
||||||
|
"publishTime": "10:00",
|
||||||
|
"imageUrl": "post_image_1705312800000_1234567890.png",
|
||||||
|
"imageFilename": "post_image_1705312800000_1234567890.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"publishDate": "2024-01-18T14:00:00",
|
||||||
|
"platform": "Instagram",
|
||||||
|
"contentType": "сторис",
|
||||||
|
"theme": "Преимущества продукта",
|
||||||
|
"postText": "Узнайте о главных преимуществах нашего продукта...",
|
||||||
|
"hashtags": ["#преимущества", "#качество"],
|
||||||
|
"publishTime": "14:00",
|
||||||
|
"imageUrl": null,
|
||||||
|
"imageFilename": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Обратите внимание, что второй пост не имеет изображения (`imageUrl` и `imageFilename` равны `null`). Это нормальная ситуация, если генерация изображения не удалась.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Дата обновления:** 2024-01-15
|
||||||
|
**Версия API:** 1.0
|
||||||
@@ -343,6 +343,8 @@ GET /api/marketing/strategy/507f1f77bcf86cd799439012
|
|||||||
| `data.strategy.postCalendar[].postText` | string | Полный текст поста (готовый к публикации) |
|
| `data.strategy.postCalendar[].postText` | string | Полный текст поста (готовый к публикации) |
|
||||||
| `data.strategy.postCalendar[].hashtags` | array | Список хештегов (массив строк) |
|
| `data.strategy.postCalendar[].hashtags` | array | Список хештегов (массив строк) |
|
||||||
| `data.strategy.postCalendar[].publishTime` | string | Время публикации в формате HH:mm |
|
| `data.strategy.postCalendar[].publishTime` | string | Время публикации в формате HH:mm |
|
||||||
|
| `data.strategy.postCalendar[].imageUrl` | string | Путь к изображению в MinIO (может быть null) |
|
||||||
|
| `data.strategy.postCalendar[].imageFilename` | string | Имя файла изображения в MinIO (может быть null) |
|
||||||
|
|
||||||
#### Пример ошибки (404 Not Found)
|
#### Пример ошибки (404 Not Found)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
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;
|
||||||
|
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.netty.http.client.HttpClient;
|
import reactor.netty.http.client.HttpClient;
|
||||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||||
@@ -58,9 +59,16 @@ public class OpenAIImageGenerationService {
|
|||||||
HttpClient httpClient = HttpClient.create()
|
HttpClient httpClient = HttpClient.create()
|
||||||
.responseTimeout(Duration.ofMillis(90000));
|
.responseTimeout(Duration.ofMillis(90000));
|
||||||
|
|
||||||
|
// Configure exchange strategies to increase buffer limit for large base64
|
||||||
|
// responses
|
||||||
|
ExchangeStrategies strategies = ExchangeStrategies.builder()
|
||||||
|
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) // 10 MB
|
||||||
|
.build();
|
||||||
|
|
||||||
this.webClient = WebClient.builder()
|
this.webClient = WebClient.builder()
|
||||||
.baseUrl(DALL_E_API_URL)
|
.baseUrl(DALL_E_API_URL)
|
||||||
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||||
|
.exchangeStrategies(strategies)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +109,8 @@ public class OpenAIImageGenerationService {
|
|||||||
requestBody.put("quality", imageQuality);
|
requestBody.put("quality", imageQuality);
|
||||||
requestBody.put("response_format", "b64_json"); // Получаем изображение в base64
|
requestBody.put("response_format", "b64_json"); // Получаем изображение в base64
|
||||||
|
|
||||||
logger.info("Requesting image generation from DALL-E with prompt: {}", prompt.substring(0, Math.min(100, prompt.length())));
|
logger.info("Requesting image generation from DALL-E with prompt: {}",
|
||||||
|
prompt.substring(0, Math.min(100, prompt.length())));
|
||||||
|
|
||||||
Map<String, Object> response = webClient.post()
|
Map<String, Object> response = webClient.post()
|
||||||
.header("Authorization", "Bearer " + apiKey)
|
.header("Authorization", "Bearer " + apiKey)
|
||||||
@@ -109,7 +118,8 @@ public class OpenAIImageGenerationService {
|
|||||||
.accept(MediaType.APPLICATION_JSON)
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(requestBody)
|
.bodyValue(requestBody)
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
|
||||||
|
})
|
||||||
.retryWhen(createRetrySpec("generateImage"))
|
.retryWhen(createRetrySpec("generateImage"))
|
||||||
.onErrorResume(err -> {
|
.onErrorResume(err -> {
|
||||||
logger.error("DALL-E API request failed after retries: {} - {}", err.getMessage(),
|
logger.error("DALL-E API request failed after retries: {} - {}", err.getMessage(),
|
||||||
@@ -117,7 +127,8 @@ public class OpenAIImageGenerationService {
|
|||||||
if (err instanceof WebClientResponseException) {
|
if (err instanceof WebClientResponseException) {
|
||||||
WebClientResponseException wcre = (WebClientResponseException) err;
|
WebClientResponseException wcre = (WebClientResponseException) err;
|
||||||
if (wcre.getStatusCode().value() == 401) {
|
if (wcre.getStatusCode().value() == 401) {
|
||||||
logger.error("OpenAI API key is invalid or expired. Please check your openai.api.key configuration.");
|
logger.error(
|
||||||
|
"OpenAI API key is invalid or expired. Please check your openai.api.key configuration.");
|
||||||
} else if (wcre.getStatusCode().value() == 429) {
|
} else if (wcre.getStatusCode().value() == 429) {
|
||||||
logger.error("OpenAI API rate limit exceeded after all retry attempts.");
|
logger.error("OpenAI API rate limit exceeded after all retry attempts.");
|
||||||
} else if (wcre.getStatusCode().value() >= 500) {
|
} else if (wcre.getStatusCode().value() >= 500) {
|
||||||
@@ -210,4 +221,3 @@ public class OpenAIImageGenerationService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user