feat: Add logging to API endpoints, update generation response model, and refine project configurations.

This commit is contained in:
xds
2026-02-05 15:29:31 +03:00
parent 9ae6e8e08e
commit 736e5a8c12
55 changed files with 244 additions and 35 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -25,6 +25,6 @@ def get_dao(mongo_client: AsyncIOMotorClient = Depends(get_mongo_client)) -> DAO
# Провайдер сервиса (собирается из DAO и Gemini)
def get_generation_service(
dao: DAO = Depends(get_dao),
gemini: GoogleAdapter = Depends(get_gemini_client)
gemini: GoogleAdapter = Depends(get_gemini_client),
) -> GenerationService:
return GenerationService(dao, gemini)

Binary file not shown.

View File

@@ -13,12 +13,16 @@ from models.Asset import Asset, AssetType
from repos.dao import DAO
from api.dependency import get_dao
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/assets", tags=["Assets"])
@router.get("/{asset_id}")
async def get_asset(asset_id: str, request: Request,dao: DAO = Depends(get_dao),) -> Response:
logger.debug(f"get_asset called for ID: {asset_id}")
asset = await dao.assets.get_asset(asset_id)
# 2. Проверка на существование
if not asset:
@@ -32,8 +36,9 @@ async def get_asset(asset_id: str, request: Request,dao: DAO = Depends(get_dao),
@router.get("")
async def get_assets(request: Request, dao: DAO = Depends(get_dao), limit: int = 10, offset: int = 0) -> AssetsResponse:
logger.info(f"get_assets called. Limit: {limit}, Offset: {offset}")
assets = await dao.assets.get_assets(limit, offset)
assets = await dao.assets.get_assets()
# assets = await dao.assets.get_assets() # This line seemed redundant/conflicting in original code
total_count = await dao.assets.get_asset_count()
return AssetsResponse(assets=assets, total_count=total_count)
@@ -46,6 +51,7 @@ async def upload_asset(
linked_char_id: Optional[str] = Form(None),
dao: DAO = Depends(get_dao),
):
logger.info(f"upload_asset called. Filename: {file.filename}, ContentType: {file.content_type}, LinkedCharId: {linked_char_id}")
if not file.content_type:
raise HTTPException(status_code=400, detail="Unknown file type")
@@ -65,6 +71,7 @@ async def upload_asset(
asset_id = await dao.assets.create_asset(asset)
asset.id = str(asset_id)
logger.info(f"Asset created successfully. ID: {asset_id}")
return AssetResponse(
id=asset.id,

View File

@@ -12,11 +12,16 @@ from models.Character import Character
from repos.dao import DAO
from api.dependency import get_dao
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/characters", tags=["Characters"])
@router.get("/", response_model=List[Character])
async def get_characters(request: Request, dao: DAO = Depends(get_dao), ) -> List[Character]:
logger.info("get_characters called")
characters = await dao.chars.get_all_characters()
return characters
@@ -24,6 +29,7 @@ async def get_characters(request: Request, dao: DAO = Depends(get_dao), ) -> Lis
@router.get("/{character_id}/assets", response_model=AssetsResponse)
async def get_character_assets(character_id: str, dao: DAO = Depends(get_dao), limit: int = 10,
offset: int = 0, ) -> AssetsResponse:
logger.info(f"get_character_assets called. CharacterID: {character_id}, Limit: {limit}, Offset: {offset}")
character = await dao.chars.get_character(character_id)
if character is None:
raise HTTPException(status_code=404, detail="Character not found")
@@ -34,11 +40,13 @@ async def get_character_assets(character_id: str, dao: DAO = Depends(get_dao), l
@router.get("/{character_id}", response_model=Character)
async def get_character_by_id(character_id: str, request: Request, dao: DAO = Depends(get_dao)) -> Character:
logger.debug(f"get_character_by_id called. ID: {character_id}")
character = await dao.chars.get_character(character_id)
return character
@router.post("/{character_id}/_run", response_model=Asset)
@router.post("/{character_id}/_run", response_model=GenerationResponse)
async def post_character_generation(character_id: str, generation: GenerationRequest,
request: Request) -> GenerationResponse:
logger.info(f"post_character_generation called. CharacterID: {character_id}")
generation_service = request.app.state.generation_service

View File

@@ -1,6 +1,6 @@
from typing import List, Optional
from fastapi import APIRouter
from fastapi import APIRouter, UploadFile, File, Form
from fastapi.params import Depends
from starlette.requests import Request
@@ -11,6 +11,10 @@ from api.models.GenerationRequest import GenerationResponse, GenerationRequest,
from api.service.generation_service import GenerationService
from models.Generation import Generation
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix='/api/generations', tags=["Generation"])
@@ -18,13 +22,31 @@ router = APIRouter(prefix='/api/generations', tags=["Generation"])
async def ask_prompt_assistant(prompt_request: PromptRequest, request: Request,
generation_service: GenerationService = Depends(
get_generation_service)) -> PromptResponse:
logger.info(f"ask_prompt_assistant called with prompt length: {len(prompt_request.prompt)}. Linked assets: {len(prompt_request.linked_assets) if prompt_request.linked_assets else 0}")
generated_prompt = await generation_service.ask_prompt_assistant(prompt_request.prompt, prompt_request.linked_assets)
return PromptResponse(prompt=generated_prompt)
@router.post("/prompt-from-image", response_model=PromptResponse)
async def prompt_from_image(
prompt: Optional[str] = Form(None),
images: List[UploadFile] = File(...),
generation_service: GenerationService = Depends(get_generation_service)
) -> PromptResponse:
logger.info(f"prompt_from_image called. Images count: {len(images)}. Prompt provided: {bool(prompt)}")
images_bytes = []
for image in images:
content = await image.read()
images_bytes.append(content)
generated_prompt = await generation_service.generate_prompt_from_images(images_bytes, prompt)
return PromptResponse(prompt=generated_prompt)
@router.get("", response_model=List[GenerationResponse])
async def get_generations(character_id: Optional[str], limit: int = 10, offset: int = 0,
async def get_generations(character_id: Optional[str] = None, limit: int = 10, offset: int = 0,
generation_service: GenerationService = Depends(get_generation_service)):
logger.info(f"get_generations called. CharacterId: {character_id}, Limit: {limit}, Offset: {offset}")
return await generation_service.get_generations(character_id, limit=limit, offset=offset)
@@ -32,12 +54,14 @@ async def get_generations(character_id: Optional[str], limit: int = 10, offset:
async def post_generation(generation: GenerationRequest, request: Request,
generation_service: GenerationService = Depends(
get_generation_service)) -> GenerationResponse:
logger.info(f"post_generation (run) called. LinkedCharId: {generation.linked_character_id}, PromptLength: {len(generation.prompt)}")
return await generation_service.create_generation_task(generation)
@router.get("/{generation_id}", response_model=GenerationResponse)
async def get_generation(generation_id: str,
generation_service: GenerationService = Depends(get_generation_service)) -> GenerationResponse:
logger.debug(f"get_generation called for ID: {generation_id}")
return await generation_service.get_generation(generation_id)

View File

@@ -5,7 +5,7 @@ from pydantic import BaseModel
from models.Asset import Asset
from models.Generation import GenerationStatus
from models.enums import AspectRatios, Quality
from models.enums import AspectRatios, Quality, GenType
class GenerationRequest(BaseModel):
@@ -20,12 +20,18 @@ class GenerationResponse(BaseModel):
id: str
status: GenerationStatus
failed_reason: Optional[str] = None
linked_character_id: Optional[str] = None
aspect_ratio: AspectRatios
quality: Quality
prompt: str
tech_prompt: Optional[str] = None
assets_list: List[str]
result: Optional[str] = None
execution_time_seconds: Optional[float] = None
api_execution_time_seconds: Optional[float] = None
token_usage: Optional[int] = None
progress: int = 0
created_at: datetime = datetime.now(UTC)
updated_at: datetime = datetime.now(UTC)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -2,7 +2,7 @@ import asyncio
import logging
import random
from datetime import datetime, UTC
from typing import List, Optional
from typing import List, Optional, Tuple, Any, Dict
from io import BytesIO
from adapters.Exception import GoogleGenerationException
@@ -11,7 +11,7 @@ from api.models.GenerationRequest import GenerationRequest, GenerationResponse
# Импортируйте ваши модели DAO, Asset, Generation корректно
from models.Asset import Asset, AssetType
from models.Generation import Generation, GenerationStatus
from models.enums import AspectRatios, Quality
from models.enums import AspectRatios, Quality, GenType
from repos.dao import DAO
logger = logging.getLogger(__name__)
@@ -24,20 +24,24 @@ async def generate_image_task(
aspect_ratio: AspectRatios,
quality: Quality,
gemini: GoogleAdapter
) -> List[bytes]:
) -> Tuple[List[bytes], Dict[str, Any]]:
"""
Обертка для вызова синхронного метода Gemini в отдельном потоке.
Возвращает список байтов сгенерированных изображений.
"""
try :
logger.info(f"Starting generate_image_task with prompt length: {len(prompt)}")
# Запускаем блокирующую операцию в отдельном потоке, чтобы не тормозить Event Loop
generated_images_io: List[BytesIO] = await asyncio.to_thread(
result = await asyncio.to_thread(
gemini.generate_image,
prompt=prompt,
images_list=media_group_bytes,
aspect_ratio=aspect_ratio,
quality=quality,
)
generated_images_io, metrics = result
logger.info(f"generate_image_task completed, received {len(generated_images_io) if generated_images_io else 0} images")
except GoogleGenerationException as e:
raise e
images_bytes = []
@@ -51,13 +55,13 @@ async def generate_image_task(
# Закрываем поток
img_io.close()
return images_bytes
return images_bytes, metrics
class GenerationService:
def __init__(self, dao: DAO, gemini: GoogleAdapter):
self.dao = dao
self.gemini = gemini
async def ask_prompt_assistant(self, prompt: str, assets: List[str] = None) -> str:
future_prompt = """You are an prompt-assistant. You improving user-entered prompts for image generation. User may upload reference image too.
@@ -68,11 +72,20 @@ class GenerationService:
if assets is not None:
assets_db = await self.dao.assets.get_assets_by_ids(assets)
assets_data.extend(asset.data for asset in assets_db)
generated_prompt = self.gemini.generate_text(future_prompt, assets_data)
generated_prompt = await asyncio.to_thread(self.gemini.generate_text, future_prompt, assets_data)
logger.info(future_prompt)
logger.info(generated_prompt)
return generated_prompt
async def generate_prompt_from_images(self, images: List[bytes], user_prompt: Optional[str] = None) -> str:
technical_prompt = "You are a prompt engineer. Describe this image in detail to create a stable diffusion using this image as reference. "
if user_prompt:
technical_prompt += f"User also provided this context: {user_prompt}. "
technical_prompt += "Provide ONLY the detailed prompt."
return await asyncio.to_thread(self.gemini.generate_text, prompt=technical_prompt, images_list=images)
async def get_generations(self, character_id: Optional[str] = None, limit: int = 10, offset: int = 0) -> List[
Generation]:
return await self.dao.generations.get_generations(limit=limit, offset=offset)
@@ -97,8 +110,10 @@ class GenerationService:
generation_model.id = gen_id
async def runner(gen):
logger.info(f"Starting background generation task for ID: {gen.id}")
try:
await self.create_generation(gen)
logger.info(f"Background generation task finished for ID: {gen.id}")
except Exception:
# если генерация уже пошла и упала — пометим FAILED
try:
@@ -125,6 +140,8 @@ class GenerationService:
raise
async def create_generation(self, generation: Generation):
start_time = datetime.now()
logger.info(f"Processing generation {generation.id}. Character ID: {generation.linked_character_id}")
# 2. Получаем ассеты-референсы (если они есть)
reference_assets: List[Asset] = []
@@ -146,27 +163,47 @@ class GenerationService:
if asset.data is not None and asset.type == AssetType.IMAGE
)
generation_prompt+=f"PROMPT: {generation.prompt}"
logger.info(f"Final generation prompt assembled. Length: {len(generation_prompt)}. Media count: {len(media_group_bytes)}")
# 3. Запускаем процесс генерации
# 3. Запускаем процесс генерации и симуляцию прогресса
progress_task = asyncio.create_task(self._simulate_progress(generation))
try:
generated_bytes_list = await generate_image_task(
# Default to Image Generation (Gemini)
generated_bytes_list, metrics = await generate_image_task(
prompt=generation_prompt, # или request.prompt
media_group_bytes=media_group_bytes,
aspect_ratio=generation.aspect_ratio, # предполагаем поля в request
quality=generation.quality,
gemini=self.gemini
)
# Update metrics from API (Common for both)
generation.api_execution_time_seconds = metrics.get("api_execution_time_seconds")
generation.token_usage = metrics.get("token_usage")
except GoogleGenerationException as e:
generation.status = GenerationStatus.FAILED
generation.failed_reason = str(e.message)
generation.failed_reason = str(e)
generation.updated_at = datetime.now(UTC)
await self.dao.generations.update_generation(generation)
raise
raise e
except Exception as e:
# Тут стоит добавить логирование ошибки
logging.error(f"Generation failed: {e}")
# Можно обновить статус генерации на FAILED в БД
generation.status = GenerationStatus.FAILED
generation.failed_reason = str(e)
generation.updated_at = datetime.now(UTC)
await self.dao.generations.update_generation(generation)
raise e
finally:
if not progress_task.done():
progress_task.cancel()
try:
await progress_task
except asyncio.CancelledError:
pass
# 4. Сохраняем полученные изображения как новые Ассеты
created_assets: List[Asset] = []
@@ -192,6 +229,37 @@ class GenerationService:
generation.assets_list = result_ids
generation.status = GenerationStatus.DONE
generation.progress = 100
generation.updated_at = datetime.now(UTC)
generation.tech_prompt = generation_prompt
end_time = datetime.now()
generation.execution_time_seconds = (end_time - start_time).total_seconds()
await self.dao.generations.update_generation(generation)
logger.info(f"Generation {generation.id} completed successfully. {len(created_assets)} assets created. Total Time: {generation.execution_time_seconds:.2f}s")
async def _simulate_progress(self, generation: Generation):
"""
Increments progress from 0 to 90 over ~20 seconds.
"""
current_progress = 0
try:
while current_progress < 90:
await asyncio.sleep(4)
# Random increment between 5 and 15
increment = random.randint(5, 15)
current_progress = min(current_progress + increment, 90)
# Fetch latest state (optional, but good practice to avoid overwriting unrelated fields)
# But for simplicity here we just use the object we have and save it.
# Ideally, we should fetch-update-save or use partial update if DAO supports it.
# Assuming simple update is fine for now.
generation.progress = current_progress
await self.dao.generations.update_generation(generation)
except asyncio.CancelledError:
# Task cancelled, generation finished (or failed)
pass
except Exception as e:
logger.error(f"Error in progress simulation: {e}")