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

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.env
View File

@@ -1,4 +1,5 @@
BOT_TOKEN=8495170789:AAHyjjhHwwVtd9_ROnjHqPHRdnmyVr1aeaY
#BOT_TOKEN=8495170789:AAHyjjhHwwVtd9_ROnjHqPHRdnmyVr1aeaY
BOT_TOKEN=8011562605:AAF3kyzrZJgii0Jx-H8Sum5Njbo0BdbsiAo
GEMINI_API_KEY=AIzaSyAHzDYhgjOqZZnvOnOFRGaSkKu4OAN3kZE
MONGO_HOST=mongodb://admin:super_secure_password@31.59.58.220:27017/
ADMIN_ID=567047

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

10
.idea/ai-char-bot.iml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (ai-char-bot)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,16 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyAssertTypeInspection" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
<inspection_tool class="PyAsyncCallInspection" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N802" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyTypeCheckerInspection" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
<inspection_tool class="PyUnreachableCodeInspection" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13 (ai-char-bot)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (ai-char-bot)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ai-char-bot.iml" filepath="$PROJECT_DIR$/.idea/ai-char-bot.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

5
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"configurations": [
{"name":"Python Debugger: FastAPI","type":"debugpy","request":"launch","module":"uvicorn","args":["main:app","--reload", "--port", "8090"],"jinja":true}
]
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +1,4 @@
class GoogleGenerationException(Exception):
message: str
pass
def __init__(self, message: str):
self.message = message
super().__init__(message)

Binary file not shown.

Binary file not shown.

View File

@@ -1,7 +1,7 @@
import io
import logging
from datetime import datetime
from typing import List, Union
from typing import List, Union, Tuple, Dict, Any
from PIL import Image
from google import genai
@@ -27,6 +27,7 @@ class GoogleAdapter:
"""Вспомогательный метод для подготовки контента (текст + картинки)"""
contents = [prompt]
if images_list:
logger.info(f"Preparing content with {len(images_list)} images")
for img_bytes in images_list:
try:
# Gemini API требует PIL Image на входе
@@ -34,6 +35,8 @@ class GoogleAdapter:
contents.append(image)
except Exception as e:
logger.error(f"Error processing input image: {e}")
else:
logger.info("Preparing content with no images")
return contents
def generate_text(self, prompt: str, images_list: List[bytes] = None) -> str:
@@ -59,20 +62,25 @@ class GoogleAdapter:
for part in response.parts:
if part.text:
result_text += part.text
logger.error(f"Generated text: {result_text}")
logger.info(f"Generated text length: {len(result_text)}")
return result_text
except Exception as e:
logger.error(f"Gemini Text API Error: {e}")
return f"Ошибка генерации текста: {e}"
raise GoogleGenerationException(f"Gemini Text API Error: {e}")
def generate_image(self, prompt: str, aspect_ratio: AspectRatios, quality: Quality, images_list: List[bytes] = None, ) -> List[io.BytesIO]:
def generate_image(self, prompt: str, aspect_ratio: AspectRatios, quality: Quality, images_list: List[bytes] = None, ) -> Tuple[List[io.BytesIO], Dict[str, Any]]:
"""
Генерация изображений (Text-to-Image или Image-to-Image).
Возвращает список байтовых потоков (готовых к отправке).
"""
contents = self._prepare_contents(prompt, images_list)
contents = self._prepare_contents(prompt, images_list)
logger.info(f"Generating image. Prompt length: {len(prompt)}, Ratio: {aspect_ratio}, Quality: {quality}")
start_time = datetime.now()
token_usage = 0
try:
response = self.client.models.generate_content(
model=self.IMAGE_MODEL,
@@ -86,6 +94,13 @@ class GoogleAdapter:
),
)
)
end_time = datetime.now()
api_duration = (end_time - start_time).total_seconds()
if response.usage_metadata:
token_usage = response.usage_metadata.total_token_count
if response.parts is None and response.candidates[0].finish_reason is not None:
raise GoogleGenerationException(f"Generation blocked in cause of {response.candidates[0].finish_reason.value}")
@@ -111,9 +126,17 @@ class GoogleAdapter:
except Exception as e:
logger.error(f"Error processing output image: {e}")
return generated_images
if generated_images:
logger.info(f"Successfully generated {len(generated_images)} images in {api_duration:.2f}s. Tokens: {token_usage}")
else:
logger.warning("No images text generated from parts")
metrics = {
"api_execution_time_seconds": api_duration,
"token_usage": token_usage
}
return generated_images, metrics
except Exception as e:
logger.error(f"Gemini Image API Error: {e}")
# В случае ошибки возвращаем пустой список (или можно рейзить исключение)
return []
raise GoogleGenerationException(f"Gemini Image API Error: {e}")

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}")

View File

@@ -43,6 +43,7 @@ load_dotenv()
# --- КОНФИГУРАЦИЯ ---
BOT_TOKEN = os.getenv("BOT_TOKEN")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
MONGO_HOST = os.getenv("MONGO_HOST") # Например: mongodb://localhost:27017
DB_NAME = os.getenv("DB_NAME", "my_bot_db") # Имя базы данных
ADMIN_ID = int(os.getenv("ADMIN_ID", 0))
@@ -63,6 +64,7 @@ mongo_client = AsyncIOMotorClient(MONGO_HOST)
users_repo = UsersRepo(mongo_client)
char_repo = CharacterRepo(mongo_client)
dao = DAO(mongo_client) # Главный DAO для бота
dao = DAO(mongo_client) # Главный DAO для бота
gemini = GoogleAdapter(api_key=GEMINI_API_KEY)
generation_service = GenerationService(dao, gemini)
@@ -113,6 +115,7 @@ async def lifespan(app: FastAPI):
# Инициализируем DAO для ассетов и кладем в state приложения
# Теперь в эндпоинтах можно делать request.app.state.assets_dao
app.state.mongo_client = mongo_client
app.state.mongo_client = mongo_client
app.state.gemini_client = gemini

Binary file not shown.

Binary file not shown.

View File

@@ -5,7 +5,7 @@ from typing import List, Optional
from pydantic import BaseModel, Field
from models.Asset import Asset
from models.enums import AspectRatios, Quality
from models.enums import AspectRatios, Quality, GenType
class GenerationStatus(str, Enum):
@@ -24,5 +24,10 @@ class Generation(BaseModel):
tech_prompt: Optional[str] = None
assets_list: List[str]
result: Optional[str] = None
progress: int = 0
execution_time_seconds: Optional[float] = None
api_execution_time_seconds: Optional[float] = None
token_usage: Optional[int] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -39,5 +39,5 @@ class GenType(str, Enum):
def value_type(self) -> str:
return {
GenType.TEXT: 'Text',
GenType.IMAGE: 'Image'
GenType.IMAGE: 'Image',
}[self]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -236,9 +236,6 @@ async def handle_album(
for msg in album:
if msg.photo:
file_ids.append(msg.photo[-1].file_id)
elif msg.video:
# Если нужно, можно добавить обработку видео (пока пропускаем)
pass
await message.answer(f"📥 Принято {len(album)} файлов. Начинаю генерацию...")
wait_msg = await message.answer("🎨 Генерирую...")