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
This commit is contained in:
1 parent
81625b2e6f
commit
c6048aeea0
37 files changed
+7037
-225
No files matched your search
+281
-14
@@ -7,13 +7,32 @@ from config import PIAPI_API_KEY, PIAPI_IMAGE_MODEL, get_proxies
|
||||
|
||||
PIAPI_IMAGE_URL = "https://api.piapi.ai/v1/images/generations"
|
||||
|
||||
# Доступные размеры (gpt-image-1)
|
||||
IMAGE_SIZES = {
|
||||
"square": ("1024x1024", "Квадрат 1024x1024"),
|
||||
"wide": ("1536x1024", "Горизонтальный 1536x1024"),
|
||||
"tall": ("1024x1536", "Вертикальный 1024x1536"),
|
||||
# Размеры по моделям
|
||||
# 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": ("Без стиля", ""),
|
||||
@@ -24,6 +43,132 @@ STYLE_PRESETS = {
|
||||
"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-байтам."""
|
||||
@@ -38,24 +183,56 @@ def detect_ext(data: bytes) -> str:
|
||||
return ".png"
|
||||
|
||||
|
||||
class ImageGenerator:
|
||||
"""Генерация изображений через PiAPI (OpenAI-совместимый /images/generations)."""
|
||||
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
|
||||
|
||||
def __init__(self, model: str | None = None):
|
||||
self.model = model or PIAPI_IMAGE_MODEL
|
||||
self.proxies = get_proxies()
|
||||
|
||||
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:
|
||||
"""Генерирует картинку и возвращает её байты. Поднимает исключение при ошибке."""
|
||||
if size not in IMAGE_SIZES:
|
||||
sizes = get_sizes_for_model(self.model)
|
||||
if size not in sizes:
|
||||
size = "square"
|
||||
size_value = IMAGE_SIZES[size][0]
|
||||
size_value = sizes[size][0]
|
||||
|
||||
# DALL-E 3 больше не поддерживается OpenAI — используем gpt-image-1
|
||||
# response_format и quality не передаём (gpt-image-1 возвращает b64_json по умолчанию)
|
||||
response = requests.post(
|
||||
PIAPI_IMAGE_URL,
|
||||
self.image_url,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {PIAPI_API_KEY}",
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
},
|
||||
json={
|
||||
"model": self.model,
|
||||
@@ -87,3 +264,93 @@ class ImageGenerator:
|
||||
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)
|
||||
Reference in new issue
Block a user