Files
press/ai_core/image_generator.py
T
apuc c6048aeea0 feat: AI Press Secretary - content generation and publishing platform
Features:
- AI-powered post generation from brand profiles
- Image generation with templates
- Content planning
- Telegram publishing
- Social profile analyzer
- Community organization support
- Markdown to HTML conversion for Telegram
- Inline AI chat for post editing

Deploy:
- Add deployment scripts for Linux servers
- Add PowerShell deploy script for Windows
- Add Nginx and SSL setup scripts
- Add backup and restore scripts
2026-08-27 15:24:15 +03:00

357 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import base64
import struct
import requests
from config import PIAPI_API_KEY, PIAPI_IMAGE_MODEL, get_proxies
PIAPI_IMAGE_URL = "https://api.piapi.ai/v1/images/generations"
# Размеры по моделям
# gpt-image-1: 1024x1024, 1536x1024, 1024x1536
# dall-e-3: 1024x1024, 1792x1024, 1024x1792
_MODEL_SIZES = {
"gpt-image-1": {
"square": ("1024x1024", "Квадрат 1024×1024"),
"wide": ("1536x1024", "Горизонтальный 1536×1024"),
"tall": ("1024x1536", "Вертикальный 1024×1536"),
},
"dall-e-3": {
"square": ("1024x1024", "Квадрат 1024×1024"),
"wide": ("1792x1024", "Горизонтальный 1792×1024"),
"tall": ("1024x1792", "Вертикальный 1024×1792"),
},
}
# Размеры по умолчанию (для неизвестных моделей — как у gpt-image-1)
DEFAULT_SIZES = {
"square": ("1024x1024", "Квадрат 1024×1024"),
"wide": ("1536x1024", "Горизонтальный 1536×1024"),
"tall": ("1024x1536", "Вертикальный 1024×1536"),
}
# Обратная совместимость: IMAGE_SIZES для шаблонов
IMAGE_SIZES = DEFAULT_SIZES
# Пресеты стилей - дописываются к промпту
STYLE_PRESETS = {
"none": ("Без стиля", ""),
"photo": ("Фотореализм", ", ultra realistic photo, professional photography, natural lighting"),
"illustration": ("Иллюстрация", ", digital illustration, vibrant colors, detailed"),
"minimal": ("Минимализм", ", minimalist style, clean background, simple composition"),
"3d": ("3D-рендер", ", 3D render, soft shadows, modern design"),
"flat": ("Плоская графика", ", flat vector illustration, bold shapes, social media style"),
}
# Шаблоны изображений — готовые пресеты для быстрой генерации
# prompt — для свободной генерации (подставляется в textarea)
# prompt_template — для генерации к посту ({post_text} заменяется на текст поста)
IMAGE_TEMPLATES = [
{
"key": "product",
"name": "Товар",
"icon": "🛍️",
"prompt": "Professional product photo on a clean studio background, soft gradient lighting, high-end commercial photography",
"prompt_template": "Professional product photo on a clean studio background, soft gradient lighting, high-end commercial photography. Product related to: {post_text}",
"style": "photo",
"size": "square",
"hint": "Фото товара на чистом фоне — для карточек и каталогов",
},
{
"key": "lifestyle",
"name": "Лайфстайл",
"icon": "📸",
"prompt": "Natural lifestyle scene, warm tones, candid feel, natural light, everyday setting",
"prompt_template": "Natural lifestyle scene related to: {post_text}. A person in a cozy everyday setting, warm tones, candid feel, natural light",
"style": "photo",
"size": "wide",
"hint": "Живая сцена из жизни — для постов и сторис",
},
{
"key": "food",
"name": "Еда",
"icon": "🍕",
"prompt": "Beautiful food photography, overhead flat lay or 45-degree angle, garnished plate, rustic wooden table, soft natural light, appetizing colors",
"prompt_template": "Beautiful food photography of {post_text}, overhead flat lay or 45-degree angle, garnished plate, rustic wooden table, soft natural light, appetizing colors",
"style": "photo",
"size": "square",
"hint": "Вкусная еда — для кафе, ресторанов, рецептов",
},
{
"key": "business",
"name": "Бизнес",
"icon": "🏢",
"prompt": "Professional business setting, modern office or meeting room, people in smart casual clothing, clean and bright atmosphere",
"prompt_template": "Professional business setting illustrating: {post_text}. Modern office, people in smart casual, clean bright atmosphere",
"style": "photo",
"size": "wide",
"hint": "Деловая атмосфера — для LinkedIn и презентаций",
},
{
"key": "abstract",
"name": "Абстрактный фон",
"icon": "🎨",
"prompt": "Abstract gradient background with flowing organic shapes, vibrant modern color palette, smooth transitions, depth effect",
"prompt_template": "Abstract gradient background with flowing organic shapes and stylized text overlay reading: {post_text}. Vibrant modern color palette, smooth transitions, depth effect",
"style": "none",
"size": "square",
"hint": "Красивый фон — для цитат, анонсов, обложек",
},
{
"key": "portrait",
"name": "Портрет",
"icon": "👤",
"prompt": "Professional portrait photo, soft studio lighting, blurred background, sharp focus on face, natural skin tones, confident expression",
"prompt_template": "Professional portrait photo of a person who {post_text}. Soft studio lighting, blurred background, sharp focus, natural skin tones, confident expression",
"style": "photo",
"size": "square",
"hint": "Портрет или аватар — для профилей и «О нас»",
},
{
"key": "infographic",
"name": "Инфографика",
"icon": "📊",
"prompt": "Clean infographic layout with icons, charts, and data visualization on white background, modern flat design, bold readable typography",
"prompt_template": "Clean infographic design with the following text and data clearly rendered on it: {post_text}. White background, modern flat design, icons and charts, bold readable typography — the text from the post MUST be visible and legible on the image",
"style": "flat",
"size": "tall",
"hint": "Визуализация данных — для статистик и гайдов",
},
{
"key": "banner",
"name": "Рекламный баннер",
"icon": "🎯",
"prompt": "Eye-catching advertising banner with bold typography, vibrant colors, modern promotional design, call to action",
"prompt_template": "Eye-catching advertising banner with the following text prominently displayed: {post_text}. Vibrant colors, modern promotional design, the text must be the main focus of the image, bold readable typography",
"style": "none",
"size": "wide",
"hint": "Баннер для рекламы — с текстом поста",
},
{
"key": "nature",
"name": "Природа",
"icon": "🌄",
"prompt": "Breathtaking nature landscape, golden hour lighting, vivid colors, dramatic sky, professional landscape photography",
"prompt_template": "Breathtaking nature landscape illustrating the theme: {post_text}. Golden hour lighting, vivid colors, dramatic sky, professional landscape photography",
"style": "photo",
"size": "wide",
"hint": "Пейзажи и природа — для мотивации и фона",
},
{
"key": "minimal",
"name": "Минимализм",
"icon": "✨",
"prompt": "Minimalist composition, single focal point, lots of white space, clean geometric shapes, soft muted colors, elegant simplicity",
"prompt_template": "Minimalist design with the following text as the centerpiece: {post_text}. Clean white background, elegant typography, lots of breathing space, soft muted accent color",
"style": "minimal",
"size": "square",
"hint": "Чистый минимализм — для премиальных брендов",
},
{
"key": "story",
"name": "Сторис",
"icon": "📱",
"prompt": "Vertical social media story, gradient background, centered composition, modern aesthetic, bold colors",
"prompt_template": "Vertical social media story with the following text as the main element: {post_text}. Gradient background, centered composition, modern aesthetic, bold colors, the text must be clearly readable",
"style": "none",
"size": "tall",
"hint": "Вертикальный формат — для Instagram/TikTok сторис",
},
{
"key": "before_after",
"name": "До / После",
"icon": "🔄",
"prompt": "Split composition before/after comparison, left side 'before', right side 'after', clean dividing line, matching lighting",
"prompt_template": "Split composition before/after comparison related to: {post_text}. Left side shows 'before' state, right side shows 'after' transformation, clean dividing line, matching lighting",
"style": "photo",
"size": "wide",
"hint": "Сравнение до/после — для кейсов и результатов",
},
]
def detect_ext(data: bytes) -> str:
"""Определяет расширение файла по magic-байтам."""
if data[:8] == b"\x89PNG\r\n\x1a\n":
return ".png"
if data[:2] == b"\xff\xd8":
return ".jpg"
if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
return ".webp"
if data[:4] == b"GIF8":
return ".gif"
return ".png"
def get_sizes_for_model(model: str) -> dict:
"""Возвращает доступные размеры для конкретной модели."""
if not model:
return DEFAULT_SIZES
# Точное совпадение
if model in _MODEL_SIZES:
return _MODEL_SIZES[model]
# Частичное совпадение (напр. openai/dall-e-3)
for key, sizes in _MODEL_SIZES.items():
if key in model:
return sizes
return DEFAULT_SIZES
class ImageGenerator:
"""Генерация изображений через OpenAI-совместимый /images/generations."""
def __init__(self, model=None, provider_settings=None):
"""provider_settings — опциональный dict:
api_key, base_url, image_model
Если не передан — используются глобальные настройки PiAPI."""
if provider_settings and provider_settings.get("api_key"):
self.api_key = provider_settings["api_key"]
base = provider_settings.get("base_url") or "https://api.piapi.ai/v1"
self.image_url = base.rstrip("/") + "/images/generations"
self.model = model or provider_settings.get("image_model") or PIAPI_IMAGE_MODEL
self.provider_type = provider_settings.get("provider_type", "custom")
# Прокси для всех провайдеров (доступ из России к зарубежным API)
self.proxies = get_proxies()
else:
self.api_key = PIAPI_API_KEY
self.image_url = PIAPI_IMAGE_URL
self.model = model or PIAPI_IMAGE_MODEL
self.provider_type = "piapi"
self.proxies = get_proxies()
def generate(self, prompt: str, size: str = "square") -> bytes:
"""Генерирует картинку и возвращает её байты. Поднимает исключение при ошибке."""
sizes = get_sizes_for_model(self.model)
if size not in sizes:
size = "square"
size_value = sizes[size][0]
# DALL-E 3 больше не поддерживается OpenAI — используем gpt-image-1
# response_format и quality не передаём (gpt-image-1 возвращает b64_json по умолчанию)
response = requests.post(
self.image_url,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
},
json={
"model": self.model,
"prompt": prompt,
"n": 1,
"size": size_value,
},
proxies=self.proxies,
timeout=240,
)
response.raise_for_status()
data = response.json().get("data") or []
if not data:
raise RuntimeError("Пустой ответ от API картинок")
item = data[0]
if item.get("b64_json"):
return base64.b64decode(item["b64_json"])
if item.get("url"):
img_response = requests.get(item["url"], proxies=self.proxies, timeout=120)
img_response.raise_for_status()
return img_response.content
raise RuntimeError("Неожиданный формат ответа от API картинок")
def build_prompt(self, prompt: str, style: str = "none") -> str:
"""Дополняет промпт пресетом стиля."""
if style in STYLE_PRESETS:
return prompt.strip() + STYLE_PRESETS[style][1]
return prompt.strip()
def generate_with_reference(self, prompt: str, reference_image_bytes: bytes, size: str = "square", chat_settings=None) -> bytes:
"""Генерирует изображение на основе промпта + исходного изображения.
Пробует: 1) text-провайдер chat/completions, 2) image-провайдер chat/completions, 3) fallback без картинки.
"""
sizes = get_sizes_for_model(self.model)
if size not in sizes:
size = "square"
size_value = sizes[size][0]
# Кодируем исходное изображение в base64
img_b64 = base64.b64encode(reference_image_bytes).decode("utf-8")
mime = "image/png"
if reference_image_bytes[:2] == b"\xff\xd8":
mime = "image/jpeg"
elif reference_image_bytes[:4] == b"RIFF":
mime = "image/webp"
# Список провайдеров для попытки chat/completions с image input
candidates = []
if chat_settings and chat_settings.get("api_key"):
candidates.append((chat_settings["api_key"], chat_settings.get("base_url", "https://api.openai.com/v1"), "text"))
# image-провайдер тоже может уметь chat/completions (PiAPI/OpenAI)
if self.api_key and (not chat_settings or self.api_key != chat_settings.get("api_key")):
candidates.append((self.api_key, self.image_url.rsplit("/images/generations", 1)[0], "image"))
# Fallback: default OpenAI
if not candidates:
candidates.append((self.api_key, "https://api.openai.com/v1", "default"))
last_error = None
for api_key, base_url, label in candidates:
chat_url = base_url.rstrip("/") + "/chat/completions"
print(f"[ImageGenerator] reference image -> {label} provider: {chat_url}")
try:
response = requests.post(
chat_url,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
},
json={
"model": self.model,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {"url": f"data:{mime};base64,{img_b64}"}
}
]
}
],
"max_tokens": 4096,
},
proxies=self.proxies,
timeout=240,
)
if response.status_code >= 400:
last_error = f"{response.status_code} {response.reason}"
print(f"[ImageGenerator] {label} провайдер вернул {last_error}, пробуем следующий")
continue
resp_json = response.json()
msg = resp_json.get("choices", [{}])[0].get("message", {})
content = msg.get("content", "")
# Парсим картинку из ответа
if isinstance(content, list):
for part in content:
if isinstance(part, dict) and part.get("type") == "image_url":
url = part.get("image_url", {}).get("url", "")
if url.startswith("data:"):
return base64.b64decode(url.split(",", 1)[1])
if isinstance(content, str) and len(content) > 1000:
try:
return base64.b64decode(content)
except Exception:
pass
print(f"[ImageGenerator] {label} провайдер не вернул картинку")
last_error = "no image in response"
except Exception as e:
last_error = str(e)
print(f"[ImageGenerator] {label} провайдер ошибка: {last_error}")
# Все провайдеры не сработали — fallback без картинки
print(f"[ImageGenerator] ни один провайдер не поддержал image input, генерируем без референса")
return self.generate(prompt, size)