feat: Add logging to API endpoints, update generation response model, and refine project configurations.
This commit is contained in:
BIN
api/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
api/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/dependency.cpython-313.pyc
Normal file
BIN
api/__pycache__/dependency.cpython-313.pyc
Normal file
Binary file not shown.
@@ -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)
|
||||
BIN
api/endpoints/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
api/endpoints/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
api/endpoints/__pycache__/assets_router.cpython-313.pyc
Normal file
BIN
api/endpoints/__pycache__/assets_router.cpython-313.pyc
Normal file
Binary file not shown.
BIN
api/endpoints/__pycache__/character_router.cpython-313.pyc
Normal file
BIN
api/endpoints/__pycache__/character_router.cpython-313.pyc
Normal file
Binary file not shown.
BIN
api/endpoints/__pycache__/generation_router.cpython-313.pyc
Normal file
BIN
api/endpoints/__pycache__/generation_router.cpython-313.pyc
Normal file
Binary file not shown.
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
BIN
api/models/__pycache__/AssetDTO.cpython-313.pyc
Normal file
BIN
api/models/__pycache__/AssetDTO.cpython-313.pyc
Normal file
Binary file not shown.
BIN
api/models/__pycache__/GenerationRequest.cpython-313.pyc
Normal file
BIN
api/models/__pycache__/GenerationRequest.cpython-313.pyc
Normal file
Binary file not shown.
BIN
api/models/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
api/models/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
api/service/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
api/service/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
api/service/__pycache__/generation_service.cpython-313.pyc
Normal file
BIN
api/service/__pycache__/generation_service.cpython-313.pyc
Normal file
Binary file not shown.
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user