feat: Separate asset origin type from content type for improved asset categorization and handling.

This commit is contained in:
xds
2026-02-07 14:41:03 +03:00
parent ce87ac7edb
commit 305ad24576
13 changed files with 53 additions and 17 deletions

2
.env
View File

@@ -3,7 +3,7 @@ BOT_TOKEN=8495170789:AAHyjjhHwwVtd9_ROnjHqPHRdnmyVr1aeaY
GEMINI_API_KEY=AIzaSyAHzDYhgjOqZZnvOnOFRGaSkKu4OAN3kZE
MONGO_HOST=mongodb://admin:super_secure_password@31.59.58.220:27017/
ADMIN_ID=567047
MINIO_ENDPOINT=https://minio.luminic.space
MINIO_ENDPOINT=http://31.59.58.220:9000
MINIO_ACCESS_KEY=admin
MINIO_SECRET_KEY=SuperSecretPassword123!
MINIO_BUCKET=ai-char

View File

@@ -9,7 +9,7 @@ from starlette.requests import Request
from starlette.responses import Response, JSONResponse
from api.models.AssetDTO import AssetsResponse, AssetResponse
from models.Asset import Asset, AssetType
from models.Asset import Asset, AssetType, AssetContentType
from repos.dao import DAO
from api.dependency import get_dao
import asyncio
@@ -105,7 +105,8 @@ async def upload_asset(
asset = Asset(
name=file.filename or "upload",
type=AssetType.IMAGE,
type=AssetType.UPLOADED,
content_type=AssetContentType.IMAGE,
linked_char_id=linked_char_id,
data=data,
thumbnail=thumbnail_bytes
@@ -119,6 +120,7 @@ async def upload_asset(
id=asset.id,
name=asset.name,
type=asset.type.value if hasattr(asset.type, "value") else asset.type,
content_type=asset.content_type.value if hasattr(asset.content_type, "value") else asset.content_type,
linked_char_id=asset.linked_char_id,
created_at=asset.created_at,
url=asset.url
@@ -145,7 +147,7 @@ async def regenerate_thumbnails(dao: DAO = Depends(get_dao)):
updated = 0
for asset in assets:
if asset.type == AssetType.IMAGE and asset.data :
if asset.content_type == AssetContentType.IMAGE and asset.data :
try:
thumb = await asyncio.to_thread(create_thumbnail, asset.data)
if thumb:

View File

@@ -9,7 +9,8 @@ from models.Asset import Asset
class AssetResponse(BaseModel):
id: str
name: str
type: str
type: str # uploaded / generated
content_type: str # image / prompt
linked_char_id: Optional[str] = None
created_at: datetime
url: Optional[str] = None

View File

@@ -11,7 +11,7 @@ from adapters.Exception import GoogleGenerationException
from adapters.google_adapter import GoogleAdapter
from api.models.GenerationRequest import GenerationRequest, GenerationResponse
# Импортируйте ваши модели DAO, Asset, Generation корректно
from models.Asset import Asset, AssetType
from models.Asset import Asset, AssetType, AssetContentType
from models.Generation import Generation, GenerationStatus
from models.enums import AspectRatios, Quality, GenType
from repos.dao import DAO
@@ -174,7 +174,7 @@ class GenerationService:
# Извлекаем данные (bytes) из ассетов для отправки в Gemini
for asset in reference_assets:
if asset.type != AssetType.IMAGE:
if asset.content_type != AssetContentType.IMAGE:
continue
img_data = None
@@ -249,7 +249,8 @@ class GenerationService:
new_asset = Asset(
name=f"Generated_{generation.linked_character_id}",
type=AssetType.IMAGE,
type=AssetType.GENERATED,
content_type=AssetContentType.IMAGE,
linked_char_id=generation.linked_character_id,
data=None, # Not storing bytes in DB anymore
minio_object_name=filename,

View File

@@ -2,18 +2,23 @@ from datetime import datetime, UTC
from enum import Enum
from typing import Optional, Any, List
from pydantic import BaseModel, computed_field, Field
from pydantic import BaseModel, computed_field, Field, model_validator
class AssetType(str, Enum):
class AssetContentType(str, Enum):
IMAGE = 'image'
PROMPT = 'prompt'
class AssetType(str, Enum):
UPLOADED = 'uploaded'
GENERATED = 'generated'
class Asset(BaseModel):
id: Optional[str] = None
name: str
type: AssetType
type: AssetType = AssetType.GENERATED
content_type: AssetContentType = AssetContentType.IMAGE
linked_char_id: Optional[str] = None
data: Optional[bytes] = None
tg_doc_file_id: Optional[str] = None
@@ -26,6 +31,33 @@ class Asset(BaseModel):
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
@model_validator(mode='before')
@classmethod
def check_legacy_type(cls, data: Any) -> Any:
if isinstance(data, dict):
# Если поле type содержит старые значения ("image", "prompt"),
# переносим их в content_type, а type ставим по умолчанию (GENERATED)
# или пытаемся угадать.
# Но по задаче мы дефолтим в GENERATED, и script'ом поправим.
raw_type = data.get('type')
if raw_type in ['image', 'prompt']:
data['content_type'] = raw_type
# Если в базе нет нового поля type, оно встанет в default=GENERATED
# Чтобы не вызывало ошибку валидации AssetType, меняем его или удаляем,
# полагаясь на default.
# Но если мы просто удалим, поле type примет дефолтное значение.
# Однако, если мы хотим явно отличить, можно ничего не делать,
# но тогда валидация поля `type` упадет, т.к. "image" != "generated".
# Поэтому удаляем старое значение из type, чтобы сработал дефолт.
if 'type' in data:
del data['type']
# Если content_type нет в данных (легаси), пытаемся его восстановить из удалённого type
# (выше мы его переложили).
return data
# --- CALCULATED FIELD ---
@computed_field
def url(self) -> str:

View File

@@ -8,7 +8,7 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import *
from aiogram import Router, F, Bot
from models.Asset import Asset, AssetType
from models.Asset import Asset, AssetType, AssetContentType
from models.Character import Character
from repos.dao import DAO
@@ -74,7 +74,7 @@ async def new_char_bio(message: Message, state: FSMContext, dao: DAO, bot: Bot):
file_bytes = await bot.download_file(file_info.file_path)
file_io = file_bytes.read()
avatar_asset = await dao.assets.create_asset(
Asset(name="avatar.png", type=AssetType.IMAGE, linked_char_id=str(char.id), data=file_io,
Asset(name="avatar.png", type=AssetType.UPLOADED, content_type=AssetContentType.IMAGE, linked_char_id=str(char.id), data=file_io,
tg_doc_file_id=file_id))
char.avatar_image = avatar_asset.link
# Отправляем подтверждение

View File

@@ -14,7 +14,7 @@ from aiogram.types import *
import keyboards
from adapters.google_adapter import GoogleAdapter
from models.Asset import Asset, AssetType
from models.Asset import Asset, AssetType, AssetContentType
from models.Character import Character
from models.enums import AspectRatios, Quality, GenType
from repos.dao import DAO
@@ -50,7 +50,7 @@ async def generate_image_cmd(message: Message, state: FSMContext, dao: DAO, gemi
gemini=gemini)
await wait_msg.delete()
doc = await message.answer_document(res[0], caption="Generated result 💫")
await dao.assets.create_asset(Asset(id=None, name=res[0].filename, type=AssetType.IMAGE, data=res[0].data,
await dao.assets.create_asset(Asset(id=None, name=res[0].filename, type=AssetType.GENERATED, content_type=AssetContentType.IMAGE, data=res[0].data,
tg_doc_file_id=doc.document.file_id, tg_photo_file_id=None, linked_char_id=None))
@@ -257,7 +257,7 @@ async def handle_album(
if generated_files:
for file in generated_files:
doc = await message.answer_document(file, caption="✨ Generated result")
await dao.assets.create_asset(Asset(id=None, name=file.filename, type=AssetType.IMAGE, data=file.data,
await dao.assets.create_asset(Asset(id=None, name=file.filename, type=AssetType.GENERATED, content_type=AssetContentType.IMAGE, data=file.data,
tg_doc_file_id = doc.document.file_id, tg_photo_file_id = None,
linked_char_id = data["char_id"]))
else:
@@ -312,7 +312,7 @@ async def gen_mode_start(
if generated_files:
for file in generated_files:
doc = await message.answer_document(file, caption="✨ Generated result")
await dao.assets.create_asset(Asset(id=None, name=file.filename, type=AssetType.IMAGE, data=file.data,
await dao.assets.create_asset(Asset(id=None, name=file.filename, type=AssetType.GENERATED, content_type=AssetContentType.IMAGE, data=file.data,
tg_doc_file_id=doc.document.file_id, tg_photo_file_id=None,
linked_char_id=data["char_id"]))