From 305ad24576faad9e9e1997dac745156f8f330092 Mon Sep 17 00:00:00 2001 From: xds Date: Sat, 7 Feb 2026 14:41:03 +0300 Subject: [PATCH] feat: Separate asset origin type from content type for improved asset categorization and handling. --- .env | 2 +- .../__pycache__/assets_router.cpython-313.pyc | Bin 8590 -> 8855 bytes api/endpoints/assets_router.py | 8 ++-- api/models/AssetDTO.py | 3 +- .../__pycache__/AssetDTO.cpython-313.pyc | Bin 1069 -> 1099 bytes .../generation_service.cpython-313.pyc | Bin 19966 -> 19822 bytes api/service/generation_service.py | 7 ++-- models/Asset.py | 38 ++++++++++++++++-- models/__pycache__/Asset.cpython-313.pyc | Bin 2302 -> 3203 bytes .../__pycache__/char_router.cpython-313.pyc | Bin 10838 -> 10947 bytes .../__pycache__/gen_router.cpython-313.pyc | Bin 27905 -> 28182 bytes routers/char_router.py | 4 +- routers/gen_router.py | 8 ++-- 13 files changed, 53 insertions(+), 17 deletions(-) diff --git a/.env b/.env index 369204b..ff9fabb 100644 --- a/.env +++ b/.env @@ -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 diff --git a/api/endpoints/__pycache__/assets_router.cpython-313.pyc b/api/endpoints/__pycache__/assets_router.cpython-313.pyc index 467c20944cc66bf07342301b9f3f9490818b96cb..79f5d02eaca114a637a5cac9743e1709a9bf6d1f 100644 GIT binary patch delta 2144 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 diff --git a/api/endpoints/assets_router.py b/api/endpoints/assets_router.py index b469e34..b9b2ae5 100644 --- a/api/endpoints/assets_router.py +++ b/api/endpoints/assets_router.py @@ -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: diff --git a/api/models/AssetDTO.py b/api/models/AssetDTO.py index 9fefd7c..8d46977 100644 --- a/api/models/AssetDTO.py +++ b/api/models/AssetDTO.py @@ -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 diff --git a/api/models/__pycache__/AssetDTO.cpython-313.pyc b/api/models/__pycache__/AssetDTO.cpython-313.pyc index 2bfba91c24b0b8cc39fb10f1247fec95b01e05b4..8885db48c0bc44e637782ede5ec336bce399208f 100644 GIT binary patch delta 389 zcmZ3>ahikoGcPX}0}$}=wP)^|$a{&gVB!Nqml&29)?ik3pm;D_5nBv<3=1oe4HRPs ziN$aLSsYMVP9#}QuozbnS2~*}_hfHI6|P%6$@zIDsd**wld~Di1o(lvi$F#dF@p%f z$sZY|8HFbEGDS&S0tMO`Zg6lnyWimAZT5r^LZX*hL?_oUDKknMhH#LWtL$BOUO>{XLgMQ%b=>h#bJ}1pHiBWYFDHR)W-X`wWUt8RUTI3mZG5+yu`r3_$9O5EG;5gvhT9KniRz0C28QvH$=8 delta 320 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 delta 3920 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 W68Oo_uwP7tNRYid^(E~=5c~%bDsMIb diff --git a/api/service/generation_service.py b/api/service/generation_service.py index a168442..7b31ba9 100644 --- a/api/service/generation_service.py +++ b/api/service/generation_service.py @@ -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, diff --git a/models/Asset.py b/models/Asset.py index 3c982e0..81a34bd 100644 --- a/models/Asset.py +++ b/models/Asset.py @@ -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: diff --git a/models/__pycache__/Asset.cpython-313.pyc b/models/__pycache__/Asset.cpython-313.pyc index 1459742615cad8234423874336106bf7ddc5f6a0..f1873efe36bac9fc638283d8cfc3d72106f31559 100644 GIT binary patch delta 1798 zcmZuxOK%%h6u$HH`x(FDIM{LX8XgIc1Zo9}0?w;SLzRgUQb$CtJxv3y6Wy86gpDml zqCsqIMip6fQFX-+V1KUBgKq_s;&?_OAf0_z&X=#WkT=byZ3(InS1V>bME;& zbU5hm`h0Ez!|lJbJ??G#6U^7miKVNTz0{i_36dqUkR-CG_!MrIM?*rW-{Kv>JO9H6 zEZzmYTXD^DwsmmJBYB$--emaZZ`qI;xV2MOE1RDcE8FFwzC)W&a^B#)rm6b-J5^n+ z8ad{3CTx7iEQo%~neh)Z2xp>Sfaa5Y%?q>X+}o|}6w7MabZx0sl@@iiOv9k8Ne()I z<`IM_LJYuk7Ot*-FrQz@o35pWwT1QhjfFfNL>a%LmWLr+4fg>3Xb4}Bc3`l++zt*M zuF#~brQw270jei0$CdO9FDH`Z6MhyB z#L=5+Mi~KJoMGj`ew}9JB)zc3N<*e#70#a_qs~QL-jY>7`liv~I-b6y8ej3li8P$t z^#Rn$4~ZXB-=}^q{hGT>lg3ZHR+|ByqTlni@D_3x0W!GAAz-1H?!8^8UbVbg)J^fa zdSi#G5@YgM+oniWeUDa4EEx6%J{^1z-F<+&1?{mG=gFOCvYVAe7H z=Z4J8+DBB`0xPp~pcmoqJ{plZjI+!X7>fg^1x99EfTKj>*C?JXu`~!14IxMb{7ana z-qyCOnqI7yRFf}nmvoccQgslxQbnULfvM@F#oL?u!LF*|zhp8QiVwyNm2Rq~Pd6*- zR1NzY%+&vcEZA!+0CjRE5^tj!OxM#J!Sa$(7bi!o_l`v(jXtkk&~f7zFS_3k51Ot8q3FW^Gb`KY^O5DT}PwvScn`?o(LD(La@Weg~)xmLty$= z?&8$th!{c`?eMVe%n&isVMQU_039ZjB3&e&wphI8niMjwI2Wx(PS^1AR;#yyZT(B% z;|0}pz6}p+ucBU}1=wO{wRZtJ9K$e=NcMLUeMH7j$oL;*u|*coM3#v)wB!64fyJ4V ZWl{~Uv4OkPnR|u_AFcdFV4^(Mz`wDJYDfS8 delta 953 zcmZ9KO=}ZD7=UN?E7{#_(tI{aHMUXHEK2J~qC`b0*ov2WSdWVm&9+q2R%SPXrwBpu zAd)!0z<(hgJn0YUMaqJdp43|)=~aB+bsM4!`^-DfygTpg%=8Mslkt&ZXat_audlWq zNP~EuJ|8UJsm+r-DHBUB5=*gTwpf=ii4bN4J_0^!t93E7@lDm#2A{7`ad1@TRodY1 z1f9Foqe=dguHG_&PXSSQ-5B6C*^i}PuI?Mz-Wn?$4?l)r@`NSH=o-H&UhdivOJ0OQ zOJJ%k+lmY^I3;u%I6ZWc(8Z?mqM~Ksthgzd zv%b9Ix{mh?cXr(uwpzZl)7*A^iOcd(PX@V&;(O&JE%K6*qXk}9+}r2So<~e2VRs3! z44A{<0s_y)*E-KyP0wjPY1{7@`(R_`2;!y7Iwc;!ry9nNlW+c@h{qk>QEs{W+*6fs&9=5 zG$Rqj2*uwki=VEW3I0i6dwLz`3VtA%_1{W<^v3pXtJ8LFvK7d~-EuDih9af(fRw+J l>;b9lk;)HJ-y`+0BG6o4?B5>~2*z5Trrz8=BG3eN{{Y7lsVV>f diff --git a/routers/__pycache__/char_router.cpython-313.pyc b/routers/__pycache__/char_router.cpython-313.pyc index 6356e12636f28209dfb05b9ec18e0831316b0272..478ec0b96f6d6048f271e6911fc01670f04ea1c1 100644 GIT binary patch delta 2274 zcmZ`)Urd`-6#s7fwX~G}qlFeqM}bi&g)+vU$sgGo#Fp7-Q7A9k7>mi!>5{&<#0T|_WbwfVH_y2R9K_w^`+eu0 zd(XM&-21!d{&H$1YJ6-o77Nf;Oz%uDEg37wp{0fk%|oTM)FVWMh%Rg!vQs;k_2IIi za$4>YVkNagCmKPcj-{JrE@#X!)?QFNi!tL^$I^zs(-kqrv=MWxyh9tYR0`+BZR@J% z7&^y#*a_keKWk~-VY%nImi^C$P>>;zL+m@^uG{t(DUF#rbP?MQpEc-nX~V(HD!Y4o zDg<4&V_I#@!ERdYB6vN_eyqJ(Ok|M$wI4XF`8&mRpeDbVB<0C%OZ< zUk&?JhP|NRZOqHQb=r)$ByZ8;u_j%=Zbi<1@XlDV23M8~9wFSTlRlOVfpto*=IMg~w|`z#gFBRb)zYP6E08jy_M;CPke zJjW89F!}|2!|-*%=cy6K4{v5C6baIcf{-$gAKB6X?LM*8ZPM;%%b>OvMyQ{8tUbif z#;sG$TIvT?Ud$i`N+$W~XX3nPN=~O{7`RYhXwVTx@D+Wy|#}GwQJYfG$?mzz~@@821f4*YLIDA z$o=VXs7MPVl^<*INNJFLR@pnu&t^aFvptjoM#y3L;zU}ai4lp;WM}CK81m_MU10hW zcYs#aH;^`1v-Tab0Gu_hcgl~g4K;%>qwIsnR5Q*HOg#qIz!Vq zGl?*RfGN@uWDo5?*ybc8H;anx0#~3S&+s%3q!3;PP;>xTu(0v+_uu;Q>C~AtozJ~k z<5w?Iv`_$iMLbNJMFl7K97;(2yje;q^D>Pm(v#WqawG+xHq)cCf$ybEj?|Z)@4ZQLazLtV7##Y2qV)M$Mm6Nk{f!**PENaTD;A%zK zKi-cP3s>24KAn)0N?-sAa$#>x;3Kc6@@!fOB&0MoV_!?tS=@MD@cE6dLv_)JtSHiy zTzDSv8KJER{7K+Ng&zbzBAzebn|d@28V96gazdJ%IhmE|MLLgmm=|3@up#h=it|j@ g2XSVTazDKc1DK}rl#Mmki@obYZ|;@GJP~p7zeXYduK)l5 delta 2188 zcma)7OKclO7~Wa$I<}Lw<4 zHwFQ8;PwtK$fYeFF5R3pj~O0m-P!ikXkC61vr=jwi5dcj__{VNWj$$Y)|j?sT`^<2 z)x%v78kxOM$>An@ZBBC6Y)bA5x9nikGHvWIdBf-+caUXIqos6P)*3UV z9Xs;+AuoA|2F4nGv~|}Gp8)ll{Ld=K$W_sSWb&QpAgjF7+_$}a@-gr7_W!TAzj>#x z5l3r#cGM2B{-TBfhbP9Ty*q4SvS2uE<#jQxX=+6`!ymbC_zks@o5G1mD{UDAw9%gi z!du*Zmxr8tJ$c{RNv2k(v4^?T_KTnM!_3`H6%hmO27M2JkAem~@caGP3z{wlHJj;8 zKG_d)7=T50fYe7JBJ4_`3wfoYXacp-MDU~pt)1P^v8)uD zIOPz>m2BsaZfjC6kbA9r&=ljB**<9uPz#QdGxmchO5U^2_lP)3Rk~2QysYZhVxhFC z=IdKp$m1arY}yz z3*A5>y`^#VGleqFQWdnPohLsy|FVBE^4Z9@dp7cS<~QfR8NB;SzV?SJhgK3X^gfa$ z;e%l+t%*o(71oRb$jweU%B}g3>=4%caVwqN97b}xaC0O`<#jufJB9T&CdZK6C9IFd zd#K?Sy}CZ(mNOorliTA+4hpv?;$Ax0a3i@_*ywPBJc8tYVPo$KOg5X492PcB0>~{q zExy@`0BI_!X=m8Uu!|vWit2+5dl~jK3^R;c5~GIA*id57uoXawUgK6!1UV$qoLhq; zwQh|XsEm(rwPc5zM}uU}bIk}Rh>wsa@AwHge+=Z-opOm1Ks~Nro+&FhKds=!>N1|D zrif|Rb;M^_0*Ep%KJwS_S@N#eYiH+Uo>wc3M;b2HzVt>AdbRdT*G~px4gwXjD_;~xig9u&5EDJaW`Mmwifr0smlY(>S46S}A^7Yh|!1kVD%0st%pCjfwXk7-8N zump<6t29xk7hB0$uq*<+OhGeIfM*5MDQ+x}X-9@fFe)I?@!;d3qsEk(tnk1ce3SY0k_!+E8QtuQy@auv3bJ-Y19 z1wq|3t!kPwuhySvc(3p<0DG3%&4-It-*C2Nd05M*kPJjD(9<3^)P;$8emDQY0_D*b(xDWp7D5Y59wv6eW0EODXD%?b{R8PAplP9<(oWCWYuT0|O*FIL zJ@@RnXU{pi=j^WTlF9czBuQ72k`e^?olZXLeffCO8uID!@@K=n25K+~cEPUe%k9mh zd7Mt{%kM3q1-(XU>@`sn=Oy(O_7>41li<)43H7K5ezZ78C=%?+2Em@4ic&N(Of?>L|?5ATF-ykeC^x$rz()!&Nm6t+3`ZG1i*G7#$n0OUF&BSRKVNI%=<} zW7(sRRk0qG#%QX$rY4B5rL3h#5b8xGZuSDl%2=1j=&Qe`zGch)AjMi{j?vU`O-(rS z7Ikc#Dr0msUQ@@iHS_9NYpP;2HC z(!K=@X>)9bAv*92h+Q{JOgL;($?A?^&=Zoi{4sQ3#v`Y4vMb;ZdHj*b610S@VL#XP zY4l(t)w8d31)4%mMsgDmlD+9b%en#EfW>Yb8M0UgWXbQE_RtLEOWOu4eKZqEwXMIy zYLRubGj3PN;{jT4yT!o@v`W3(@JDi0LY0GJR%`y2!cdw*lTGGMd`A} z7~Uy8tJjRofFe;x&vg&panOvkJ8_0`QujSrLE5Q{C9jM z2mxXdzfc~Z3&K`$n+T8X;tphP6$eES*(&Z*-gb%Xg^Yo)MIhfN-L=vWI^uvlZ6w_d z(rJ?s>CL3ONjf9wfj*-r-7V6Y90SrDN%tn{Ol?OCyv$XR?sjReT7z^0L2TmK!f_MF zc0_6fn1FKd^MFE*5-ovb0-QWYB;IX)P|AP4+G31ncwC@Y5R zrAGE#_IYBDP<@7q46%C+73n)s(+Ge$SqZb1oidd4bOHyaaAwBichlV{!{@sV*;+^o zCm)7#V+yeQCIGXBx{zL;sY$jj*QCQqqIM3EUAaC&+>yDwzmo8>+$es%V$IVrWaucG zv1k@j)d+lbsDm>;Z-}l(#w`fTVR6Fi2^F12?$QXc>7xsJe4dHKkaJ>>i_%$;9Au{Y zyvVTeU9pL~uAcCDToj@Su44MFFd_uvG3w~8IMl6?u_C=He*$TYr<}?M2|4|q{d^oJ z9b(5y8^a!yz_3UCn?%MG!X5;iP&v`#h6JB*&4j!GKlLI%b{3)mk9CuBU3*d72N2W% z;2Maof`fvf66nE5PuX@tz7zRjc?uZ{p!5vFJ_HJILZd`}Dc&@QT^Ly<5Z{P_tbRVmDS&y>V_{t7r zKVMwxRZ~jQO<4^!#Ty7>aa-Z%J@^&E9ngX&1cAM>-oSM0PLiYC>0T@itDL5HA@7@> zWh{E;3eI>YLQXy&eF$XlN8P*Gjxs&VGizB}btgFzxwCpd0muJYQwEM3*Oz#Cf}v>C zV=Vv69@7W#l_%Hr_)E)i&{f_ZLlwu^^Xp&MJc#6R?jzgMsJ6t;>(yLLUUYHatP8U0 z0DT0s{1`Rejc_D#dgE8bjkTVyj;JGMWMm@ns*Q_9mWC`NLy9{~tHms>o;gh6RrTpn z)bnU0uf9awz(=|+%ekfW5w@>!ukHwH!1<>4u}>Nc$-_+B^mqv_XQ(W^kStlWYL?lB zrabak2S@>aOZn7^^aiB)!cCyu@kv(L)}-faFl7p=M07KD?j5IL zHq(}E;Pbgufp|&d#f$pblWkc_Rpcf)Bd6NFBnind1P1&{d*sySOi}wIK^RWR5PBH< zsN)H7%*5Uv$&-c?2TNGEGmDfh?%|p2wa!G?#|t}hl1)NC#8D@hgeh@YU@w|7nW`(F zm|0<0W!IDO0_^h*98DY(VW0mJ+Ml4k0j&qxI%v6t!q{3cg#Y3M7pysd^00QW3h#2y zJlRD^3wx?N8;%htyWb`I*ilP}G_tQP`MMVPgz^w>VkI~9g|V)mkd&Gc8;O6UL)Tv( zf6#RCiA(ogJbCe@i@&@0VvEVFheML*wvaMmU8v5BU_s#Ju^TBX0-uH+q-+Si2pB`! zhmZmgrdyFrMOZpn(0+Wyz{^_8Z4(})#6Au0<`l)!pQ6CBxyo?^c}@6H{G`_hrxsB|?#O!`Cj~`0O-&8CdT_3#N;ar>*Nq zwfE9a@81RC0I}gAFN2!{L1L60xR9zs~K+ zC#TaolHhTsfLJr7GsYaG8;MmfooSMgZcDS8g|o@TYLL#R=#efX)_mz~Ij7r+)hM0a zTnzlVB4RDn%oTSu!pk`Wu@-C2<;EjjOhD1Ol1!wVh_y5Q~HIhJ!YC-fLp^R@b(EY(5+=}FQo=+sE3szKXAmKu5& z3@V@}uoMbu2=H6VpwY`0Q zEzh%8YBQ4U;&8lOiv!vdnt8%Wm~bpS&_=4*>jU+qhFuw`Asblnwo03krQB(qYBmzs00W8jc&R!K91)Vp|d^;rsLRAi&Svv=OOaBCJfi z54LaVUR8OP7ck}{eF5QD0A_g{%_i?$hsOF3lolC z0PxgE;humAjtQrnC|*KS7EhnCSr=BW9X#!{({Lnm@w~&7Qz~f$sox-M2UxyjxRo(} z3(O!c3Vt+L0_6^cHxhn*Qmmq{fm{lK4@N{mxGdZ$e4Z2W4z`i%=}V*2kd=o>KPKQI zV#fr;2V~`_xEq=&bFnBxg4e8|2hRJ|&&wiLFxeW=lsSyv&1&D$V@He4rOnh^7Ryr@G+Zw~d`D|jVm(J@Y?8p^iwDU%SSj4fEAisiRCC4g`H5_3u z=&j{g&#^I~w?cJ+=zEJ*7jj5%uI9oTHPQuY(0QR)jlC5rpm~X;q&f1`@a>Y0pR(}8 zLIbRPv^>lwQpYcVL8s3f4ACVCS@KR#IlZ5%1i|~0dx>yRs-E#rnF%lE=4|RfEx2b; z+?f{d&_lbn_3>>$R>2l5C+x(-qlMA{RSv>WfGA!h_xpVTms@7So`F-uE6v(Y_)P+zb6>P{~LB- zpAo?&gaL6{<)ki`&o}Ox*t-Mn90!yuY5{g6@In>b?+xwY*F)uscMv~6jBpRaLkPzZ z9zozI*vF9KXNzQ|UQL04Gf=hoX3uMktl9z#pT32XeBJP)rHrpUkZX`CL?{A~<2OM( gW__M^3Ux++`ULkGW}mE+S}zH$k^3fRH4+N{4`oe7sQ>@~ delta 6008 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