diff --git a/.gitignore b/.gitignore index 4a15c02..7d53abf 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,18 @@ minio_backup.tar.gz .idea/ai-char-bot.iml .idea .venv -.vscode \ No newline at end of file +.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 diff --git a/.vscode/launch.json b/.vscode/launch.json index 4af4fe0..b294351 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "module": "uvicorn", "args": [ - "main:app", + "aiws:app", "--reload", "--port", "8090", @@ -16,31 +16,6 @@ ], "jinja": 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}" - } } ] } \ No newline at end of file diff --git a/adapters/__pycache__/Exception.cpython-313.pyc b/adapters/__pycache__/Exception.cpython-313.pyc index 713f69c..691416b 100644 Binary files a/adapters/__pycache__/Exception.cpython-313.pyc and b/adapters/__pycache__/Exception.cpython-313.pyc differ diff --git a/adapters/__pycache__/google_adapter.cpython-313.pyc b/adapters/__pycache__/google_adapter.cpython-313.pyc index 221010d..817d8b4 100644 Binary files a/adapters/__pycache__/google_adapter.cpython-313.pyc and b/adapters/__pycache__/google_adapter.cpython-313.pyc differ diff --git a/adapters/__pycache__/s3_adapter.cpython-313.pyc b/adapters/__pycache__/s3_adapter.cpython-313.pyc index 563d847..70a075b 100644 Binary files a/adapters/__pycache__/s3_adapter.cpython-313.pyc and b/adapters/__pycache__/s3_adapter.cpython-313.pyc differ diff --git a/adapters/google_adapter.py b/adapters/google_adapter.py index d40d60f..4245c87 100644 --- a/adapters/google_adapter.py +++ b/adapters/google_adapter.py @@ -23,28 +23,30 @@ class GoogleAdapter: self.TEXT_MODEL = "gemini-3-pro-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] + opened_images = [] if images_list: logger.info(f"Preparing content with {len(images_list)} images") for img_bytes in images_list: try: - # Gemini API требует PIL Image на входе image = Image.open(io.BytesIO(img_bytes)) contents.append(image) + opened_images.append(image) except Exception as e: logger.error(f"Error processing input image: {e}") else: 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: """ Генерация текста (Чат или Vision). Возвращает строку с ответом. """ - contents = self._prepare_contents(prompt, images_list) + contents, opened_images = self._prepare_contents(prompt, images_list) logger.info(f"Generating text: {prompt}") try: response = self.client.models.generate_content( @@ -68,6 +70,9 @@ class GoogleAdapter: except Exception as e: logger.error(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]]: """ @@ -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}") start_time = datetime.now() @@ -147,4 +152,8 @@ class GoogleAdapter: except Exception as e: logger.error(f"Gemini Image API Error: {e}") - raise GoogleGenerationException(f"Gemini Image API Error: {e}") \ No newline at end of file + raise GoogleGenerationException(f"Gemini Image API Error: {e}") + finally: + for img in opened_images: + img.close() + del contents \ No newline at end of file diff --git a/adapters/s3_adapter.py b/adapters/s3_adapter.py index 611229e..fe992dd 100644 --- a/adapters/s3_adapter.py +++ b/adapters/s3_adapter.py @@ -56,6 +56,21 @@ class S3Adapter: print(f"Error downloading from S3: {e}") 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): """Deletes a file from S3.""" try: diff --git a/api/__pycache__/dependency.cpython-313.pyc b/api/__pycache__/dependency.cpython-313.pyc index bc2abeb..eb067d1 100644 Binary files a/api/__pycache__/dependency.cpython-313.pyc and b/api/__pycache__/dependency.cpython-313.pyc differ diff --git a/api/endpoints/__pycache__/admin.cpython-313.pyc b/api/endpoints/__pycache__/admin.cpython-313.pyc index 5162338..4654462 100644 Binary files a/api/endpoints/__pycache__/admin.cpython-313.pyc and b/api/endpoints/__pycache__/admin.cpython-313.pyc differ diff --git a/api/endpoints/__pycache__/assets_router.cpython-313.pyc b/api/endpoints/__pycache__/assets_router.cpython-313.pyc index 208aa91..2cd08ea 100644 Binary files a/api/endpoints/__pycache__/assets_router.cpython-313.pyc and b/api/endpoints/__pycache__/assets_router.cpython-313.pyc differ diff --git a/api/endpoints/__pycache__/auth.cpython-313.pyc b/api/endpoints/__pycache__/auth.cpython-313.pyc index 4330bff..5e122e0 100644 Binary files a/api/endpoints/__pycache__/auth.cpython-313.pyc and b/api/endpoints/__pycache__/auth.cpython-313.pyc differ diff --git a/api/endpoints/__pycache__/character_router.cpython-313.pyc b/api/endpoints/__pycache__/character_router.cpython-313.pyc index f1780ef..6925a78 100644 Binary files a/api/endpoints/__pycache__/character_router.cpython-313.pyc and b/api/endpoints/__pycache__/character_router.cpython-313.pyc differ diff --git a/api/endpoints/__pycache__/generation_router.cpython-313.pyc b/api/endpoints/__pycache__/generation_router.cpython-313.pyc index 93dae8d..fb99eaf 100644 Binary files a/api/endpoints/__pycache__/generation_router.cpython-313.pyc and b/api/endpoints/__pycache__/generation_router.cpython-313.pyc differ diff --git a/api/endpoints/assets_router.py b/api/endpoints/assets_router.py index 9b6d49b..00ed898 100644 --- a/api/endpoints/assets_router.py +++ b/api/endpoints/assets_router.py @@ -9,7 +9,7 @@ from pymongo import MongoClient from starlette import status from starlette.exceptions import HTTPException 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 api.models.AssetDTO import AssetsResponse, AssetResponse @@ -33,27 +33,46 @@ async def get_asset( asset_id: str, request: Request, thumbnail: bool = False, - dao: DAO = Depends(get_dao) + dao: DAO = Depends(get_dao), + s3_adapter: S3Adapter = Depends(get_s3_adapter), ) -> Response: logger.debug(f"get_asset called for ID: {asset_id}, thumbnail={thumbnail}") - asset = await dao.assets.get_asset(asset_id) - # 2. Проверка на существование + # Загружаем только метаданные (без data/thumbnail bytes) + asset = await dao.assets.get_asset(asset_id, with_data=False) if not asset: raise HTTPException(status_code=404, detail="Asset not found") headers = { - # Кэшировать на 1 год (31536000 сек) "Cache-Control": "public, max-age=31536000, immutable" } - content = asset.data - media_type = "image/png" # Default, or detect + # Thumbnail: маленький, можно грузить в RAM + 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: - content = asset.thumbnail - media_type = "image/jpeg" - - return Response(content=content, media_type=media_type, headers=headers) + # Main content: стримим из S3 без загрузки в RAM + if asset.minio_object_name and s3_adapter: + 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, + ) + + # 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)]) async def delete_orphan_assets_from_minio( diff --git a/api/models/__pycache__/AssetDTO.cpython-313.pyc b/api/models/__pycache__/AssetDTO.cpython-313.pyc index 8885db4..aa78e0c 100644 Binary files a/api/models/__pycache__/AssetDTO.cpython-313.pyc and b/api/models/__pycache__/AssetDTO.cpython-313.pyc differ diff --git a/api/models/__pycache__/GenerationRequest.cpython-313.pyc b/api/models/__pycache__/GenerationRequest.cpython-313.pyc index 0fe1b17..b958d60 100644 Binary files a/api/models/__pycache__/GenerationRequest.cpython-313.pyc and b/api/models/__pycache__/GenerationRequest.cpython-313.pyc differ diff --git a/api/service/__pycache__/generation_service.cpython-313.pyc b/api/service/__pycache__/generation_service.cpython-313.pyc index ea28bd2..b8700ed 100644 Binary files a/api/service/__pycache__/generation_service.cpython-313.pyc and b/api/service/__pycache__/generation_service.cpython-313.pyc differ diff --git a/api/service/generation_service.py b/api/service/generation_service.py index b433eac..8cf3b49 100644 --- a/api/service/generation_service.py +++ b/api/service/generation_service.py @@ -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") except GoogleGenerationException as e: raise e + finally: + # Освобождаем входные данные — они больше не нужны + del media_group_bytes + images_bytes = [] if generated_images_io: for img_io in generated_images_io: - # Читаем байты из BytesIO img_io.seek(0) - content = img_io.read() - images_bytes.append(content) - - # Закрываем поток + images_bytes.append(img_io.read()) img_io.close() + # Освобождаем список BytesIO сразу + del generated_images_io return images_bytes, metrics diff --git a/models/__pycache__/Asset.cpython-313.pyc b/models/__pycache__/Asset.cpython-313.pyc index 13bfb7f..90efbf5 100644 Binary files a/models/__pycache__/Asset.cpython-313.pyc and b/models/__pycache__/Asset.cpython-313.pyc differ diff --git a/models/__pycache__/Character.cpython-313.pyc b/models/__pycache__/Character.cpython-313.pyc index 191ad85..2fdabb0 100644 Binary files a/models/__pycache__/Character.cpython-313.pyc and b/models/__pycache__/Character.cpython-313.pyc differ diff --git a/models/__pycache__/Generation.cpython-313.pyc b/models/__pycache__/Generation.cpython-313.pyc index dfc0bfd..b052293 100644 Binary files a/models/__pycache__/Generation.cpython-313.pyc and b/models/__pycache__/Generation.cpython-313.pyc differ diff --git a/models/__pycache__/enums.cpython-313.pyc b/models/__pycache__/enums.cpython-313.pyc index e307fb2..086f9c9 100644 Binary files a/models/__pycache__/enums.cpython-313.pyc and b/models/__pycache__/enums.cpython-313.pyc differ diff --git a/repos/__pycache__/assets_repo.cpython-313.pyc b/repos/__pycache__/assets_repo.cpython-313.pyc index 8bee858..60c530c 100644 Binary files a/repos/__pycache__/assets_repo.cpython-313.pyc and b/repos/__pycache__/assets_repo.cpython-313.pyc differ diff --git a/repos/__pycache__/char_repo.cpython-313.pyc b/repos/__pycache__/char_repo.cpython-313.pyc index ccd7a75..e3e55a5 100644 Binary files a/repos/__pycache__/char_repo.cpython-313.pyc and b/repos/__pycache__/char_repo.cpython-313.pyc differ diff --git a/repos/__pycache__/dao.cpython-313.pyc b/repos/__pycache__/dao.cpython-313.pyc index 8c96e7b..8427c18 100644 Binary files a/repos/__pycache__/dao.cpython-313.pyc and b/repos/__pycache__/dao.cpython-313.pyc differ diff --git a/repos/__pycache__/generation_repo.cpython-313.pyc b/repos/__pycache__/generation_repo.cpython-313.pyc index f4bb2f3..7ef0c95 100644 Binary files a/repos/__pycache__/generation_repo.cpython-313.pyc and b/repos/__pycache__/generation_repo.cpython-313.pyc differ diff --git a/repos/__pycache__/user_repo.cpython-313.pyc b/repos/__pycache__/user_repo.cpython-313.pyc index 35baa97..722aa77 100644 Binary files a/repos/__pycache__/user_repo.cpython-313.pyc and b/repos/__pycache__/user_repo.cpython-313.pyc differ diff --git a/routers/__pycache__/char_router.cpython-313.pyc b/routers/__pycache__/char_router.cpython-313.pyc index 7dabe6b..a0f1716 100644 Binary files a/routers/__pycache__/char_router.cpython-313.pyc and b/routers/__pycache__/char_router.cpython-313.pyc differ diff --git a/routers/__pycache__/gen_router.cpython-313.pyc b/routers/__pycache__/gen_router.cpython-313.pyc index 669f7de..d148581 100644 Binary files a/routers/__pycache__/gen_router.cpython-313.pyc and b/routers/__pycache__/gen_router.cpython-313.pyc differ diff --git a/utils/__pycache__/image_utils.cpython-313.pyc b/utils/__pycache__/image_utils.cpython-313.pyc index a9c80bf..13ca8be 100644 Binary files a/utils/__pycache__/image_utils.cpython-313.pyc and b/utils/__pycache__/image_utils.cpython-313.pyc differ diff --git a/utils/__pycache__/security.cpython-313.pyc b/utils/__pycache__/security.cpython-313.pyc index 13ec706..a3e9652 100644 Binary files a/utils/__pycache__/security.cpython-313.pyc and b/utils/__pycache__/security.cpython-313.pyc differ