From c6142715d903d2b635ca3d11caa7fad87ac452bc Mon Sep 17 00:00:00 2001 From: xds Date: Thu, 12 Feb 2026 14:02:36 +0300 Subject: [PATCH] feat: Add image, update VS Code launch configuration, and enhance gitignore rules for build artifacts. --- .gitignore | 16 ++++++- .vscode/launch.json | 27 +---------- .../__pycache__/Exception.cpython-313.pyc | Bin 716 -> 716 bytes .../google_adapter.cpython-313.pyc | Bin 7787 -> 7787 bytes .../__pycache__/s3_adapter.cpython-313.pyc | Bin 5532 -> 5532 bytes adapters/google_adapter.py | 23 +++++++--- adapters/s3_adapter.py | 15 ++++++ api/__pycache__/dependency.cpython-313.pyc | Bin 2468 -> 2468 bytes .../__pycache__/admin.cpython-313.pyc | Bin 5109 -> 5109 bytes .../__pycache__/assets_router.cpython-313.pyc | Bin 10185 -> 13690 bytes .../__pycache__/auth.cpython-313.pyc | Bin 6133 -> 6133 bytes .../character_router.cpython-313.pyc | Bin 10349 -> 10349 bytes .../generation_router.cpython-313.pyc | Bin 10511 -> 10511 bytes api/endpoints/assets_router.py | 43 +++++++++++++----- .../__pycache__/AssetDTO.cpython-313.pyc | Bin 1099 -> 1099 bytes .../GenerationRequest.cpython-313.pyc | Bin 3258 -> 3258 bytes .../generation_service.cpython-313.pyc | Bin 26126 -> 26120 bytes api/service/generation_service.py | 12 +++-- models/__pycache__/Asset.cpython-313.pyc | Bin 3305 -> 3305 bytes models/__pycache__/Character.cpython-313.pyc | Bin 1099 -> 1099 bytes models/__pycache__/Generation.cpython-313.pyc | Bin 3268 -> 3268 bytes models/__pycache__/enums.cpython-313.pyc | Bin 2112 -> 2112 bytes repos/__pycache__/assets_repo.cpython-313.pyc | Bin 14845 -> 14845 bytes repos/__pycache__/char_repo.cpython-313.pyc | Bin 3774 -> 3774 bytes repos/__pycache__/dao.cpython-313.pyc | Bin 1466 -> 1466 bytes .../generation_repo.cpython-313.pyc | Bin 5472 -> 5472 bytes repos/__pycache__/user_repo.cpython-313.pyc | Bin 6660 -> 6660 bytes .../__pycache__/char_router.cpython-313.pyc | Bin 11050 -> 11050 bytes .../__pycache__/gen_router.cpython-313.pyc | Bin 28520 -> 28520 bytes utils/__pycache__/image_utils.cpython-313.pyc | Bin 1557 -> 1557 bytes utils/__pycache__/security.cpython-313.pyc | Bin 1745 -> 1745 bytes 31 files changed, 85 insertions(+), 51 deletions(-) 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 713f69cd0033ac76ab57f5ce362eb29437a4af61..691416b8b1b318326e824b988df54d6fa3ac7637 100644 GIT binary patch delta 20 acmX@ZdWMzzGcPX}0}yn7?%BwFkO=@lcm_@Y delta 20 acmX@ZdWMzzGcPX}0}%MlZrR9vkO=@hl?AZ? diff --git a/adapters/__pycache__/google_adapter.cpython-313.pyc b/adapters/__pycache__/google_adapter.cpython-313.pyc index 221010d0d5c3b72691cd3c4249ec4296b12e2eb1..817d8b42a73ab22d0e33deb8e08bbecc97b794d4 100644 GIT binary patch delta 20 acmaED^V){{GcPX}0}yn7?%BwlAO`?UC 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 bc2abebebe41ac5692b6250dd6d44bbc7c33707d..eb067d1ef5835eda4ff5c7e6a03a68aa9bef1acd 100644 GIT binary patch delta 20 acmZ1?yhND$GcPX}0}xzj=-tRYlM?_tZ3WB# delta 20 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 Nh+ delta 20 acmaDG@HT+^GcPX}0}zP4=-kMiqyYd+&IW1# diff --git a/api/endpoints/__pycache__/generation_router.cpython-313.pyc b/api/endpoints/__pycache__/generation_router.cpython-313.pyc index 93dae8db56dba4cd73ad30ba37422f73811cf5ee..fb99eafe262ca3d83034cdbbbc2883a81c565f81 100644 GIT binary patch delta 20 acmeAV>JQ@n%*)Hg00b8rdN*>jXaWF0AqA@d delta 20 ZcmeAV>JQ@n%*)Hg00iY+-5a@CGyyu~1lj-q 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 8885db48c0bc44e637782ede5ec336bce399208f..aa78e0c778885329d4bf21501e94631444e04809 100644 GIT binary patch delta 20 acmX@jahiksGcPX}0}yn7?%Bxgzybh2z6H|& delta 20 acmX@jahiksGcPX}0}$}=wQuBhU;zL&&ID=z diff --git a/api/models/__pycache__/GenerationRequest.cpython-313.pyc b/api/models/__pycache__/GenerationRequest.cpython-313.pyc index 0fe1b17356e5451541685ef016e8fb200e61f144..b958d6046962717bf8d4280f85aad8b55bb9d2cc 100644 GIT binary patch delta 20 acmdlbxl5A!GcPX}0}xzj=-tS@o(BLwxCR{n delta 20 acmdlbxl5A!GcPX}0}yOF+PRT?Jr4jta0WmC diff --git a/api/service/__pycache__/generation_service.cpython-313.pyc b/api/service/__pycache__/generation_service.cpython-313.pyc index ea28bd274ebb81da074b762951a1c195ff315d58..b8700ed5bb13dd258c1e0549317afecda1e799b6 100644 GIT binary patch delta 1376 zcmZvZZA?>V6oBvPy}gA(DbU-}LJM_L=x{9s3Y79;iU`6I7C?|zV!1GXxS)n^%i?0f zl9|o1xTyzC%#1(g{wxbJcC#;)sSw=esvC+9nM)~&OZH<1oY_Y*$KG?zK7Q=}c;4rn z^X7fedG8x>I2Z@R&jy2@W8XI~9oltf(jd7~>#{g*J?G?(VWMRnKJ=z4TV44ggq3GE z`~pHZbRIz2W#V&d^tjvOfmbn^ZaV5s#c)m*e&K0`J~VnanVaZP`nI<}S~;$B6gw&; zoGRCgU3uLw)QpMpW}&xZsD+7}-DH9zm{sb4RxB+Y$EI=%-Y-3i^F9+^@R?x@Z~E53 zas1WS#Tb%IaDvXWcaHsi!N2SFw;nlOa$AM&l+KelTx7iW+e(m=_0w4M(bbqRaBdAn%sRx)9d)0Fq&AWO$8rY_Z?NELQ zbO1bsf#8R5Lb(@g1D3?fx>s1E=j(33DLh>NGTc?9hU0wpLZ0i}Jl|ZNZ`GV;%Q)D& zVn-!ZDOPg@4XX{xy--*K`;;wB5dbHi-W(pW!?Xn=UST>Xw0^)2GeL-y2{W}JD+^{# z5b+DMQmBvxmje)~7BANnGyl9WRKb=nt=>pIcSVFqP`IKC<+I?cA{Q&f)3s427teyI zLx^V!OX6V!pURV0#zKfaq6Qt&+`C&n?#Lqq+Qdy3iAS zwfvn5=&KU%_;utXwFXxCKBZ9`HS-ArL^H)ihK}5SwBSSp78l9VO4>K>rHSCX~5q(;L$DFC@vAWx-+lYS$0naoyc z7RepJ=1DsB12#Tc;u?_nWuunmmnALBEZf+X%6Q8bO~qM`<6`F+7Wv(~_Qa+s3=(9* zKEg+Y7YKy}1>J2S7{_PY9)}ubtSy+Pi5@_ zyfBGx_JrXs{@BwBXRu;h^T;Pu93>PJ+ytI*n(!&%pPpm26rLw!6VwJwWK#r*@E+ka z!VZEu+a|ILgbBj)1T(?HIT$aI{@)zbvlTtPg{Mry?bD*@_TQG z2yf$)(V_yiX@QE91ob`t`;>>HQJ7FxqOSu?;C{I!Yn-+^#WP$+6){UoN!bC0{$f z=kwAf@hN{cyn}LKVNMqe?f9o0{az;y`A@(}Jm`^77r4xl?6%=zK*EQ98#;q}-DQsJ z;&z=vV=xA%Fe2IDjCzwbzg^RJ4lf3a;XFItj*G$L>?8-iP~|8vW^rxW=%i{lQ;0rN z^(q&uS~YM5U#oRV0>`%Pq{Vytxklc}?T2DqD=)*A+M~L4wf+R&sEx6!+Lhpz!y1iO zj~~|UR)qQ}Kon!4Svafc8#(}c!pC3&)8V^t0jC>}L0YM4I?d}J*bD`xvG%n>Oh;

