initial commit
Build and Push Docker Images / build-and-push (push) Failing after 3m33s

This commit is contained in:
admin committed 2026-11-07 16:39:43 +03:00
commit 46af290db3
100 files changed
+16663

No files matched your search

+203
View File
@@ -0,0 +1,203 @@
import base64
import logging
import re
from pathlib import Path
from typing import Optional
import httpx
from config.settings import settings
from fastapi import HTTPException
from services.project_service import project_service
logger = logging.getLogger(__name__)
class ImageGenerator:
def __init__(self):
self.api_key = settings.OPENROUTER_API_KEY
self.base_url = getattr(
settings, "OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"
)
self.default_model = "qwen/qwen-image-2.1-turbo"
def _get_images_dir(self, project_id: str, user_id: str) -> Path:
"""Получить или создать директорию для изображений проекта"""
project_dir = project_service.get_project_dir(project_id, user_id)
images_dir = project_dir / "images"
images_dir.mkdir(exist_ok=True)
return images_dir
async def generate_image(
self,
prompt: str,
project_id: str,
user_id: str,
document_id: str,
document_type: str, # 'book' или 'chapter'
model: Optional[str] = None,
size: str = "1024x1024",
) -> dict:
"""
Генерирует изображение по промпту и сохраняет его в папку проекта.
Имя файла определяется по конвенции:
- document_type='book' -> cover.png (обложка книги)
- document_type='chapter' -> {document_id}.png (иллюстрация главы)
Возвращает информацию о сохранённом файле.
"""
if not self.api_key or self.api_key == "your_api_key_here":
raise ValueError("OpenRouter API key not configured")
model = model or self.default_model
# Генерация изображений использует OpenAI Images API endpoint,
# а не Chat Completions. См. официальную документацию модели.
payload = {
"model": model,
"prompt": prompt,
"n": 1,
"response_format": "b64_json",
}
# Мапим size в aspect_ratio для моделей, которые его поддерживают
aspect_ratio_map = {
"1024x1024": "1:1",
"1024x1792": "9:16",
"1792x1024": "16:9",
"512x512": "1:1",
}
if size in aspect_ratio_map:
payload["size"] = size
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
"HTTP-Referer": "http://localhost:3000",
"X-Title": "Writer IDE",
}
try:
async with httpx.AsyncClient(timeout=120.0) as client:
response = await client.post(
f"{self.base_url}/images/generations",
json=payload,
headers=headers,
)
response.raise_for_status()
data = response.json()
# Проверяем наличие ошибки в ответе (некоторые провайдеры
# возвращают 200 с полем error вместо HTTP error status)
if "error" in data:
error_info = data["error"]
error_msg = (
error_info.get("message", str(error_info))
if isinstance(error_info, dict)
else str(error_info)
)
raise ValueError(f"API returned error: {error_msg}")
# OpenAI Images API формат: {"data": [{"b64_json": "..."}]}
images_data = data.get("data", [])
if not images_data:
raise ValueError(
f"No image data in response. Keys: {list(data.keys())}"
)
img_data = images_data[0]
image_url = None
image_base64 = None
if isinstance(img_data, dict):
image_base64 = img_data.get("b64_json")
image_url = img_data.get("url")
elif isinstance(img_data, str):
# Некоторые провайдеры могут вернуть URL напрямую в data
image_url = img_data if img_data.startswith("http") else None
# Фоллбэк: пробуем извлечь из text content (если провайдер
# использует chat/completions формат поверх images/generations)
if not image_url and not image_base64:
content = (
data.get("choices", [{}])[0].get("message", {}).get("content", "")
)
if content:
base64_match = re.search(
r"data:image/[^;]+;base64,([A-Za-z0-9+/=]+)", content
)
if base64_match:
image_base64 = base64_match.group(1)
if not image_url and not image_base64:
logger.warning(
"Could not extract image. Response keys: %s",
list(data.keys()),
)
raise ValueError(
f"Could not extract image from response. Available keys: {list(data.keys())}"
)
# Сохраняем изображение
images_dir = self._get_images_dir(project_id, user_id)
# Определяем имя файла по конвенции
if document_type == "book":
filename = "cover.png" # Обложка книги
else: # chapter
filename = f"{document_id}.png" # Изображение главы
filepath = images_dir / filename
if image_url:
async with httpx.AsyncClient(timeout=60.0) as client:
img_response = await client.get(image_url)
img_response.raise_for_status()
image_bytes = img_response.content
filepath.write_bytes(image_bytes)
data_url = f"data:image/png;base64,{base64.b64encode(image_bytes).decode('ascii')}"
elif image_base64:
image_bytes = base64.b64decode(image_base64)
filepath.write_bytes(image_bytes)
# Определяем MIME тип по первым байтам изображения
mime = "image/png" # default
if image_bytes[:2] == b"\xff\xd8":
mime = "image/jpeg"
elif image_bytes[:8] == b"\x89PNG\r\n\x1a\n":
mime = "image/png"
elif image_bytes[:6] in (b"GIF87a", b"GIF89a"):
mime = "image/gif"
elif image_bytes[:4] == b"RIFF" and image_bytes[8:12] == b"WEBP":
mime = "image/webp"
data_url = f"data:{mime};base64,{image_base64}"
else:
# Должно быть обработано выше, но на всякий случай
raise ValueError("No image data available to encode")
return {
"image_id": document_id,
"filename": filename,
"path": f"/api/images/{project_id}/{filename}",
"prompt": prompt,
"model": model,
"data_url": data_url,
"document_id": document_id,
"document_type": document_type,
}
except httpx.HTTPStatusError as e:
raise HTTPException(
status_code=502,
detail=f"Image generation API error: {e.response.status_code} - {e.response.text[:200]}",
)
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="Image generation timed out")
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Image generation failed: {str(e)}"
)
# Singleton
image_generator = ImageGenerator()