feat: Add image, update VS Code launch configuration, and enhance gitignore rules for build artifacts.
This commit is contained in:
14
.gitignore
vendored
14
.gitignore
vendored
@@ -9,3 +9,17 @@ minio_backup.tar.gz
|
|||||||
.idea
|
.idea
|
||||||
.venv
|
.venv
|
||||||
.vscode
|
.vscode
|
||||||
|
.vscode/launch.json
|
||||||
|
middlewares/__pycache__/
|
||||||
|
middlewares/*.pyc
|
||||||
|
api/__pycache__/
|
||||||
|
api/*.pyc
|
||||||
|
repos/__pycache__/
|
||||||
|
repos/*.pyc
|
||||||
|
adapters/__pycache__/
|
||||||
|
adapters/*.pyc
|
||||||
|
services/__pycache__/
|
||||||
|
services/*.pyc
|
||||||
|
utils/__pycache__/
|
||||||
|
utils/*.pyc
|
||||||
|
.vscode/launch.json
|
||||||
|
|||||||
27
.vscode/launch.json
vendored
27
.vscode/launch.json
vendored
@@ -7,7 +7,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "uvicorn",
|
"module": "uvicorn",
|
||||||
"args": [
|
"args": [
|
||||||
"main:app",
|
"aiws:app",
|
||||||
"--reload",
|
"--reload",
|
||||||
"--port",
|
"--port",
|
||||||
"8090",
|
"8090",
|
||||||
@@ -16,31 +16,6 @@
|
|||||||
],
|
],
|
||||||
"jinja": true,
|
"jinja": true,
|
||||||
"justMyCode": true
|
"justMyCode": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Python: Current File",
|
|
||||||
"type": "debugpy",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "${file}",
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"justMyCode": true,
|
|
||||||
"env": {
|
|
||||||
"PYTHONPATH": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debug Tests: Current File",
|
|
||||||
"type": "debugpy",
|
|
||||||
"request": "launch",
|
|
||||||
"module": "pytest",
|
|
||||||
"args": [
|
|
||||||
"${file}"
|
|
||||||
],
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"justMyCode": true,
|
|
||||||
"env": {
|
|
||||||
"PYTHONPATH": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -23,28 +23,30 @@ class GoogleAdapter:
|
|||||||
self.TEXT_MODEL = "gemini-3-pro-preview"
|
self.TEXT_MODEL = "gemini-3-pro-preview"
|
||||||
self.IMAGE_MODEL = "gemini-3-pro-image-preview"
|
self.IMAGE_MODEL = "gemini-3-pro-image-preview"
|
||||||
|
|
||||||
def _prepare_contents(self, prompt: str, images_list: List[bytes] = None) -> list:
|
def _prepare_contents(self, prompt: str, images_list: List[bytes] = None) -> tuple:
|
||||||
"""Вспомогательный метод для подготовки контента (текст + картинки)"""
|
"""Вспомогательный метод для подготовки контента (текст + картинки).
|
||||||
|
Returns (contents, opened_images) — caller MUST close opened_images after use."""
|
||||||
contents = [prompt]
|
contents = [prompt]
|
||||||
|
opened_images = []
|
||||||
if images_list:
|
if images_list:
|
||||||
logger.info(f"Preparing content with {len(images_list)} images")
|
logger.info(f"Preparing content with {len(images_list)} images")
|
||||||
for img_bytes in images_list:
|
for img_bytes in images_list:
|
||||||
try:
|
try:
|
||||||
# Gemini API требует PIL Image на входе
|
|
||||||
image = Image.open(io.BytesIO(img_bytes))
|
image = Image.open(io.BytesIO(img_bytes))
|
||||||
contents.append(image)
|
contents.append(image)
|
||||||
|
opened_images.append(image)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing input image: {e}")
|
logger.error(f"Error processing input image: {e}")
|
||||||
else:
|
else:
|
||||||
logger.info("Preparing content with no images")
|
logger.info("Preparing content with no images")
|
||||||
return contents
|
return contents, opened_images
|
||||||
|
|
||||||
def generate_text(self, prompt: str, images_list: List[bytes] = None) -> str:
|
def generate_text(self, prompt: str, images_list: List[bytes] = None) -> str:
|
||||||
"""
|
"""
|
||||||
Генерация текста (Чат или Vision).
|
Генерация текста (Чат или Vision).
|
||||||
Возвращает строку с ответом.
|
Возвращает строку с ответом.
|
||||||
"""
|
"""
|
||||||
contents = self._prepare_contents(prompt, images_list)
|
contents, opened_images = self._prepare_contents(prompt, images_list)
|
||||||
logger.info(f"Generating text: {prompt}")
|
logger.info(f"Generating text: {prompt}")
|
||||||
try:
|
try:
|
||||||
response = self.client.models.generate_content(
|
response = self.client.models.generate_content(
|
||||||
@@ -68,6 +70,9 @@ class GoogleAdapter:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Gemini Text API Error: {e}")
|
logger.error(f"Gemini Text API Error: {e}")
|
||||||
raise GoogleGenerationException(f"Gemini Text API Error: {e}")
|
raise GoogleGenerationException(f"Gemini Text API Error: {e}")
|
||||||
|
finally:
|
||||||
|
for img in opened_images:
|
||||||
|
img.close()
|
||||||
|
|
||||||
def generate_image(self, prompt: str, aspect_ratio: AspectRatios, quality: Quality, images_list: List[bytes] = None, ) -> Tuple[List[io.BytesIO], Dict[str, Any]]:
|
def generate_image(self, prompt: str, aspect_ratio: AspectRatios, quality: Quality, images_list: List[bytes] = None, ) -> Tuple[List[io.BytesIO], Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -75,7 +80,7 @@ class GoogleAdapter:
|
|||||||
Возвращает список байтовых потоков (готовых к отправке).
|
Возвращает список байтовых потоков (готовых к отправке).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
contents = self._prepare_contents(prompt, images_list)
|
contents, opened_images = self._prepare_contents(prompt, images_list)
|
||||||
logger.info(f"Generating image. Prompt length: {len(prompt)}, Ratio: {aspect_ratio}, Quality: {quality}")
|
logger.info(f"Generating image. Prompt length: {len(prompt)}, Ratio: {aspect_ratio}, Quality: {quality}")
|
||||||
|
|
||||||
start_time = datetime.now()
|
start_time = datetime.now()
|
||||||
@@ -148,3 +153,7 @@ class GoogleAdapter:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Gemini Image API Error: {e}")
|
logger.error(f"Gemini Image API Error: {e}")
|
||||||
raise GoogleGenerationException(f"Gemini Image API Error: {e}")
|
raise GoogleGenerationException(f"Gemini Image API Error: {e}")
|
||||||
|
finally:
|
||||||
|
for img in opened_images:
|
||||||
|
img.close()
|
||||||
|
del contents
|
||||||
@@ -56,6 +56,21 @@ class S3Adapter:
|
|||||||
print(f"Error downloading from S3: {e}")
|
print(f"Error downloading from S3: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def stream_file(self, object_name: str, chunk_size: int = 65536):
|
||||||
|
"""Streams a file from S3 yielding chunks. Memory-efficient for large files."""
|
||||||
|
try:
|
||||||
|
async with self._get_client() as client:
|
||||||
|
response = await client.get_object(Bucket=self.bucket_name, Key=object_name)
|
||||||
|
async with response['Body'] as stream:
|
||||||
|
while True:
|
||||||
|
chunk = await stream.read(chunk_size)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
yield chunk
|
||||||
|
except ClientError as e:
|
||||||
|
print(f"Error streaming from S3: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
async def delete_file(self, object_name: str):
|
async def delete_file(self, object_name: str):
|
||||||
"""Deletes a file from S3."""
|
"""Deletes a file from S3."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,7 +9,7 @@ from pymongo import MongoClient
|
|||||||
from starlette import status
|
from starlette import status
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import Response, JSONResponse
|
from starlette.responses import Response, JSONResponse, StreamingResponse
|
||||||
|
|
||||||
from adapters.s3_adapter import S3Adapter
|
from adapters.s3_adapter import S3Adapter
|
||||||
from api.models.AssetDTO import AssetsResponse, AssetResponse
|
from api.models.AssetDTO import AssetsResponse, AssetResponse
|
||||||
@@ -33,27 +33,46 @@ async def get_asset(
|
|||||||
asset_id: str,
|
asset_id: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
thumbnail: bool = False,
|
thumbnail: bool = False,
|
||||||
dao: DAO = Depends(get_dao)
|
dao: DAO = Depends(get_dao),
|
||||||
|
s3_adapter: S3Adapter = Depends(get_s3_adapter),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
logger.debug(f"get_asset called for ID: {asset_id}, thumbnail={thumbnail}")
|
logger.debug(f"get_asset called for ID: {asset_id}, thumbnail={thumbnail}")
|
||||||
asset = await dao.assets.get_asset(asset_id)
|
# Загружаем только метаданные (без data/thumbnail bytes)
|
||||||
# 2. Проверка на существование
|
asset = await dao.assets.get_asset(asset_id, with_data=False)
|
||||||
if not asset:
|
if not asset:
|
||||||
raise HTTPException(status_code=404, detail="Asset not found")
|
raise HTTPException(status_code=404, detail="Asset not found")
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
# Кэшировать на 1 год (31536000 сек)
|
|
||||||
"Cache-Control": "public, max-age=31536000, immutable"
|
"Cache-Control": "public, max-age=31536000, immutable"
|
||||||
}
|
}
|
||||||
|
|
||||||
content = asset.data
|
# Thumbnail: маленький, можно грузить в RAM
|
||||||
media_type = "image/png" # Default, or detect
|
if thumbnail:
|
||||||
|
if asset.minio_thumbnail_object_name and s3_adapter:
|
||||||
|
thumb_bytes = await s3_adapter.get_file(asset.minio_thumbnail_object_name)
|
||||||
|
if thumb_bytes:
|
||||||
|
return Response(content=thumb_bytes, media_type="image/jpeg", headers=headers)
|
||||||
|
# Fallback: thumbnail in DB
|
||||||
|
if asset.thumbnail:
|
||||||
|
return Response(content=asset.thumbnail, media_type="image/jpeg", headers=headers)
|
||||||
|
# No thumbnail available — fall through to main content
|
||||||
|
|
||||||
if thumbnail and asset.thumbnail:
|
# Main content: стримим из S3 без загрузки в RAM
|
||||||
content = asset.thumbnail
|
if asset.minio_object_name and s3_adapter:
|
||||||
media_type = "image/jpeg"
|
content_type = "image/png"
|
||||||
|
if asset.content_type == AssetContentType.VIDEO:
|
||||||
|
content_type = "video/mp4"
|
||||||
|
return StreamingResponse(
|
||||||
|
s3_adapter.stream_file(asset.minio_object_name),
|
||||||
|
media_type=content_type,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
return Response(content=content, media_type=media_type, headers=headers)
|
# Fallback: data stored in DB (legacy)
|
||||||
|
if asset.data:
|
||||||
|
return Response(content=asset.data, media_type="image/png", headers=headers)
|
||||||
|
|
||||||
|
raise HTTPException(status_code=404, detail="Asset data not found")
|
||||||
|
|
||||||
@router.delete("/orphans", dependencies=[Depends(get_current_user)])
|
@router.delete("/orphans", dependencies=[Depends(get_current_user)])
|
||||||
async def delete_orphan_assets_from_minio(
|
async def delete_orphan_assets_from_minio(
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -50,16 +50,18 @@ async def generate_image_task(
|
|||||||
logger.info(f"generate_image_task completed, received {len(generated_images_io) if generated_images_io else 0} images")
|
logger.info(f"generate_image_task completed, received {len(generated_images_io) if generated_images_io else 0} images")
|
||||||
except GoogleGenerationException as e:
|
except GoogleGenerationException as e:
|
||||||
raise e
|
raise e
|
||||||
|
finally:
|
||||||
|
# Освобождаем входные данные — они больше не нужны
|
||||||
|
del media_group_bytes
|
||||||
|
|
||||||
images_bytes = []
|
images_bytes = []
|
||||||
if generated_images_io:
|
if generated_images_io:
|
||||||
for img_io in generated_images_io:
|
for img_io in generated_images_io:
|
||||||
# Читаем байты из BytesIO
|
|
||||||
img_io.seek(0)
|
img_io.seek(0)
|
||||||
content = img_io.read()
|
images_bytes.append(img_io.read())
|
||||||
images_bytes.append(content)
|
|
||||||
|
|
||||||
# Закрываем поток
|
|
||||||
img_io.close()
|
img_io.close()
|
||||||
|
# Освобождаем список BytesIO сразу
|
||||||
|
del generated_images_io
|
||||||
|
|
||||||
return images_bytes, metrics
|
return images_bytes, metrics
|
||||||
|
|
||||||
|
|||||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user