mXhsUav1@`MZ)xH9OqM_reN4k8;={8-zR777ZILN+6qsMZ4PG z5A~94=YF(A^4a0O7i6n=zkCbJOPd5%yW|1Vt6HJz2iDb6l_6o~m%@>XJpn#l275M( zX|Im*Xu!x;(v8j9gn`d!AfXjAypFO#%N8sOG1E=3P>MNVoiqhH^bn4hfW-sNt0wfx2ALsTxGeR$%*C zu^R?V{EE{(pyyWwEt_A}YuU`IiQQ6JY}=u!{+#`klV32F_~#D{C$G>jM99R;#4K@y zC?P(<&W;F7B6fJ8PMPZnWe2^6031SmKXtdm$5^?0`{<`sFiw;bWdu*WPkcuFyUk<+ z4cRNnC9QNUDMe-|Ria1Qz2?w{4`3=SY+Y9v_X5H@WDa*Y> zMHh&>#63LTGYm0hy{A)zOW2z5IMs&PTg@e>3H9;+_sNeZ5-_Fwl6Vtf3SX4lER$6B zJ7Y;07#SH#{;4d;Z$WOaM*A#Gmp|gvu=Wx@H@G!>hHBLZv4*pQb?~e5+n@t-pU|}W c&8j=optP?PY#81*XMRNs1rIqg+03f`0%qE9=>Px# 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 13bfb7f10f5de5baf05fa550ec4cd736ece6ebca..90efbf52318691d8bc0dee63592fa724fe5ad889 100644 GIT binary patch delta 20 acmaDU`BIYmGcPX}0}xzj=-tSDj|Tup5C*CM delta 20 acmaDU`BIYmGcPX}0}$x$>)gnFj|Tul`vyS( diff --git a/models/__pycache__/Character.cpython-313.pyc b/models/__pycache__/Character.cpython-313.pyc index 191ad85aae50bf098bd971a2f768cfa2e0b9a0d0..2fdabb092f087d9190abf320685c72775da87fea 100644 GIT binary patch delta 20 acmX@jahiksGcPX}0}yn7?%Bxgzybh2z6H|& delta 20 acmX@jahiksGcPX}0}zBi@7&1kzybh0`vs5y diff --git a/models/__pycache__/Generation.cpython-313.pyc b/models/__pycache__/Generation.cpython-313.pyc index dfc0bfd5e1d6b4e9809748e3dbf58ce0dd18eef2..b052293dfd5f8e05d03ae7ee0feab8dd2fb2bb8d 100644 GIT binary patch delta 20 acmX>ic|?-?GcPX}0}xzj=-tS@lLr7nNCrCq delta 20 acmX>ic|?-?GcPX}0}xC*+PRT?Cl3HYJO)Al diff --git a/models/__pycache__/enums.cpython-313.pyc b/models/__pycache__/enums.cpython-313.pyc index e307fb2eee8b161b5ef41a201bcad77c8bb708a7..086f9c951b0fbec54417f8a3fe9512b3c30d3a81 100644 GIT binary patch delta 20 acmX>ga6o|jGcPX}0}xzj=-tR|$^igAUga6o|jGcPX}0}#xf)v}S>lmh@gTm^Li diff --git a/repos/__pycache__/assets_repo.cpython-313.pyc b/repos/__pycache__/assets_repo.cpython-313.pyc index 8bee858966c1f65d25c76ef1ab12b08124b23646..60c530c6293ff21aea8fb8b3a33b9fba6eef626e 100644 GIT binary patch delta 20 acmexc{I{6)gow$r1omR0nbZ diff --git a/repos/__pycache__/char_repo.cpython-313.pyc b/repos/__pycache__/char_repo.cpython-313.pyc index ccd7a75b783757ccffa08d8eb4d89a01c57ef392..e3e55a5e2202bd70449610e4dbcf6f651a8e2a9c 100644 GIT binary patch delta 20 acmdldyHA$;GcPX}0}yn7?%Bw_nGXO#v<6@R delta 20 acmdldyHA$;GcPX}0}yoI>fFe^nGXO#4+ca4 diff --git a/repos/__pycache__/dao.cpython-313.pyc b/repos/__pycache__/dao.cpython-313.pyc index 8c96e7b8c4b164b885d7483f17942ce7e1790c36..8427c184aabfd89143eb70a632e9aa57c4daf903 100644 GIT binary patch delta 20 acmdnRy^EXsGcPX}0}yn7?%Bw_o)rK-g9ax6 delta 20 acmdnRy^EXsGcPX}0}!k{(z%g)Ju3h{ECv$* diff --git a/repos/__pycache__/generation_repo.cpython-313.pyc b/repos/__pycache__/generation_repo.cpython-313.pyc index f4bb2f37b2f72582120f2feb3ca87c2b325c3a4e..7ef0c95795a538ea8205ef9fb45e67414a306b5c 100644 GIT binary patch delta 20 acmaE$^+1dJGcPX}0}yn7?%Bv4DhdEc-v(y@ delta 20 acmaE$^+1dJGcPX}0}zDX=-kL1DhdEav<4FZ diff --git a/repos/__pycache__/user_repo.cpython-313.pyc b/repos/__pycache__/user_repo.cpython-313.pyc index 35baa979c62cf48dd8b9f2d15d90bca29a0cbeb4..722aa7785f72b094cbb3622dc24ceabd8e60e4b7 100644 GIT binary patch delta 20 acmZoMX))pc%*)Hg00iBidp2_almq}g>jrB8 delta 20 acmZoMX))pc%*)Hg00d6=IyZ9vlmq}eUj_;Q diff --git a/routers/__pycache__/char_router.cpython-313.pyc b/routers/__pycache__/char_router.cpython-313.pyc index 7dabe6b7ee1718625e23251ce7e4b38610f5b44b..a0f1716700b3505171ed80bfe7848b4fc35481ab 100644 GIT binary patch delta 20 acmZ1#wknMKGcPX}0}yn7?%BvKuMGf2wFWZ) delta 20 acmZ1#wknMKGcPX}0}$+9)47pbUK;>KOa>VM diff --git a/routers/__pycache__/gen_router.cpython-313.pyc b/routers/__pycache__/gen_router.cpython-313.pyc index 669f7de1b54a84ecf88b043a9ec0fb98fa72ca27..d1485819c7a6535c3cd1b1262e1d0352a9182c25 100644 GIT binary patch delta 22 ccmaEHkMYGlM()qNyj%=G(EYh*BX?{*0BAx8LjV8( delta 22 ccmaEHkMYGlM()qNyj%=Gkg>XRBX?{*0A-E`)c^nh diff --git a/utils/__pycache__/image_utils.cpython-313.pyc b/utils/__pycache__/image_utils.cpython-313.pyc index a9c80bf2386eb809899812a5cb829243b87e81a9..13ca8beb1f428197db79fbd290d3ba02196eeb06 100644 GIT binary patch delta 20 acmbQrGnI$?GcPX}0}yn7?%BxA#RdR3-~~GX delta 20 acmbQrGnI$?GcPX}0}zN_YuU)n#RdQ~&;+0W diff --git a/utils/__pycache__/security.cpython-313.pyc b/utils/__pycache__/security.cpython-313.pyc index 13ec70643d26db54ff7207012e549c8fcda9ca5e..a3e9652e9c061d5e3b314c1d03f0ddcc04cbb881 100644 GIT binary patch delta 20 acmcb}dy$v>GcPX}0}yn7?%BwFj12%nWCn2n delta 20 acmcb}dy$v>GcPX}0}zBP?%2qEj12%j?*--n